gt-sanity 0.0.6 → 1.0.0
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.
- package/LICENSE.md +1 -1
- package/dist/index.d.mts +95 -73
- package/dist/index.d.ts +95 -73
- package/dist/index.js +9066 -1207
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +9083 -1197
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -3
- package/src/adapter/core.ts +41 -4
- package/src/adapter/getLocales.ts +2 -2
- package/src/components/TranslationsProvider.tsx +942 -0
- package/src/components/page/BatchProgress.tsx +27 -0
- package/src/components/page/ImportAllDialog.tsx +51 -0
- package/src/components/page/ImportMissingDialog.tsx +55 -0
- package/src/components/page/TranslateAllDialog.tsx +55 -0
- package/src/components/page/TranslationsTable.tsx +81 -0
- package/src/components/page/TranslationsTool.tsx +299 -837
- package/src/components/shared/BaseTranslationWrapper.tsx +82 -0
- package/src/components/shared/LocaleCheckbox.tsx +47 -0
- package/src/components/shared/SingleDocumentView.tsx +108 -0
- package/src/components/tab/TranslationView.tsx +379 -0
- package/src/components/tab/TranslationsTab.tsx +25 -0
- package/src/configuration/baseDocumentLevelConfig/documentLevelPatch.ts +6 -9
- package/src/configuration/baseDocumentLevelConfig/helpers/createI18nDocAndPatchMetadata.ts +5 -24
- package/src/configuration/baseDocumentLevelConfig/helpers/patchI18nDoc.ts +3 -23
- package/src/configuration/baseDocumentLevelConfig/index.ts +16 -68
- package/src/configuration/baseFieldLevelConfig.ts +15 -50
- package/src/index.ts +29 -43
- package/src/sanity-api/findDocuments.ts +44 -0
- package/src/sanity-api/publishDocuments.ts +49 -0
- package/src/sanity-api/resolveRefs.ts +146 -0
- package/src/serialization/BaseDocumentMerger.ts +138 -0
- package/src/serialization/BaseSerializationConfig.ts +220 -0
- package/src/serialization/__tests__/BaseDocumentDeserializer/__snapshots__/documentLevelDeserialization.test.ts.snap +189 -0
- package/src/serialization/__tests__/BaseDocumentDeserializer/__snapshots__/fieldLevelDeserialization.test.ts.snap +107 -0
- package/src/serialization/__tests__/BaseDocumentDeserializer/baseDeserialization.test.ts +397 -0
- package/src/serialization/__tests__/BaseDocumentDeserializer/documentLevelDeserialization.test.ts +107 -0
- package/src/serialization/__tests__/BaseDocumentDeserializer/fieldLevelDeserialization.test.ts +107 -0
- package/src/serialization/__tests__/BaseDocumentMerger/__snapshots__/documentLevelMerge.test.ts.snap +193 -0
- package/src/serialization/__tests__/BaseDocumentMerger/__snapshots__/fieldLevelMerge.test.ts.snap +97 -0
- package/src/serialization/__tests__/BaseDocumentMerger/baseMerge.test.ts +36 -0
- package/src/serialization/__tests__/BaseDocumentMerger/documentLevelMerge.test.ts +96 -0
- package/src/serialization/__tests__/BaseDocumentMerger/fieldLevelMerge.test.ts +142 -0
- package/src/serialization/__tests__/BaseDocumentMerger/utils.ts +52 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/__snapshots__/documentInlineMarks.test.ts.snap +39 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/__snapshots__/documentLevelSerialization.test.ts.snap +8 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/__snapshots__/fieldLevelSerialization.test.ts.snap +8 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/baseSerialization.test.ts +345 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/documentInlineMarks.test.ts +53 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/documentLevelSerialization.test.ts +120 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/fieldLevelSerialization.test.ts +153 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/utils.ts +27 -0
- package/src/serialization/__tests__/README +2 -0
- package/src/serialization/__tests__/__fixtures__/annotationAndInlineBlocks.json +140 -0
- package/src/serialization/__tests__/__fixtures__/customStyles.json +62 -0
- package/src/serialization/__tests__/__fixtures__/documentInlineMarks.json +70 -0
- package/src/serialization/__tests__/__fixtures__/documentLevelArticle.json +185 -0
- package/src/serialization/__tests__/__fixtures__/fieldLevelArticle.json +107 -0
- package/src/serialization/__tests__/__fixtures__/inlineDocumentLevelArticle.json +134 -0
- package/src/serialization/__tests__/__fixtures__/inlineSchema.ts +270 -0
- package/src/serialization/__tests__/__fixtures__/messy-html.html +26 -0
- package/src/serialization/__tests__/__fixtures__/nestedLanguageFields.json +54 -0
- package/src/serialization/__tests__/__fixtures__/schema.ts +310 -0
- package/src/serialization/__tests__/global.setup.ts +40 -0
- package/src/serialization/__tests__/helpers.ts +132 -0
- package/src/serialization/data.ts +82 -0
- package/src/serialization/deserialize/BaseDocumentDeserializer.ts +171 -0
- package/src/serialization/deserialize/helpers.ts +42 -0
- package/src/serialization/helpers.ts +18 -0
- package/src/serialization/index.ts +11 -0
- package/src/serialization/serialize/fieldFilters.ts +124 -0
- package/src/serialization/serialize/index.ts +284 -0
- package/src/serialization/types.ts +41 -0
- package/src/translation/importDocument.ts +4 -5
- package/src/translation/uploadFiles.ts +1 -1
- package/src/types.ts +3 -19
- package/src/utils/batchProcessor.ts +111 -0
- package/src/utils/importUtils.ts +95 -0
- package/src/utils/serialize.ts +25 -5
- package/src/utils/shared.ts +1 -1
- package/src/adapter/index.ts +0 -13
- package/src/components/NewTask.tsx +0 -251
- package/src/components/TaskView.tsx +0 -257
- package/src/components/TranslationContext.tsx +0 -24
- package/src/components/TranslationView.tsx +0 -114
- package/src/components/TranslationsTab.tsx +0 -181
- /package/src/components/{LanguageStatus.tsx → shared/LanguageStatus.tsx} +0 -0
- /package/src/components/{ProgressBar.tsx → shared/ProgressBar.tsx} +0 -0
|
@@ -0,0 +1,942 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useState,
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
ReactNode,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import { SanityDocument, useSchema } from 'sanity';
|
|
10
|
+
import { useToast } from '@sanity/ui';
|
|
11
|
+
import { useClient } from '../hooks/useClient';
|
|
12
|
+
import { useSecrets } from '../hooks/useSecrets';
|
|
13
|
+
import {
|
|
14
|
+
GTFile,
|
|
15
|
+
Secrets,
|
|
16
|
+
TranslationLocale,
|
|
17
|
+
TranslationFunctionContext,
|
|
18
|
+
} from '../types';
|
|
19
|
+
import { pluginConfig } from '../adapter/core';
|
|
20
|
+
import { serializeDocument } from '../utils/serialize';
|
|
21
|
+
import { uploadFiles } from '../translation/uploadFiles';
|
|
22
|
+
import { initProject } from '../translation/initProject';
|
|
23
|
+
import { createJobs } from '../translation/createJobs';
|
|
24
|
+
import { downloadTranslations } from '../translation/downloadTranslations';
|
|
25
|
+
import { checkTranslationStatus } from '../translation/checkTranslationStatus';
|
|
26
|
+
import { importDocument } from '../translation/importDocument';
|
|
27
|
+
import { resolveRefs } from '../sanity-api/resolveRefs';
|
|
28
|
+
import { findTranslatedDocumentForLocale } from '../sanity-api/findDocuments';
|
|
29
|
+
import {
|
|
30
|
+
getReadyFilesForImport,
|
|
31
|
+
importTranslations,
|
|
32
|
+
ImportOptions,
|
|
33
|
+
} from '../utils/importUtils';
|
|
34
|
+
import { processBatch } from '../utils/batchProcessor';
|
|
35
|
+
import { publishTranslations } from '../sanity-api/publishDocuments';
|
|
36
|
+
import { getLocales } from '../adapter/getLocales';
|
|
37
|
+
|
|
38
|
+
interface ImportProgress {
|
|
39
|
+
current: number;
|
|
40
|
+
total: number;
|
|
41
|
+
isImporting: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface DownloadStatus {
|
|
45
|
+
downloaded: Set<string>;
|
|
46
|
+
failed: Set<string>;
|
|
47
|
+
skipped: Set<string>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface TranslationStatus {
|
|
51
|
+
progress: number;
|
|
52
|
+
isReady: boolean;
|
|
53
|
+
translationId?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface TranslationsContextType {
|
|
57
|
+
// State
|
|
58
|
+
isBusy: boolean;
|
|
59
|
+
documents: SanityDocument[];
|
|
60
|
+
locales: TranslationLocale[];
|
|
61
|
+
autoRefresh: boolean;
|
|
62
|
+
loadingDocuments: boolean;
|
|
63
|
+
importProgress: ImportProgress;
|
|
64
|
+
importedTranslations: Set<string>;
|
|
65
|
+
existingTranslations: Set<string>;
|
|
66
|
+
downloadStatus: DownloadStatus;
|
|
67
|
+
translationStatuses: Map<string, TranslationStatus>;
|
|
68
|
+
isRefreshing: boolean;
|
|
69
|
+
loadingSecrets: boolean;
|
|
70
|
+
secrets: Secrets | null;
|
|
71
|
+
|
|
72
|
+
// Actions
|
|
73
|
+
setLocales: (locales: TranslationLocale[]) => void;
|
|
74
|
+
setAutoRefresh: (value: boolean) => void;
|
|
75
|
+
handleTranslateAll: () => Promise<void>;
|
|
76
|
+
handleImportAll: () => Promise<void>;
|
|
77
|
+
handleImportMissing: () => Promise<void>;
|
|
78
|
+
handleRefreshAll: () => Promise<void>;
|
|
79
|
+
handleImportDocument: (documentId: string, localeId: string) => Promise<void>;
|
|
80
|
+
handlePatchDocumentReferences: () => Promise<void>;
|
|
81
|
+
handlePublishAllTranslations: () => Promise<void>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const TranslationsContext = createContext<TranslationsContextType | null>(null);
|
|
85
|
+
|
|
86
|
+
export const useTranslations = () => {
|
|
87
|
+
const context = useContext(TranslationsContext);
|
|
88
|
+
if (!context) {
|
|
89
|
+
throw new Error('useTranslations must be used within TranslationsProvider');
|
|
90
|
+
}
|
|
91
|
+
return context;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
interface TranslationsProviderProps {
|
|
95
|
+
children: ReactNode;
|
|
96
|
+
singleDocument?: SanityDocument | null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const TranslationsProvider: React.FC<TranslationsProviderProps> = ({
|
|
100
|
+
children,
|
|
101
|
+
singleDocument,
|
|
102
|
+
}) => {
|
|
103
|
+
const [isBusy, setIsBusy] = useState(false);
|
|
104
|
+
const [documents, setDocuments] = useState<SanityDocument[]>([]);
|
|
105
|
+
const [locales, setLocales] = useState<TranslationLocale[]>([]);
|
|
106
|
+
const [autoRefresh, setAutoRefresh] = useState(false);
|
|
107
|
+
const [loadingDocuments, setLoadingDocuments] = useState(false);
|
|
108
|
+
const [importProgress, setImportProgress] = useState<ImportProgress>({
|
|
109
|
+
current: 0,
|
|
110
|
+
total: 0,
|
|
111
|
+
isImporting: false,
|
|
112
|
+
});
|
|
113
|
+
const [importedTranslations, setImportedTranslations] = useState<Set<string>>(
|
|
114
|
+
new Set()
|
|
115
|
+
);
|
|
116
|
+
const [existingTranslations, setExistingTranslations] = useState<Set<string>>(
|
|
117
|
+
new Set()
|
|
118
|
+
);
|
|
119
|
+
const [downloadStatus, setDownloadStatus] = useState<DownloadStatus>({
|
|
120
|
+
downloaded: new Set<string>(),
|
|
121
|
+
failed: new Set<string>(),
|
|
122
|
+
skipped: new Set<string>(),
|
|
123
|
+
});
|
|
124
|
+
const [translationStatuses, setTranslationStatuses] = useState<
|
|
125
|
+
Map<string, TranslationStatus>
|
|
126
|
+
>(new Map());
|
|
127
|
+
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
128
|
+
|
|
129
|
+
const client = useClient();
|
|
130
|
+
const schema = useSchema();
|
|
131
|
+
const translationContext: TranslationFunctionContext = { client, schema };
|
|
132
|
+
const toast = useToast();
|
|
133
|
+
const { loading: loadingSecrets, secrets } = useSecrets<Secrets>(
|
|
134
|
+
pluginConfig.getSecretsNamespace()
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const fetchDocuments = useCallback(async () => {
|
|
138
|
+
setLoadingDocuments(true);
|
|
139
|
+
try {
|
|
140
|
+
if (singleDocument) {
|
|
141
|
+
setDocuments([singleDocument]);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const translateDocuments = pluginConfig.getTranslateDocuments();
|
|
146
|
+
|
|
147
|
+
const filterConditions = translateDocuments
|
|
148
|
+
.map((filter) => {
|
|
149
|
+
if (filter.type && filter.documentId) {
|
|
150
|
+
return `(_type == "${filter.type}" && _id == "${filter.documentId}")`;
|
|
151
|
+
} else if (filter.type) {
|
|
152
|
+
return `_type == "${filter.type}"`;
|
|
153
|
+
} else if (filter.documentId) {
|
|
154
|
+
return `_id == "${filter.documentId}"`;
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
})
|
|
158
|
+
.filter(Boolean);
|
|
159
|
+
|
|
160
|
+
const languageField = pluginConfig.getLanguageField();
|
|
161
|
+
const sourceLocale = pluginConfig.getSourceLocale();
|
|
162
|
+
const languageFilter = `(!defined(${languageField}) || ${languageField} == "${sourceLocale}")`;
|
|
163
|
+
|
|
164
|
+
let query;
|
|
165
|
+
if (filterConditions.length === 0) {
|
|
166
|
+
query = `*[!(_type in ["system.group"]) && !(_id in path("_.**")) && ${languageFilter}]`;
|
|
167
|
+
} else {
|
|
168
|
+
const filterQuery = filterConditions.join(' || ');
|
|
169
|
+
query = `*[!(_type in ["system.group"]) && !(_id in path("_.**")) && (${filterQuery}) && ${languageFilter}]`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const docs = await client.fetch(query);
|
|
173
|
+
setDocuments(docs);
|
|
174
|
+
} catch {
|
|
175
|
+
toast.push({
|
|
176
|
+
title: 'Error fetching documents',
|
|
177
|
+
status: 'error',
|
|
178
|
+
closable: true,
|
|
179
|
+
});
|
|
180
|
+
} finally {
|
|
181
|
+
setLoadingDocuments(false);
|
|
182
|
+
}
|
|
183
|
+
}, [client, singleDocument]);
|
|
184
|
+
|
|
185
|
+
const fetchLocales = useCallback(async () => {
|
|
186
|
+
if (!secrets) return;
|
|
187
|
+
try {
|
|
188
|
+
const availableLocales = await getLocales(secrets);
|
|
189
|
+
setLocales(availableLocales);
|
|
190
|
+
} catch {
|
|
191
|
+
toast.push({
|
|
192
|
+
title: 'Error fetching locales',
|
|
193
|
+
status: 'error',
|
|
194
|
+
closable: true,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}, [secrets]);
|
|
198
|
+
|
|
199
|
+
const fetchExistingTranslations = useCallback(async () => {
|
|
200
|
+
if (!documents.length || !locales.length) return;
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const sourceLocale = pluginConfig.getSourceLocale();
|
|
204
|
+
const availableLocaleIds = locales
|
|
205
|
+
.filter((locale) => locale.enabled !== false)
|
|
206
|
+
.map((locale) => locale.localeId);
|
|
207
|
+
|
|
208
|
+
const documentIds = documents.map(
|
|
209
|
+
(doc) => doc._id?.replace('drafts.', '') || doc._id
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const query = `*[
|
|
213
|
+
_type == 'translation.metadata' &&
|
|
214
|
+
translations[_key == $sourceLocale][0].value._ref in $documentIds
|
|
215
|
+
] {
|
|
216
|
+
'sourceDocId': translations[_key == $sourceLocale][0].value._ref,
|
|
217
|
+
'existingTranslations': translations[_key in $localeIds]._key
|
|
218
|
+
}`;
|
|
219
|
+
|
|
220
|
+
const existingMetadata = await client.fetch(query, {
|
|
221
|
+
sourceLocale,
|
|
222
|
+
documentIds,
|
|
223
|
+
localeIds: availableLocaleIds,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const existing = new Set<string>();
|
|
227
|
+
existingMetadata.forEach((metadata: any) => {
|
|
228
|
+
metadata.existingTranslations?.forEach((localeId: string) => {
|
|
229
|
+
if (localeId !== sourceLocale) {
|
|
230
|
+
existing.add(`${metadata.sourceDocId}:${localeId}`);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
setExistingTranslations(existing);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
console.error('Error fetching existing translations:', error);
|
|
238
|
+
toast.push({
|
|
239
|
+
title: 'Error fetching existing translations',
|
|
240
|
+
status: 'error',
|
|
241
|
+
closable: true,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}, [documents, locales, client]);
|
|
245
|
+
|
|
246
|
+
const handleTranslateAll = useCallback(async () => {
|
|
247
|
+
if (!secrets || documents.length === 0) return;
|
|
248
|
+
|
|
249
|
+
setIsBusy(true);
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const availableLocaleIds = locales
|
|
253
|
+
.filter((locale) => locale.enabled !== false)
|
|
254
|
+
.map((locale) => locale.localeId);
|
|
255
|
+
|
|
256
|
+
const transformedDocuments = documents
|
|
257
|
+
.map((doc) => {
|
|
258
|
+
delete doc[pluginConfig.getLanguageField()];
|
|
259
|
+
const baseLanguage = pluginConfig.getSourceLocale();
|
|
260
|
+
try {
|
|
261
|
+
const serialized = serializeDocument(doc, schema, baseLanguage);
|
|
262
|
+
return {
|
|
263
|
+
info: {
|
|
264
|
+
documentId: doc._id?.replace('drafts.', '') || doc._id,
|
|
265
|
+
versionId: doc._rev,
|
|
266
|
+
},
|
|
267
|
+
serializedDocument: serialized,
|
|
268
|
+
};
|
|
269
|
+
} catch (error) {
|
|
270
|
+
console.error('Error transforming document', doc._id, error);
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
})
|
|
274
|
+
.filter((doc) => doc !== null);
|
|
275
|
+
|
|
276
|
+
const uploadResult = await uploadFiles(transformedDocuments, secrets);
|
|
277
|
+
await initProject(uploadResult, { timeout: 600 }, secrets);
|
|
278
|
+
await createJobs(uploadResult, availableLocaleIds, secrets);
|
|
279
|
+
|
|
280
|
+
toast.push({
|
|
281
|
+
title: `Translation tasks created for ${documents.length} documents`,
|
|
282
|
+
status: 'success',
|
|
283
|
+
closable: true,
|
|
284
|
+
});
|
|
285
|
+
} catch {
|
|
286
|
+
toast.push({
|
|
287
|
+
title: 'Error creating translation tasks',
|
|
288
|
+
status: 'error',
|
|
289
|
+
closable: true,
|
|
290
|
+
});
|
|
291
|
+
} finally {
|
|
292
|
+
setIsBusy(false);
|
|
293
|
+
}
|
|
294
|
+
}, [secrets, documents, locales, schema]);
|
|
295
|
+
|
|
296
|
+
const handleImportAll = useCallback(async () => {
|
|
297
|
+
if (!secrets || documents.length === 0) return;
|
|
298
|
+
|
|
299
|
+
setIsBusy(true);
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
const readyFiles = await getReadyFilesForImport(
|
|
303
|
+
documents,
|
|
304
|
+
translationStatuses
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
if (readyFiles.length === 0) {
|
|
308
|
+
toast.push({
|
|
309
|
+
title: 'No ready translations to import',
|
|
310
|
+
status: 'warning',
|
|
311
|
+
closable: true,
|
|
312
|
+
});
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
setImportProgress({
|
|
317
|
+
current: 0,
|
|
318
|
+
total: readyFiles.length,
|
|
319
|
+
isImporting: true,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const importOptions: ImportOptions = {
|
|
323
|
+
onProgress: (current, total) => {
|
|
324
|
+
setImportProgress({
|
|
325
|
+
current,
|
|
326
|
+
total,
|
|
327
|
+
isImporting: true,
|
|
328
|
+
});
|
|
329
|
+
},
|
|
330
|
+
onImportSuccess: (key) => {
|
|
331
|
+
setImportedTranslations((prev) => new Set([...prev, key]));
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const result = await importTranslations(
|
|
336
|
+
readyFiles,
|
|
337
|
+
secrets,
|
|
338
|
+
translationContext,
|
|
339
|
+
importOptions
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
if (result.successfulImports.length > 0) {
|
|
343
|
+
setDownloadStatus((prev) => ({
|
|
344
|
+
...prev,
|
|
345
|
+
downloaded: new Set([
|
|
346
|
+
...prev.downloaded,
|
|
347
|
+
...result.successfulImports,
|
|
348
|
+
]),
|
|
349
|
+
}));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
toast.push({
|
|
353
|
+
title: `Imported ${result.successCount} translations${result.failureCount > 0 ? `, ${result.failureCount} failed` : ''}`,
|
|
354
|
+
status: result.successCount > 0 ? 'success' : 'error',
|
|
355
|
+
closable: true,
|
|
356
|
+
});
|
|
357
|
+
} catch (error) {
|
|
358
|
+
console.error('Error importing translations:', error);
|
|
359
|
+
toast.push({
|
|
360
|
+
title: 'Error importing translations',
|
|
361
|
+
status: 'error',
|
|
362
|
+
closable: true,
|
|
363
|
+
});
|
|
364
|
+
} finally {
|
|
365
|
+
setIsBusy(false);
|
|
366
|
+
setImportProgress({ current: 0, total: 0, isImporting: false });
|
|
367
|
+
}
|
|
368
|
+
}, [
|
|
369
|
+
secrets,
|
|
370
|
+
documents,
|
|
371
|
+
translationStatuses,
|
|
372
|
+
downloadStatus,
|
|
373
|
+
translationContext,
|
|
374
|
+
]);
|
|
375
|
+
|
|
376
|
+
const getMissingTranslations = useCallback(
|
|
377
|
+
async (
|
|
378
|
+
documentIds: string[],
|
|
379
|
+
localeIds: string[]
|
|
380
|
+
): Promise<Set<string>> => {
|
|
381
|
+
const sourceLocale = pluginConfig.getSourceLocale();
|
|
382
|
+
|
|
383
|
+
const query = `*[
|
|
384
|
+
_type == 'translation.metadata' &&
|
|
385
|
+
translations[_key == $sourceLocale][0].value._ref in $documentIds
|
|
386
|
+
] {
|
|
387
|
+
'sourceDocId': translations[_key == $sourceLocale][0].value._ref,
|
|
388
|
+
'existingTranslations': translations[_key in $localeIds]._key
|
|
389
|
+
}`;
|
|
390
|
+
|
|
391
|
+
const existingMetadata = await client.fetch(query, {
|
|
392
|
+
sourceLocale,
|
|
393
|
+
documentIds,
|
|
394
|
+
localeIds,
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
const existing = new Set<string>();
|
|
398
|
+
existingMetadata.forEach((metadata: any) => {
|
|
399
|
+
metadata.existingTranslations?.forEach((localeId: string) => {
|
|
400
|
+
if (localeId !== sourceLocale) {
|
|
401
|
+
existing.add(`${metadata.sourceDocId}:${localeId}`);
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const missing = new Set<string>();
|
|
407
|
+
documentIds.forEach((docId) => {
|
|
408
|
+
localeIds.forEach((localeId) => {
|
|
409
|
+
if (localeId !== sourceLocale) {
|
|
410
|
+
const key = `${docId}:${localeId}`;
|
|
411
|
+
if (!existing.has(key)) {
|
|
412
|
+
missing.add(key);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
return missing;
|
|
419
|
+
},
|
|
420
|
+
[client]
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
const handleImportMissing = useCallback(async () => {
|
|
424
|
+
if (!secrets || documents.length === 0) return;
|
|
425
|
+
|
|
426
|
+
setIsBusy(true);
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
const availableLocaleIds = locales
|
|
430
|
+
.filter((locale) => locale.enabled !== false)
|
|
431
|
+
.map((locale) => locale.localeId);
|
|
432
|
+
|
|
433
|
+
const documentIds = documents.map(
|
|
434
|
+
(doc) => doc._id?.replace('drafts.', '') || doc._id
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
const missingTranslations = await getMissingTranslations(
|
|
438
|
+
documentIds,
|
|
439
|
+
availableLocaleIds
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
console.log('missingTranslations', missingTranslations);
|
|
443
|
+
const readyFiles = await getReadyFilesForImport(
|
|
444
|
+
documents,
|
|
445
|
+
translationStatuses,
|
|
446
|
+
{
|
|
447
|
+
filterReadyFiles: (key) => missingTranslations.has(key),
|
|
448
|
+
}
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
if (readyFiles.length === 0) {
|
|
452
|
+
toast.push({
|
|
453
|
+
title: 'No missing translations to import',
|
|
454
|
+
status: 'warning',
|
|
455
|
+
closable: true,
|
|
456
|
+
});
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
setImportProgress({
|
|
461
|
+
current: 0,
|
|
462
|
+
total: readyFiles.length,
|
|
463
|
+
isImporting: true,
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
const importOptions: ImportOptions = {
|
|
467
|
+
onProgress: (current, total) => {
|
|
468
|
+
setImportProgress({
|
|
469
|
+
current,
|
|
470
|
+
total,
|
|
471
|
+
isImporting: true,
|
|
472
|
+
});
|
|
473
|
+
},
|
|
474
|
+
onImportSuccess: (key) => {
|
|
475
|
+
setImportedTranslations((prev) => new Set([...prev, key]));
|
|
476
|
+
setExistingTranslations((prev) => new Set([...prev, key]));
|
|
477
|
+
},
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const result = await importTranslations(
|
|
481
|
+
readyFiles,
|
|
482
|
+
secrets,
|
|
483
|
+
translationContext,
|
|
484
|
+
importOptions
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
if (result.successfulImports.length > 0) {
|
|
488
|
+
setDownloadStatus((prev) => ({
|
|
489
|
+
...prev,
|
|
490
|
+
downloaded: new Set([
|
|
491
|
+
...prev.downloaded,
|
|
492
|
+
...result.successfulImports,
|
|
493
|
+
]),
|
|
494
|
+
}));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
toast.push({
|
|
498
|
+
title: `Imported ${result.successCount} missing translations${result.failureCount > 0 ? `, ${result.failureCount} failed` : ''}`,
|
|
499
|
+
status: result.successCount > 0 ? 'success' : 'error',
|
|
500
|
+
closable: true,
|
|
501
|
+
});
|
|
502
|
+
} catch (error) {
|
|
503
|
+
console.error('Error importing missing translations:', error);
|
|
504
|
+
toast.push({
|
|
505
|
+
title: 'Error importing missing translations',
|
|
506
|
+
status: 'error',
|
|
507
|
+
closable: true,
|
|
508
|
+
});
|
|
509
|
+
} finally {
|
|
510
|
+
setIsBusy(false);
|
|
511
|
+
setImportProgress({ current: 0, total: 0, isImporting: false });
|
|
512
|
+
}
|
|
513
|
+
}, [
|
|
514
|
+
secrets,
|
|
515
|
+
documents,
|
|
516
|
+
locales,
|
|
517
|
+
translationStatuses,
|
|
518
|
+
downloadStatus,
|
|
519
|
+
translationContext,
|
|
520
|
+
getMissingTranslations,
|
|
521
|
+
]);
|
|
522
|
+
|
|
523
|
+
const handleRefreshAll = useCallback(async () => {
|
|
524
|
+
if (!secrets || documents.length === 0) return;
|
|
525
|
+
|
|
526
|
+
setIsRefreshing(true);
|
|
527
|
+
|
|
528
|
+
try {
|
|
529
|
+
const availableLocaleIds = locales
|
|
530
|
+
.filter((locale) => locale.enabled !== false)
|
|
531
|
+
.map((locale) => locale.localeId);
|
|
532
|
+
|
|
533
|
+
const fileQueryData = [];
|
|
534
|
+
for (const doc of documents) {
|
|
535
|
+
for (const localeId of availableLocaleIds) {
|
|
536
|
+
const documentId = doc._id?.replace('drafts.', '') || doc._id;
|
|
537
|
+
fileQueryData.push({
|
|
538
|
+
versionId: doc._rev,
|
|
539
|
+
fileId: documentId,
|
|
540
|
+
locale: localeId,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const readyTranslations = await checkTranslationStatus(
|
|
546
|
+
fileQueryData,
|
|
547
|
+
downloadStatus,
|
|
548
|
+
secrets
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
setTranslationStatuses((prevStatuses) => {
|
|
552
|
+
const newStatuses = new Map();
|
|
553
|
+
|
|
554
|
+
for (const doc of documents) {
|
|
555
|
+
for (const localeId of availableLocaleIds) {
|
|
556
|
+
const documentId = doc._id?.replace('drafts.', '') || doc._id;
|
|
557
|
+
const key = `${documentId}:${localeId}`;
|
|
558
|
+
newStatuses.set(key, { progress: 0, isReady: false });
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (Array.isArray(readyTranslations)) {
|
|
563
|
+
for (const translation of readyTranslations) {
|
|
564
|
+
const key = `${translation.fileId}:${translation.locale}`;
|
|
565
|
+
newStatuses.set(key, {
|
|
566
|
+
progress: 100,
|
|
567
|
+
isReady: true,
|
|
568
|
+
translationId: translation.id,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return newStatuses;
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
toast.push({
|
|
577
|
+
title: `Refreshed status for ${documents.length} documents`,
|
|
578
|
+
status: 'success',
|
|
579
|
+
closable: true,
|
|
580
|
+
});
|
|
581
|
+
} catch (error) {
|
|
582
|
+
console.error('Error refreshing translation status:', error);
|
|
583
|
+
toast.push({
|
|
584
|
+
title: 'Error refreshing translation status',
|
|
585
|
+
status: 'error',
|
|
586
|
+
closable: true,
|
|
587
|
+
});
|
|
588
|
+
} finally {
|
|
589
|
+
setIsRefreshing(false);
|
|
590
|
+
}
|
|
591
|
+
}, [secrets, documents, locales]);
|
|
592
|
+
|
|
593
|
+
const handleImportDocument = useCallback(
|
|
594
|
+
async (documentId: string, localeId: string) => {
|
|
595
|
+
if (!secrets) return;
|
|
596
|
+
|
|
597
|
+
try {
|
|
598
|
+
const key = `${documentId}:${localeId}`;
|
|
599
|
+
const status = translationStatuses.get(key);
|
|
600
|
+
|
|
601
|
+
if (!status?.isReady || !status.translationId) {
|
|
602
|
+
toast.push({
|
|
603
|
+
title: `Translation not ready for ${documentId} (${localeId})`,
|
|
604
|
+
status: 'warning',
|
|
605
|
+
closable: true,
|
|
606
|
+
});
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const document = documents.find(
|
|
611
|
+
(doc) => (doc._id?.replace('drafts.', '') || doc._id) === documentId
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
if (!document) {
|
|
615
|
+
toast.push({
|
|
616
|
+
title: `Document ${documentId} not found`,
|
|
617
|
+
status: 'error',
|
|
618
|
+
closable: true,
|
|
619
|
+
});
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const downloadedFiles = await downloadTranslations(
|
|
624
|
+
[
|
|
625
|
+
{
|
|
626
|
+
documentId,
|
|
627
|
+
versionId: document._rev,
|
|
628
|
+
translationId: status.translationId,
|
|
629
|
+
locale: localeId,
|
|
630
|
+
},
|
|
631
|
+
],
|
|
632
|
+
secrets
|
|
633
|
+
);
|
|
634
|
+
|
|
635
|
+
if (downloadedFiles.length > 0) {
|
|
636
|
+
try {
|
|
637
|
+
const docInfo: GTFile = {
|
|
638
|
+
documentId,
|
|
639
|
+
versionId: document._rev,
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
await importDocument(
|
|
643
|
+
docInfo,
|
|
644
|
+
localeId,
|
|
645
|
+
downloadedFiles[0].data,
|
|
646
|
+
translationContext,
|
|
647
|
+
false
|
|
648
|
+
);
|
|
649
|
+
|
|
650
|
+
setDownloadStatus((prev) => ({
|
|
651
|
+
...prev,
|
|
652
|
+
downloaded: new Set([...prev.downloaded, key]),
|
|
653
|
+
}));
|
|
654
|
+
setImportedTranslations((prev) => new Set([...prev, key]));
|
|
655
|
+
|
|
656
|
+
toast.push({
|
|
657
|
+
title: `Successfully imported translation for ${documentId} (${localeId})`,
|
|
658
|
+
status: 'success',
|
|
659
|
+
closable: true,
|
|
660
|
+
});
|
|
661
|
+
} catch (importError) {
|
|
662
|
+
console.error('Failed to import translation:', importError);
|
|
663
|
+
toast.push({
|
|
664
|
+
title: `Failed to import translation for ${documentId} (${localeId})`,
|
|
665
|
+
status: 'error',
|
|
666
|
+
closable: true,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
} else {
|
|
670
|
+
toast.push({
|
|
671
|
+
title: `No translation content received for ${documentId}`,
|
|
672
|
+
status: 'warning',
|
|
673
|
+
closable: true,
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
} catch (error) {
|
|
677
|
+
console.error('Error importing translation:', error);
|
|
678
|
+
toast.push({
|
|
679
|
+
title: `Error importing translation for ${documentId}`,
|
|
680
|
+
status: 'error',
|
|
681
|
+
closable: true,
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
},
|
|
685
|
+
[secrets, documents, translationContext]
|
|
686
|
+
);
|
|
687
|
+
|
|
688
|
+
const handlePatchDocumentReferences = useCallback(async () => {
|
|
689
|
+
if (!secrets || documents.length === 0) return;
|
|
690
|
+
|
|
691
|
+
setIsBusy(true);
|
|
692
|
+
|
|
693
|
+
try {
|
|
694
|
+
const availableLocaleIds = locales
|
|
695
|
+
.filter((locale) => locale.enabled !== false)
|
|
696
|
+
.map((locale) => locale.localeId);
|
|
697
|
+
|
|
698
|
+
const patchTasks: Array<{ doc: SanityDocument; localeId: string }> = [];
|
|
699
|
+
for (const doc of documents) {
|
|
700
|
+
for (const localeId of availableLocaleIds) {
|
|
701
|
+
patchTasks.push({ doc, localeId });
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
setImportProgress({
|
|
706
|
+
current: 0,
|
|
707
|
+
total: patchTasks.length,
|
|
708
|
+
isImporting: true,
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
const result = await processBatch(
|
|
712
|
+
patchTasks,
|
|
713
|
+
async ({ doc, localeId }) => {
|
|
714
|
+
const sourceLocale = pluginConfig.getSourceLocale();
|
|
715
|
+
|
|
716
|
+
// Skip source locale - only process translated documents
|
|
717
|
+
if (localeId === sourceLocale) {
|
|
718
|
+
return { patched: false, doc, localeId, skipped: true };
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Find the translated document for this locale
|
|
722
|
+
const translatedDoc = await findTranslatedDocumentForLocale(
|
|
723
|
+
doc._id,
|
|
724
|
+
localeId,
|
|
725
|
+
client
|
|
726
|
+
);
|
|
727
|
+
|
|
728
|
+
if (!translatedDoc) {
|
|
729
|
+
return { patched: false, doc, localeId, noTranslation: true };
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const resolvedDoc = await resolveRefs(
|
|
733
|
+
translatedDoc,
|
|
734
|
+
localeId,
|
|
735
|
+
client
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
if (resolvedDoc !== translatedDoc) {
|
|
739
|
+
const mutation = {
|
|
740
|
+
patch: {
|
|
741
|
+
id: translatedDoc._id,
|
|
742
|
+
set: resolvedDoc,
|
|
743
|
+
},
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
await client.mutate([mutation]);
|
|
747
|
+
return { patched: true, doc: translatedDoc, localeId };
|
|
748
|
+
}
|
|
749
|
+
return { patched: false, doc: translatedDoc, localeId };
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
onProgress: (current, total) => {
|
|
753
|
+
setImportProgress({
|
|
754
|
+
current,
|
|
755
|
+
total,
|
|
756
|
+
isImporting: true,
|
|
757
|
+
});
|
|
758
|
+
},
|
|
759
|
+
onItemFailure: ({ doc, localeId }, error) => {
|
|
760
|
+
console.error(
|
|
761
|
+
`Failed to patch references for ${doc._id} (${localeId}):`,
|
|
762
|
+
error
|
|
763
|
+
);
|
|
764
|
+
},
|
|
765
|
+
}
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
const patchedCount = result.successfulItems.filter(
|
|
769
|
+
(item) => item.patched
|
|
770
|
+
).length;
|
|
771
|
+
|
|
772
|
+
toast.push({
|
|
773
|
+
title: `Patched references in ${patchedCount} documents${result.failureCount > 0 ? `, ${result.failureCount} failed` : ''}`,
|
|
774
|
+
status:
|
|
775
|
+
patchedCount > 0 || result.failureCount === 0 ? 'success' : 'error',
|
|
776
|
+
closable: true,
|
|
777
|
+
});
|
|
778
|
+
} catch (error) {
|
|
779
|
+
console.error('Error patching document references:', error);
|
|
780
|
+
toast.push({
|
|
781
|
+
title: 'Error patching document references',
|
|
782
|
+
status: 'error',
|
|
783
|
+
closable: true,
|
|
784
|
+
});
|
|
785
|
+
} finally {
|
|
786
|
+
setIsBusy(false);
|
|
787
|
+
setImportProgress({ current: 0, total: 0, isImporting: false });
|
|
788
|
+
}
|
|
789
|
+
}, [secrets, documents, locales, client]);
|
|
790
|
+
|
|
791
|
+
const handlePublishAllTranslations = useCallback(async () => {
|
|
792
|
+
if (!secrets || documents.length === 0) return;
|
|
793
|
+
|
|
794
|
+
setIsBusy(true);
|
|
795
|
+
|
|
796
|
+
try {
|
|
797
|
+
const sourceLocale = pluginConfig.getSourceLocale();
|
|
798
|
+
const publishedDocumentIds = documents
|
|
799
|
+
.filter((doc) => !doc._id.startsWith('drafts.'))
|
|
800
|
+
.map((doc) => doc._id);
|
|
801
|
+
|
|
802
|
+
if (publishedDocumentIds.length === 0) {
|
|
803
|
+
toast.push({
|
|
804
|
+
title:
|
|
805
|
+
'No published source documents found to publish translations for',
|
|
806
|
+
status: 'warning',
|
|
807
|
+
closable: true,
|
|
808
|
+
});
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const query = `*[
|
|
813
|
+
_type == 'translation.metadata' &&
|
|
814
|
+
translations[_key == $sourceLocale][0].value._ref in $publishedDocumentIds
|
|
815
|
+
] {
|
|
816
|
+
'sourceDocId': translations[_key == $sourceLocale][0].value._ref,
|
|
817
|
+
'translationDocs': translations[_key != $sourceLocale && defined(value._ref)]{
|
|
818
|
+
_key,
|
|
819
|
+
'docId': value._ref
|
|
820
|
+
}
|
|
821
|
+
}`;
|
|
822
|
+
|
|
823
|
+
const translationMetadata = await client.fetch(query, {
|
|
824
|
+
sourceLocale,
|
|
825
|
+
publishedDocumentIds,
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
const translationDocIds: string[] = [];
|
|
829
|
+
translationMetadata.forEach((metadata: any) => {
|
|
830
|
+
metadata.translationDocs?.forEach((translation: any) => {
|
|
831
|
+
if (translation.docId) {
|
|
832
|
+
translationDocIds.push(translation.docId);
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
if (translationDocIds.length === 0) {
|
|
838
|
+
toast.push({
|
|
839
|
+
title: 'No translation documents found to publish',
|
|
840
|
+
status: 'warning',
|
|
841
|
+
closable: true,
|
|
842
|
+
});
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const translatedDocumentIds = await publishTranslations(
|
|
847
|
+
translationDocIds,
|
|
848
|
+
client
|
|
849
|
+
);
|
|
850
|
+
|
|
851
|
+
toast.push({
|
|
852
|
+
title: `Published ${translatedDocumentIds.length} translation documents`,
|
|
853
|
+
status: 'success',
|
|
854
|
+
closable: true,
|
|
855
|
+
});
|
|
856
|
+
} catch (error) {
|
|
857
|
+
console.error('Error publishing translations:', error);
|
|
858
|
+
toast.push({
|
|
859
|
+
title: 'Error publishing translations',
|
|
860
|
+
status: 'error',
|
|
861
|
+
closable: true,
|
|
862
|
+
});
|
|
863
|
+
} finally {
|
|
864
|
+
setIsBusy(false);
|
|
865
|
+
}
|
|
866
|
+
}, [secrets, documents, client]);
|
|
867
|
+
|
|
868
|
+
useEffect(() => {
|
|
869
|
+
fetchDocuments();
|
|
870
|
+
}, [fetchDocuments]);
|
|
871
|
+
|
|
872
|
+
useEffect(() => {
|
|
873
|
+
if (secrets) {
|
|
874
|
+
fetchLocales();
|
|
875
|
+
}
|
|
876
|
+
}, [fetchLocales, secrets]);
|
|
877
|
+
|
|
878
|
+
useEffect(() => {
|
|
879
|
+
if (documents.length > 0 && locales.length > 0) {
|
|
880
|
+
fetchExistingTranslations();
|
|
881
|
+
}
|
|
882
|
+
}, [fetchExistingTranslations]);
|
|
883
|
+
|
|
884
|
+
useEffect(() => {
|
|
885
|
+
if (
|
|
886
|
+
documents.length > 0 &&
|
|
887
|
+
locales.length > 0 &&
|
|
888
|
+
secrets &&
|
|
889
|
+
!loadingDocuments
|
|
890
|
+
) {
|
|
891
|
+
handleRefreshAll();
|
|
892
|
+
}
|
|
893
|
+
}, [documents]);
|
|
894
|
+
|
|
895
|
+
useEffect(() => {
|
|
896
|
+
if (!autoRefresh || documents.length === 0 || !secrets) return;
|
|
897
|
+
|
|
898
|
+
const interval = setInterval(async () => {
|
|
899
|
+
await handleRefreshAll();
|
|
900
|
+
}, 10000);
|
|
901
|
+
|
|
902
|
+
return () => clearInterval(interval);
|
|
903
|
+
}, [autoRefresh, documents.length, secrets, handleRefreshAll]);
|
|
904
|
+
|
|
905
|
+
useEffect(() => {
|
|
906
|
+
setImportedTranslations(new Set(downloadStatus.downloaded));
|
|
907
|
+
}, [downloadStatus.downloaded]);
|
|
908
|
+
|
|
909
|
+
const contextValue: TranslationsContextType = {
|
|
910
|
+
// State
|
|
911
|
+
isBusy,
|
|
912
|
+
documents,
|
|
913
|
+
locales,
|
|
914
|
+
autoRefresh,
|
|
915
|
+
loadingDocuments,
|
|
916
|
+
importProgress,
|
|
917
|
+
importedTranslations,
|
|
918
|
+
existingTranslations,
|
|
919
|
+
downloadStatus,
|
|
920
|
+
translationStatuses,
|
|
921
|
+
isRefreshing,
|
|
922
|
+
loadingSecrets,
|
|
923
|
+
secrets,
|
|
924
|
+
|
|
925
|
+
// Actions
|
|
926
|
+
setLocales,
|
|
927
|
+
setAutoRefresh,
|
|
928
|
+
handleTranslateAll,
|
|
929
|
+
handleImportAll,
|
|
930
|
+
handleImportMissing,
|
|
931
|
+
handleRefreshAll,
|
|
932
|
+
handleImportDocument,
|
|
933
|
+
handlePatchDocumentReferences,
|
|
934
|
+
handlePublishAllTranslations,
|
|
935
|
+
};
|
|
936
|
+
|
|
937
|
+
return (
|
|
938
|
+
<TranslationsContext.Provider value={contextValue}>
|
|
939
|
+
{children}
|
|
940
|
+
</TranslationsContext.Provider>
|
|
941
|
+
);
|
|
942
|
+
};
|