gt-sanity 0.0.6 → 1.0.1

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 (88) hide show
  1. package/LICENSE.md +1 -1
  2. package/dist/index.d.mts +95 -73
  3. package/dist/index.d.ts +95 -73
  4. package/dist/index.js +6233 -1162
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +6292 -1193
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +9 -4
  9. package/src/adapter/core.ts +41 -4
  10. package/src/adapter/getLocales.ts +2 -2
  11. package/src/components/TranslationsProvider.tsx +942 -0
  12. package/src/components/page/BatchProgress.tsx +27 -0
  13. package/src/components/page/ImportAllDialog.tsx +51 -0
  14. package/src/components/page/ImportMissingDialog.tsx +55 -0
  15. package/src/components/page/TranslateAllDialog.tsx +55 -0
  16. package/src/components/page/TranslationsTable.tsx +81 -0
  17. package/src/components/page/TranslationsTool.tsx +299 -837
  18. package/src/components/shared/BaseTranslationWrapper.tsx +82 -0
  19. package/src/components/shared/LocaleCheckbox.tsx +47 -0
  20. package/src/components/shared/SingleDocumentView.tsx +108 -0
  21. package/src/components/tab/TranslationView.tsx +379 -0
  22. package/src/components/tab/TranslationsTab.tsx +25 -0
  23. package/src/configuration/baseDocumentLevelConfig/documentLevelPatch.ts +6 -9
  24. package/src/configuration/baseDocumentLevelConfig/helpers/createI18nDocAndPatchMetadata.ts +5 -24
  25. package/src/configuration/baseDocumentLevelConfig/helpers/patchI18nDoc.ts +3 -23
  26. package/src/configuration/baseDocumentLevelConfig/index.ts +16 -68
  27. package/src/configuration/baseFieldLevelConfig.ts +15 -50
  28. package/src/index.ts +29 -43
  29. package/src/sanity-api/findDocuments.ts +44 -0
  30. package/src/sanity-api/publishDocuments.ts +49 -0
  31. package/src/sanity-api/resolveRefs.ts +146 -0
  32. package/src/serialization/BaseDocumentMerger.ts +138 -0
  33. package/src/serialization/BaseSerializationConfig.ts +220 -0
  34. package/src/serialization/__tests__/BaseDocumentDeserializer/__snapshots__/documentLevelDeserialization.test.ts.snap +189 -0
  35. package/src/serialization/__tests__/BaseDocumentDeserializer/__snapshots__/fieldLevelDeserialization.test.ts.snap +107 -0
  36. package/src/serialization/__tests__/BaseDocumentDeserializer/baseDeserialization.test.ts +397 -0
  37. package/src/serialization/__tests__/BaseDocumentDeserializer/documentLevelDeserialization.test.ts +107 -0
  38. package/src/serialization/__tests__/BaseDocumentDeserializer/fieldLevelDeserialization.test.ts +107 -0
  39. package/src/serialization/__tests__/BaseDocumentMerger/__snapshots__/documentLevelMerge.test.ts.snap +193 -0
  40. package/src/serialization/__tests__/BaseDocumentMerger/__snapshots__/fieldLevelMerge.test.ts.snap +97 -0
  41. package/src/serialization/__tests__/BaseDocumentMerger/baseMerge.test.ts +36 -0
  42. package/src/serialization/__tests__/BaseDocumentMerger/documentLevelMerge.test.ts +96 -0
  43. package/src/serialization/__tests__/BaseDocumentMerger/fieldLevelMerge.test.ts +142 -0
  44. package/src/serialization/__tests__/BaseDocumentMerger/utils.ts +52 -0
  45. package/src/serialization/__tests__/BaseDocumentSerializer/__snapshots__/documentInlineMarks.test.ts.snap +39 -0
  46. package/src/serialization/__tests__/BaseDocumentSerializer/__snapshots__/documentLevelSerialization.test.ts.snap +8 -0
  47. package/src/serialization/__tests__/BaseDocumentSerializer/__snapshots__/fieldLevelSerialization.test.ts.snap +8 -0
  48. package/src/serialization/__tests__/BaseDocumentSerializer/baseSerialization.test.ts +345 -0
  49. package/src/serialization/__tests__/BaseDocumentSerializer/documentInlineMarks.test.ts +53 -0
  50. package/src/serialization/__tests__/BaseDocumentSerializer/documentLevelSerialization.test.ts +120 -0
  51. package/src/serialization/__tests__/BaseDocumentSerializer/fieldLevelSerialization.test.ts +153 -0
  52. package/src/serialization/__tests__/BaseDocumentSerializer/utils.ts +27 -0
  53. package/src/serialization/__tests__/README +2 -0
  54. package/src/serialization/__tests__/__fixtures__/annotationAndInlineBlocks.json +140 -0
  55. package/src/serialization/__tests__/__fixtures__/customStyles.json +62 -0
  56. package/src/serialization/__tests__/__fixtures__/documentInlineMarks.json +70 -0
  57. package/src/serialization/__tests__/__fixtures__/documentLevelArticle.json +185 -0
  58. package/src/serialization/__tests__/__fixtures__/fieldLevelArticle.json +107 -0
  59. package/src/serialization/__tests__/__fixtures__/inlineDocumentLevelArticle.json +134 -0
  60. package/src/serialization/__tests__/__fixtures__/inlineSchema.ts +270 -0
  61. package/src/serialization/__tests__/__fixtures__/messy-html.html +26 -0
  62. package/src/serialization/__tests__/__fixtures__/nestedLanguageFields.json +54 -0
  63. package/src/serialization/__tests__/__fixtures__/schema.ts +310 -0
  64. package/src/serialization/__tests__/global.setup.ts +40 -0
  65. package/src/serialization/__tests__/helpers.ts +132 -0
  66. package/src/serialization/data.ts +82 -0
  67. package/src/serialization/deserialize/BaseDocumentDeserializer.ts +171 -0
  68. package/src/serialization/deserialize/helpers.ts +42 -0
  69. package/src/serialization/helpers.ts +18 -0
  70. package/src/serialization/index.ts +11 -0
  71. package/src/serialization/serialize/fieldFilters.ts +124 -0
  72. package/src/serialization/serialize/index.ts +284 -0
  73. package/src/serialization/types.ts +41 -0
  74. package/src/translation/importDocument.ts +4 -5
  75. package/src/translation/uploadFiles.ts +1 -1
  76. package/src/types.ts +3 -19
  77. package/src/utils/batchProcessor.ts +111 -0
  78. package/src/utils/importUtils.ts +95 -0
  79. package/src/utils/serialize.ts +25 -5
  80. package/src/utils/shared.ts +1 -1
  81. package/src/adapter/index.ts +0 -13
  82. package/src/components/NewTask.tsx +0 -251
  83. package/src/components/TaskView.tsx +0 -257
  84. package/src/components/TranslationContext.tsx +0 -24
  85. package/src/components/TranslationView.tsx +0 -114
  86. package/src/components/TranslationsTab.tsx +0 -181
  87. /package/src/components/{LanguageStatus.tsx → shared/LanguageStatus.tsx} +0 -0
  88. /package/src/components/{ProgressBar.tsx → shared/ProgressBar.tsx} +0 -0
@@ -1,875 +1,337 @@
1
- import React, { useState, useCallback, useEffect, useMemo } from 'react';
1
+ import React, { useState } from 'react';
2
2
  import {
3
3
  Box,
4
4
  Button,
5
- Card,
6
5
  Container,
7
- Dialog,
8
6
  Flex,
9
7
  Heading,
10
- Spinner,
11
8
  Stack,
12
9
  Switch,
13
10
  Text,
14
- ThemeProvider,
15
- ToastProvider,
16
- useToast,
11
+ Spinner,
12
+ Tooltip,
17
13
  } from '@sanity/ui';
18
- import { DownloadIcon, CheckmarkCircleIcon } from '@sanity/icons';
19
- import { buildTheme } from '@sanity/ui/theme';
20
- import { Link } from 'sanity/router';
21
- import { SanityDocument, useSchema } from 'sanity';
22
- import { useClient } from '../../hooks/useClient';
23
- import { useSecrets } from '../../hooks/useSecrets';
24
- import { GTAdapter } from '../../adapter';
25
- import {
26
- GTFile,
27
- Secrets,
28
- TranslationLocale,
29
- TranslationFunctionContext,
30
- } from '../../types';
31
- import { gtConfig } from '../../adapter/core';
32
- import { LanguageStatus } from '../LanguageStatus';
33
- import { serializeDocument } from '../../utils/serialize';
34
- import { uploadFiles } from '../../translation/uploadFiles';
35
- import { initProject } from '../../translation/initProject';
36
- import { createJobs } from '../../translation/createJobs';
37
14
  import {
38
- downloadTranslations,
39
- BatchedFiles,
40
- } from '../../translation/downloadTranslations';
41
- import { checkTranslationStatus } from '../../translation/checkTranslationStatus';
42
- import { importDocument } from '../../translation/importDocument';
43
-
44
- const theme = buildTheme();
45
-
46
- const TranslationsTool = () => {
15
+ DownloadIcon,
16
+ CheckmarkCircleIcon,
17
+ LinkIcon,
18
+ PublishIcon,
19
+ } from '@sanity/icons';
20
+ import { Link } from 'sanity/router';
21
+ import { BaseTranslationWrapper } from '../shared/BaseTranslationWrapper';
22
+ import { TranslationsProvider, useTranslations } from '../TranslationsProvider';
23
+ import { TranslationsTable } from './TranslationsTable';
24
+ import { TranslateAllDialog } from './TranslateAllDialog';
25
+ import { ImportAllDialog } from './ImportAllDialog';
26
+ import { ImportMissingDialog } from './ImportMissingDialog';
27
+ import { BatchProgress } from './BatchProgress';
28
+
29
+ const TranslationsToolContent: React.FC = () => {
47
30
  const [isTranslateAllDialogOpen, setIsTranslateAllDialogOpen] =
48
31
  useState(false);
49
32
  const [isImportAllDialogOpen, setIsImportAllDialogOpen] = useState(false);
50
- const [isBusy, setIsBusy] = useState(false);
51
- const [documents, setDocuments] = useState<SanityDocument[]>([]);
52
- const [locales, setLocales] = useState<TranslationLocale[]>([]);
53
- const [autoPublish, setAutoPublish] = useState(true);
54
- const [autoRefresh, setAutoRefresh] = useState(false);
55
- const [loadingDocuments, setLoadingDocuments] = useState(false);
56
- const [importProgress, setImportProgress] = useState({
57
- current: 0,
58
- total: 0,
59
- isImporting: false,
60
- });
61
- const [importedTranslations, setImportedTranslations] = useState<Set<string>>(
62
- new Set()
63
- );
64
- const [downloadStatus, setDownloadStatus] = useState({
65
- downloaded: new Set<string>(),
66
- failed: new Set<string>(),
67
- skipped: new Set<string>(),
68
- });
69
- const [translationStatuses, setTranslationStatuses] = useState<
70
- Map<string, { progress: number; isReady: boolean; translationId?: string }>
71
- >(new Map());
72
- const [isRefreshing, setIsRefreshing] = useState(false);
73
-
74
- const client = useClient();
75
- const schema = useSchema();
76
- const translationContext: TranslationFunctionContext = { client, schema };
77
- const toast = useToast();
78
- const { loading: loadingSecrets, secrets } = useSecrets<Secrets>(
79
- `${gtConfig.getSecretsNamespace()}.secrets`
80
- );
81
-
82
- const fetchDocuments = useCallback(async () => {
83
- setLoadingDocuments(true);
84
- try {
85
- const translateDocuments = gtConfig.getTranslateDocuments();
86
-
87
- // Build filter conditions based on translateDocuments configuration
88
- const filterConditions = translateDocuments
89
- .map((filter) => {
90
- if (filter.type && filter.documentId) {
91
- // Both type and documentId must match
92
- return `(_type == "${filter.type}" && _id == "${filter.documentId}")`;
93
- } else if (filter.type) {
94
- // Only type must match
95
- return `_type == "${filter.type}"`;
96
- } else if (filter.documentId) {
97
- // Only documentId must match
98
- return `_id == "${filter.documentId}"`;
99
- }
100
- return null;
101
- })
102
- .filter(Boolean);
103
-
104
- // If no filters are configured, fall back to the original query
105
- const languageField = gtConfig.getLanguageField();
106
- const sourceLocale = gtConfig.getSourceLocale();
107
- const languageFilter = `(!defined(${languageField}) || ${languageField} == "${sourceLocale}")`;
108
-
109
- let query;
110
- if (filterConditions.length === 0) {
111
- query = `*[!(_type in ["system.group"]) && !(_id in path("_.**")) && ${languageFilter}]`;
112
- } else {
113
- const filterQuery = filterConditions.join(' || ');
114
- query = `*[!(_type in ["system.group"]) && !(_id in path("_.**")) && (${filterQuery}) && ${languageFilter}]`;
115
- }
116
-
117
- const docs = await client.fetch(query);
118
- setDocuments(docs);
119
- } catch {
120
- toast.push({
121
- title: 'Error fetching documents',
122
- status: 'error',
123
- closable: true,
124
- });
125
- } finally {
126
- setLoadingDocuments(false);
127
- }
128
- }, [client, toast]);
129
-
130
- const fetchLocales = useCallback(async () => {
131
- if (!secrets) return;
132
- try {
133
- const availableLocales = await GTAdapter.getLocales(secrets);
134
- setLocales(availableLocales);
135
- } catch {
136
- toast.push({
137
- title: 'Error fetching locales',
138
- status: 'error',
139
- closable: true,
140
- });
141
- }
142
- }, [secrets, toast]);
143
-
144
- useEffect(() => {
145
- fetchDocuments();
146
- }, [fetchDocuments]);
147
-
148
- useEffect(() => {
149
- if (secrets) {
150
- fetchLocales();
151
- }
152
- }, [fetchLocales, secrets]);
153
-
154
- const handleTranslateAll = useCallback(async () => {
155
- if (!secrets || documents.length === 0) return;
156
-
157
- setIsBusy(true);
158
- setIsTranslateAllDialogOpen(false);
159
-
160
- try {
161
- const availableLocaleIds = locales
162
- .filter((locale) => locale.enabled !== false)
163
- .map((locale) => locale.localeId);
164
-
165
- console.log('documents', documents);
166
- // Transform documents to the required format
167
- const transformedDocuments = documents
168
- .map((doc) => {
169
- delete doc[gtConfig.getLanguageField()];
170
- const baseLanguage = gtConfig.getSourceLocale();
171
- try {
172
- const serialized = serializeDocument(doc, schema, baseLanguage);
173
- return {
174
- info: {
175
- documentId: doc._id?.replace('drafts.', '') || doc._id,
176
- versionId: doc._rev,
177
- },
178
- serializedDocument: serialized,
179
- };
180
- } catch (error) {
181
- console.error('Error transforming document', doc._id, error);
182
- }
183
- return null;
184
- })
185
- .filter((doc) => doc !== null);
186
-
187
- console.log('transformedDocuments', transformedDocuments);
188
- console.log('transformedDocuments.length', transformedDocuments.length);
189
- const uploadResult = await uploadFiles(transformedDocuments, secrets);
190
- await initProject(uploadResult, { timeout: 600 }, secrets);
191
- await createJobs(uploadResult, availableLocaleIds, secrets);
192
-
193
- toast.push({
194
- title: `Translation tasks created for ${documents.length} documents`,
195
- status: 'success',
196
- closable: true,
197
- });
198
- } catch {
199
- toast.push({
200
- title: 'Error creating translation tasks',
201
- status: 'error',
202
- closable: true,
203
- });
204
- } finally {
205
- setIsBusy(false);
206
- }
207
- }, [secrets, documents, locales, toast]);
208
-
209
- const handleImportAll = useCallback(async () => {
210
- if (!secrets || documents.length === 0) return;
211
-
212
- setIsBusy(true);
213
- setIsImportAllDialogOpen(false);
214
-
215
- try {
216
- // Collect all ready translations
217
- const readyFiles: BatchedFiles = [];
218
-
219
- for (const [key, status] of translationStatuses.entries()) {
220
- if (status.isReady && status.translationId) {
221
- const [documentId, locale] = key.split(':');
222
- const document = documents.find(
223
- (doc) => (doc._id?.replace('drafts.', '') || doc._id) === documentId
224
- );
225
-
226
- if (document) {
227
- readyFiles.push({
228
- documentId,
229
- versionId: document._rev,
230
- translationId: status.translationId,
231
- locale,
232
- });
233
- }
234
- }
235
- }
236
-
237
- if (readyFiles.length === 0) {
238
- toast.push({
239
- title: 'No ready translations to import',
240
- status: 'warning',
241
- closable: true,
242
- });
243
- return;
244
- }
245
-
246
- // Download all ready translations
247
- const downloadedFiles = await downloadTranslations(readyFiles, secrets);
248
-
249
- // Set up progress tracking
250
- setImportProgress({
251
- current: 0,
252
- total: downloadedFiles.length,
253
- isImporting: true,
254
- });
255
-
256
- // Batch import in groups of 10
257
- const batchSize = 10;
258
- let successCount = 0;
259
- let failureCount = 0;
260
- const successfulImports: string[] = [];
261
-
262
- for (let i = 0; i < downloadedFiles.length; i += batchSize) {
263
- const batch = downloadedFiles.slice(i, i + batchSize);
264
-
265
- // Process batch in parallel
266
- const batchPromises = batch.map(async (file) => {
267
- try {
268
- const docInfo: GTFile = {
269
- documentId: file.docData.documentId,
270
- versionId: file.docData.versionId,
271
- };
272
-
273
- await importDocument(
274
- docInfo,
275
- file.docData.locale,
276
- file.data,
277
- translationContext,
278
- autoPublish
279
- );
280
-
281
- const key = `${file.docData.documentId}:${file.docData.locale}`;
282
- successfulImports.push(key);
283
- setImportedTranslations((prev) => new Set([...prev, key]));
284
- return { success: true, file };
285
- } catch (error) {
286
- console.error(
287
- `Failed to import ${file.docData.documentId} (${file.docData.locale}):`,
288
- error
289
- );
290
- return { success: false, file, error };
291
- }
292
- });
293
-
294
- const batchResults = await Promise.all(batchPromises);
295
-
296
- batchResults.forEach((result) => {
297
- if (result.success) {
298
- successCount++;
299
- } else {
300
- failureCount++;
301
- }
302
- });
303
-
304
- // Update progress
305
- setImportProgress({
306
- current: i + batch.length,
307
- total: downloadedFiles.length,
308
- isImporting: true,
309
- });
310
- }
311
-
312
- // Update download status for successful imports
313
- if (successfulImports.length > 0) {
314
- const newDownloadStatus = {
315
- ...downloadStatus,
316
- downloaded: new Set([
317
- ...downloadStatus.downloaded,
318
- ...successfulImports,
319
- ]),
320
- };
321
- setDownloadStatus(newDownloadStatus);
322
- }
33
+ const [isImportMissingDialogOpen, setIsImportMissingDialogOpen] =
34
+ useState(false);
323
35
 
324
- toast.push({
325
- title: `Imported ${successCount} translations${failureCount > 0 ? `, ${failureCount} failed` : ''}`,
326
- status: successCount > 0 ? 'success' : 'error',
327
- closable: true,
328
- });
329
- } catch (error) {
330
- console.error('Error importing translations:', error);
331
- toast.push({
332
- title: 'Error importing translations',
333
- status: 'error',
334
- closable: true,
335
- });
336
- } finally {
337
- setIsBusy(false);
338
- setImportProgress({ current: 0, total: 0, isImporting: false });
339
- }
340
- }, [
341
- secrets,
36
+ const {
37
+ isBusy,
342
38
  documents,
343
- translationStatuses,
344
- downloadStatus,
345
- toast,
346
- autoPublish,
347
- translationContext,
348
- ]);
349
-
350
- const handleRefreshAll = useCallback(async () => {
351
- if (!secrets || documents.length === 0) return;
352
-
353
- setIsRefreshing(true);
354
-
355
- try {
356
- const availableLocaleIds = locales
357
- .filter((locale) => locale.enabled !== false)
358
- .map((locale) => locale.localeId);
359
-
360
- // Create file query data for all document/locale combinations
361
- const fileQueryData = [];
362
- for (const doc of documents) {
363
- for (const localeId of availableLocaleIds) {
364
- const documentId = doc._id?.replace('drafts.', '') || doc._id;
365
- fileQueryData.push({
366
- versionId: doc._rev,
367
- fileId: documentId,
368
- locale: localeId,
369
- });
370
- }
371
- }
372
- // Check translation status for all files
373
- const readyTranslations = await checkTranslationStatus(
374
- fileQueryData,
375
- downloadStatus,
376
- secrets
377
- );
378
-
379
- // Update translation statuses
380
- const newStatuses = new Map(translationStatuses);
381
-
382
- // Reset all to not ready first
383
- for (const doc of documents) {
384
- for (const localeId of availableLocaleIds) {
385
- const documentId = doc._id?.replace('drafts.', '') || doc._id;
386
- const key = `${documentId}:${localeId}`;
387
- newStatuses.set(key, { progress: 0, isReady: false });
388
- }
389
- }
390
-
391
- // Update with ready translations
392
- if (Array.isArray(readyTranslations)) {
393
- for (const translation of readyTranslations) {
394
- const key = `${translation.fileId}:${translation.locale}`;
395
- newStatuses.set(key, {
396
- progress: 100,
397
- isReady: true,
398
- translationId: translation.id,
399
- });
400
- }
401
- }
402
-
403
- setTranslationStatuses(newStatuses);
404
-
405
- toast.push({
406
- title: `Refreshed status for ${documents.length} documents`,
407
- status: 'success',
408
- closable: true,
409
- });
410
- } catch (error) {
411
- console.error('Error refreshing translation status:', error);
412
- toast.push({
413
- title: 'Error refreshing translation status',
414
- status: 'error',
415
- closable: true,
416
- });
417
- } finally {
418
- setIsRefreshing(false);
39
+ locales,
40
+ autoRefresh,
41
+ loadingDocuments,
42
+ importProgress,
43
+ importedTranslations,
44
+ isRefreshing,
45
+ setAutoRefresh,
46
+ handleRefreshAll,
47
+ handlePatchDocumentReferences,
48
+ handlePublishAllTranslations,
49
+ } = useTranslations();
50
+
51
+ // Track which specific operation is running
52
+ const [currentOperation, setCurrentOperation] = useState<string | null>(null);
53
+
54
+ const getOperationText = (
55
+ operationName: string | null,
56
+ isProcessing: boolean
57
+ ) => {
58
+ if (!isProcessing || !operationName) return operationName;
59
+
60
+ switch (operationName) {
61
+ case 'Translate All':
62
+ return 'Translating...';
63
+ case 'Import All':
64
+ return 'Importing...';
65
+ case 'Import Missing':
66
+ return 'Importing...';
67
+ case 'Patch Document References':
68
+ return 'Patching...';
69
+ case 'Publish Translations':
70
+ return 'Publishing...';
71
+ default:
72
+ return 'Processing...';
419
73
  }
420
- }, [secrets, documents, locales, downloadStatus, translationStatuses, toast]);
421
-
422
- // Auto-refresh on page load after documents and locales are loaded
423
- useEffect(() => {
424
- if (
425
- documents.length > 0 &&
426
- locales.length > 0 &&
427
- secrets &&
428
- !loadingDocuments
429
- ) {
430
- handleRefreshAll();
74
+ };
75
+
76
+ const getProgressOperationName = () => {
77
+ switch (currentOperation) {
78
+ case 'Import All':
79
+ return 'Importing';
80
+ case 'Import Missing':
81
+ return 'Importing missing';
82
+ case 'Patch Document References':
83
+ return 'Patching';
84
+ case 'Publish Translations':
85
+ return 'Publishing';
86
+ default:
87
+ return 'Processing';
431
88
  }
432
- }, [documents]);
433
-
434
- // Auto-refresh functionality
435
- useEffect(() => {
436
- if (!autoRefresh || documents.length === 0 || !secrets) return;
437
-
438
- const interval = setInterval(async () => {
439
- await handleRefreshAll();
440
- }, 10000); // Refresh every 10 seconds
441
-
442
- return () => clearInterval(interval);
443
- }, [autoRefresh, documents.length, secrets, handleRefreshAll]);
444
-
445
- // Initialize imported translations from download status
446
- useEffect(() => {
447
- setImportedTranslations(new Set(downloadStatus.downloaded));
448
- }, [downloadStatus.downloaded]);
449
-
450
- const handleImportDocument = useCallback(
451
- async (documentId: string, localeId: string) => {
452
- if (!secrets) return;
453
-
454
- try {
455
- const key = `${documentId}:${localeId}`;
456
- const status = translationStatuses.get(key);
457
-
458
- if (!status?.isReady || !status.translationId) {
459
- toast.push({
460
- title: `Translation not ready for ${documentId} (${localeId})`,
461
- status: 'warning',
462
- closable: true,
463
- });
464
- return;
465
- }
466
-
467
- const document = documents.find(
468
- (doc) => (doc._id?.replace('drafts.', '') || doc._id) === documentId
469
- );
470
-
471
- if (!document) {
472
- toast.push({
473
- title: `Document ${documentId} not found`,
474
- status: 'error',
475
- closable: true,
476
- });
477
- return;
478
- }
479
-
480
- // Download single translation
481
- const downloadedFiles = await downloadTranslations(
482
- [
483
- {
484
- documentId,
485
- versionId: document._rev,
486
- translationId: status.translationId,
487
- locale: localeId,
488
- },
489
- ],
490
- secrets
491
- );
492
-
493
- if (downloadedFiles.length > 0) {
494
- try {
495
- const docInfo: GTFile = {
496
- documentId,
497
- versionId: document._rev,
498
- };
499
-
500
- await importDocument(
501
- docInfo,
502
- localeId,
503
- downloadedFiles[0].data,
504
- translationContext,
505
- autoPublish
506
- );
89
+ };
507
90
 
508
- // Update download status and imported translations
509
- const newDownloadStatus = {
510
- ...downloadStatus,
511
- downloaded: new Set([...downloadStatus.downloaded, key]),
512
- };
513
- setDownloadStatus(newDownloadStatus);
514
- setImportedTranslations((prev) => new Set([...prev, key]));
515
-
516
- toast.push({
517
- title: `Successfully imported translation for ${documentId} (${localeId})`,
518
- status: 'success',
519
- closable: true,
520
- });
521
- } catch (importError) {
522
- console.error('Failed to import translation:', importError);
523
- toast.push({
524
- title: `Failed to import translation for ${documentId} (${localeId})`,
525
- status: 'error',
526
- closable: true,
527
- });
528
- }
529
- } else {
530
- toast.push({
531
- title: `No translation content received for ${documentId}`,
532
- status: 'warning',
533
- closable: true,
534
- });
535
- }
536
- } catch (error) {
537
- console.error('Error importing translation:', error);
538
- toast.push({
539
- title: `Error importing translation for ${documentId}`,
540
- status: 'error',
541
- closable: true,
542
- });
543
- }
544
- },
545
- [
546
- secrets,
547
- translationStatuses,
548
- documents,
549
- downloadStatus,
550
- toast,
551
- autoPublish,
552
- translationContext,
553
- ]
554
- );
555
-
556
- if (loadingSecrets) {
557
- return (
558
- <ThemeProvider theme={theme}>
559
- <Container width={2}>
560
- <Flex padding={5} align='center' justify='center'>
561
- <Spinner />
562
- </Flex>
563
- </Container>
564
- </ThemeProvider>
565
- );
566
- }
567
-
568
- if (!secrets) {
569
- return (
570
- <ThemeProvider theme={theme}>
571
- <Container width={2}>
572
- <Box padding={4} marginTop={5}>
573
- <Card tone='caution' padding={[2, 3, 4, 4]} shadow={1} radius={2}>
574
- <Text>
575
- Can't find secrets for your translation service. Did you load
576
- them into this dataset?
577
- </Text>
578
- </Card>
579
- </Box>
580
- </Container>
581
- </ThemeProvider>
582
- );
583
- }
91
+ // Reset current operation when operation completes
92
+ React.useEffect(() => {
93
+ if (!isBusy && !importProgress.isImporting) {
94
+ setCurrentOperation(null);
95
+ }
96
+ }, [isBusy, importProgress.isImporting]);
584
97
 
585
98
  return (
586
- <ThemeProvider theme={theme}>
587
- <ToastProvider paddingY={7}>
588
- <Container width={2}>
589
- <Box padding={4} marginTop={5}>
590
- <Stack space={4}>
591
- <Flex align='center' justify='space-between'>
592
- <Stack space={2}>
593
- <Heading as='h2' size={3}>
594
- Translations
595
- </Heading>
596
- <Text size={2}>
597
- Manage your document translations from this centralized
598
- location.
599
- </Text>
600
- </Stack>
601
-
602
- <Flex gap={3} align='center'>
603
- <Flex gap={2} align='center'>
604
- <Text size={1}>Auto-refresh</Text>
605
- <Switch
606
- checked={autoRefresh}
607
- onChange={() => setAutoRefresh(!autoRefresh)}
608
- />
609
- </Flex>
610
- <Button
611
- fontSize={1}
612
- padding={2}
613
- text={isRefreshing ? 'Refreshing...' : 'Refresh Status'}
614
- onClick={handleRefreshAll}
615
- disabled={
616
- isRefreshing ||
617
- isBusy ||
618
- loadingDocuments ||
619
- documents.length === 0
620
- }
621
- />
622
- </Flex>
623
- </Flex>
624
-
625
- <Stack space={4}>
626
- <Box>
627
- <Text size={1} muted>
628
- {loadingDocuments
629
- ? 'Loading documents...'
630
- : `Found ${documents.length} documents available for translation`}
631
- </Text>
632
- </Box>
633
-
634
- <Flex justify='center'>
635
- <Button
636
- style={{ width: '200px' }}
637
- tone='critical'
638
- text={isBusy ? 'Processing...' : 'Translate All'}
639
- onClick={() => setIsTranslateAllDialogOpen(true)}
640
- disabled={
641
- isBusy || loadingDocuments || documents.length === 0
642
- }
643
- />
644
- </Flex>
645
-
646
- {loadingDocuments ? (
647
- <Flex align='center' justify='center' padding={4}>
648
- <Spinner />
649
- </Flex>
650
- ) : (
651
- <Box style={{ maxHeight: '60vh', overflowY: 'auto' }}>
652
- <Stack space={2}>
653
- {documents.map((document) => (
654
- <Card key={document._id} shadow={1} padding={3}>
655
- <Stack space={3}>
656
- <Flex justify='space-between' align='flex-start'>
657
- <Box flex={1}>
658
- <Text weight='semibold' size={1}>
659
- {document._id?.replace('drafts.', '') ||
660
- document._id}
661
- </Text>
662
- <Text
663
- size={0}
664
- muted
665
- style={{ marginTop: '2px' }}
666
- >
667
- {document._type}
668
- </Text>
669
- </Box>
670
- </Flex>
671
-
672
- <Stack space={2}>
673
- {locales.length > 0 ? (
674
- locales
675
- .filter((locale) => locale.enabled !== false)
676
- .map((locale) => {
677
- const documentId =
678
- document._id?.replace('drafts.', '') ||
679
- document._id;
680
- const key = `${documentId}:${locale.localeId}`;
681
- const status = translationStatuses.get(key);
682
- const isDownloaded =
683
- downloadStatus.downloaded.has(key);
684
- const isImported =
685
- importedTranslations.has(key);
686
-
687
- return (
688
- <LanguageStatus
689
- key={`${document._id}-${locale.localeId}`}
690
- title={
691
- locale.description || locale.localeId
692
- }
693
- progress={status?.progress || 0}
694
- isImported={isImported || isDownloaded}
695
- importFile={async () => {
696
- await handleImportDocument(
697
- documentId,
698
- locale.localeId
699
- );
700
- }}
701
- />
702
- );
703
- })
704
- ) : (
705
- <Text size={1} muted>
706
- No locales configured
707
- </Text>
708
- )}
709
- </Stack>
710
- </Stack>
711
- </Card>
712
- ))}
713
- </Stack>
714
- </Box>
715
- )}
716
-
717
- <Stack space={3}>
718
- <Flex gap={3} align='center' justify='space-between'>
719
- <Flex gap={2} align='center'>
720
- <Button
721
- mode='ghost'
722
- onClick={() => setIsImportAllDialogOpen(true)}
723
- text={isBusy ? 'Importing...' : 'Import All'}
724
- icon={isBusy ? null : DownloadIcon}
725
- disabled={
726
- isBusy || loadingDocuments || documents.length === 0
727
- }
728
- />
729
- {importedTranslations.size ===
730
- documents.length *
731
- locales.filter((l) => l.enabled !== false).length &&
732
- documents.length > 0 &&
733
- locales.length > 0 && (
734
- <Flex
735
- gap={2}
736
- align='center'
737
- style={{ color: 'green' }}
738
- >
739
- <CheckmarkCircleIcon />
740
- <Text size={1}>All translations imported</Text>
741
- </Flex>
742
- )}
743
- {importedTranslations.size > 0 &&
744
- importedTranslations.size <
745
- documents.length *
746
- locales.filter((l) => l.enabled !== false)
747
- .length && (
748
- <Text size={1} style={{ color: '#666' }}>
749
- {importedTranslations.size}/
750
- {documents.length *
751
- locales.filter((l) => l.enabled !== false)
752
- .length}{' '}
753
- imported
754
- </Text>
755
- )}
756
- </Flex>
757
- <Flex
758
- gap={2}
759
- align='center'
760
- style={{ whiteSpace: 'nowrap' }}
761
- >
762
- <Text size={1}>Auto-Publish</Text>
763
- <Switch
764
- checked={autoPublish}
765
- onChange={() => setAutoPublish(!autoPublish)}
766
- disabled={isBusy}
767
- />
768
- </Flex>
769
- </Flex>
770
-
771
- {/* Import Progress UI */}
772
- {importProgress.isImporting && (
773
- <Flex justify='center' align='center' gap={3}>
774
- <Spinner size={1} />
775
- <Text size={1}>
776
- Importing {importProgress.current} of{' '}
777
- {importProgress.total} translations...
778
- </Text>
779
- </Flex>
780
- )}
781
- </Stack>
782
- </Stack>
783
-
99
+ <Container width={2}>
100
+ <Box padding={4} marginTop={5}>
101
+ <Stack space={4}>
102
+ <Flex align='center' justify='space-between'>
103
+ <Stack space={2}>
104
+ <Heading as='h2' size={3}>
105
+ Translations
106
+ </Heading>
784
107
  <Text size={2}>
785
- For more information, see the{' '}
786
- <Link href='https://dash.generaltranslation.com'>
787
- General Translation Dashboard
788
- </Link>
789
- .
108
+ Manage your document translations from this centralized
109
+ location.
790
110
  </Text>
791
111
  </Stack>
792
- </Box>
793
112
 
794
- {/* Translate All Confirmation Dialog */}
795
- {isTranslateAllDialogOpen && (
796
- <Dialog
797
- header='Confirm Translation'
798
- id='translate-all-dialog'
799
- onClose={() => setIsTranslateAllDialogOpen(false)}
800
- footer={
801
- <Box padding={3}>
802
- <Flex gap={2}>
113
+ <Flex gap={3} align='center'>
114
+ <Flex gap={2} align='center'>
115
+ <Text size={1}>Auto-refresh</Text>
116
+ <Switch
117
+ checked={autoRefresh}
118
+ onChange={() => setAutoRefresh(!autoRefresh)}
119
+ />
120
+ </Flex>
121
+ <Button
122
+ fontSize={1}
123
+ padding={2}
124
+ text={isRefreshing ? 'Refreshing...' : 'Refresh Status'}
125
+ onClick={handleRefreshAll}
126
+ disabled={
127
+ isRefreshing ||
128
+ isBusy ||
129
+ loadingDocuments ||
130
+ documents.length === 0
131
+ }
132
+ />
133
+ </Flex>
134
+ </Flex>
135
+
136
+ <Stack space={4}>
137
+ <Box>
138
+ <Text size={1} muted>
139
+ {loadingDocuments
140
+ ? 'Loading documents...'
141
+ : `Found ${documents.length} documents available for translation`}
142
+ </Text>
143
+ </Box>
144
+
145
+ <Flex justify='center'>
146
+ <Button
147
+ style={{ width: '200px' }}
148
+ tone='critical'
149
+ text={getOperationText(
150
+ 'Translate All',
151
+ isBusy && currentOperation === 'Translate All'
152
+ )}
153
+ onClick={() => {
154
+ setCurrentOperation('Translate All');
155
+ setIsTranslateAllDialogOpen(true);
156
+ }}
157
+ disabled={isBusy || loadingDocuments || documents.length === 0}
158
+ />
159
+ </Flex>
160
+
161
+ <TranslationsTable />
162
+
163
+ <Stack space={3}>
164
+ <Flex gap={3} align='center' justify='space-between'>
165
+ <Flex gap={2} align='center'>
166
+ <Tooltip
167
+ placement='top'
168
+ content='Imports and overrides all translations'
169
+ >
803
170
  <Button
804
- text='Cancel'
805
171
  mode='ghost'
806
- onClick={() => setIsTranslateAllDialogOpen(false)}
172
+ onClick={() => {
173
+ setCurrentOperation('Import All');
174
+ setIsImportAllDialogOpen(true);
175
+ }}
176
+ text={getOperationText(
177
+ 'Import All',
178
+ isBusy && currentOperation === 'Import All'
179
+ )}
180
+ icon={
181
+ isBusy && currentOperation === 'Import All'
182
+ ? null
183
+ : DownloadIcon
184
+ }
185
+ disabled={
186
+ isBusy || loadingDocuments || documents.length === 0
187
+ }
807
188
  />
189
+ </Tooltip>
190
+ <Tooltip
191
+ placement='top'
192
+ content="Imports all translations that are not yet imported (according to the source document's translation.metadata document)"
193
+ >
808
194
  <Button
809
- text='Translate All'
810
- tone='critical'
811
- onClick={handleTranslateAll}
195
+ mode='ghost'
196
+ tone='primary'
197
+ onClick={() => {
198
+ setCurrentOperation('Import Missing');
199
+ setIsImportMissingDialogOpen(true);
200
+ }}
201
+ text={getOperationText(
202
+ 'Import Missing',
203
+ isBusy && currentOperation === 'Import Missing'
204
+ )}
205
+ icon={
206
+ isBusy && currentOperation === 'Import Missing'
207
+ ? null
208
+ : DownloadIcon
209
+ }
210
+ disabled={
211
+ isBusy || loadingDocuments || documents.length === 0
212
+ }
812
213
  />
813
- </Flex>
814
- </Box>
815
- }
816
- >
817
- <Box padding={4}>
818
- <Stack space={3}>
819
- <Text>
820
- Are you sure you want to create translation tasks for all{' '}
821
- {documents.length} documents?
822
- </Text>
823
- <Text size={1} muted>
824
- This will submit all documents to General Translation for
825
- processing.
826
- </Text>
827
- </Stack>
828
- </Box>
829
- </Dialog>
830
- )}
831
-
832
- {/* Import All Confirmation Dialog */}
833
- {isImportAllDialogOpen && (
834
- <Dialog
835
- header='Confirm Import'
836
- id='import-all-dialog'
837
- onClose={() => setIsImportAllDialogOpen(false)}
838
- footer={
839
- <Box padding={3}>
840
- <Flex gap={2}>
214
+ </Tooltip>
215
+ <Tooltip
216
+ placement='top'
217
+ content='Replaces references in documents with the corresponding translated document reference'
218
+ >
841
219
  <Button
842
- text='Cancel'
843
220
  mode='ghost'
844
- onClick={() => setIsImportAllDialogOpen(false)}
221
+ tone='caution'
222
+ onClick={() => {
223
+ setCurrentOperation('Patch Document References');
224
+ handlePatchDocumentReferences();
225
+ }}
226
+ text={getOperationText(
227
+ 'Patch Document References',
228
+ isBusy &&
229
+ currentOperation === 'Patch Document References'
230
+ )}
231
+ icon={
232
+ isBusy &&
233
+ currentOperation === 'Patch Document References'
234
+ ? null
235
+ : LinkIcon
236
+ }
237
+ disabled={
238
+ isBusy || loadingDocuments || documents.length === 0
239
+ }
845
240
  />
241
+ </Tooltip>
242
+ <Tooltip
243
+ placement='top'
244
+ content='Publishes all translations whose source document is published'
245
+ >
846
246
  <Button
847
- text='Import All'
848
- tone='primary'
849
- onClick={handleImportAll}
247
+ mode='ghost'
248
+ tone='positive'
249
+ onClick={() => {
250
+ setCurrentOperation('Publish Translations');
251
+ handlePublishAllTranslations();
252
+ }}
253
+ text={getOperationText(
254
+ 'Publish Translations',
255
+ isBusy && currentOperation === 'Publish Translations'
256
+ )}
257
+ icon={
258
+ isBusy && currentOperation === 'Publish Translations'
259
+ ? null
260
+ : PublishIcon
261
+ }
262
+ disabled={
263
+ isBusy || loadingDocuments || documents.length === 0
264
+ }
850
265
  />
851
- </Flex>
852
- </Box>
853
- }
854
- >
855
- <Box padding={4}>
856
- <Stack space={3}>
857
- <Text>
858
- Are you sure you want to import translations for all{' '}
859
- {documents.length} documents?
860
- </Text>
861
- <Text size={1} muted>
862
- This will download and apply translated content to your
863
- documents. Note that this will overwrite any existing
864
- translations!
865
- </Text>
866
- </Stack>
867
- </Box>
868
- </Dialog>
869
- )}
870
- </Container>
871
- </ToastProvider>
872
- </ThemeProvider>
266
+ </Tooltip>
267
+ {importedTranslations.size ===
268
+ documents.length *
269
+ locales.filter((l) => l.enabled !== false).length &&
270
+ documents.length > 0 &&
271
+ locales.length > 0 && (
272
+ <Flex gap={2} align='center' style={{ color: 'green' }}>
273
+ <CheckmarkCircleIcon />
274
+ <Text size={1}>All translations imported</Text>
275
+ </Flex>
276
+ )}
277
+ {importedTranslations.size > 0 &&
278
+ importedTranslations.size <
279
+ documents.length *
280
+ locales.filter((l) => l.enabled !== false).length && (
281
+ <Text size={1} style={{ color: '#666' }}>
282
+ {importedTranslations.size}/
283
+ {documents.length *
284
+ locales.filter((l) => l.enabled !== false)
285
+ .length}{' '}
286
+ imported
287
+ </Text>
288
+ )}
289
+ </Flex>
290
+ <Box />
291
+ </Flex>
292
+
293
+ <BatchProgress
294
+ isActive={importProgress.isImporting}
295
+ current={importProgress.current}
296
+ total={importProgress.total}
297
+ operationName={getProgressOperationName()}
298
+ />
299
+ </Stack>
300
+ </Stack>
301
+
302
+ <Text size={2}>
303
+ For more information, see the{' '}
304
+ <Link href='https://dash.generaltranslation.com'>
305
+ General Translation Dashboard
306
+ </Link>
307
+ .
308
+ </Text>
309
+ </Stack>
310
+ </Box>
311
+
312
+ <TranslateAllDialog
313
+ isOpen={isTranslateAllDialogOpen}
314
+ onClose={() => setIsTranslateAllDialogOpen(false)}
315
+ />
316
+ <ImportAllDialog
317
+ isOpen={isImportAllDialogOpen}
318
+ onClose={() => setIsImportAllDialogOpen(false)}
319
+ />
320
+ <ImportMissingDialog
321
+ isOpen={isImportMissingDialogOpen}
322
+ onClose={() => setIsImportMissingDialogOpen(false)}
323
+ />
324
+ </Container>
325
+ );
326
+ };
327
+
328
+ const TranslationsTool: React.FC = () => {
329
+ return (
330
+ <BaseTranslationWrapper showContainer={false}>
331
+ <TranslationsProvider>
332
+ <TranslationsToolContent />
333
+ </TranslationsProvider>
334
+ </BaseTranslationWrapper>
873
335
  );
874
336
  };
875
337