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,44 @@
1
+ import { GT } from 'generaltranslation';
2
+ import { libraryDefaultLocale } from 'generaltranslation/internal';
3
+ import type { Secrets } from '../types';
4
+
5
+ export const gt = new GT();
6
+
7
+ export function overrideConfig(secrets: Secrets | null) {
8
+ gt.setConfig({
9
+ ...(secrets?.project && { projectId: secrets?.project }),
10
+ ...(secrets?.secret && { apiKey: secrets?.secret }),
11
+ });
12
+ }
13
+
14
+ export class GTConfig {
15
+ sourceLocale: string;
16
+ locales: string[];
17
+ private static instance: GTConfig;
18
+ constructor(sourceLocale: string, locales: string[]) {
19
+ this.sourceLocale = sourceLocale;
20
+ this.locales = locales;
21
+ }
22
+
23
+ static getInstance() {
24
+ if (!this.instance) {
25
+ this.instance = new GTConfig(gt.sourceLocale || libraryDefaultLocale, []);
26
+ }
27
+ return this.instance;
28
+ }
29
+
30
+ setSourceLocale(sourceLocale: string) {
31
+ this.sourceLocale = sourceLocale;
32
+ }
33
+ getSourceLocale() {
34
+ return this.sourceLocale;
35
+ }
36
+
37
+ setLocales(locales: string[]) {
38
+ this.locales = locales;
39
+ }
40
+ getLocales() {
41
+ return this.locales;
42
+ }
43
+ }
44
+ export const gtConfig = GTConfig.getInstance();
@@ -0,0 +1,41 @@
1
+ import type { Adapter, GTFile, GTSerializedDocument, Secrets } from '../types';
2
+ import { getTranslationTask } from './getTranslationTask';
3
+ import { gt, overrideConfig } from './core';
4
+ import { libraryDefaultLocale } from 'generaltranslation/internal';
5
+
6
+ // note: this function is used to create a new translation task
7
+ // uploads files & calls the getTranslationTask function
8
+ export const createTask: Adapter['createTask'] = async (
9
+ documentInfo: GTFile,
10
+ serializedDocument: GTSerializedDocument,
11
+ localeIds: string[],
12
+ secrets: Secrets | null,
13
+ workflowUid?: string,
14
+ callbackUrl?: string
15
+ ) => {
16
+ const fileName = `sanity-${documentInfo.documentId}`;
17
+ overrideConfig(secrets);
18
+ const uploadResult = await gt.uploadSourceFiles(
19
+ [
20
+ {
21
+ source: {
22
+ content: serializedDocument.content,
23
+ fileName,
24
+ fileId: documentInfo.documentId,
25
+ fileFormat: 'HTML',
26
+ locale: gt.sourceLocale || libraryDefaultLocale,
27
+ versionId: documentInfo.versionId || undefined,
28
+ },
29
+ },
30
+ ],
31
+ {
32
+ sourceLocale: gt.sourceLocale || libraryDefaultLocale,
33
+ }
34
+ );
35
+ const enqueueResult = await gt.enqueueFiles(uploadResult.uploadedFiles, {
36
+ sourceLocale: gt.sourceLocale || libraryDefaultLocale,
37
+ targetLocales: localeIds,
38
+ });
39
+ const task = await getTranslationTask(documentInfo, secrets);
40
+ return task;
41
+ };
@@ -0,0 +1,13 @@
1
+ import type { Adapter, Secrets } from '../types';
2
+ import { gt, gtConfig } from './core';
3
+
4
+ // note: this function is used to get the available locales for a project
5
+ export const getLocales: Adapter['getLocales'] = async (
6
+ secrets: Secrets | null
7
+ ) => {
8
+ return gtConfig.getLocales().map((locale: string) => ({
9
+ localeId: locale,
10
+ description: gt.getLocaleProperties(locale).name,
11
+ enabled: true,
12
+ }));
13
+ };
@@ -0,0 +1,20 @@
1
+ import type { Adapter, GTFile, Secrets } from '../types';
2
+ import { gt, overrideConfig } from './core';
3
+
4
+ // note: downloads the translation for a given task and locale
5
+ export const getTranslation: Adapter['getTranslation'] = async (
6
+ documentInfo: GTFile,
7
+ localeId: string,
8
+ secrets: Secrets | null
9
+ ) => {
10
+ if (!secrets) {
11
+ return '';
12
+ }
13
+ overrideConfig(secrets);
14
+ const text = await gt.downloadTranslatedFile({
15
+ fileId: documentInfo.documentId,
16
+ versionId: documentInfo.versionId || undefined,
17
+ locale: localeId,
18
+ });
19
+ return text;
20
+ };
@@ -0,0 +1,31 @@
1
+ import type { Adapter, GTFile, Secrets } from '../types';
2
+ import { gt, overrideConfig } from './core';
3
+
4
+ // note: this function is used to get the status of a current translation task
5
+ export const getTranslationTask: Adapter['getTranslationTask'] = async (
6
+ documentInfo: GTFile,
7
+ secrets: Secrets | null
8
+ ) => {
9
+ if (!documentInfo.documentId || !secrets) {
10
+ return {
11
+ document: documentInfo,
12
+ locales: [],
13
+ };
14
+ }
15
+ overrideConfig(secrets);
16
+ const task = await gt.querySourceFile({
17
+ fileId: documentInfo.documentId,
18
+ versionId: documentInfo.versionId || undefined,
19
+ });
20
+
21
+ return {
22
+ document: {
23
+ documentId: task.sourceFile.fileId,
24
+ versionId: task.sourceFile.versionId,
25
+ },
26
+ locales: task.translations.map((translation) => ({
27
+ localeId: translation.locale,
28
+ progress: translation.completedAt ? 100 : 0,
29
+ })),
30
+ };
31
+ };
@@ -0,0 +1,13 @@
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
+ };
@@ -0,0 +1,65 @@
1
+ import { useCallback, useState } from 'react';
2
+ import { Flex, Card, Text, Grid, Box, Button } from '@sanity/ui';
3
+ import { DownloadIcon, CheckmarkCircleIcon } from '@sanity/icons';
4
+ import ProgressBar from './ProgressBar';
5
+
6
+ type LanguageStatusProps = {
7
+ title: string;
8
+ progress: number;
9
+ importFile: () => Promise<void>;
10
+ isImported?: boolean;
11
+ };
12
+
13
+ export const LanguageStatus = ({
14
+ title,
15
+ progress,
16
+ importFile,
17
+ isImported = false,
18
+ }: LanguageStatusProps) => {
19
+ const [isBusy, setIsBusy] = useState(false);
20
+
21
+ const handleImport = useCallback(async () => {
22
+ setIsBusy(true);
23
+ try {
24
+ await importFile();
25
+ } finally {
26
+ setIsBusy(false);
27
+ }
28
+ }, [importFile, setIsBusy]);
29
+
30
+ return (
31
+ <Card shadow={1}>
32
+ <Grid columns={5} gap={3} padding={3}>
33
+ <Flex columnStart={1} columnEnd={3} align='center'>
34
+ <Text weight='bold' size={1}>
35
+ {title}
36
+ </Text>
37
+ </Flex>
38
+ {typeof progress === 'number' ? (
39
+ <Flex columnStart={3} columnEnd={5} align='center'>
40
+ <ProgressBar progress={progress} />
41
+ </Flex>
42
+ ) : null}
43
+ <Box columnStart={5} columnEnd={6}>
44
+ {isImported ? (
45
+ <Flex align='center' justify='center' style={{ color: 'green' }}>
46
+ <CheckmarkCircleIcon />
47
+ <Text size={1} style={{ marginLeft: '4px' }}>
48
+ Imported
49
+ </Text>
50
+ </Flex>
51
+ ) : (
52
+ <Button
53
+ style={{ width: `100%` }}
54
+ mode='ghost'
55
+ onClick={handleImport}
56
+ text={isBusy ? 'Importing...' : 'Import'}
57
+ icon={isBusy ? null : DownloadIcon}
58
+ disabled={isBusy || !progress || progress < 100}
59
+ />
60
+ )}
61
+ </Box>
62
+ </Grid>
63
+ </Card>
64
+ );
65
+ };
@@ -0,0 +1,249 @@
1
+ import React, {
2
+ useState,
3
+ useContext,
4
+ ChangeEvent,
5
+ useCallback,
6
+ useEffect,
7
+ } from 'react';
8
+ import styled from 'styled-components';
9
+ import {
10
+ Button,
11
+ Box,
12
+ Flex,
13
+ Grid,
14
+ Select,
15
+ Stack,
16
+ Switch,
17
+ Text,
18
+ useToast,
19
+ } from '@sanity/ui';
20
+
21
+ import { TranslationContext } from './TranslationContext';
22
+ import { TranslationLocale } from '../types';
23
+
24
+ type Props = {
25
+ locales: TranslationLocale[];
26
+ refreshTask: () => Promise<void>;
27
+ };
28
+
29
+ type LocaleCheckboxProps = {
30
+ locale: TranslationLocale;
31
+ toggle: (locale: string, checked: boolean) => void;
32
+ checked: boolean;
33
+ };
34
+
35
+ const WrapText = styled(Box)`
36
+ white-space: normal;
37
+ `;
38
+
39
+ const LocaleCheckbox = ({ locale, toggle, checked }: LocaleCheckboxProps) => {
40
+ const onClick = useCallback(
41
+ () => toggle(locale.localeId, !checked),
42
+ [locale, toggle, checked]
43
+ );
44
+
45
+ return (
46
+ <Button
47
+ mode='ghost'
48
+ onClick={onClick}
49
+ disabled={locale.enabled === false}
50
+ style={{ cursor: `pointer` }}
51
+ radius={2}
52
+ >
53
+ <Flex align='center' gap={3}>
54
+ <Switch
55
+ style={{ pointerEvents: `none` }}
56
+ disabled={locale.enabled === false}
57
+ onChange={onClick}
58
+ checked={checked}
59
+ />
60
+ <WrapText>
61
+ <Text size={1} weight='semibold'>
62
+ {locale.description}
63
+ </Text>
64
+ </WrapText>
65
+ </Flex>
66
+ </Button>
67
+ );
68
+ };
69
+
70
+ export const NewTask = ({ locales, refreshTask }: Props) => {
71
+ const possibleLocales = locales.filter((locale) => locale.enabled !== false);
72
+ // Lets just stick to the canonical document id for keeping track of
73
+ // translations
74
+ const [selectedLocales, setSelectedLocales] = useState<React.ReactNode[]>(
75
+ locales
76
+ .filter((locale) => locale.enabled !== false)
77
+ .map((locale) => locale.localeId)
78
+ );
79
+
80
+ useEffect(() => {
81
+ setSelectedLocales(
82
+ locales
83
+ .filter((locale) => locale.enabled !== false)
84
+ .map((locale) => locale.localeId)
85
+ );
86
+ }, [locales]);
87
+
88
+ const [selectedWorkflowUid, setSelectedWorkflowUid] = useState<string>();
89
+ const [isBusy, setIsBusy] = useState(false);
90
+
91
+ const context = useContext(TranslationContext);
92
+ const toast = useToast();
93
+
94
+ const toggleLocale = useCallback(
95
+ (locale: string, selected: boolean) => {
96
+ if (!selected) {
97
+ setSelectedLocales(selectedLocales.filter((l) => l !== locale));
98
+ } else if (!selectedLocales.includes(locale)) {
99
+ setSelectedLocales([...selectedLocales, locale]);
100
+ }
101
+ },
102
+ [selectedLocales, setSelectedLocales]
103
+ );
104
+
105
+ const createTask = useCallback(() => {
106
+ if (!context) {
107
+ toast.push({
108
+ title: 'Unable to create task: missing context',
109
+ status: 'error',
110
+ closable: true,
111
+ });
112
+ return;
113
+ }
114
+
115
+ setIsBusy(true);
116
+
117
+ context
118
+ .exportForTranslation(context.documentInfo)
119
+ .then((serialized) =>
120
+ context.adapter.createTask(
121
+ context.documentInfo,
122
+ serialized,
123
+ selectedLocales as string[],
124
+ context.secrets,
125
+ selectedWorkflowUid,
126
+ context.callbackUrl
127
+ )
128
+ )
129
+ .then(() => {
130
+ toast.push({
131
+ title: 'Job successfully created',
132
+ status: 'success',
133
+ closable: true,
134
+ });
135
+
136
+ /** Reset form fields */
137
+ setSelectedLocales([]);
138
+ setSelectedWorkflowUid('');
139
+
140
+ /** Update task data in TranslationView */
141
+ refreshTask();
142
+ })
143
+ .catch((err) => {
144
+ let errorMsg;
145
+ if (err instanceof Error) {
146
+ errorMsg = err.message;
147
+ } else {
148
+ errorMsg = err ? String(err) : null;
149
+ }
150
+
151
+ toast.push({
152
+ title: `Error creating translation job`,
153
+ description: errorMsg,
154
+ status: 'error',
155
+ closable: true,
156
+ });
157
+ })
158
+ .finally(() => {
159
+ setIsBusy(false);
160
+ });
161
+ }, [context, selectedLocales, selectedWorkflowUid, toast, refreshTask]);
162
+
163
+ const onClick = useCallback(() => {
164
+ setSelectedLocales(
165
+ possibleLocales.length === selectedLocales.length
166
+ ? // Disable all
167
+ []
168
+ : // Enable all
169
+ locales
170
+ .filter((locale) => locale.enabled !== false)
171
+ .map((locale) => locale.localeId)
172
+ );
173
+ }, [possibleLocales, selectedLocales, setSelectedLocales, locales]);
174
+
175
+ const onToggle = useCallback(
176
+ (locale: string, checked: boolean) => {
177
+ toggleLocale(locale, checked);
178
+ },
179
+ [toggleLocale]
180
+ );
181
+
182
+ const onWorkflowChange = useCallback(
183
+ (e: ChangeEvent<HTMLSelectElement>) => {
184
+ setSelectedWorkflowUid(e.target.value);
185
+ },
186
+ [setSelectedWorkflowUid]
187
+ );
188
+
189
+ return (
190
+ <Stack paddingTop={4} space={4}>
191
+ <Text as='h2' weight='semibold' size={2}>
192
+ Generate New Translations
193
+ </Text>
194
+ <Stack space={3}>
195
+ <Flex align='center' justify='space-between'>
196
+ <Text weight='semibold' size={1}>
197
+ {possibleLocales.length === 1 ? `Select locale` : `Select locales`}
198
+ </Text>
199
+
200
+ <Button
201
+ fontSize={1}
202
+ padding={2}
203
+ text='Toggle All'
204
+ onClick={onClick}
205
+ />
206
+ </Flex>
207
+
208
+ <Grid columns={[1, 1, 2, 3]} gap={1}>
209
+ {(locales || []).map((l) => (
210
+ <LocaleCheckbox
211
+ key={l.localeId}
212
+ locale={l}
213
+ toggle={onToggle}
214
+ checked={selectedLocales.includes(l.localeId)}
215
+ />
216
+ ))}
217
+ </Grid>
218
+ </Stack>
219
+
220
+ {context?.workflowOptions && context.workflowOptions.length > 0 && (
221
+ <Stack space={3}>
222
+ <Text weight='semibold' size={1} as='label' htmlFor='workflow-select'>
223
+ Select translation workflow
224
+ </Text>
225
+ <Grid columns={[1, 1, 2]}>
226
+ <Select id='workflowSelect' onChange={onWorkflowChange}>
227
+ <option>Default locale workflows</option>
228
+ {context.workflowOptions.map((w) => (
229
+ <option
230
+ key={`workflow-opt-${w.workflowUid}`}
231
+ value={w.workflowUid}
232
+ >
233
+ {w.workflowName}
234
+ </option>
235
+ ))}
236
+ </Select>
237
+ </Grid>
238
+ </Stack>
239
+ )}
240
+
241
+ <Button
242
+ onClick={createTask}
243
+ disabled={isBusy || !selectedLocales.length}
244
+ tone='positive'
245
+ text={isBusy ? 'Queueing translations...' : 'Generate Translations'}
246
+ />
247
+ </Stack>
248
+ );
249
+ };
@@ -0,0 +1,38 @@
1
+ import { Card, Flex, Label } from '@sanity/ui';
2
+
3
+ export default function ProgressBar({ progress }: { progress: number }) {
4
+ if (typeof progress === 'undefined') {
5
+ console.warn('No progress prop passed to ProgressBar');
6
+ return null;
7
+ }
8
+
9
+ return (
10
+ <Card border radius={2} style={{ width: `100%`, position: `relative` }}>
11
+ <Flex
12
+ style={{
13
+ position: `absolute`,
14
+ left: 0,
15
+ right: 0,
16
+ top: 0,
17
+ bottom: 0,
18
+ zIndex: 1,
19
+ }}
20
+ align='center'
21
+ justify='center'
22
+ >
23
+ <Label size={1}>{progress}%</Label>
24
+ </Flex>
25
+ <Card
26
+ style={{
27
+ width: '100%',
28
+ transform: `scaleX(${progress / 100})`,
29
+ transformOrigin: 'left',
30
+ transition: 'transform .2s ease',
31
+ boxSizing: 'border-box',
32
+ }}
33
+ padding={2}
34
+ tone='positive'
35
+ />
36
+ </Card>
37
+ );
38
+ }