gt-sanity 0.0.5 → 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.
Files changed (108) hide show
  1. package/LICENSE.md +1 -8
  2. package/README.md +5 -5
  3. package/dist/index.d.mts +122 -95
  4. package/dist/index.d.ts +122 -95
  5. package/dist/index.js +9089 -1119
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +9099 -1100
  8. package/dist/index.mjs.map +1 -1
  9. package/package.json +11 -4
  10. package/src/adapter/core.ts +111 -9
  11. package/src/adapter/createTask.ts +1 -1
  12. package/src/adapter/getLocales.ts +2 -2
  13. package/src/adapter/types.ts +9 -0
  14. package/src/components/TranslationsProvider.tsx +942 -0
  15. package/src/components/page/BatchProgress.tsx +27 -0
  16. package/src/components/page/ImportAllDialog.tsx +51 -0
  17. package/src/components/page/ImportMissingDialog.tsx +55 -0
  18. package/src/components/page/TranslateAllDialog.tsx +55 -0
  19. package/src/components/page/TranslationsTable.tsx +81 -0
  20. package/src/components/page/TranslationsTool.tsx +338 -0
  21. package/src/components/shared/BaseTranslationWrapper.tsx +82 -0
  22. package/src/components/{LanguageStatus.tsx → shared/LanguageStatus.tsx} +2 -0
  23. package/src/components/shared/LocaleCheckbox.tsx +47 -0
  24. package/src/components/{ProgressBar.tsx → shared/ProgressBar.tsx} +2 -0
  25. package/src/components/shared/SingleDocumentView.tsx +108 -0
  26. package/src/components/tab/TranslationView.tsx +379 -0
  27. package/src/components/tab/TranslationsTab.tsx +25 -0
  28. package/src/configuration/baseDocumentLevelConfig/documentLevelPatch.ts +21 -11
  29. package/src/configuration/baseDocumentLevelConfig/helpers/createI18nDocAndPatchMetadata.ts +57 -23
  30. package/src/configuration/baseDocumentLevelConfig/helpers/createTranslationMetadata.ts +2 -0
  31. package/src/configuration/baseDocumentLevelConfig/helpers/getOrCreateTranslationMetadata.ts +2 -0
  32. package/src/configuration/baseDocumentLevelConfig/helpers/getTranslationMetadata.ts +2 -0
  33. package/src/configuration/baseDocumentLevelConfig/helpers/patchI18nDoc.ts +31 -8
  34. package/src/configuration/baseDocumentLevelConfig/index.ts +18 -101
  35. package/src/configuration/baseFieldLevelConfig.ts +19 -51
  36. package/src/configuration/utils/checkSerializationVersion.ts +2 -0
  37. package/src/configuration/utils/findDocumentAtRevision.ts +2 -0
  38. package/src/configuration/utils/findLatestDraft.ts +2 -0
  39. package/src/hooks/useClient.ts +3 -1
  40. package/src/hooks/useSecrets.ts +2 -0
  41. package/src/index.ts +91 -67
  42. package/src/sanity-api/findDocuments.ts +44 -0
  43. package/src/sanity-api/publishDocuments.ts +49 -0
  44. package/src/sanity-api/resolveRefs.ts +146 -0
  45. package/src/serialization/BaseDocumentMerger.ts +138 -0
  46. package/src/serialization/BaseSerializationConfig.ts +220 -0
  47. package/src/serialization/__tests__/BaseDocumentDeserializer/__snapshots__/documentLevelDeserialization.test.ts.snap +189 -0
  48. package/src/serialization/__tests__/BaseDocumentDeserializer/__snapshots__/fieldLevelDeserialization.test.ts.snap +107 -0
  49. package/src/serialization/__tests__/BaseDocumentDeserializer/baseDeserialization.test.ts +397 -0
  50. package/src/serialization/__tests__/BaseDocumentDeserializer/documentLevelDeserialization.test.ts +107 -0
  51. package/src/serialization/__tests__/BaseDocumentDeserializer/fieldLevelDeserialization.test.ts +107 -0
  52. package/src/serialization/__tests__/BaseDocumentMerger/__snapshots__/documentLevelMerge.test.ts.snap +193 -0
  53. package/src/serialization/__tests__/BaseDocumentMerger/__snapshots__/fieldLevelMerge.test.ts.snap +97 -0
  54. package/src/serialization/__tests__/BaseDocumentMerger/baseMerge.test.ts +36 -0
  55. package/src/serialization/__tests__/BaseDocumentMerger/documentLevelMerge.test.ts +96 -0
  56. package/src/serialization/__tests__/BaseDocumentMerger/fieldLevelMerge.test.ts +142 -0
  57. package/src/serialization/__tests__/BaseDocumentMerger/utils.ts +52 -0
  58. package/src/serialization/__tests__/BaseDocumentSerializer/__snapshots__/documentInlineMarks.test.ts.snap +39 -0
  59. package/src/serialization/__tests__/BaseDocumentSerializer/__snapshots__/documentLevelSerialization.test.ts.snap +8 -0
  60. package/src/serialization/__tests__/BaseDocumentSerializer/__snapshots__/fieldLevelSerialization.test.ts.snap +8 -0
  61. package/src/serialization/__tests__/BaseDocumentSerializer/baseSerialization.test.ts +345 -0
  62. package/src/serialization/__tests__/BaseDocumentSerializer/documentInlineMarks.test.ts +53 -0
  63. package/src/serialization/__tests__/BaseDocumentSerializer/documentLevelSerialization.test.ts +120 -0
  64. package/src/serialization/__tests__/BaseDocumentSerializer/fieldLevelSerialization.test.ts +153 -0
  65. package/src/serialization/__tests__/BaseDocumentSerializer/utils.ts +27 -0
  66. package/src/serialization/__tests__/README +2 -0
  67. package/src/serialization/__tests__/__fixtures__/annotationAndInlineBlocks.json +140 -0
  68. package/src/serialization/__tests__/__fixtures__/customStyles.json +62 -0
  69. package/src/serialization/__tests__/__fixtures__/documentInlineMarks.json +70 -0
  70. package/src/serialization/__tests__/__fixtures__/documentLevelArticle.json +185 -0
  71. package/src/serialization/__tests__/__fixtures__/fieldLevelArticle.json +107 -0
  72. package/src/serialization/__tests__/__fixtures__/inlineDocumentLevelArticle.json +134 -0
  73. package/src/serialization/__tests__/__fixtures__/inlineSchema.ts +270 -0
  74. package/src/serialization/__tests__/__fixtures__/messy-html.html +26 -0
  75. package/src/serialization/__tests__/__fixtures__/nestedLanguageFields.json +54 -0
  76. package/src/serialization/__tests__/__fixtures__/schema.ts +310 -0
  77. package/src/serialization/__tests__/global.setup.ts +40 -0
  78. package/src/serialization/__tests__/helpers.ts +132 -0
  79. package/src/serialization/data.ts +82 -0
  80. package/src/serialization/deserialize/BaseDocumentDeserializer.ts +171 -0
  81. package/src/serialization/deserialize/helpers.ts +42 -0
  82. package/src/serialization/helpers.ts +18 -0
  83. package/src/serialization/index.ts +11 -0
  84. package/src/serialization/serialize/fieldFilters.ts +124 -0
  85. package/src/serialization/serialize/index.ts +284 -0
  86. package/src/serialization/types.ts +41 -0
  87. package/src/translation/checkTranslationStatus.ts +42 -0
  88. package/src/translation/createJobs.ts +16 -0
  89. package/src/translation/downloadTranslations.ts +68 -0
  90. package/src/translation/importDocument.ts +23 -0
  91. package/src/translation/initProject.ts +61 -0
  92. package/src/translation/uploadFiles.ts +32 -0
  93. package/src/types.ts +7 -20
  94. package/src/utils/applyDocuments.ts +72 -0
  95. package/src/utils/batchProcessor.ts +111 -0
  96. package/src/utils/importUtils.ts +95 -0
  97. package/src/utils/serialize.ts +52 -0
  98. package/src/utils/shared.ts +1 -0
  99. package/src/adapter/index.ts +0 -13
  100. package/src/components/NewTask.tsx +0 -249
  101. package/src/components/TaskView.tsx +0 -255
  102. package/src/components/TranslationContext.tsx +0 -19
  103. package/src/components/TranslationView.tsx +0 -82
  104. package/src/components/TranslationsTab.tsx +0 -177
  105. package/src/configuration/baseDocumentLevelConfig/helpers/index.ts +0 -5
  106. package/src/configuration/baseDocumentLevelConfig/legacyDocumentLevelPatch.ts +0 -69
  107. package/src/configuration/index.ts +0 -18
  108. package/src/configuration/utils/index.ts +0 -3
@@ -0,0 +1,42 @@
1
+ import type { Secrets } from '../types';
2
+ import { gt, overrideConfig } from '../adapter/core';
3
+
4
+ export async function checkTranslationStatus(
5
+ fileQueryData: { versionId: string; fileId: string; locale: string }[],
6
+ downloadStatus: {
7
+ downloaded: Set<string>;
8
+ failed: Set<string>;
9
+ skipped: Set<string>;
10
+ },
11
+ secrets: Secrets
12
+ ) {
13
+ overrideConfig(secrets);
14
+ try {
15
+ // Only query for files that haven't been downloaded yet
16
+ const currentQueryData = fileQueryData.filter(
17
+ (item) =>
18
+ !downloadStatus.downloaded.has(`${item.fileId}:${item.locale}`) &&
19
+ !downloadStatus.failed.has(`${item.fileId}:${item.locale}`) &&
20
+ !downloadStatus.skipped.has(`${item.fileId}:${item.locale}`)
21
+ );
22
+
23
+ // If all files have been downloaded, we're done
24
+ if (currentQueryData.length === 0) {
25
+ return true;
26
+ }
27
+ // Check for translations
28
+ const responseData = await gt.checkFileTranslations(currentQueryData);
29
+
30
+ const translations = responseData.translations || [];
31
+
32
+ // Filter for ready translations
33
+ const readyTranslations = translations.filter(
34
+ (translation) => translation.isReady && translation.fileId
35
+ );
36
+
37
+ return readyTranslations;
38
+ } catch (error) {
39
+ console.error('Error checking translation status', error);
40
+ return [];
41
+ }
42
+ }
@@ -0,0 +1,16 @@
1
+ import { libraryDefaultLocale } from 'generaltranslation/internal';
2
+ import { gt, overrideConfig } from '../adapter/core';
3
+ import type { Secrets } from '../types';
4
+
5
+ export async function createJobs(
6
+ uploadResult: Awaited<ReturnType<typeof gt.uploadSourceFiles>>,
7
+ localeIds: string[],
8
+ secrets: Secrets
9
+ ): Promise<Awaited<ReturnType<typeof gt.enqueueFiles>>> {
10
+ overrideConfig(secrets);
11
+ const enqueueResult = await gt.enqueueFiles(uploadResult.uploadedFiles, {
12
+ sourceLocale: gt.sourceLocale || libraryDefaultLocale,
13
+ targetLocales: localeIds,
14
+ });
15
+ return enqueueResult;
16
+ }
@@ -0,0 +1,68 @@
1
+ import { gt, overrideConfig } from '../adapter/core';
2
+ import type { Secrets } from '../types';
3
+
4
+ export type BatchedFiles = Array<{
5
+ documentId: string;
6
+ versionId: string;
7
+ translationId: string;
8
+ locale: string;
9
+ }>;
10
+
11
+ export type DownloadedFile = {
12
+ docData: {
13
+ documentId: string;
14
+ versionId: string;
15
+ translationId: string;
16
+ locale: string;
17
+ };
18
+ data: string;
19
+ };
20
+ /**
21
+ * Downloads multiple translation files in a single batch request
22
+ * @param files - Array of files to download with their output paths
23
+ * @param maxRetries - Maximum number of retry attempts
24
+ * @param retryDelay - Delay between retries in milliseconds
25
+ * @returns Object containing successful and failed file IDs
26
+ */
27
+ export async function downloadTranslations(
28
+ files: BatchedFiles,
29
+ secrets: Secrets,
30
+ maxRetries = 3,
31
+ retryDelay = 1000
32
+ ): Promise<DownloadedFile[]> {
33
+ overrideConfig(secrets);
34
+ let retries = 0;
35
+ const fileIds = files.map((file) => file.translationId);
36
+
37
+ const map = new Map(files.map((file) => [file.translationId, file]));
38
+ const result = [] as DownloadedFile[];
39
+
40
+ while (retries <= maxRetries) {
41
+ try {
42
+ // Download the files
43
+ const responseData = await gt.downloadFileBatch(fileIds);
44
+ const downloadedFiles = responseData.files || [];
45
+
46
+ // Process each file in the response
47
+ for (const file of downloadedFiles) {
48
+ const documentData = map.get(file.id);
49
+ if (!documentData) {
50
+ continue;
51
+ }
52
+
53
+ result.push({
54
+ docData: documentData,
55
+ data: file.data,
56
+ });
57
+ }
58
+
59
+ return result;
60
+ } catch (error) {
61
+ // Increment retry counter and wait before next attempt
62
+ retries++;
63
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
64
+ }
65
+ }
66
+
67
+ return result;
68
+ }
@@ -0,0 +1,23 @@
1
+ import { pluginConfig } from '../adapter/core';
2
+ import { documentLevelPatch } from '../configuration/baseDocumentLevelConfig/documentLevelPatch';
3
+ import type { GTFile, TranslationFunctionContext } from '../types';
4
+ import { deserializeDocument } from '../utils/serialize';
5
+
6
+ export async function importDocument(
7
+ docInfo: GTFile,
8
+ localeId: string,
9
+ document: string,
10
+ context: TranslationFunctionContext,
11
+ mergeWithTargetLocale: boolean = false
12
+ ) {
13
+ const { client } = context;
14
+ const deserialized = deserializeDocument(document);
15
+ return documentLevelPatch(
16
+ docInfo, // versionId is not used here, since we just use the _rev id in the deserialized HTML itself
17
+ deserialized,
18
+ localeId,
19
+ client,
20
+ pluginConfig.getLanguageField(),
21
+ mergeWithTargetLocale
22
+ );
23
+ }
@@ -0,0 +1,61 @@
1
+ import { gt, overrideConfig } from '../adapter/core';
2
+ import type { Secrets } from '../types';
3
+
4
+ export async function initProject(
5
+ uploadResult: Awaited<ReturnType<typeof gt.uploadSourceFiles>>,
6
+ options: { timeout?: number },
7
+ secrets: Secrets
8
+ ): Promise<boolean> {
9
+ overrideConfig(secrets);
10
+ const setupDecision = await Promise.resolve(gt.shouldSetupProject?.())
11
+ .then((v: any) => v)
12
+ .catch(() => ({ shouldSetupProject: false }));
13
+ const shouldSetupProject = Boolean(setupDecision?.shouldSetupProject);
14
+
15
+ // Step 2: Setup if needed and poll until complete
16
+ if (shouldSetupProject) {
17
+ // Calculate timeout once for setup fetching
18
+ // Accept number or numeric string, default to 600s
19
+ const timeoutVal =
20
+ options?.timeout !== undefined ? Number(options.timeout) : 600;
21
+ const setupTimeoutMs =
22
+ (Number.isFinite(timeoutVal) ? timeoutVal : 600) * 1000;
23
+
24
+ const { setupJobId } = await gt.setupProject(uploadResult.uploadedFiles);
25
+
26
+ const start = Date.now();
27
+ const pollInterval = 2000;
28
+
29
+ let setupCompleted = false;
30
+ let setupFailedMessage: string | null = null;
31
+
32
+ while (true) {
33
+ const status = await gt.checkSetupStatus(setupJobId);
34
+
35
+ if (status.status === 'completed') {
36
+ setupCompleted = true;
37
+ break;
38
+ }
39
+ if (status.status === 'failed') {
40
+ setupFailedMessage = status.error?.message || 'Unknown error';
41
+ break;
42
+ }
43
+ if (Date.now() - start > setupTimeoutMs) {
44
+ setupFailedMessage = 'Timed out while waiting for setup generation';
45
+ break;
46
+ }
47
+ await new Promise((r) => setTimeout(r, pollInterval));
48
+ }
49
+
50
+ if (setupCompleted) {
51
+ console.log('Setup successfully completed');
52
+ return true;
53
+ } else {
54
+ console.log(`Setup ${setupFailedMessage ? 'failed' : 'timed out'} `);
55
+ return false;
56
+ }
57
+ } else {
58
+ console.log('Setup not needed');
59
+ }
60
+ return true;
61
+ }
@@ -0,0 +1,32 @@
1
+ import type { GTFile, Secrets } from '../types';
2
+ import { gt, overrideConfig } from '../adapter/core';
3
+ import { libraryDefaultLocale } from 'generaltranslation/internal';
4
+ import type { SerializedDocument } from '../serialization';
5
+
6
+ // note: this function is used to create a new translation task
7
+ // uploads files & calls the getTranslationTask function
8
+ export async function uploadFiles(
9
+ documents: {
10
+ info: GTFile;
11
+ serializedDocument: SerializedDocument;
12
+ }[],
13
+ secrets: Secrets | null
14
+ ): Promise<Awaited<ReturnType<typeof gt.uploadSourceFiles>>> {
15
+ overrideConfig(secrets);
16
+ const uploadResult = await gt.uploadSourceFiles(
17
+ documents.map(({ info, serializedDocument }) => ({
18
+ source: {
19
+ content: serializedDocument.content,
20
+ fileName: `sanity/${info.documentId}`,
21
+ fileId: info.documentId,
22
+ fileFormat: 'HTML',
23
+ locale: gt.sourceLocale || libraryDefaultLocale,
24
+ versionId: info.versionId || undefined,
25
+ },
26
+ })),
27
+ {
28
+ sourceLocale: gt.sourceLocale || libraryDefaultLocale,
29
+ }
30
+ );
31
+ return uploadResult;
32
+ }
package/src/types.ts CHANGED
@@ -1,7 +1,9 @@
1
+ // adapted from https://github.com/sanity-io/sanity-translations-tab. See LICENSE.md for more details.
2
+
1
3
  import { SanityClient, Schema, TypedObject } from 'sanity';
2
- import { SerializedDocument } from 'sanity-naive-html-serializer';
4
+ import type { SerializedDocument } from './serialization';
3
5
  import { PortableTextTypeComponent } from '@portabletext/to-html';
4
- import { DeserializerRule } from '@sanity/block-tools';
6
+ import type { DeserializerRule } from '@portabletext/block-tools';
5
7
 
6
8
  export type TranslationTaskLocaleStatus = {
7
9
  localeId: string;
@@ -72,15 +74,7 @@ export type GTSerializedDocument = Omit<SerializedDocument, 'name'> & GTFile;
72
74
 
73
75
  export type ExportForTranslation = (
74
76
  documentInfo: GTFile,
75
- context: TranslationFunctionContext,
76
- serializationOptions?: {
77
- additionalStopTypes?: string[];
78
- additionalSerializers?: Record<
79
- string,
80
- PortableTextTypeComponent | undefined
81
- >;
82
- },
83
- languageField?: string
77
+ context: TranslationFunctionContext
84
78
  ) => Promise<GTSerializedDocument>;
85
79
 
86
80
  export type ImportTranslation = (
@@ -88,15 +82,8 @@ export type ImportTranslation = (
88
82
  localeId: string,
89
83
  document: string,
90
84
  context: TranslationFunctionContext,
91
- serializationOptions?: {
92
- additionalDeserializers?: Record<
93
- string,
94
- (value: HTMLElement) => TypedObject
95
- >;
96
- additionalBlockDeserializers?: DeserializerRule[];
97
- },
98
- languageField?: string,
99
- mergeWithTargetLocale?: boolean
85
+ mergeWithTargetLocale?: boolean,
86
+ publish?: boolean
100
87
  ) => Promise<void>;
101
88
 
102
89
  export type TranslationsTabConfigOptions = {
@@ -0,0 +1,72 @@
1
+ import { JSONPath } from 'jsonpath-plus';
2
+ import JSONPointer from 'jsonpointer';
3
+ import type { IgnoreFields } from '../adapter/types';
4
+
5
+ export function applyDocuments(
6
+ documentId: string,
7
+ sourceDocument: Record<string, any>,
8
+ targetDocument: Record<string, any>,
9
+ ignore: IgnoreFields[]
10
+ ) {
11
+ const ignoreFields = ignore.filter(
12
+ (field) =>
13
+ field.documentId === documentId ||
14
+ field.documentId === undefined ||
15
+ field.documentId === null
16
+ );
17
+
18
+ // Start with a shallow copy of the source document
19
+ const mergedDocument = { ...sourceDocument };
20
+
21
+ // Merge top-level properties of targetDocument
22
+ for (const [key, value] of Object.entries(targetDocument)) {
23
+ mergedDocument[key] = value;
24
+ }
25
+
26
+ // Process ignored fields and restore them from source document
27
+ for (const ignoreField of ignoreFields) {
28
+ if (!ignoreField.fields) continue;
29
+
30
+ for (const field of ignoreField.fields) {
31
+ const { property, type } = field;
32
+
33
+ try {
34
+ // Use JSONPath to find matching paths, then JSONPointer to get/set values
35
+ const sourceResults = JSONPath({
36
+ json: sourceDocument,
37
+ path: property,
38
+ resultType: 'all',
39
+ flatten: true,
40
+ wrap: true,
41
+ });
42
+
43
+ if (sourceResults && sourceResults.length > 0) {
44
+ // Process each matching path
45
+ sourceResults.forEach((result: { pointer: string; value: any }) => {
46
+ const sourceValue = result.value;
47
+
48
+ // If type is specified, check if it matches the object's _type property
49
+ if (type !== undefined) {
50
+ if (
51
+ typeof sourceValue === 'object' &&
52
+ sourceValue !== null &&
53
+ sourceValue._type === type
54
+ ) {
55
+ // Type matches, restore the entire object using JSONPointer
56
+ JSONPointer.set(mergedDocument, result.pointer, sourceValue);
57
+ }
58
+ } else {
59
+ // No type specified, restore the value using JSONPointer
60
+ JSONPointer.set(mergedDocument, result.pointer, sourceValue);
61
+ }
62
+ });
63
+ }
64
+ } catch (error) {
65
+ // Invalid JSONPath, skip this field
66
+ console.warn(`Invalid JSONPath: ${property}`, error);
67
+ }
68
+ }
69
+ }
70
+
71
+ return mergedDocument;
72
+ }
@@ -0,0 +1,111 @@
1
+ import { GTFile, TranslationFunctionContext } from '../types';
2
+ import { importDocument } from '../translation/importDocument';
3
+
4
+ export interface BatchProcessorOptions {
5
+ batchSize?: number;
6
+ onProgress?: (current: number, total: number) => void;
7
+ onItemSuccess?: (item: any, result: any) => void;
8
+ onItemFailure?: (item: any, error: any) => void;
9
+ }
10
+
11
+ export interface ImportBatchItem {
12
+ docInfo: GTFile;
13
+ locale: string;
14
+ data: any;
15
+ translationContext: TranslationFunctionContext;
16
+ key: string;
17
+ }
18
+
19
+ export async function processBatch<T>(
20
+ items: T[],
21
+ processor: (item: T) => Promise<any>,
22
+ options: BatchProcessorOptions = {}
23
+ ): Promise<{
24
+ successCount: number;
25
+ failureCount: number;
26
+ successfulItems: any[];
27
+ failedItems: { item: T; error: any }[];
28
+ }> {
29
+ const { batchSize = 20, onProgress, onItemSuccess, onItemFailure } = options;
30
+
31
+ let successCount = 0;
32
+ let failureCount = 0;
33
+ const successfulItems: any[] = [];
34
+ const failedItems: { item: T; error: any }[] = [];
35
+
36
+ for (let i = 0; i < items.length; i += batchSize) {
37
+ const batch = items.slice(i, i + batchSize);
38
+
39
+ const batchPromises = batch.map(async (item) => {
40
+ try {
41
+ const result = await processor(item);
42
+ onItemSuccess?.(item, result);
43
+ return { success: true, item, result };
44
+ } catch (error) {
45
+ onItemFailure?.(item, error);
46
+ return { success: false, item, error };
47
+ }
48
+ });
49
+
50
+ const batchResults = await Promise.all(batchPromises);
51
+
52
+ batchResults.forEach((result) => {
53
+ if (result.success) {
54
+ successCount++;
55
+ successfulItems.push(result.result);
56
+ } else {
57
+ failureCount++;
58
+ failedItems.push({ item: result.item, error: result.error });
59
+ }
60
+ });
61
+
62
+ onProgress?.(i + batch.length, items.length);
63
+ }
64
+
65
+ return { successCount, failureCount, successfulItems, failedItems };
66
+ }
67
+
68
+ export async function processImportBatch(
69
+ items: ImportBatchItem[],
70
+ options: BatchProcessorOptions = {}
71
+ ): Promise<{
72
+ successCount: number;
73
+ failureCount: number;
74
+ successfulImports: string[];
75
+ failedItems: { item: ImportBatchItem; error: any }[];
76
+ }> {
77
+ const successfulImports: string[] = [];
78
+
79
+ const result = await processBatch(
80
+ items,
81
+ async (item: ImportBatchItem) => {
82
+ await importDocument(
83
+ item.docInfo,
84
+ item.locale,
85
+ item.data,
86
+ item.translationContext,
87
+ false
88
+ );
89
+ return item.key;
90
+ },
91
+ {
92
+ ...options,
93
+ onItemSuccess: (item: ImportBatchItem, key: string) => {
94
+ successfulImports.push(key);
95
+ options.onItemSuccess?.(item, key);
96
+ },
97
+ onItemFailure: (item: ImportBatchItem, error: any) => {
98
+ console.error(
99
+ `Failed to import ${item.docInfo.documentId} (${item.locale}):`,
100
+ error
101
+ );
102
+ options.onItemFailure?.(item, error);
103
+ },
104
+ }
105
+ );
106
+
107
+ return {
108
+ ...result,
109
+ successfulImports,
110
+ };
111
+ }
@@ -0,0 +1,95 @@
1
+ import { SanityDocument } from 'sanity';
2
+ import { GTFile, Secrets, TranslationFunctionContext } from '../types';
3
+ import {
4
+ downloadTranslations,
5
+ BatchedFiles,
6
+ } from '../translation/downloadTranslations';
7
+ import { processImportBatch, ImportBatchItem } from './batchProcessor';
8
+
9
+ export interface TranslationStatus {
10
+ progress: number;
11
+ isReady: boolean;
12
+ translationId?: string;
13
+ }
14
+
15
+ export interface ImportResult {
16
+ successCount: number;
17
+ failureCount: number;
18
+ successfulImports: string[];
19
+ }
20
+
21
+ export interface ImportOptions {
22
+ filterReadyFiles?: (key: string, status: TranslationStatus) => boolean;
23
+ onProgress?: (current: number, total: number) => void;
24
+ onImportSuccess?: (key: string) => void;
25
+ }
26
+
27
+ export async function getReadyFilesForImport(
28
+ documents: SanityDocument[],
29
+ translationStatuses: Map<string, TranslationStatus>,
30
+ options: ImportOptions = {}
31
+ ): Promise<BatchedFiles> {
32
+ const { filterReadyFiles = () => true } = options;
33
+ const readyFiles: BatchedFiles = [];
34
+
35
+ for (const [key, status] of translationStatuses.entries()) {
36
+ if (
37
+ status.isReady &&
38
+ status.translationId &&
39
+ filterReadyFiles(key, status)
40
+ ) {
41
+ const [documentId, locale] = key.split(':');
42
+ const document = documents.find(
43
+ (doc) => (doc._id?.replace('drafts.', '') || doc._id) === documentId
44
+ );
45
+
46
+ if (document) {
47
+ readyFiles.push({
48
+ documentId,
49
+ versionId: document._rev,
50
+ translationId: status.translationId,
51
+ locale,
52
+ });
53
+ }
54
+ }
55
+ }
56
+
57
+ return readyFiles;
58
+ }
59
+
60
+ export async function importTranslations(
61
+ readyFiles: BatchedFiles,
62
+ secrets: Secrets,
63
+ translationContext: TranslationFunctionContext,
64
+ options: ImportOptions = {}
65
+ ): Promise<ImportResult> {
66
+ if (readyFiles.length === 0) {
67
+ return { successCount: 0, failureCount: 0, successfulImports: [] };
68
+ }
69
+
70
+ const downloadedFiles = await downloadTranslations(readyFiles, secrets);
71
+
72
+ const importItems: ImportBatchItem[] = downloadedFiles.map((file) => ({
73
+ docInfo: {
74
+ documentId: file.docData.documentId,
75
+ versionId: file.docData.versionId,
76
+ },
77
+ locale: file.docData.locale,
78
+ data: file.data,
79
+ translationContext,
80
+ key: `${file.docData.documentId}:${file.docData.locale}`,
81
+ }));
82
+
83
+ const result = await processImportBatch(importItems, {
84
+ onProgress: options.onProgress,
85
+ onItemSuccess: (item, key) => {
86
+ options.onImportSuccess?.(key);
87
+ },
88
+ });
89
+
90
+ return {
91
+ successCount: result.successCount,
92
+ failureCount: result.failureCount,
93
+ successfulImports: result.successfulImports,
94
+ };
95
+ }
@@ -0,0 +1,52 @@
1
+ import { SanityDocument, Schema } from 'sanity';
2
+ import {
3
+ BaseDocumentDeserializer,
4
+ BaseDocumentSerializer,
5
+ defaultStopTypes,
6
+ customSerializers,
7
+ customBlockDeserializers,
8
+ } from '../serialization/';
9
+ import { PortableTextHtmlComponents } from '@portabletext/to-html';
10
+ import { pluginConfig } from '../adapter/core';
11
+ import { merge } from 'lodash';
12
+
13
+ export function deserializeDocument(document: string) {
14
+ const deserializers = merge(
15
+ { types: {} },
16
+ pluginConfig.getAdditionalDeserializers()
17
+ ) satisfies Partial<PortableTextHtmlComponents>;
18
+ const blockDeserializers = [
19
+ ...customBlockDeserializers,
20
+ ...pluginConfig.getAdditionalBlockDeserializers(),
21
+ ];
22
+ const deserialized = BaseDocumentDeserializer.deserializeDocument(
23
+ document,
24
+ deserializers,
25
+ blockDeserializers
26
+ ) as SanityDocument;
27
+ return deserialized;
28
+ }
29
+
30
+ export function serializeDocument(
31
+ document: SanityDocument,
32
+ schema: Schema,
33
+ baseLanguage: string
34
+ ) {
35
+ const stopTypes = [
36
+ ...defaultStopTypes,
37
+ ...pluginConfig.getAdditionalStopTypes(),
38
+ ];
39
+ const serializers = merge(
40
+ customSerializers,
41
+ pluginConfig.getAdditionalSerializers()
42
+ ) satisfies Partial<PortableTextHtmlComponents>;
43
+
44
+ const serialized = BaseDocumentSerializer(schema).serializeDocument(
45
+ document,
46
+ 'document',
47
+ baseLanguage,
48
+ stopTypes,
49
+ serializers
50
+ );
51
+ return serialized;
52
+ }
@@ -0,0 +1 @@
1
+ export const SECRETS_NAMESPACE = 'generaltranslation.secrets';
@@ -1,13 +0,0 @@
1
- import type { Adapter } from '../types';
2
-
3
- import { getLocales } from './getLocales';
4
- import { getTranslationTask } from './getTranslationTask';
5
- import { getTranslation } from './getTranslation';
6
- import { createTask } from './createTask';
7
-
8
- export const GTAdapter: Adapter = {
9
- getLocales,
10
- getTranslationTask,
11
- createTask,
12
- getTranslation,
13
- };