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.
- package/LICENSE.md +1 -8
- package/README.md +5 -5
- package/dist/index.d.mts +122 -95
- package/dist/index.d.ts +122 -95
- package/dist/index.js +9089 -1119
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +9099 -1100
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -4
- package/src/adapter/core.ts +111 -9
- package/src/adapter/createTask.ts +1 -1
- package/src/adapter/getLocales.ts +2 -2
- package/src/adapter/types.ts +9 -0
- 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 +338 -0
- package/src/components/shared/BaseTranslationWrapper.tsx +82 -0
- package/src/components/{LanguageStatus.tsx → shared/LanguageStatus.tsx} +2 -0
- package/src/components/shared/LocaleCheckbox.tsx +47 -0
- package/src/components/{ProgressBar.tsx → shared/ProgressBar.tsx} +2 -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 +21 -11
- package/src/configuration/baseDocumentLevelConfig/helpers/createI18nDocAndPatchMetadata.ts +57 -23
- package/src/configuration/baseDocumentLevelConfig/helpers/createTranslationMetadata.ts +2 -0
- package/src/configuration/baseDocumentLevelConfig/helpers/getOrCreateTranslationMetadata.ts +2 -0
- package/src/configuration/baseDocumentLevelConfig/helpers/getTranslationMetadata.ts +2 -0
- package/src/configuration/baseDocumentLevelConfig/helpers/patchI18nDoc.ts +31 -8
- package/src/configuration/baseDocumentLevelConfig/index.ts +18 -101
- package/src/configuration/baseFieldLevelConfig.ts +19 -51
- package/src/configuration/utils/checkSerializationVersion.ts +2 -0
- package/src/configuration/utils/findDocumentAtRevision.ts +2 -0
- package/src/configuration/utils/findLatestDraft.ts +2 -0
- package/src/hooks/useClient.ts +3 -1
- package/src/hooks/useSecrets.ts +2 -0
- package/src/index.ts +91 -67
- 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/checkTranslationStatus.ts +42 -0
- package/src/translation/createJobs.ts +16 -0
- package/src/translation/downloadTranslations.ts +68 -0
- package/src/translation/importDocument.ts +23 -0
- package/src/translation/initProject.ts +61 -0
- package/src/translation/uploadFiles.ts +32 -0
- package/src/types.ts +7 -20
- package/src/utils/applyDocuments.ts +72 -0
- package/src/utils/batchProcessor.ts +111 -0
- package/src/utils/importUtils.ts +95 -0
- package/src/utils/serialize.ts +52 -0
- package/src/utils/shared.ts +1 -0
- package/src/adapter/index.ts +0 -13
- package/src/components/NewTask.tsx +0 -249
- package/src/components/TaskView.tsx +0 -255
- package/src/components/TranslationContext.tsx +0 -19
- package/src/components/TranslationView.tsx +0 -82
- package/src/components/TranslationsTab.tsx +0 -177
- package/src/configuration/baseDocumentLevelConfig/helpers/index.ts +0 -5
- package/src/configuration/baseDocumentLevelConfig/legacyDocumentLevelPatch.ts +0 -69
- package/src/configuration/index.ts +0 -18
- 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 '
|
|
4
|
+
import type { SerializedDocument } from './serialization';
|
|
3
5
|
import { PortableTextTypeComponent } from '@portabletext/to-html';
|
|
4
|
-
import { DeserializerRule } from '@
|
|
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
|
-
|
|
92
|
-
|
|
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';
|
package/src/adapter/index.ts
DELETED
|
@@ -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
|
-
};
|