gt-sanity 0.0.5

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 (44) hide show
  1. package/LICENSE.md +138 -0
  2. package/README.md +100 -0
  3. package/dist/index.d.mts +241 -0
  4. package/dist/index.d.ts +241 -0
  5. package/dist/index.js +2119 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/index.mjs +2099 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/package.json +92 -0
  10. package/sanity.json +8 -0
  11. package/src/adapter/core.ts +44 -0
  12. package/src/adapter/createTask.ts +41 -0
  13. package/src/adapter/getLocales.ts +13 -0
  14. package/src/adapter/getTranslation.ts +20 -0
  15. package/src/adapter/getTranslationTask.ts +31 -0
  16. package/src/adapter/index.ts +13 -0
  17. package/src/components/LanguageStatus.tsx +65 -0
  18. package/src/components/NewTask.tsx +249 -0
  19. package/src/components/ProgressBar.tsx +38 -0
  20. package/src/components/TaskView.tsx +255 -0
  21. package/src/components/TranslationContext.tsx +19 -0
  22. package/src/components/TranslationView.tsx +82 -0
  23. package/src/components/TranslationsTab.tsx +177 -0
  24. package/src/configuration/README.md +8 -0
  25. package/src/configuration/baseDocumentLevelConfig/documentLevelPatch.ts +108 -0
  26. package/src/configuration/baseDocumentLevelConfig/helpers/createI18nDocAndPatchMetadata.ts +47 -0
  27. package/src/configuration/baseDocumentLevelConfig/helpers/createTranslationMetadata.ts +43 -0
  28. package/src/configuration/baseDocumentLevelConfig/helpers/getOrCreateTranslationMetadata.ts +77 -0
  29. package/src/configuration/baseDocumentLevelConfig/helpers/getTranslationMetadata.ts +15 -0
  30. package/src/configuration/baseDocumentLevelConfig/helpers/index.ts +5 -0
  31. package/src/configuration/baseDocumentLevelConfig/helpers/patchI18nDoc.ts +25 -0
  32. package/src/configuration/baseDocumentLevelConfig/index.ts +129 -0
  33. package/src/configuration/baseDocumentLevelConfig/legacyDocumentLevelPatch.ts +69 -0
  34. package/src/configuration/baseFieldLevelConfig.ts +118 -0
  35. package/src/configuration/index.ts +18 -0
  36. package/src/configuration/utils/checkSerializationVersion.ts +13 -0
  37. package/src/configuration/utils/findDocumentAtRevision.ts +22 -0
  38. package/src/configuration/utils/findLatestDraft.ts +16 -0
  39. package/src/configuration/utils/index.ts +3 -0
  40. package/src/hooks/useClient.ts +5 -0
  41. package/src/hooks/useSecrets.ts +33 -0
  42. package/src/index.ts +120 -0
  43. package/src/types.ts +124 -0
  44. package/v2-incompatible.js +11 -0
@@ -0,0 +1,255 @@
1
+ import { useCallback, useContext, useState, useEffect } from 'react';
2
+ import { Box, Button, Flex, Text, Stack, useToast, Switch } from '@sanity/ui';
3
+ import {
4
+ ArrowTopRightIcon,
5
+ DownloadIcon,
6
+ CheckmarkCircleIcon,
7
+ } from '@sanity/icons';
8
+
9
+ import { TranslationContext } from './TranslationContext';
10
+ import { TranslationLocale, TranslationTask } from '../types';
11
+ import { LanguageStatus } from './LanguageStatus';
12
+
13
+ type JobProps = {
14
+ task: TranslationTask;
15
+ locales: TranslationLocale[];
16
+ refreshTask: () => Promise<void>;
17
+ };
18
+
19
+ const getLocale = (
20
+ localeId: string,
21
+ locales: TranslationLocale[]
22
+ ): TranslationLocale | undefined =>
23
+ locales.find((l) => l.localeId === localeId);
24
+
25
+ export const TaskView = ({ task, locales, refreshTask }: JobProps) => {
26
+ const context = useContext(TranslationContext);
27
+ const toast = useToast();
28
+
29
+ const [isRefreshing, setIsRefreshing] = useState(false);
30
+ const [autoRefresh, setAutoRefresh] = useState(true);
31
+ const [isBusy, setIsBusy] = useState(false);
32
+ const [autoImport, setAutoImport] = useState(true);
33
+ const [importedFiles, setImportedFiles] = useState<Set<string>>(new Set());
34
+
35
+ const importFile = useCallback(
36
+ async (localeId: string) => {
37
+ if (!context) {
38
+ toast.push({
39
+ title:
40
+ 'Missing context, unable to import translation. Try refreshing or clicking away from this tab and back.',
41
+ status: 'error',
42
+ closable: true,
43
+ });
44
+ return;
45
+ }
46
+
47
+ const locale = getLocale(localeId, locales);
48
+ const localeTitle = locale?.description || localeId;
49
+
50
+ try {
51
+ const translation = await context.adapter.getTranslation(
52
+ task.document,
53
+ localeId,
54
+ context.secrets
55
+ );
56
+
57
+ const sanityId = context.localeIdAdapter
58
+ ? await context.localeIdAdapter(localeId)
59
+ : localeId;
60
+
61
+ await context.importTranslation(sanityId, translation);
62
+
63
+ setImportedFiles((prev) => new Set([...prev, localeId]));
64
+
65
+ toast.push({
66
+ title: `Imported ${localeTitle} translation`,
67
+ status: 'success',
68
+ closable: true,
69
+ });
70
+ } catch (err) {
71
+ let errorMsg;
72
+ if (err instanceof Error) {
73
+ errorMsg = err.message;
74
+ } else {
75
+ errorMsg = err ? String(err) : null;
76
+ }
77
+
78
+ toast.push({
79
+ title: `Error getting ${localeTitle} translation`,
80
+ description: errorMsg,
81
+ status: 'error',
82
+ closable: true,
83
+ });
84
+ }
85
+ },
86
+ [locales, context, task.document, toast]
87
+ );
88
+
89
+ const checkAndImportCompletedFiles = useCallback(async () => {
90
+ if (!autoImport || isBusy) return;
91
+
92
+ const completedFiles = task.locales.filter(
93
+ (locale) =>
94
+ (locale.progress || 0) >= 100 && !importedFiles.has(locale.localeId)
95
+ );
96
+
97
+ if (completedFiles.length === 0) return;
98
+
99
+ setIsBusy(true);
100
+ try {
101
+ for (const locale of completedFiles) {
102
+ await importFile(locale.localeId);
103
+ }
104
+ } finally {
105
+ setIsBusy(false);
106
+ }
107
+ }, [autoImport, isBusy, task.locales, importedFiles, importFile]);
108
+
109
+ const handleRefreshClick = useCallback(async () => {
110
+ if (isRefreshing) return;
111
+ setIsRefreshing(true);
112
+ await refreshTask();
113
+ await checkAndImportCompletedFiles();
114
+ setIsRefreshing(false);
115
+ }, [refreshTask, setIsRefreshing, checkAndImportCompletedFiles]);
116
+
117
+ const handleImportAll = useCallback(async () => {
118
+ if (isBusy) return;
119
+ setIsBusy(true);
120
+
121
+ try {
122
+ const filesToImport = task.locales.filter(
123
+ (locale) => !importedFiles.has(locale.localeId)
124
+ );
125
+ for (const locale of filesToImport) {
126
+ await importFile(locale.localeId);
127
+ }
128
+ } finally {
129
+ setIsBusy(false);
130
+ }
131
+ }, [task.locales, importFile, isBusy, importedFiles]);
132
+
133
+ useEffect(() => {
134
+ if (!autoRefresh || importedFiles.size === task.locales.length) return;
135
+
136
+ const interval = setInterval(async () => {
137
+ await handleRefreshClick();
138
+ }, 5000);
139
+
140
+ return () => clearInterval(interval);
141
+ }, [
142
+ handleRefreshClick,
143
+ autoRefresh,
144
+ importedFiles.size,
145
+ task.locales.length,
146
+ ]);
147
+
148
+ useEffect(() => {
149
+ checkAndImportCompletedFiles();
150
+ }, [checkAndImportCompletedFiles, task.locales]);
151
+
152
+ useEffect(() => {
153
+ setImportedFiles((prev) => {
154
+ const newSet = new Set<string>();
155
+ for (const localeId of prev) {
156
+ if (task.locales.some((locale) => locale.localeId === localeId)) {
157
+ newSet.add(localeId);
158
+ }
159
+ }
160
+ return newSet;
161
+ });
162
+ }, [task.locales]);
163
+
164
+ return (
165
+ <Stack space={4}>
166
+ <Flex align='center' justify='space-between'>
167
+ <Text as='h2' weight='semibold' size={2}>
168
+ Translation Progress
169
+ </Text>
170
+
171
+ <Flex gap={3} align='center'>
172
+ <Flex gap={2} align='center'>
173
+ <Text size={1}>Auto-refresh</Text>
174
+ <Switch
175
+ checked={autoRefresh}
176
+ onChange={() => setAutoRefresh(!autoRefresh)}
177
+ />
178
+ </Flex>
179
+ {task.linkToVendorTask && (
180
+ <Button
181
+ as='a'
182
+ text='View Job'
183
+ iconRight={ArrowTopRightIcon}
184
+ href={task.linkToVendorTask}
185
+ target='_blank'
186
+ rel='noreferrer noopener'
187
+ fontSize={1}
188
+ padding={2}
189
+ mode='bleed'
190
+ />
191
+ )}
192
+ <Button
193
+ fontSize={1}
194
+ padding={2}
195
+ text='Refresh Status'
196
+ onClick={handleRefreshClick}
197
+ disabled={isRefreshing}
198
+ />
199
+ </Flex>
200
+ </Flex>
201
+
202
+ <Box>
203
+ {task.locales.map((localeTask) => {
204
+ const reportPercent = localeTask.progress || 0;
205
+ const locale = getLocale(localeTask.localeId, locales);
206
+ return (
207
+ <LanguageStatus
208
+ key={[task.document.documentId, localeTask.localeId].join('.')}
209
+ importFile={async () => {
210
+ await importFile(localeTask.localeId);
211
+ }}
212
+ title={locale?.description || localeTask.localeId}
213
+ progress={reportPercent}
214
+ isImported={importedFiles.has(localeTask.localeId)}
215
+ />
216
+ );
217
+ })}
218
+ </Box>
219
+ <Stack space={3}>
220
+ <Flex gap={3} align='center' justify='space-between'>
221
+ <Flex gap={2} align='center'>
222
+ <Button
223
+ mode='ghost'
224
+ onClick={handleImportAll}
225
+ text={isBusy ? 'Importing...' : 'Import All'}
226
+ icon={isBusy ? null : DownloadIcon}
227
+ disabled={isBusy || importedFiles.size === task.locales.length}
228
+ />
229
+ {importedFiles.size === task.locales.length &&
230
+ task.locales.length > 0 && (
231
+ <Flex gap={2} align='center' style={{ color: 'green' }}>
232
+ <CheckmarkCircleIcon />
233
+ <Text size={1}>All translations imported</Text>
234
+ </Flex>
235
+ )}
236
+ {importedFiles.size > 0 &&
237
+ importedFiles.size < task.locales.length && (
238
+ <Text size={1} style={{ color: '#666' }}>
239
+ {importedFiles.size}/{task.locales.length} imported
240
+ </Text>
241
+ )}
242
+ </Flex>
243
+ <Flex gap={2} align='center' style={{ whiteSpace: 'nowrap' }}>
244
+ <Text size={1}>Auto-import when complete</Text>
245
+ <Switch
246
+ checked={autoImport}
247
+ onChange={() => setAutoImport(!autoImport)}
248
+ disabled={isBusy}
249
+ />
250
+ </Flex>
251
+ </Flex>
252
+ </Stack>
253
+ </Stack>
254
+ );
255
+ };
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import { GTSerializedDocument } from '../types';
3
+ import { Adapter, GTFile, Secrets, WorkflowIdentifiers } from '../types';
4
+
5
+ export type ContextProps = {
6
+ documentInfo: GTFile;
7
+ adapter: Adapter;
8
+ importTranslation: (languageId: string, document: string) => Promise<void>;
9
+ exportForTranslation: (documentInfo: GTFile) => Promise<GTSerializedDocument>;
10
+ secrets: Secrets;
11
+ workflowOptions?: WorkflowIdentifiers[];
12
+ localeIdAdapter?: (id: string) => string | Promise<string>;
13
+ callbackUrl?: string;
14
+ mergeWithTargetLocale?: boolean;
15
+ };
16
+
17
+ export const TranslationContext = React.createContext<ContextProps | null>(
18
+ null
19
+ );
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Add cleanup function to cancel async tasks
3
+ */
4
+
5
+ import { useCallback, useContext, useEffect, useState } from 'react';
6
+ import { Stack, useToast } from '@sanity/ui';
7
+ import { TranslationContext } from './TranslationContext';
8
+
9
+ import { NewTask } from './NewTask';
10
+ import { TaskView } from './TaskView';
11
+ import { TranslationTask, TranslationLocale } from '../types';
12
+
13
+ export const TranslationView = () => {
14
+ const [locales, setLocales] = useState<TranslationLocale[]>([]);
15
+ const [task, setTask] = useState<TranslationTask | null>(null);
16
+
17
+ const context = useContext(TranslationContext);
18
+ const toast = useToast();
19
+
20
+ useEffect(() => {
21
+ async function fetchData() {
22
+ if (!context) {
23
+ toast.push({
24
+ title: 'Unable to load translation data: missing context',
25
+ status: 'error',
26
+ closable: true,
27
+ });
28
+ return;
29
+ }
30
+
31
+ const locales = await context.adapter.getLocales(context.secrets);
32
+ setLocales(locales);
33
+ try {
34
+ const task = await context?.adapter.getTranslationTask(
35
+ context.documentInfo,
36
+ context.secrets
37
+ );
38
+ setTask(task);
39
+ } catch (err) {
40
+ let errorMsg;
41
+ if (err instanceof Error) {
42
+ errorMsg = err.message;
43
+ } else {
44
+ errorMsg = err ? String(err) : null;
45
+ }
46
+
47
+ // Hacky bypass for when a document is not yet translated and has never been uploaded
48
+ if (errorMsg?.toLowerCase().includes('no source file found')) {
49
+ return;
50
+ }
51
+
52
+ toast.push({
53
+ title: `Error creating translation job`,
54
+ description: errorMsg,
55
+ status: 'error',
56
+ closable: true,
57
+ });
58
+ }
59
+ }
60
+
61
+ fetchData();
62
+ }, [context, toast]);
63
+
64
+ const refreshTask = useCallback(async () => {
65
+ const task = await context?.adapter.getTranslationTask(
66
+ context.documentInfo,
67
+ context.secrets
68
+ );
69
+ if (task) {
70
+ setTask(task);
71
+ }
72
+ }, [context, setTask]);
73
+
74
+ return (
75
+ <Stack space={6}>
76
+ <NewTask locales={locales} refreshTask={refreshTask} />
77
+ {task && (
78
+ <TaskView task={task} locales={locales} refreshTask={refreshTask} />
79
+ )}
80
+ </Stack>
81
+ );
82
+ };
@@ -0,0 +1,177 @@
1
+ import { useMemo } from 'react';
2
+ import { SanityDocument, useSchema } from 'sanity';
3
+ import { randomKey } from '@sanity/util/content';
4
+ import {
5
+ ThemeProvider,
6
+ ToastProvider,
7
+ Stack,
8
+ Text,
9
+ Layer,
10
+ Box,
11
+ Card,
12
+ Flex,
13
+ Spinner,
14
+ } from '@sanity/ui';
15
+
16
+ import { TranslationContext } from './TranslationContext';
17
+ import { TranslationView } from './TranslationView';
18
+ import { useClient } from '../hooks/useClient';
19
+ import { useSecrets } from '../hooks/useSecrets';
20
+ import { GTFile, Secrets, TranslationsTabConfigOptions } from '../types';
21
+
22
+ type TranslationTabProps = {
23
+ document: {
24
+ displayed: SanityDocument;
25
+ };
26
+ options: TranslationsTabConfigOptions;
27
+ };
28
+
29
+ const TranslationTab = (props: TranslationTabProps) => {
30
+ const { displayed } = props.document;
31
+ const client = useClient();
32
+ const schema = useSchema();
33
+
34
+ const documentId =
35
+ displayed && displayed._id
36
+ ? (displayed._id.split('drafts.').pop() as string)
37
+ : '';
38
+
39
+ const revisionId = displayed && displayed._rev ? displayed._rev : undefined;
40
+
41
+ const { errors, importTranslation, exportForTranslation } = useMemo(() => {
42
+ const { serializationOptions, languageField, mergeWithTargetLocale } =
43
+ props.options;
44
+ const ctx = {
45
+ client,
46
+ schema,
47
+ };
48
+
49
+ const allErrors = [];
50
+
51
+ const importTranslationFunc = props.options.importTranslation;
52
+ if (!importTranslationFunc) {
53
+ allErrors.push({
54
+ key: randomKey(12),
55
+ text: (
56
+ <>
57
+ You need to provide an <code>importTranslation</code> function. See
58
+ documentation.
59
+ </>
60
+ ),
61
+ });
62
+ }
63
+
64
+ const contextImportTranslation = (localeId: string, doc: string) => {
65
+ return importTranslationFunc(
66
+ { documentId, versionId: revisionId },
67
+ localeId,
68
+ doc,
69
+ ctx,
70
+ serializationOptions,
71
+ languageField,
72
+ mergeWithTargetLocale
73
+ );
74
+ };
75
+
76
+ const exportTranslationFunc = props.options.exportForTranslation;
77
+ if (!exportTranslationFunc) {
78
+ allErrors.push({
79
+ key: randomKey(12),
80
+ text: (
81
+ <>
82
+ You need to provide an <code>exportForTranslation</code> function.
83
+ See documentation.
84
+ </>
85
+ ),
86
+ });
87
+ }
88
+
89
+ const contextExportForTranslation = (docInfo: GTFile) => {
90
+ return exportTranslationFunc(
91
+ docInfo,
92
+ ctx,
93
+ serializationOptions,
94
+ languageField
95
+ );
96
+ };
97
+
98
+ return {
99
+ errors: allErrors,
100
+ importTranslation: contextImportTranslation,
101
+ exportForTranslation: contextExportForTranslation,
102
+ };
103
+ }, [props.options, documentId, revisionId, client, schema]);
104
+
105
+ const { loading, secrets } = useSecrets<Secrets>(
106
+ `${props.options.secretsNamespace || 'translationService'}.secrets`
107
+ );
108
+
109
+ const hasErrors = errors.length > 0;
110
+
111
+ if (loading || !secrets) {
112
+ return (
113
+ <ThemeProvider>
114
+ <Flex padding={5} align='center' justify='center'>
115
+ <Spinner />
116
+ </Flex>
117
+ </ThemeProvider>
118
+ );
119
+ } else if (!secrets) {
120
+ return (
121
+ <ThemeProvider>
122
+ <Box padding={4}>
123
+ <Card tone='caution' padding={[2, 3, 4, 4]} shadow={1} radius={2}>
124
+ <Text>
125
+ Can't find secrets for your translation service. Did you load them
126
+ into this dataset?
127
+ </Text>
128
+ </Card>
129
+ </Box>
130
+ </ThemeProvider>
131
+ );
132
+ }
133
+ return (
134
+ <ThemeProvider>
135
+ <Box padding={4}>
136
+ <Layer>
137
+ <ToastProvider paddingY={7}>
138
+ {hasErrors && (
139
+ <Stack space={3}>
140
+ {errors.map((error) => (
141
+ <Card
142
+ key={error.key}
143
+ tone='caution'
144
+ padding={[2, 3, 4, 4]}
145
+ shadow={1}
146
+ radius={2}
147
+ >
148
+ <Text>{error.text}</Text>
149
+ </Card>
150
+ ))}
151
+ </Stack>
152
+ )}
153
+ {!hasErrors && (
154
+ <TranslationContext.Provider
155
+ value={{
156
+ documentInfo: { documentId, versionId: revisionId },
157
+ secrets,
158
+ importTranslation,
159
+ exportForTranslation,
160
+ adapter: props.options.adapter,
161
+ workflowOptions: props.options.workflowOptions,
162
+ localeIdAdapter: props.options.localeIdAdapter,
163
+ callbackUrl: props.options.callbackUrl,
164
+ mergeWithTargetLocale: props.options.mergeWithTargetLocale,
165
+ }}
166
+ >
167
+ <TranslationView />
168
+ </TranslationContext.Provider>
169
+ )}
170
+ </ToastProvider>
171
+ </Layer>
172
+ </Box>
173
+ </ThemeProvider>
174
+ );
175
+ };
176
+
177
+ export default TranslationTab;
@@ -0,0 +1,8 @@
1
+ # Configuration
2
+
3
+ This folder holds the default configurations to be provided as options to the Translations Tab, namely:
4
+
5
+ - `exportForTranslation`: Process a Sanity document before sending it off to a TMS. By default, the documents will be serialized to HTML: content will be rendered in nested divs in the HTML `body`, and any relevant metadata will be in the HTML `head`.
6
+ For more information on this process, please refer to the [Sanity Naive HTML Serializer](https://github.com/sanity-io/sanity-naive-html-serializer)
7
+
8
+ - `importTranslation`: Receive a translated document back from a TMS, parse it into a Sanity-readable format, and patch it back to the relevant Sanity document. Again, the [Sanity Naive HTML Serializer](https://github.com/sanity-io/sanity-naive-html-serializer) is used for parsing under the assumption that the file sent over was HTML, but any function could be used here.
@@ -0,0 +1,108 @@
1
+ import { SanityClient, SanityDocument, SanityDocumentLike } from 'sanity';
2
+ import { BaseDocumentMerger } from 'sanity-naive-html-serializer';
3
+
4
+ import { findLatestDraft, findDocumentAtRevision } from '../utils';
5
+ import {
6
+ createI18nDocAndPatchMetadata,
7
+ getOrCreateTranslationMetadata,
8
+ patchI18nDoc,
9
+ } from './helpers';
10
+ import type { GTFile } from '../../types';
11
+ import { gtConfig } from '../../adapter/core';
12
+
13
+ export const documentLevelPatch = async (
14
+ docInfo: GTFile,
15
+ translatedFields: SanityDocument,
16
+ localeId: string,
17
+ client: SanityClient,
18
+ languageField: string = 'language',
19
+ mergeWithTargetLocale: boolean = false
20
+ ): Promise<void> => {
21
+ const baseLanguage = gtConfig.getSourceLocale();
22
+ //this is the document we use to merge with the translated fields
23
+ let baseDoc: SanityDocument | null = null;
24
+
25
+ //this is the document that will serve as the translated doc
26
+ let i18nDoc: SanityDocument | null = null;
27
+
28
+ /*
29
+ * we send over the _rev with our translation file so we can
30
+ * accurately coalesce the translations in case something has
31
+ * changed in the base document since translating
32
+ */
33
+ if (docInfo.documentId && docInfo.versionId) {
34
+ baseDoc = await findDocumentAtRevision(
35
+ docInfo.documentId,
36
+ docInfo.versionId,
37
+ client
38
+ );
39
+ }
40
+ if (!baseDoc) {
41
+ baseDoc = await findLatestDraft(docInfo.documentId, client);
42
+ }
43
+
44
+ /* first, check our metadata to see if a translated document exists
45
+ * if no metadata exists, we create it atomically
46
+ */
47
+ const translationMetadata = await getOrCreateTranslationMetadata(
48
+ docInfo.documentId,
49
+ baseDoc,
50
+ client,
51
+ baseLanguage
52
+ );
53
+
54
+ //the id of the translated document should be on the metadata if it exists
55
+ const i18nDocId = (
56
+ translationMetadata.translations as Array<Record<string, any>>
57
+ ).find((translation) => translation._key === localeId)?.value?._ref;
58
+
59
+ if (i18nDocId) {
60
+ //get draft or published
61
+ i18nDoc = await findLatestDraft(i18nDocId, client);
62
+ }
63
+
64
+ //if the user has chosen to merge with the target locale,
65
+ //any existing target document will serve as our base document
66
+ if (mergeWithTargetLocale && i18nDoc) {
67
+ baseDoc = i18nDoc;
68
+ } else if (docInfo.documentId && docInfo.versionId) {
69
+ /*
70
+ * we send over the _rev with our translation file so we can
71
+ * accurately coalesce the translations in case something has
72
+ * changed in the base document since translating
73
+ */
74
+ baseDoc = await findDocumentAtRevision(
75
+ docInfo.documentId,
76
+ docInfo.versionId,
77
+ client
78
+ );
79
+ }
80
+
81
+ if (!baseDoc) {
82
+ baseDoc = await findLatestDraft(docInfo.documentId, client);
83
+ }
84
+ /*
85
+ * we then merge the translation with the base document
86
+ * to create a document that contains the translation and everything
87
+ * that wasn't sent over for translation
88
+ */
89
+ const merged = BaseDocumentMerger.documentLevelMerge(
90
+ translatedFields,
91
+ baseDoc
92
+ ) as SanityDocumentLike;
93
+
94
+ if (i18nDoc) {
95
+ patchI18nDoc(docInfo.documentId, merged, translatedFields, client);
96
+ }
97
+ //otherwise, create a new document
98
+ //and add the document reference to the metadata document
99
+ else {
100
+ createI18nDocAndPatchMetadata(
101
+ merged,
102
+ localeId,
103
+ client,
104
+ translationMetadata,
105
+ languageField
106
+ );
107
+ }
108
+ };
@@ -0,0 +1,47 @@
1
+ import { SanityClient, SanityDocumentLike } from 'sanity';
2
+
3
+ export const createI18nDocAndPatchMetadata = (
4
+ translatedDoc: SanityDocumentLike,
5
+ localeId: string,
6
+ client: SanityClient,
7
+ translationMetadata: SanityDocumentLike,
8
+ languageField: string = 'language'
9
+ ): void => {
10
+ translatedDoc[languageField] = localeId;
11
+ const translations = translationMetadata.translations as Record<
12
+ string,
13
+ any
14
+ >[];
15
+ const existingLocaleKey = translations.find(
16
+ (translation) => translation._key === localeId
17
+ );
18
+ const operation = existingLocaleKey ? 'replace' : 'after';
19
+ const location = existingLocaleKey
20
+ ? `translations[_key == "${localeId}"]`
21
+ : 'translations[-1]';
22
+
23
+ //remove system fields
24
+ const { _updatedAt, _createdAt, ...rest } = translatedDoc;
25
+ client.create({ ...rest, _id: 'drafts.' }).then((doc) => {
26
+ const _ref = doc._id.replace('drafts.', '');
27
+ client
28
+ .transaction()
29
+ .patch(translationMetadata._id, (p) =>
30
+ p.insert(operation, location, [
31
+ {
32
+ _key: localeId,
33
+ _type: 'internationalizedArrayReferenceValue',
34
+ value: {
35
+ _type: 'reference',
36
+ _ref,
37
+ _weak: true,
38
+ _strengthenOnPublish: {
39
+ type: doc._type,
40
+ },
41
+ },
42
+ },
43
+ ])
44
+ )
45
+ .commit();
46
+ });
47
+ };