@parhelia/localization 0.1.10745

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +29 -0
  3. package/dist/core/src/editor/ui/DragPreview.d.ts +15 -0
  4. package/dist/core/src/editor/ui/DragPreview.d.ts.map +1 -0
  5. package/dist/core/src/editor/ui/DragPreview.js +32 -0
  6. package/dist/core/src/editor/ui/PerfectTree.d.ts +79 -0
  7. package/dist/core/src/editor/ui/PerfectTree.d.ts.map +1 -0
  8. package/dist/core/src/editor/ui/PerfectTree.js +857 -0
  9. package/dist/localization/src/LocalizeItemCommand.d.ts +8 -0
  10. package/dist/localization/src/LocalizeItemCommand.d.ts.map +1 -0
  11. package/dist/localization/src/LocalizeItemCommand.js +44 -0
  12. package/dist/localization/src/LocalizeItemDialog.d.ts +4 -0
  13. package/dist/localization/src/LocalizeItemDialog.d.ts.map +1 -0
  14. package/dist/localization/src/LocalizeItemDialog.js +126 -0
  15. package/dist/localization/src/LocalizeItemUtils.d.ts +17 -0
  16. package/dist/localization/src/LocalizeItemUtils.d.ts.map +1 -0
  17. package/dist/localization/src/LocalizeItemUtils.js +93 -0
  18. package/dist/localization/src/api/discovery.d.ts +36 -0
  19. package/dist/localization/src/api/discovery.d.ts.map +1 -0
  20. package/dist/localization/src/api/discovery.js +29 -0
  21. package/dist/localization/src/constants.d.ts +15 -0
  22. package/dist/localization/src/constants.d.ts.map +1 -0
  23. package/dist/localization/src/constants.js +21 -0
  24. package/dist/localization/src/hooks/useTranslationWizard.d.ts +6 -0
  25. package/dist/localization/src/hooks/useTranslationWizard.d.ts.map +1 -0
  26. package/dist/localization/src/hooks/useTranslationWizard.js +78 -0
  27. package/dist/localization/src/index.d.ts +69 -0
  28. package/dist/localization/src/index.d.ts.map +1 -0
  29. package/dist/localization/src/index.js +152 -0
  30. package/dist/localization/src/services/translationService.d.ts +102 -0
  31. package/dist/localization/src/services/translationService.d.ts.map +1 -0
  32. package/dist/localization/src/services/translationService.js +37 -0
  33. package/dist/localization/src/setup/LocalizationSetupStep.d.ts +3 -0
  34. package/dist/localization/src/setup/LocalizationSetupStep.d.ts.map +1 -0
  35. package/dist/localization/src/setup/LocalizationSetupStep.js +108 -0
  36. package/dist/localization/src/sidebar/TranslationSidebar.d.ts +2 -0
  37. package/dist/localization/src/sidebar/TranslationSidebar.d.ts.map +1 -0
  38. package/dist/localization/src/sidebar/TranslationSidebar.js +93 -0
  39. package/dist/localization/src/steps/MetadataInputStep.d.ts +4 -0
  40. package/dist/localization/src/steps/MetadataInputStep.d.ts.map +1 -0
  41. package/dist/localization/src/steps/MetadataInputStep.js +38 -0
  42. package/dist/localization/src/steps/ServiceLanguageSelectionStep.d.ts +3 -0
  43. package/dist/localization/src/steps/ServiceLanguageSelectionStep.d.ts.map +1 -0
  44. package/dist/localization/src/steps/ServiceLanguageSelectionStep.js +91 -0
  45. package/dist/localization/src/steps/SubitemDiscoveryStep.d.ts +3 -0
  46. package/dist/localization/src/steps/SubitemDiscoveryStep.d.ts.map +1 -0
  47. package/dist/localization/src/steps/SubitemDiscoveryStep.js +391 -0
  48. package/dist/localization/src/steps/index.d.ts +5 -0
  49. package/dist/localization/src/steps/index.d.ts.map +1 -0
  50. package/dist/localization/src/steps/index.js +4 -0
  51. package/dist/localization/src/steps/types.d.ts +68 -0
  52. package/dist/localization/src/steps/types.d.ts.map +1 -0
  53. package/dist/localization/src/steps/types.js +1 -0
  54. package/dist/localization/src/translation-center/BatchTranslationView.d.ts +7 -0
  55. package/dist/localization/src/translation-center/BatchTranslationView.d.ts.map +1 -0
  56. package/dist/localization/src/translation-center/BatchTranslationView.js +487 -0
  57. package/dist/localization/src/translation-center/RecentTranslations.d.ts +2 -0
  58. package/dist/localization/src/translation-center/RecentTranslations.d.ts.map +1 -0
  59. package/dist/localization/src/translation-center/RecentTranslations.js +199 -0
  60. package/dist/localization/src/translation-center/TranslationManagement.d.ts +2 -0
  61. package/dist/localization/src/translation-center/TranslationManagement.d.ts.map +1 -0
  62. package/dist/localization/src/translation-center/TranslationManagement.js +25 -0
  63. package/dist/localization/src/types.d.ts +18 -0
  64. package/dist/localization/src/types.d.ts.map +1 -0
  65. package/dist/localization/src/types.js +1 -0
  66. package/dist/localization/src/utils/createVersions.d.ts +14 -0
  67. package/dist/localization/src/utils/createVersions.d.ts.map +1 -0
  68. package/dist/localization/src/utils/createVersions.js +26 -0
  69. package/package.json +47 -0
  70. package/styles.css +1 -0
@@ -0,0 +1,487 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useState, useMemo, useRef, useCallback } from "react";
3
+ import { useDebouncedCallback } from "use-debounce";
4
+ import { useEditContext, SimpleTable, Progress, Button } from "@parhelia/core";
5
+ import { listBatchTranslationJobs, subscribeToBatch, unsubscribeFromBatch, getBatchInfo, getTranslationProviders } from "../services/translationService";
6
+ // Normalize API shape/casing to what the UI expects
7
+ function normalizeBatchInfo(raw) {
8
+ if (!raw)
9
+ return null;
10
+ const pick = (a, b) => (a !== undefined ? a : b);
11
+ const toNum = (v) => (typeof v === "number" ? v : v != null ? parseInt(v, 10) : undefined);
12
+ const info = {
13
+ batchId: pick(raw?.batchId, raw?.BatchId),
14
+ createdAtUtc: pick(raw?.createdAtUtc, raw?.CreatedAtUtc),
15
+ startedAtUtc: pick(raw?.startedAtUtc, raw?.StartedAtUtc),
16
+ completedAtUtc: pick(raw?.completedAtUtc, raw?.CompletedAtUtc),
17
+ initiatedByUser: pick(raw?.initiatedByUser, raw?.InitiatedByUser),
18
+ initiatorSessionId: pick(raw?.initiatorSessionId, raw?.InitiatorSessionId),
19
+ provider: pick(raw?.provider, raw?.Provider),
20
+ status: pick(raw?.status, raw?.Status),
21
+ expectedJobs: toNum(pick(raw?.expectedJobs, raw?.ExpectedJobs)),
22
+ completedJobs: toNum(pick(raw?.completedJobs, raw?.CompletedJobs)),
23
+ errorJobs: toNum(pick(raw?.errorJobs, raw?.ErrorJobs)),
24
+ includeSubitems: pick(raw?.includeSubitems, raw?.IncludeSubitems) ?? null,
25
+ scopeItemId: pick(raw?.scopeItemId, raw?.ScopeItemId),
26
+ scopeItemPath: pick(raw?.scopeItemPath, raw?.ScopeItemPath),
27
+ metadata: pick(raw?.metadata, raw?.Metadata),
28
+ lastUpdatedUtc: pick(raw?.lastUpdatedUtc, raw?.LastUpdatedUtc),
29
+ exists: pick(raw?.exists, raw?.Exists),
30
+ };
31
+ return info.batchId ? info : null;
32
+ }
33
+ export function BatchTranslationView({ batchId, onBack }) {
34
+ const editContext = useEditContext();
35
+ const [batchJobs, setBatchJobs] = useState([]);
36
+ const [batchInfo, setBatchInfo] = useState(null);
37
+ const [providers, setProviders] = useState([]);
38
+ const [itemNameById, setItemNameById] = useState({});
39
+ const [isLoading, setIsLoading] = useState(false);
40
+ const [translationProgress, setTranslationProgress] = useState(new Map());
41
+ const firstProgressSeenRef = useRef(false);
42
+ const lastWsAtRef = useRef(0);
43
+ const batchJobsRef = useRef([]);
44
+ const hasLoadedRef = useRef(false);
45
+ const isSubscribedRef = useRef(false);
46
+ const initGuardRef = useRef(false);
47
+ const lastBatchIdRef = useRef(null);
48
+ // Helper function to get display name from service name
49
+ const getProviderDisplayName = useCallback((serviceName) => {
50
+ if (!serviceName)
51
+ return 'Unknown';
52
+ const provider = providers.find(p => p.name === serviceName);
53
+ return provider?.displayName || serviceName;
54
+ }, [providers]);
55
+ // Keep ref in sync with state
56
+ useEffect(() => {
57
+ batchJobsRef.current = batchJobs;
58
+ }, [batchJobs]);
59
+ const loadProviders = useCallback(async () => {
60
+ try {
61
+ const res = await getTranslationProviders();
62
+ const providerData = (res?.data ?? res ?? []);
63
+ setProviders(providerData);
64
+ }
65
+ catch (error) {
66
+ console.error("Failed to load translation providers:", error);
67
+ }
68
+ }, []);
69
+ const loadBatchJobs = useCallback(async () => {
70
+ setIsLoading(true);
71
+ try {
72
+ // Use efficient batch-specific endpoint
73
+ const res = await listBatchTranslationJobs(batchId);
74
+ const jobs = (res?.data ?? res ?? []);
75
+ setBatchJobs(jobs);
76
+ // Resolve item names only for new items we haven't seen before
77
+ const ids = Array.from(new Set(jobs.map((j) => j.itemId)));
78
+ const newIds = ids.filter((id) => !itemNameById[id]);
79
+ if (newIds.length && editContext?.itemsRepository) {
80
+ const descriptors = newIds.map((id) => ({ id, language: "en", version: 0 }));
81
+ try {
82
+ const stubs = await editContext.itemsRepository.getItemsStubs(descriptors);
83
+ const map = {};
84
+ stubs?.forEach((s) => {
85
+ if (s)
86
+ map[s.id] = s.name || s.id;
87
+ });
88
+ setItemNameById((prev) => ({ ...prev, ...map }));
89
+ }
90
+ catch (error) {
91
+ console.error("Failed to load item names:", error);
92
+ }
93
+ }
94
+ }
95
+ catch (error) {
96
+ console.error("Failed to load batch jobs:", error);
97
+ }
98
+ finally {
99
+ setIsLoading(false);
100
+ }
101
+ }, [batchId, editContext, itemNameById]);
102
+ const debouncedRefresh = useDebouncedCallback(() => {
103
+ void loadBatchJobs();
104
+ }, 800);
105
+ // Load initial data and conditionally subscribe to batch updates
106
+ useEffect(() => {
107
+ // Reset guards if batchId changed
108
+ if (lastBatchIdRef.current !== batchId) {
109
+ lastBatchIdRef.current = batchId;
110
+ hasLoadedRef.current = false;
111
+ isSubscribedRef.current = false;
112
+ initGuardRef.current = false;
113
+ }
114
+ let cancelled = false;
115
+ const init = async () => {
116
+ if (initGuardRef.current)
117
+ return; // prevent duplicate init under StrictMode
118
+ initGuardRef.current = true;
119
+ try {
120
+ // Always load batch info first to decide on subscriptions
121
+ try {
122
+ const resInfo = await getBatchInfo(batchId);
123
+ const raw = resInfo?.data ?? resInfo;
124
+ const info = normalizeBatchInfo(raw);
125
+ if (!cancelled && info)
126
+ setBatchInfo(info);
127
+ const isTerminal = info?.status === 'Completed' || info?.status === 'Error';
128
+ // Conditionally subscribe only for non-terminal batches
129
+ if (!isTerminal && editContext?.sessionId) {
130
+ await subscribeToBatch(batchId, editContext.sessionId);
131
+ isSubscribedRef.current = true;
132
+ }
133
+ }
134
+ catch { }
135
+ // Load providers for display name mapping
136
+ await loadProviders();
137
+ // Load jobs once per mount/batch change
138
+ if (!hasLoadedRef.current) {
139
+ await loadBatchJobs();
140
+ hasLoadedRef.current = true;
141
+ }
142
+ }
143
+ catch (e) {
144
+ // no-op
145
+ }
146
+ };
147
+ init();
148
+ return () => {
149
+ // Cleanup: unsubscribe when component unmounts or batch changes
150
+ if (isSubscribedRef.current && editContext && editContext.sessionId && batchId) {
151
+ unsubscribeFromBatch(batchId, editContext.sessionId).catch(console.error);
152
+ }
153
+ };
154
+ }, [batchId, editContext?.sessionId]);
155
+ // Listen for translation events - only for this batch
156
+ useEffect(() => {
157
+ const removeSocketMessageListener = editContext?.addSocketMessageListener((message) => {
158
+ // Mark last websocket activity time for adaptive polling
159
+ if (message.type === "translation-started" ||
160
+ message.type === "translation-finished" ||
161
+ message.type === "translation-error" ||
162
+ message.type === "translation-progress" ||
163
+ message.type === "batch-completed") {
164
+ lastWsAtRef.current = Date.now();
165
+ }
166
+ if (message.type === "translation-started" ||
167
+ message.type === "translation-finished" ||
168
+ message.type === "translation-error") {
169
+ // Only update if this message is for our batch
170
+ if (message.payload.batchId === batchId) {
171
+ // Update the specific job in our local state instead of refetching all jobs
172
+ setBatchJobs(prevJobs => {
173
+ const jobIndex = prevJobs.findIndex(job => job.itemId === message.payload.itemId &&
174
+ job.targetLanguage === message.payload.language);
175
+ if (jobIndex >= 0) {
176
+ // Update existing job
177
+ const updatedJobs = [...prevJobs];
178
+ const existingJob = updatedJobs[jobIndex];
179
+ if (existingJob) {
180
+ updatedJobs[jobIndex] = {
181
+ ...existingJob,
182
+ status: message.payload.status,
183
+ timestamp: message.payload.timestamp || existingJob.timestamp,
184
+ message: message.payload.message || existingJob.message
185
+ };
186
+ }
187
+ return updatedJobs;
188
+ }
189
+ else if (message.type === "translation-started") {
190
+ // Add new job if it doesn't exist (shouldn't happen with batch subscriptions, but safe fallback)
191
+ const newJob = {
192
+ itemId: message.payload.itemId,
193
+ targetLanguage: message.payload.language,
194
+ sourceLanguage: message.payload.sourceLanguage || "",
195
+ status: message.payload.status,
196
+ timestamp: message.payload.timestamp || new Date().toISOString(),
197
+ batchId: batchId,
198
+ message: message.payload.message || ""
199
+ };
200
+ // Resolve item name for new job if we don't have it
201
+ if (!itemNameById[message.payload.itemId]) {
202
+ void resolveItemNames([message.payload.itemId]);
203
+ }
204
+ return [...prevJobs, newJob];
205
+ }
206
+ return prevJobs;
207
+ });
208
+ // Kick reconciliatory refresh the first time we see start/progress
209
+ if (!firstProgressSeenRef.current) {
210
+ firstProgressSeenRef.current = true;
211
+ debouncedRefresh();
212
+ setTimeout(() => debouncedRefresh(), 1500);
213
+ }
214
+ }
215
+ }
216
+ if (message.type === "translation-progress") {
217
+ // Only track progress for jobs in this batch
218
+ if (message.payload.batchId === batchId) {
219
+ const itemId = (message.payload.itemId ?? "").toString().toLowerCase();
220
+ const language = message.payload.language;
221
+ const progressKey = `${itemId}-${language}`;
222
+ // Ensure a row exists; if not, add a placeholder "In Progress" job
223
+ setBatchJobs(prevJobs => {
224
+ const idx = prevJobs.findIndex(j => j.itemId === itemId && j.targetLanguage === language);
225
+ if (idx >= 0)
226
+ return prevJobs;
227
+ const newJob = {
228
+ itemId,
229
+ targetLanguage: language,
230
+ sourceLanguage: message.payload.sourceLanguage || "",
231
+ status: "In Progress",
232
+ timestamp: new Date().toISOString(),
233
+ batchId: batchId,
234
+ message: message.payload.message || ""
235
+ };
236
+ return [...prevJobs, newJob];
237
+ });
238
+ setTranslationProgress((prev) => {
239
+ const next = new Map(prev);
240
+ next.set(progressKey, {
241
+ progress: message.payload.progress || 0,
242
+ message: message.payload.message || ""
243
+ });
244
+ return next;
245
+ });
246
+ // Kick reconciliatory refresh the first time we see progress
247
+ if (!firstProgressSeenRef.current) {
248
+ firstProgressSeenRef.current = true;
249
+ debouncedRefresh();
250
+ setTimeout(() => debouncedRefresh(), 1500);
251
+ }
252
+ }
253
+ }
254
+ // Handle batch-completed messages for this specific batch
255
+ if (message.type === "batch-completed" && message.payload.batchId === batchId) {
256
+ // Refresh header info and jobs
257
+ (async () => {
258
+ try {
259
+ const resInfo = await getBatchInfo(batchId);
260
+ const raw = resInfo?.data ?? resInfo;
261
+ const info = normalizeBatchInfo(raw);
262
+ if (info)
263
+ setBatchInfo(info);
264
+ }
265
+ catch { }
266
+ })();
267
+ // Debounce refresh to avoid bursts near completion
268
+ debouncedRefresh();
269
+ }
270
+ });
271
+ return () => {
272
+ if (removeSocketMessageListener) {
273
+ removeSocketMessageListener();
274
+ }
275
+ };
276
+ }, [editContext, batchId, loadBatchJobs, debouncedRefresh, itemNameById]);
277
+ // Adaptive polling while batch is in progress: poll only if no WS in last 6s
278
+ useEffect(() => {
279
+ const isRunning = batchInfo?.status === 'In Progress';
280
+ if (!isRunning)
281
+ return;
282
+ const id = setInterval(() => {
283
+ if (Date.now() - lastWsAtRef.current > 6000) {
284
+ void loadBatchJobs();
285
+ }
286
+ }, 12000);
287
+ return () => clearInterval(id);
288
+ }, [batchInfo?.status, loadBatchJobs]);
289
+ // Helper function to resolve item names for new jobs
290
+ const resolveItemNames = async (itemIds) => {
291
+ if (itemIds.length && editContext?.itemsRepository) {
292
+ const descriptors = itemIds.map((id) => ({ id, language: "en", version: 0 }));
293
+ try {
294
+ const stubs = await editContext.itemsRepository.getItemsStubs(descriptors);
295
+ const map = {};
296
+ stubs?.forEach((s) => {
297
+ if (s)
298
+ map[s.id] = s.name || s.id;
299
+ });
300
+ setItemNameById((prev) => ({ ...prev, ...map }));
301
+ }
302
+ catch (error) {
303
+ console.error("Failed to load item names:", error);
304
+ }
305
+ }
306
+ };
307
+ // Group jobs by item for better organization
308
+ const itemGroups = useMemo(() => {
309
+ const groups = {};
310
+ for (const job of batchJobs) {
311
+ if (!groups[job.itemId]) {
312
+ groups[job.itemId] = [];
313
+ }
314
+ groups[job.itemId].push(job);
315
+ }
316
+ return groups;
317
+ }, [batchJobs]);
318
+ // Sort items by status priority: in-progress first, then errors, then completed
319
+ const sortedItemGroups = useMemo(() => {
320
+ const entries = Object.entries(itemGroups);
321
+ return entries.sort(([, jobsA], [, jobsB]) => {
322
+ // Calculate status priority for each item group
323
+ const getStatusPriority = (jobs) => {
324
+ const hasInProgress = jobs.some(j => j.status === "In Progress");
325
+ const hasError = jobs.some(j => j.status === "Error");
326
+ const allCompleted = jobs.every(j => j.status === "Completed");
327
+ if (hasInProgress)
328
+ return 1; // Highest priority - show first
329
+ if (hasError)
330
+ return 2; // Second priority
331
+ if (allCompleted)
332
+ return 4; // Lowest priority - show last
333
+ return 3; // Pending/other statuses
334
+ };
335
+ const priorityA = getStatusPriority(jobsA);
336
+ const priorityB = getStatusPriority(jobsB);
337
+ return priorityA - priorityB;
338
+ });
339
+ }, [itemGroups]);
340
+ // Calculate overall progress segments including in-progress job progress.
341
+ // Incorporate server-side expected/completed/error counts so pending server jobs are reflected.
342
+ const progressSegments = useMemo(() => {
343
+ const expectedTotal = typeof batchInfo?.expectedJobs === 'number' && batchInfo.expectedJobs > 0
344
+ ? batchInfo.expectedJobs
345
+ : batchJobs.length;
346
+ if (expectedTotal === 0) {
347
+ return {
348
+ completed: 0,
349
+ inProgress: 0,
350
+ error: 0,
351
+ pending: 0,
352
+ completedCount: 0,
353
+ inProgressCount: 0,
354
+ errorCount: 0,
355
+ pendingCount: 0,
356
+ total: 0,
357
+ actualProgress: 0
358
+ };
359
+ }
360
+ const localCompleted = batchJobs.filter(job => job.status === 'Completed').length;
361
+ const localErrors = batchJobs.filter(job => job.status === 'Error').length;
362
+ const serverCompleted = typeof batchInfo?.completedJobs === 'number' ? batchInfo.completedJobs : undefined;
363
+ const serverErrors = typeof batchInfo?.errorJobs === 'number' ? batchInfo.errorJobs : undefined;
364
+ // Prefer the larger of local/server so UI can progress smoothly with local updates
365
+ const completedCount = Math.max(0, Math.min(expectedTotal, Math.max(localCompleted, serverCompleted ?? 0)));
366
+ const errorCount = Math.max(0, Math.min(expectedTotal - completedCount, Math.max(localErrors, serverErrors ?? 0)));
367
+ // Known in-progress from client state
368
+ const knownInProgressCount = batchJobs.filter(job => job.status === 'In Progress').length;
369
+ // Cap to not exceed remaining after completed+error
370
+ const inProgressCount = Math.max(0, Math.min(knownInProgressCount, expectedTotal - completedCount - errorCount));
371
+ const pendingCount = Math.max(0, expectedTotal - completedCount - errorCount - inProgressCount);
372
+ const pct = (count) => Math.max(0, Math.min(100, Math.round((count / expectedTotal) * 100)));
373
+ // Actual progress: completed=100 each, in-progress use live progress if available, others 0
374
+ let totalActualProgress = completedCount * 100;
375
+ for (const job of batchJobs) {
376
+ if (job.status === 'In Progress') {
377
+ const progressKey = `${job.itemId}-${job.targetLanguage}`;
378
+ const jobProgress = translationProgress.get(progressKey);
379
+ totalActualProgress += jobProgress?.progress || 0;
380
+ }
381
+ }
382
+ const actualProgress = Math.max(0, Math.min(100, Math.round(totalActualProgress / expectedTotal)));
383
+ return {
384
+ completed: pct(completedCount),
385
+ inProgress: pct(inProgressCount),
386
+ error: pct(errorCount),
387
+ pending: pct(pendingCount),
388
+ completedCount,
389
+ inProgressCount,
390
+ errorCount,
391
+ pendingCount,
392
+ total: expectedTotal,
393
+ actualProgress,
394
+ };
395
+ }, [batchJobs, batchInfo, translationProgress]);
396
+ const overallProgress = progressSegments.actualProgress;
397
+ return (_jsxs("div", { className: "flex h-full flex-col min-h-0", children: [_jsxs("div", { className: "border-b p-4 flex-shrink-0", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-3", children: [onBack && (_jsxs(Button, { size: "sm", variant: "outline", onClick: onBack, children: [_jsx("i", { className: "pi pi-arrow-left" }), "Back"] })), _jsxs("div", { children: [_jsx("h1", { className: "text-xl font-semibold", children: "Translation Batch" }), _jsxs("p", { className: "text-sm text-gray-600 mt-1", children: ["Batch ID: ", _jsx("span", { className: "font-mono text-xs bg-gray-100 px-2 py-0.5 rounded", children: batchId })] }), batchInfo && (_jsxs("div", { className: "mt-2 flex flex-wrap gap-2 items-center text-xs", children: [batchInfo.status && (_jsx("span", { className: `inline-flex items-center rounded-full px-2 py-1 font-medium ${batchInfo.status === 'Completed'
398
+ ? 'bg-green-100 text-green-700'
399
+ : batchInfo.status === 'In Progress'
400
+ ? 'bg-blue-100 text-blue-700'
401
+ : batchInfo.status === 'Error'
402
+ ? 'bg-red-100 text-red-700'
403
+ : 'bg-gray-100 text-gray-700'}`, children: batchInfo.status })), batchInfo.provider && (_jsxs("span", { className: "text-gray-700", children: ["Provider: ", _jsx("span", { className: "font-medium", children: getProviderDisplayName(batchInfo.provider) })] })), batchInfo.initiatedByUser && (_jsxs("span", { className: "text-gray-700", children: ["By: ", _jsx("span", { className: "font-medium", children: batchInfo.initiatedByUser })] })), batchInfo.includeSubitems !== undefined && batchInfo.includeSubitems !== null && (_jsxs("span", { className: "text-gray-700", children: ["Include subitems: ", _jsx("span", { className: "font-medium", children: batchInfo.includeSubitems ? 'Yes' : 'No' })] })), batchInfo.scopeItemPath && (_jsxs("span", { className: "text-gray-700 truncate max-w-[40rem]", children: ["Scope: ", _jsx("span", { className: "font-mono", children: batchInfo.scopeItemPath })] })), (batchInfo.startedAtUtc || batchInfo.completedAtUtc || batchInfo.lastUpdatedUtc) && (_jsxs("span", { className: "text-gray-500", children: [batchInfo.startedAtUtc && (_jsxs(_Fragment, { children: ["Started: ", new Date(batchInfo.startedAtUtc).toLocaleString()] })), batchInfo.completedAtUtc && (_jsxs(_Fragment, { children: [batchInfo.startedAtUtc ? ' • ' : '', "Completed: ", new Date(batchInfo.completedAtUtc).toLocaleString()] })), !batchInfo.completedAtUtc && batchInfo.lastUpdatedUtc && (_jsxs(_Fragment, { children: [batchInfo.startedAtUtc ? ' • ' : '', "Updated: ", new Date(batchInfo.lastUpdatedUtc).toLocaleString()] }))] }))] }))] })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("div", { className: "text-sm text-gray-600", children: [`${progressSegments.completedCount}/${progressSegments.total} completed`, progressSegments.errorCount ? `, ${progressSegments.errorCount} errors` : ''] }), _jsxs(Button, { size: "sm", onClick: loadBatchJobs, disabled: isLoading, title: "Manual refresh - normally updates come through websocket subscriptions", children: [_jsx("i", { className: `pi pi-refresh ${isLoading ? 'pi-spin' : ''}` }), "Refresh"] })] })] }), _jsxs("div", { className: "mt-4", children: [_jsxs("div", { className: "flex items-center justify-between mb-2", children: [_jsx("span", { className: "text-sm font-medium", children: "Overall Progress" }), _jsxs("span", { className: "text-sm text-gray-600", children: [progressSegments.completedCount, " / ", progressSegments.total, " completed"] })] }), _jsxs("div", { className: "relative h-4 bg-gray-200 rounded-full overflow-hidden", children: [_jsxs("div", { className: "absolute inset-0 flex", children: [progressSegments.completed > 0 && (_jsx("div", { className: "bg-green-500 h-full transition-all duration-300", style: { width: `${progressSegments.completed}%` } })), progressSegments.inProgress > 0 && (_jsx("div", { className: "bg-blue-500 h-full transition-all duration-300", style: { width: `${progressSegments.inProgress}%` } })), progressSegments.error > 0 && (_jsx("div", { className: "bg-red-500 h-full transition-all duration-300", style: { width: `${progressSegments.error}%` } })), progressSegments.pending > 0 && (_jsx("div", { className: "bg-gray-300 h-full transition-all duration-300", style: { width: `${progressSegments.pending}%` } }))] }), _jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: _jsxs("span", { className: "text-xs font-medium text-white drop-shadow-sm", children: [overallProgress, "%"] }) })] }), _jsxs("div", { className: "flex gap-3 mt-2 text-xs", children: [progressSegments.completedCount > 0 && (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("div", { className: "w-3 h-3 bg-green-500 rounded-sm" }), _jsxs("span", { className: "text-gray-700", children: [progressSegments.completedCount, " completed"] })] })), progressSegments.inProgressCount > 0 && (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("div", { className: "w-3 h-3 bg-blue-500 rounded-sm" }), _jsxs("span", { className: "text-gray-700", children: [progressSegments.inProgressCount, " in progress"] })] })), progressSegments.errorCount > 0 && (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("div", { className: "w-3 h-3 bg-red-500 rounded-sm" }), _jsxs("span", { className: "text-gray-700", children: [progressSegments.errorCount, " errors"] })] })), progressSegments.pendingCount > 0 && (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("div", { className: "w-3 h-3 bg-gray-300 rounded-sm" }), _jsxs("span", { className: "text-gray-700", children: [progressSegments.pendingCount, " pending"] })] })), typeof batchInfo?.expectedJobs === 'number' && (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("div", { className: "w-3 h-3 bg-gray-300 rounded-sm" }), _jsxs("span", { className: "text-gray-700", children: [(batchInfo.expectedJobs || 0) - (batchInfo.completedJobs || 0) - (batchInfo.errorJobs || 0), " pending (server)"] })] }))] })] })] }), _jsx("div", { className: "flex-1 overflow-auto p-4 min-h-0", children: isLoading && batchJobs.length === 0 ? (_jsx("div", { className: "flex items-center justify-center h-32", children: _jsxs("div", { className: "flex items-center gap-2 text-gray-500", children: [_jsx("i", { className: "pi pi-spin pi-spinner" }), "Loading batch translations..."] }) })) : batchJobs.length === 0 ? (_jsx("div", { className: "flex items-center justify-center h-32", children: _jsxs("div", { className: "text-center text-gray-500", children: [_jsx("i", { className: "pi pi-language text-2xl block mb-2" }), _jsx("p", { children: "No translations found for this batch" }), _jsx("p", { className: "text-sm", children: "The batch may not exist or has no translation jobs" })] }) })) : (_jsx("div", { className: "space-y-6", children: sortedItemGroups.map(([itemId, jobs]) => {
404
+ const itemName = itemNameById[itemId] || itemId;
405
+ const itemCompleted = jobs.filter(j => j.status === "Completed").length;
406
+ // Calculate item progress including in-progress job progress
407
+ // Performance: item-level calculation is always fast (typically 2-10 jobs per item)
408
+ let totalProgress = 0;
409
+ for (const job of jobs) {
410
+ if (job.status === "Completed") {
411
+ totalProgress += 100; // Full credit for completed jobs
412
+ }
413
+ else if (job.status === "In Progress") {
414
+ // Get progress for this specific job
415
+ const progressKey = `${job.itemId}-${job.targetLanguage}`;
416
+ const jobProgress = translationProgress.get(progressKey);
417
+ totalProgress += jobProgress?.progress || 0; // Use actual progress or 0
418
+ }
419
+ else {
420
+ totalProgress += 0; // Pending/Error jobs contribute 0
421
+ }
422
+ }
423
+ const itemProgress = Math.round(totalProgress / jobs.length);
424
+ const itemInProgress = jobs.some(j => j.status === "In Progress");
425
+ const itemError = jobs.some(j => j.status === "Error");
426
+ return (_jsxs("div", { className: "border rounded-lg p-4", children: [_jsxs("div", { className: "mb-4", children: [_jsxs("div", { className: "flex items-center justify-between mb-2", children: [_jsx("h3", { className: "font-medium text-lg", children: itemName }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("span", { className: "text-sm text-gray-600", children: [itemCompleted, " / ", jobs.length, " languages"] }), itemInProgress && (_jsx("span", { className: "text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded-full", children: "In Progress" })), itemError && (_jsx("span", { className: "text-xs bg-red-100 text-red-700 px-2 py-1 rounded-full", children: "Error" }))] })] }), _jsx("div", { className: "relative", children: _jsx(Progress, { value: itemProgress, className: "h-3 mb-2", indicatorClassName: itemError ? "bg-red-500" :
427
+ itemProgress === 100 ? "bg-green-500" :
428
+ "bg-blue-500", showValue: true }) }), _jsxs("div", { className: "text-xs text-gray-500 font-mono", children: ["Item ID: ", itemId] })] }), _jsx(SimpleTable, { columns: [
429
+ {
430
+ header: "Language",
431
+ className: "w-16", // Fixed width for language column
432
+ body: (job) => (_jsx("div", { className: "font-medium", children: job.targetLanguage }))
433
+ },
434
+ {
435
+ header: "Status",
436
+ className: "w-32", // Fixed width for status column
437
+ body: (job) => {
438
+ const progressKey = `${job.itemId}-${job.targetLanguage}`;
439
+ const progress = translationProgress.get(progressKey);
440
+ return (_jsxs("div", { children: [_jsx("span", { className: `inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${job.status === "Completed"
441
+ ? "bg-green-100 text-green-700"
442
+ : job.status === "In Progress"
443
+ ? "bg-blue-100 text-blue-700"
444
+ : job.status === "Error"
445
+ ? "bg-red-100 text-red-700"
446
+ : "bg-gray-100 text-gray-700"}`, children: job.status }), job.status === "In Progress" && progress && Math.round(progress.progress || 0) === 0 && (_jsx("div", { className: "mt-1 text-xs text-gray-600", children: "Version created" })), progress && job.status === "In Progress" && (_jsxs("div", { className: "mt-1", children: [_jsxs("div", { className: "flex items-center justify-between mb-1", children: [_jsx("span", { className: "text-xs text-gray-500 truncate", children: progress.message }), _jsxs("span", { className: "text-xs font-medium text-gray-700 ml-1", children: [Math.round(progress.progress), "%"] })] }), _jsx(Progress, { value: progress.progress, className: "h-2", indicatorClassName: "bg-blue-500", showValue: false })] }))] }));
447
+ }
448
+ },
449
+ {
450
+ header: "Source",
451
+ className: "w-16", // Fixed width for source column
452
+ body: (job) => (_jsx("div", { className: "text-sm", children: job.sourceLanguage || "—" }))
453
+ },
454
+ {
455
+ header: "Message",
456
+ className: "w-64", // Fixed width with proper wrapping for message column
457
+ body: (job) => (_jsx("div", { className: "text-sm break-words whitespace-normal leading-relaxed", children: job.message || "—" }))
458
+ },
459
+ {
460
+ header: "Updated",
461
+ className: "w-24", // Fixed width for updated column
462
+ body: (job) => {
463
+ const date = new Date(job.timestamp);
464
+ return (_jsx("div", { className: "text-sm whitespace-nowrap", children: date.toLocaleTimeString() }));
465
+ }
466
+ },
467
+ ], items: jobs.sort((a, b) => {
468
+ // Sort by status first (in-progress first, then errors, then completed)
469
+ const getJobPriority = (job) => {
470
+ if (job.status === "In Progress")
471
+ return 1;
472
+ if (job.status === "Error")
473
+ return 2;
474
+ if (job.status === "Completed")
475
+ return 4;
476
+ return 3; // Pending/other statuses
477
+ };
478
+ const priorityA = getJobPriority(a);
479
+ const priorityB = getJobPriority(b);
480
+ // If same priority, sort by language alphabetically
481
+ if (priorityA === priorityB) {
482
+ return a.targetLanguage.localeCompare(b.targetLanguage);
483
+ }
484
+ return priorityA - priorityB;
485
+ }) })] }, itemId));
486
+ }) })) })] }));
487
+ }
@@ -0,0 +1,2 @@
1
+ export declare function RecentTranslations(): import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=RecentTranslations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RecentTranslations.d.ts","sourceRoot":"","sources":["../../../../src/translation-center/RecentTranslations.tsx"],"names":[],"mappings":"AA8BA,wBAAgB,kBAAkB,4CAgYjC"}