gt-sanity 2.0.21 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1220 -763
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +113 -1
- package/dist/index.d.ts +113 -1
- package/dist/index.js +1211 -754
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/adapter/core.ts +27 -2
- package/src/adapter/types.ts +9 -0
- package/src/components/TranslationsProvider.tsx +92 -6
- package/src/components/page/TranslationsTable.tsx +5 -3
- package/src/components/shared/SingleDocumentView.tsx +5 -3
- package/src/components/tab/TranslationView.tsx +24 -14
- package/src/configuration/baseDocumentLevelConfig/documentLevelPatch.ts +1 -1
- package/src/configuration/baseFieldLevelConfig.ts +1 -1
- package/src/configuration/internationalizedArrayConfig/internationalizedArrayPatch.ts +59 -0
- package/src/index.ts +114 -58
- package/src/schema/InternationalizedArrayInput.tsx +278 -0
- package/src/schema/__tests__/createInternationalizedArrayTypes.test.ts +169 -0
- package/src/schema/createInternationalizedArrayTypes.ts +209 -0
- package/src/schema/types.ts +84 -0
- package/src/serialization/__tests__/BaseDocumentDeserializer/baseDeserialization.test.ts +3 -3
- package/src/serialization/__tests__/BaseDocumentMerger/baseMerge.test.ts +1 -1
- package/src/serialization/__tests__/BaseDocumentMerger/documentLevelMerge.test.ts +1 -1
- package/src/serialization/__tests__/BaseDocumentMerger/fieldLevelMerge.test.ts +1 -1
- package/src/serialization/__tests__/BaseDocumentSerializer/baseSerialization.test.ts +2 -2
- package/src/serialization/__tests__/BaseDocumentSerializer/documentInlineMarks.test.ts +4 -2
- package/src/serialization/__tests__/global.setup.ts +6 -0
- package/src/serialization/__tests__/helpers.ts +2 -1
- package/src/serialization/internationalizedArray/__tests__/internationalizedArray.test.ts +265 -0
- package/src/serialization/internationalizedArray/__tests__/serializeRoundTrip.test.ts +83 -0
- package/src/serialization/internationalizedArray/collapse.ts +52 -0
- package/src/serialization/internationalizedArray/detect.ts +81 -0
- package/src/serialization/internationalizedArray/merge.ts +149 -0
- package/src/serialization/types.ts +5 -1
- package/src/translation/__tests__/strategy.test.ts +47 -0
- package/src/translation/importDocument.ts +9 -5
- package/src/translation/strategy.ts +92 -0
- package/src/translation/uploadFiles.ts +1 -1
- package/src/types.ts +1 -1
- package/src/utils/__tests__/batchProcessor.test.ts +61 -2
- package/src/utils/batchProcessor.ts +11 -6
- package/src/utils/serialize.ts +24 -7
- package/src/serialization/index.ts +0 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gt-sanity",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "General Translation integration with Sanity",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sanity",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"jsonpath-plus": "^10.3.0",
|
|
55
55
|
"jsonpointer": "^5.0.1",
|
|
56
56
|
"lodash.merge": "^4.6.2",
|
|
57
|
-
"generaltranslation": "
|
|
57
|
+
"generaltranslation": "9.0.1"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@portabletext/types": "^2.0.15",
|
package/src/adapter/core.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
DedupeFields,
|
|
7
7
|
IgnoreFields,
|
|
8
8
|
SkipFields,
|
|
9
|
+
FieldLevelTranslationMode,
|
|
9
10
|
} from './types';
|
|
10
11
|
import { SECRETS_NAMESPACE } from '../utils/shared';
|
|
11
12
|
import type { PortableTextHtmlComponents } from '@portabletext/to-html';
|
|
@@ -34,6 +35,9 @@ export class GTConfig {
|
|
|
34
35
|
additionalSerializers: Partial<PortableTextHtmlComponents>;
|
|
35
36
|
additionalDeserializers: CustomDeserializers;
|
|
36
37
|
additionalBlockDeserializers: unknown[];
|
|
38
|
+
translationLevel: FieldLevelTranslationMode;
|
|
39
|
+
fieldLevelDocuments: TranslateDocumentFilter[];
|
|
40
|
+
fieldLevelTypePrefix: string;
|
|
37
41
|
|
|
38
42
|
private static instance: GTConfig;
|
|
39
43
|
constructor(
|
|
@@ -50,7 +54,10 @@ export class GTConfig {
|
|
|
50
54
|
additionalStopTypes: string[] = [],
|
|
51
55
|
additionalSerializers: Partial<PortableTextHtmlComponents> = {},
|
|
52
56
|
additionalDeserializers: CustomDeserializers = { types: {} },
|
|
53
|
-
additionalBlockDeserializers: unknown[] = []
|
|
57
|
+
additionalBlockDeserializers: unknown[] = [],
|
|
58
|
+
translationLevel: FieldLevelTranslationMode = 'document',
|
|
59
|
+
fieldLevelDocuments: TranslateDocumentFilter[] = [],
|
|
60
|
+
fieldLevelTypePrefix: string = 'internationalizedArray'
|
|
54
61
|
) {
|
|
55
62
|
this.secretsNamespace = secretsNamespace;
|
|
56
63
|
this.languageField = languageField;
|
|
@@ -66,6 +73,9 @@ export class GTConfig {
|
|
|
66
73
|
this.additionalSerializers = additionalSerializers;
|
|
67
74
|
this.additionalDeserializers = additionalDeserializers;
|
|
68
75
|
this.additionalBlockDeserializers = additionalBlockDeserializers;
|
|
76
|
+
this.translationLevel = translationLevel;
|
|
77
|
+
this.fieldLevelDocuments = fieldLevelDocuments;
|
|
78
|
+
this.fieldLevelTypePrefix = fieldLevelTypePrefix;
|
|
69
79
|
}
|
|
70
80
|
|
|
71
81
|
static getInstance() {
|
|
@@ -104,7 +114,10 @@ export class GTConfig {
|
|
|
104
114
|
additionalStopTypes: string[] = [],
|
|
105
115
|
additionalSerializers: Partial<PortableTextHtmlComponents> = {},
|
|
106
116
|
additionalDeserializers: CustomDeserializers = { types: {} },
|
|
107
|
-
additionalBlockDeserializers: unknown[] = []
|
|
117
|
+
additionalBlockDeserializers: unknown[] = [],
|
|
118
|
+
translationLevel: FieldLevelTranslationMode = 'document',
|
|
119
|
+
fieldLevelDocuments: TranslateDocumentFilter[] = [],
|
|
120
|
+
fieldLevelTypePrefix: string = 'internationalizedArray'
|
|
108
121
|
) {
|
|
109
122
|
this.secretsNamespace = secretsNamespace;
|
|
110
123
|
this.languageField = languageField;
|
|
@@ -120,6 +133,9 @@ export class GTConfig {
|
|
|
120
133
|
this.additionalSerializers = additionalSerializers;
|
|
121
134
|
this.additionalDeserializers = additionalDeserializers;
|
|
122
135
|
this.additionalBlockDeserializers = additionalBlockDeserializers;
|
|
136
|
+
this.translationLevel = translationLevel;
|
|
137
|
+
this.fieldLevelDocuments = fieldLevelDocuments;
|
|
138
|
+
this.fieldLevelTypePrefix = fieldLevelTypePrefix;
|
|
123
139
|
}
|
|
124
140
|
|
|
125
141
|
getSecretsNamespace() {
|
|
@@ -166,5 +182,14 @@ export class GTConfig {
|
|
|
166
182
|
getAdditionalBlockDeserializers() {
|
|
167
183
|
return this.additionalBlockDeserializers;
|
|
168
184
|
}
|
|
185
|
+
getTranslationLevel() {
|
|
186
|
+
return this.translationLevel;
|
|
187
|
+
}
|
|
188
|
+
getFieldLevelDocuments() {
|
|
189
|
+
return this.fieldLevelDocuments;
|
|
190
|
+
}
|
|
191
|
+
getFieldLevelTypePrefix() {
|
|
192
|
+
return this.fieldLevelTypePrefix;
|
|
193
|
+
}
|
|
169
194
|
}
|
|
170
195
|
export const pluginConfig = GTConfig.getInstance();
|
package/src/adapter/types.ts
CHANGED
|
@@ -14,6 +14,15 @@ export type TranslateDocumentFilter = {
|
|
|
14
14
|
type?: string;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
// How matched documents are translated:
|
|
18
|
+
// - 'document' translate whole documents (per-locale documents).
|
|
19
|
+
// - 'internationalizedArray' localize fields in place (array shape).
|
|
20
|
+
// - 'mixed' array strategy for `fieldLevelDocuments`, else doc.
|
|
21
|
+
export type FieldLevelTranslationMode =
|
|
22
|
+
| 'document'
|
|
23
|
+
| 'internationalizedArray'
|
|
24
|
+
| 'mixed';
|
|
25
|
+
|
|
17
26
|
export type FileProperties = {
|
|
18
27
|
versionId: string;
|
|
19
28
|
fileId: string;
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
TranslationFunctionContext,
|
|
19
19
|
} from '../types';
|
|
20
20
|
import { gt, overrideConfig, pluginConfig } from '../adapter/core';
|
|
21
|
-
import {
|
|
21
|
+
import { getTranslationStrategy } from '../translation/strategy';
|
|
22
22
|
import { uploadFiles } from '../translation/uploadFiles';
|
|
23
23
|
import { initProject } from '../translation/initProject';
|
|
24
24
|
import { createJobs } from '../translation/createJobs';
|
|
@@ -84,6 +84,11 @@ interface TranslationsContextType {
|
|
|
84
84
|
loadingSecrets: boolean;
|
|
85
85
|
secrets: Secrets | null;
|
|
86
86
|
branchId: string | undefined;
|
|
87
|
+
// Version to use for translation status keys and GT file queries. Prefers
|
|
88
|
+
// the _rev pinned at upload time over the live _rev, so in-place imports
|
|
89
|
+
// (internationalized arrays) bumping the document _rev don't orphan the
|
|
90
|
+
// statuses of the version actually uploaded to GT.
|
|
91
|
+
getVersionId: (document: SanityDocument) => string;
|
|
87
92
|
|
|
88
93
|
// Actions
|
|
89
94
|
setLocales: (locales: TranslationLocale[]) => void;
|
|
@@ -106,6 +111,51 @@ interface TranslationsContextType {
|
|
|
106
111
|
|
|
107
112
|
const TranslationsContext = createContext<TranslationsContextType | null>(null);
|
|
108
113
|
|
|
114
|
+
// Pinned upload versions are mirrored to sessionStorage so they survive a
|
|
115
|
+
// Studio page refresh: falling back to the live document._rev after a refresh
|
|
116
|
+
// would query GT with a version that was never uploaded (in-place imports bump
|
|
117
|
+
// the _rev), making completed translations show as "not started". Keyed by
|
|
118
|
+
// project + dataset so Studios sharing an origin don't collide.
|
|
119
|
+
const getUploadedVersionsStorageKey = (
|
|
120
|
+
projectId: string | undefined,
|
|
121
|
+
dataset: string | undefined
|
|
122
|
+
) => `gt-sanity:uploadedVersions:${projectId ?? ''}:${dataset ?? ''}`;
|
|
123
|
+
|
|
124
|
+
function readUploadedVersions(storageKey: string): Map<string, string> {
|
|
125
|
+
try {
|
|
126
|
+
const raw = window.sessionStorage.getItem(storageKey);
|
|
127
|
+
if (!raw) return new Map();
|
|
128
|
+
const parsed: unknown = JSON.parse(raw);
|
|
129
|
+
if (!isPlainRecord(parsed)) return new Map();
|
|
130
|
+
return new Map(
|
|
131
|
+
Object.entries(parsed).filter(
|
|
132
|
+
(entry): entry is [string, string] => typeof entry[1] === 'string'
|
|
133
|
+
)
|
|
134
|
+
);
|
|
135
|
+
} catch {
|
|
136
|
+
return new Map();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function writeUploadedVersions(
|
|
141
|
+
storageKey: string,
|
|
142
|
+
versions: Map<string, string>
|
|
143
|
+
): void {
|
|
144
|
+
try {
|
|
145
|
+
window.sessionStorage.setItem(
|
|
146
|
+
storageKey,
|
|
147
|
+
JSON.stringify(Object.fromEntries(versions))
|
|
148
|
+
);
|
|
149
|
+
} catch {
|
|
150
|
+
// sessionStorage unavailable (SSR, private mode, quota) — pinned versions
|
|
151
|
+
// fall back to in-memory only.
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
156
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
157
|
+
}
|
|
158
|
+
|
|
109
159
|
export const useTranslations = () => {
|
|
110
160
|
const context = useContext(TranslationsContext);
|
|
111
161
|
if (!context) {
|
|
@@ -154,6 +204,14 @@ export const TranslationsProvider: React.FC<TranslationsProviderProps> = ({
|
|
|
154
204
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
155
205
|
|
|
156
206
|
const client = useClient();
|
|
207
|
+
const { projectId, dataset } = client.config();
|
|
208
|
+
const uploadedVersionsStorageKey = getUploadedVersionsStorageKey(
|
|
209
|
+
projectId,
|
|
210
|
+
dataset
|
|
211
|
+
);
|
|
212
|
+
const [uploadedVersions, setUploadedVersions] = useState<Map<string, string>>(
|
|
213
|
+
() => readUploadedVersions(uploadedVersionsStorageKey)
|
|
214
|
+
);
|
|
157
215
|
const schema = useSchema();
|
|
158
216
|
const translationContext: TranslationFunctionContext = { client, schema };
|
|
159
217
|
const toast = useToast();
|
|
@@ -166,6 +224,12 @@ export const TranslationsProvider: React.FC<TranslationsProviderProps> = ({
|
|
|
166
224
|
downloadStatusRef.current = downloadStatus;
|
|
167
225
|
}, [downloadStatus]);
|
|
168
226
|
|
|
227
|
+
const getVersionId = useCallback(
|
|
228
|
+
(document: SanityDocument) =>
|
|
229
|
+
uploadedVersions.get(getDocumentPublishedId(document)) ?? document._rev,
|
|
230
|
+
[uploadedVersions]
|
|
231
|
+
);
|
|
232
|
+
|
|
169
233
|
const fetchDocuments = useCallback(async () => {
|
|
170
234
|
setLoadingDocuments(true);
|
|
171
235
|
try {
|
|
@@ -299,7 +363,8 @@ export const TranslationsProvider: React.FC<TranslationsProviderProps> = ({
|
|
|
299
363
|
const { [pluginConfig.getLanguageField()]: _, ...cleanDoc } = doc;
|
|
300
364
|
const baseLanguage = pluginConfig.getSourceLocale();
|
|
301
365
|
try {
|
|
302
|
-
const
|
|
366
|
+
const strategy = getTranslationStrategy(doc);
|
|
367
|
+
const serialized = strategy.serialize(
|
|
303
368
|
cleanDoc as typeof doc,
|
|
304
369
|
schema,
|
|
305
370
|
baseLanguage
|
|
@@ -322,6 +387,19 @@ export const TranslationsProvider: React.FC<TranslationsProviderProps> = ({
|
|
|
322
387
|
await initProject(uploadResult, { timeout: 600 }, secrets);
|
|
323
388
|
await createJobs(uploadResult, availableLocaleIds, secrets);
|
|
324
389
|
|
|
390
|
+
// Pin the _rev each file was uploaded under. GT status queries need the
|
|
391
|
+
// exact uploaded versionId, and the live _rev moves on (in-place array
|
|
392
|
+
// imports bump it), so status lookups go through getVersionId instead.
|
|
393
|
+
// Persisted to sessionStorage so the pins survive a page refresh.
|
|
394
|
+
const nextUploadedVersions = new Map(uploadedVersions);
|
|
395
|
+
for (const { info } of transformedDocuments) {
|
|
396
|
+
if (info.versionId) {
|
|
397
|
+
nextUploadedVersions.set(info.documentId, info.versionId);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
writeUploadedVersions(uploadedVersionsStorageKey, nextUploadedVersions);
|
|
401
|
+
setUploadedVersions(nextUploadedVersions);
|
|
402
|
+
|
|
325
403
|
toast.push({
|
|
326
404
|
title: `Translation tasks created for ${documents.length} documents`,
|
|
327
405
|
status: 'success',
|
|
@@ -337,7 +415,14 @@ export const TranslationsProvider: React.FC<TranslationsProviderProps> = ({
|
|
|
337
415
|
} finally {
|
|
338
416
|
setIsBusy(false);
|
|
339
417
|
}
|
|
340
|
-
}, [
|
|
418
|
+
}, [
|
|
419
|
+
secrets,
|
|
420
|
+
documents,
|
|
421
|
+
locales,
|
|
422
|
+
schema,
|
|
423
|
+
uploadedVersions,
|
|
424
|
+
uploadedVersionsStorageKey,
|
|
425
|
+
]);
|
|
341
426
|
|
|
342
427
|
const handleImportAll = useCallback(async () => {
|
|
343
428
|
if (!secrets || documents.length === 0 || !branchId) return;
|
|
@@ -592,7 +677,7 @@ export const TranslationsProvider: React.FC<TranslationsProviderProps> = ({
|
|
|
592
677
|
for (const localeId of availableLocaleIds) {
|
|
593
678
|
const documentId = getDocumentPublishedId(doc);
|
|
594
679
|
fileQueryData.push({
|
|
595
|
-
versionId: doc
|
|
680
|
+
versionId: getVersionId(doc),
|
|
596
681
|
fileId: documentId,
|
|
597
682
|
branchId: branchId,
|
|
598
683
|
locale: localeId,
|
|
@@ -612,7 +697,7 @@ export const TranslationsProvider: React.FC<TranslationsProviderProps> = ({
|
|
|
612
697
|
for (const doc of documents) {
|
|
613
698
|
for (const localeId of availableLocaleIds) {
|
|
614
699
|
const documentId = getDocumentPublishedId(doc);
|
|
615
|
-
const versionId = doc
|
|
700
|
+
const versionId = getVersionId(doc);
|
|
616
701
|
const key = createTranslationStatusKey(
|
|
617
702
|
branchId,
|
|
618
703
|
documentId,
|
|
@@ -663,7 +748,7 @@ export const TranslationsProvider: React.FC<TranslationsProviderProps> = ({
|
|
|
663
748
|
} finally {
|
|
664
749
|
setIsRefreshing(false);
|
|
665
750
|
}
|
|
666
|
-
}, [secrets, documents, locales, branchId]);
|
|
751
|
+
}, [secrets, documents, locales, branchId, getVersionId]);
|
|
667
752
|
|
|
668
753
|
const handleImportDocument = useCallback(
|
|
669
754
|
async (documentId: string, versionId: string, localeId: string) => {
|
|
@@ -1038,6 +1123,7 @@ export const TranslationsProvider: React.FC<TranslationsProviderProps> = ({
|
|
|
1038
1123
|
loadingSecrets,
|
|
1039
1124
|
secrets,
|
|
1040
1125
|
branchId,
|
|
1126
|
+
getVersionId,
|
|
1041
1127
|
|
|
1042
1128
|
// Actions
|
|
1043
1129
|
setLocales,
|
|
@@ -17,6 +17,7 @@ export const TranslationsTable: React.FC = () => {
|
|
|
17
17
|
importedTranslations,
|
|
18
18
|
handleImportDocument,
|
|
19
19
|
branchId,
|
|
20
|
+
getVersionId,
|
|
20
21
|
} = useTranslations();
|
|
21
22
|
|
|
22
23
|
if (loadingDocuments) {
|
|
@@ -50,10 +51,11 @@ export const TranslationsTable: React.FC = () => {
|
|
|
50
51
|
.filter((locale) => locale.enabled !== false)
|
|
51
52
|
.map((locale) => {
|
|
52
53
|
const documentId = getDocumentPublishedId(document);
|
|
54
|
+
const versionId = getVersionId(document);
|
|
53
55
|
const key = createTranslationStatusKey(
|
|
54
56
|
branchId,
|
|
55
57
|
documentId,
|
|
56
|
-
|
|
58
|
+
versionId,
|
|
57
59
|
locale.localeId
|
|
58
60
|
);
|
|
59
61
|
const status = translationStatuses.get(key);
|
|
@@ -62,14 +64,14 @@ export const TranslationsTable: React.FC = () => {
|
|
|
62
64
|
|
|
63
65
|
return (
|
|
64
66
|
<LanguageStatus
|
|
65
|
-
key={`${document._id}-${
|
|
67
|
+
key={`${document._id}-${versionId}-${locale.localeId}`}
|
|
66
68
|
title={locale.description || locale.localeId}
|
|
67
69
|
progress={status?.progress || 0}
|
|
68
70
|
isImported={isImported || isDownloaded}
|
|
69
71
|
importFile={async () => {
|
|
70
72
|
await handleImportDocument(
|
|
71
73
|
documentId,
|
|
72
|
-
|
|
74
|
+
versionId,
|
|
73
75
|
locale.localeId
|
|
74
76
|
);
|
|
75
77
|
}}
|
|
@@ -18,6 +18,7 @@ export const SingleDocumentView: React.FC = () => {
|
|
|
18
18
|
importedTranslations,
|
|
19
19
|
handleImportDocument,
|
|
20
20
|
branchId,
|
|
21
|
+
getVersionId,
|
|
21
22
|
} = useTranslations();
|
|
22
23
|
|
|
23
24
|
// Get the first (and only) document in single document mode
|
|
@@ -78,10 +79,11 @@ export const SingleDocumentView: React.FC = () => {
|
|
|
78
79
|
.filter((locale) => locale.enabled !== false)
|
|
79
80
|
.map((locale) => {
|
|
80
81
|
const documentId = getDocumentPublishedId(document);
|
|
82
|
+
const versionId = getVersionId(document);
|
|
81
83
|
const key = createTranslationStatusKey(
|
|
82
84
|
branchId,
|
|
83
85
|
documentId,
|
|
84
|
-
|
|
86
|
+
versionId,
|
|
85
87
|
locale.localeId
|
|
86
88
|
);
|
|
87
89
|
const status = translationStatuses.get(key);
|
|
@@ -90,14 +92,14 @@ export const SingleDocumentView: React.FC = () => {
|
|
|
90
92
|
|
|
91
93
|
return (
|
|
92
94
|
<LanguageStatus
|
|
93
|
-
key={`${document._id}-${
|
|
95
|
+
key={`${document._id}-${versionId}-${locale.localeId}`}
|
|
94
96
|
title={locale.description || locale.localeId}
|
|
95
97
|
progress={status?.progress || 0}
|
|
96
98
|
isImported={isImported || isDownloaded}
|
|
97
99
|
importFile={async () => {
|
|
98
100
|
await handleImportDocument(
|
|
99
101
|
documentId,
|
|
100
|
-
|
|
102
|
+
versionId,
|
|
101
103
|
locale.localeId
|
|
102
104
|
);
|
|
103
105
|
}}
|
|
@@ -50,6 +50,7 @@ export const TranslationView = () => {
|
|
|
50
50
|
setAutoPatchReferences,
|
|
51
51
|
autoPublish,
|
|
52
52
|
setAutoPublish,
|
|
53
|
+
getVersionId,
|
|
53
54
|
} = useTranslations();
|
|
54
55
|
|
|
55
56
|
const [isImporting, setIsImporting] = useState(false);
|
|
@@ -92,13 +93,21 @@ export const TranslationView = () => {
|
|
|
92
93
|
return getDocumentPublishedId(document);
|
|
93
94
|
}, [document]);
|
|
94
95
|
|
|
96
|
+
// Version the status entries are keyed under: the _rev pinned at upload
|
|
97
|
+
// time when available, so importing in place (which bumps the live _rev)
|
|
98
|
+
// doesn't detach the UI from the statuses of the uploaded version.
|
|
99
|
+
const versionId = useMemo(() => {
|
|
100
|
+
if (!document) return null;
|
|
101
|
+
return getVersionId(document);
|
|
102
|
+
}, [document, getVersionId]);
|
|
103
|
+
|
|
95
104
|
// Unified import functionality
|
|
96
105
|
const handleImportTranslations = useCallback(
|
|
97
106
|
async (options: { autoOnly?: boolean } = {}) => {
|
|
98
107
|
const { autoOnly = false } = options;
|
|
99
108
|
|
|
100
109
|
// Check preconditions
|
|
101
|
-
if (isImporting || !documentId) return;
|
|
110
|
+
if (isImporting || !documentId || !versionId) return;
|
|
102
111
|
if (autoOnly && !autoImport) return;
|
|
103
112
|
|
|
104
113
|
// Find translations ready to import
|
|
@@ -106,7 +115,7 @@ export const TranslationView = () => {
|
|
|
106
115
|
const key = createTranslationStatusKey(
|
|
107
116
|
branchId,
|
|
108
117
|
documentId,
|
|
109
|
-
|
|
118
|
+
versionId,
|
|
110
119
|
locale.localeId
|
|
111
120
|
);
|
|
112
121
|
const status = translationStatuses.get(key);
|
|
@@ -117,12 +126,12 @@ export const TranslationView = () => {
|
|
|
117
126
|
|
|
118
127
|
setIsImporting(true);
|
|
119
128
|
try {
|
|
120
|
-
// Import
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
)
|
|
125
|
-
|
|
129
|
+
// Import sequentially: internationalized-array imports patch this same
|
|
130
|
+
// document in place, so concurrent locale imports would overwrite each
|
|
131
|
+
// other and only the last locale would survive.
|
|
132
|
+
for (const locale of readyTranslations) {
|
|
133
|
+
await handleImportDocument(documentId, versionId, locale.localeId);
|
|
134
|
+
}
|
|
126
135
|
|
|
127
136
|
// Auto patch document references if enabled
|
|
128
137
|
if (autoPatchReferences) {
|
|
@@ -141,6 +150,7 @@ export const TranslationView = () => {
|
|
|
141
150
|
autoImport,
|
|
142
151
|
isImporting,
|
|
143
152
|
documentId,
|
|
153
|
+
versionId,
|
|
144
154
|
availableLocales,
|
|
145
155
|
translationStatuses,
|
|
146
156
|
importedTranslations,
|
|
@@ -261,7 +271,7 @@ export const TranslationView = () => {
|
|
|
261
271
|
</Stack>
|
|
262
272
|
|
|
263
273
|
{/* Translation Status Section */}
|
|
264
|
-
{documentId && availableLocales.length > 0 && (
|
|
274
|
+
{documentId && versionId && availableLocales.length > 0 && (
|
|
265
275
|
<Stack space={4}>
|
|
266
276
|
<Flex align='center' justify='space-between'>
|
|
267
277
|
<Text as='h2' weight='semibold' size={2}>
|
|
@@ -290,7 +300,7 @@ export const TranslationView = () => {
|
|
|
290
300
|
const key = createTranslationStatusKey(
|
|
291
301
|
branchId,
|
|
292
302
|
documentId,
|
|
293
|
-
|
|
303
|
+
versionId,
|
|
294
304
|
locale.localeId
|
|
295
305
|
);
|
|
296
306
|
const status = translationStatuses.get(key);
|
|
@@ -307,7 +317,7 @@ export const TranslationView = () => {
|
|
|
307
317
|
if (!isImported && status?.isReady) {
|
|
308
318
|
await handleImportDocument(
|
|
309
319
|
documentId,
|
|
310
|
-
|
|
320
|
+
versionId,
|
|
311
321
|
locale.localeId
|
|
312
322
|
);
|
|
313
323
|
}
|
|
@@ -333,7 +343,7 @@ export const TranslationView = () => {
|
|
|
333
343
|
const key = createTranslationStatusKey(
|
|
334
344
|
branchId,
|
|
335
345
|
documentId,
|
|
336
|
-
|
|
346
|
+
versionId,
|
|
337
347
|
locale.localeId
|
|
338
348
|
);
|
|
339
349
|
const status = translationStatuses.get(key);
|
|
@@ -358,7 +368,7 @@ export const TranslationView = () => {
|
|
|
358
368
|
const key = createTranslationStatusKey(
|
|
359
369
|
branchId,
|
|
360
370
|
documentId,
|
|
361
|
-
|
|
371
|
+
versionId,
|
|
362
372
|
locale.localeId
|
|
363
373
|
);
|
|
364
374
|
return importedTranslations.has(key);
|
|
@@ -370,7 +380,7 @@ export const TranslationView = () => {
|
|
|
370
380
|
const key = createTranslationStatusKey(
|
|
371
381
|
branchId,
|
|
372
382
|
documentId,
|
|
373
|
-
|
|
383
|
+
versionId,
|
|
374
384
|
locale.localeId
|
|
375
385
|
);
|
|
376
386
|
const status = translationStatuses.get(key);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// adapted from https://github.com/sanity-io/sanity-translations-tab. See LICENSE.md for more details.
|
|
2
2
|
|
|
3
3
|
import { SanityClient, SanityDocument, SanityDocumentLike } from 'sanity';
|
|
4
|
-
import { BaseDocumentMerger } from '../../serialization';
|
|
4
|
+
import { BaseDocumentMerger } from '../../serialization/BaseDocumentMerger';
|
|
5
5
|
|
|
6
6
|
import { findLatestDraft } from '../utils/findLatestDraft';
|
|
7
7
|
import { findDocumentAtRevision } from '../utils/findDocumentAtRevision';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// adapted from https://github.com/sanity-io/sanity-translations-tab. See LICENSE.md for more details.
|
|
2
2
|
|
|
3
3
|
import { SanityClient, SanityDocument } from 'sanity';
|
|
4
|
-
import { BaseDocumentMerger } from '../serialization/';
|
|
4
|
+
import { BaseDocumentMerger } from '../serialization/BaseDocumentMerger';
|
|
5
5
|
|
|
6
6
|
import type {
|
|
7
7
|
ExportForTranslation,
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { SanityClient, SanityDocument } from 'sanity';
|
|
2
|
+
import { pluginConfig } from '../../adapter/core';
|
|
3
|
+
import { mergeInternationalizedArrays } from '../../serialization/internationalizedArray/merge';
|
|
4
|
+
import type { GTFile } from '../../types';
|
|
5
|
+
import { getPublishedId } from '../../utils/documentIds';
|
|
6
|
+
import { findLatestDraft } from '../utils/findLatestDraft';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Import a translated internationalized-array document in place.
|
|
10
|
+
*
|
|
11
|
+
* Unlike document-level import (which creates per-locale documents and tracks
|
|
12
|
+
* translation metadata), array localization upserts the target-locale `value`
|
|
13
|
+
* into the same source document. We read the latest draft so concurrent edits
|
|
14
|
+
* to the source are not clobbered, merge only the changed top-level fields, and
|
|
15
|
+
* commit a single patch.
|
|
16
|
+
*/
|
|
17
|
+
export const internationalizedArrayPatch = async (
|
|
18
|
+
docInfo: GTFile,
|
|
19
|
+
translatedFields: SanityDocument,
|
|
20
|
+
localeId: string,
|
|
21
|
+
client: SanityClient
|
|
22
|
+
): Promise<void> => {
|
|
23
|
+
const sourceLocale = pluginConfig.getSourceLocale();
|
|
24
|
+
|
|
25
|
+
const baseDoc = await findLatestDraft(docInfo.documentId, client);
|
|
26
|
+
if (!baseDoc) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const changes = mergeInternationalizedArrays(
|
|
31
|
+
baseDoc as Record<string, unknown>,
|
|
32
|
+
translatedFields as Record<string, unknown>,
|
|
33
|
+
localeId,
|
|
34
|
+
sourceLocale
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (Object.keys(changes).length === 0) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Every key in `changes` already exists on the base document (merge only
|
|
42
|
+
// returns existing fields), so a full `set` of the merged field is safe and
|
|
43
|
+
// preserves all other locale items and their random `_key`s.
|
|
44
|
+
if (baseDoc._id.startsWith('drafts.')) {
|
|
45
|
+
await client.patch(baseDoc._id).set(changes).commit();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Only the published document exists. Mutations via the client write to the
|
|
50
|
+
// exact id given (drafts are a Studio convention, not created server-side),
|
|
51
|
+
// so patch a draft seeded from the published state instead of publishing
|
|
52
|
+
// translated content directly.
|
|
53
|
+
const draftId = `drafts.${getPublishedId(baseDoc._id)}`;
|
|
54
|
+
await client
|
|
55
|
+
.transaction()
|
|
56
|
+
.createIfNotExists({ ...baseDoc, _id: draftId })
|
|
57
|
+
.patch(draftId, (patch) => patch.set(changes))
|
|
58
|
+
.commit();
|
|
59
|
+
};
|