gt-sanity 0.0.6 → 1.0.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/LICENSE.md +1 -1
- package/dist/index.d.mts +95 -73
- package/dist/index.d.ts +95 -73
- package/dist/index.js +6233 -1162
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6292 -1193
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -4
- package/src/adapter/core.ts +41 -4
- package/src/adapter/getLocales.ts +2 -2
- 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 +299 -837
- package/src/components/shared/BaseTranslationWrapper.tsx +82 -0
- package/src/components/shared/LocaleCheckbox.tsx +47 -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 +6 -9
- package/src/configuration/baseDocumentLevelConfig/helpers/createI18nDocAndPatchMetadata.ts +5 -24
- package/src/configuration/baseDocumentLevelConfig/helpers/patchI18nDoc.ts +3 -23
- package/src/configuration/baseDocumentLevelConfig/index.ts +16 -68
- package/src/configuration/baseFieldLevelConfig.ts +15 -50
- package/src/index.ts +29 -43
- 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/importDocument.ts +4 -5
- package/src/translation/uploadFiles.ts +1 -1
- package/src/types.ts +3 -19
- package/src/utils/batchProcessor.ts +111 -0
- package/src/utils/importUtils.ts +95 -0
- package/src/utils/serialize.ts +25 -5
- package/src/utils/shared.ts +1 -1
- package/src/adapter/index.ts +0 -13
- package/src/components/NewTask.tsx +0 -251
- package/src/components/TaskView.tsx +0 -257
- package/src/components/TranslationContext.tsx +0 -24
- package/src/components/TranslationView.tsx +0 -114
- package/src/components/TranslationsTab.tsx +0 -181
- /package/src/components/{LanguageStatus.tsx → shared/LanguageStatus.tsx} +0 -0
- /package/src/components/{ProgressBar.tsx → shared/ProgressBar.tsx} +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ThemeProvider,
|
|
4
|
+
ToastProvider,
|
|
5
|
+
Box,
|
|
6
|
+
Card,
|
|
7
|
+
Flex,
|
|
8
|
+
Spinner,
|
|
9
|
+
Text,
|
|
10
|
+
} from '@sanity/ui';
|
|
11
|
+
import { buildTheme } from '@sanity/ui/theme';
|
|
12
|
+
import { useSecrets } from '../../hooks/useSecrets';
|
|
13
|
+
import { Secrets } from '../../types';
|
|
14
|
+
import { pluginConfig } from '../../adapter/core';
|
|
15
|
+
|
|
16
|
+
const theme = buildTheme();
|
|
17
|
+
|
|
18
|
+
interface BaseTranslationWrapperProps {
|
|
19
|
+
children: React.ReactNode;
|
|
20
|
+
secretsNamespace?: string;
|
|
21
|
+
padding?: number;
|
|
22
|
+
showContainer?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const BaseTranslationWrapper: React.FC<BaseTranslationWrapperProps> = ({
|
|
26
|
+
children,
|
|
27
|
+
secretsNamespace = pluginConfig.getSecretsNamespace(),
|
|
28
|
+
padding = 4,
|
|
29
|
+
showContainer = true,
|
|
30
|
+
}) => {
|
|
31
|
+
const { loading: loadingSecrets, secrets } =
|
|
32
|
+
useSecrets<Secrets>(secretsNamespace);
|
|
33
|
+
|
|
34
|
+
const content = (
|
|
35
|
+
<>
|
|
36
|
+
{loadingSecrets && (
|
|
37
|
+
<Flex padding={5} align='center' justify='center'>
|
|
38
|
+
<Spinner />
|
|
39
|
+
</Flex>
|
|
40
|
+
)}
|
|
41
|
+
|
|
42
|
+
{!loadingSecrets && !secrets && (
|
|
43
|
+
<Box padding={padding}>
|
|
44
|
+
<Card tone='caution' padding={[2, 3, 4, 4]} shadow={1} radius={2}>
|
|
45
|
+
<Text>
|
|
46
|
+
Can't find secrets for your translation service. Did you load them
|
|
47
|
+
into this dataset?
|
|
48
|
+
</Text>
|
|
49
|
+
</Card>
|
|
50
|
+
</Box>
|
|
51
|
+
)}
|
|
52
|
+
|
|
53
|
+
{!loadingSecrets && secrets && children}
|
|
54
|
+
</>
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<ThemeProvider theme={theme}>
|
|
59
|
+
<ToastProvider paddingY={7}>
|
|
60
|
+
{showContainer ? <Box padding={padding}>{content}</Box> : content}
|
|
61
|
+
</ToastProvider>
|
|
62
|
+
</ThemeProvider>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export interface UseTranslationSecretsResult {
|
|
67
|
+
loadingSecrets: boolean;
|
|
68
|
+
secrets: Secrets | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const useTranslationSecrets = (
|
|
72
|
+
secretsNamespace?: string
|
|
73
|
+
): UseTranslationSecretsResult => {
|
|
74
|
+
const { loading: loadingSecrets, secrets } = useSecrets<Secrets>(
|
|
75
|
+
secretsNamespace || 'translationService.secrets'
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
loadingSecrets,
|
|
80
|
+
secrets,
|
|
81
|
+
};
|
|
82
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { Button, Flex, Switch, Box, Text } from '@sanity/ui';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
import { TranslationLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
const WrapText = styled(Box)`
|
|
7
|
+
white-space: normal;
|
|
8
|
+
`;
|
|
9
|
+
|
|
10
|
+
type LocaleCheckboxProps = {
|
|
11
|
+
locale: TranslationLocale;
|
|
12
|
+
toggle: (locale: string, shouldEnable: boolean) => void;
|
|
13
|
+
checked: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const LocaleCheckbox = ({
|
|
17
|
+
locale,
|
|
18
|
+
toggle,
|
|
19
|
+
checked,
|
|
20
|
+
}: LocaleCheckboxProps) => {
|
|
21
|
+
const onClick = useCallback(
|
|
22
|
+
() => toggle(locale.localeId, !checked),
|
|
23
|
+
[locale.localeId, toggle, checked]
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Button
|
|
28
|
+
mode='ghost'
|
|
29
|
+
onClick={onClick}
|
|
30
|
+
style={{ cursor: 'pointer' }}
|
|
31
|
+
radius={2}
|
|
32
|
+
>
|
|
33
|
+
<Flex align='center' gap={3}>
|
|
34
|
+
<Switch
|
|
35
|
+
style={{ pointerEvents: 'none' }}
|
|
36
|
+
onChange={onClick}
|
|
37
|
+
checked={checked}
|
|
38
|
+
/>
|
|
39
|
+
<WrapText>
|
|
40
|
+
<Text size={1} weight='semibold'>
|
|
41
|
+
{locale.description}
|
|
42
|
+
</Text>
|
|
43
|
+
</WrapText>
|
|
44
|
+
</Flex>
|
|
45
|
+
</Button>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Stack, Box, Card, Text, Flex, Spinner } from '@sanity/ui';
|
|
3
|
+
import { LanguageStatus } from './LanguageStatus';
|
|
4
|
+
import { useTranslations } from '../TranslationsProvider';
|
|
5
|
+
import { pluginConfig } from '../../adapter/core';
|
|
6
|
+
|
|
7
|
+
export const SingleDocumentView: React.FC = () => {
|
|
8
|
+
const {
|
|
9
|
+
documents,
|
|
10
|
+
locales,
|
|
11
|
+
loadingDocuments,
|
|
12
|
+
translationStatuses,
|
|
13
|
+
downloadStatus,
|
|
14
|
+
importedTranslations,
|
|
15
|
+
handleImportDocument,
|
|
16
|
+
} = useTranslations();
|
|
17
|
+
|
|
18
|
+
// Get the first (and only) document in single document mode
|
|
19
|
+
const document = documents[0];
|
|
20
|
+
|
|
21
|
+
if (loadingDocuments) {
|
|
22
|
+
return (
|
|
23
|
+
<Flex align='center' justify='center' padding={4}>
|
|
24
|
+
<Spinner />
|
|
25
|
+
</Flex>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!document) {
|
|
30
|
+
return (
|
|
31
|
+
<Card padding={4} tone='caution'>
|
|
32
|
+
<Text>No document found</Text>
|
|
33
|
+
</Card>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Check if this is a source language document
|
|
38
|
+
const currentDocumentLanguage =
|
|
39
|
+
document[pluginConfig.getLanguageField()] || pluginConfig.getSourceLocale();
|
|
40
|
+
const shouldShowTranslationComponents =
|
|
41
|
+
currentDocumentLanguage === pluginConfig.getSourceLocale();
|
|
42
|
+
|
|
43
|
+
if (!shouldShowTranslationComponents) {
|
|
44
|
+
return (
|
|
45
|
+
<Card padding={4} tone='neutral' border>
|
|
46
|
+
<Text size={1} muted>
|
|
47
|
+
Translation tools are only available for{' '}
|
|
48
|
+
<code>{pluginConfig.getSourceLocale()}</code> documents.
|
|
49
|
+
</Text>
|
|
50
|
+
</Card>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Box>
|
|
56
|
+
<Stack space={4}>
|
|
57
|
+
<Card shadow={1} padding={3}>
|
|
58
|
+
<Stack space={3}>
|
|
59
|
+
<Flex justify='space-between' align='flex-start'>
|
|
60
|
+
<Box flex={1}>
|
|
61
|
+
<Text weight='semibold' size={1}>
|
|
62
|
+
{document._id?.replace('drafts.', '') || document._id}
|
|
63
|
+
</Text>
|
|
64
|
+
<Text size={0} muted style={{ marginTop: '2px' }}>
|
|
65
|
+
{document._type}
|
|
66
|
+
</Text>
|
|
67
|
+
</Box>
|
|
68
|
+
</Flex>
|
|
69
|
+
|
|
70
|
+
<Stack space={2}>
|
|
71
|
+
{locales.length > 0 ? (
|
|
72
|
+
locales
|
|
73
|
+
.filter((locale) => locale.enabled !== false)
|
|
74
|
+
.map((locale) => {
|
|
75
|
+
const documentId =
|
|
76
|
+
document._id?.replace('drafts.', '') || document._id;
|
|
77
|
+
const key = `${documentId}:${locale.localeId}`;
|
|
78
|
+
const status = translationStatuses.get(key);
|
|
79
|
+
const isDownloaded = downloadStatus.downloaded.has(key);
|
|
80
|
+
const isImported = importedTranslations.has(key);
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<LanguageStatus
|
|
84
|
+
key={`${document._id}-${locale.localeId}`}
|
|
85
|
+
title={locale.description || locale.localeId}
|
|
86
|
+
progress={status?.progress || 0}
|
|
87
|
+
isImported={isImported || isDownloaded}
|
|
88
|
+
importFile={async () => {
|
|
89
|
+
await handleImportDocument(
|
|
90
|
+
documentId,
|
|
91
|
+
locale.localeId
|
|
92
|
+
);
|
|
93
|
+
}}
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
})
|
|
97
|
+
) : (
|
|
98
|
+
<Text size={1} muted>
|
|
99
|
+
No locales configured
|
|
100
|
+
</Text>
|
|
101
|
+
)}
|
|
102
|
+
</Stack>
|
|
103
|
+
</Stack>
|
|
104
|
+
</Card>
|
|
105
|
+
</Stack>
|
|
106
|
+
</Box>
|
|
107
|
+
);
|
|
108
|
+
};
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
// adapted from https://github.com/sanity-io/sanity-translations-tab. See LICENSE.md for more details.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Add cleanup function to cancel async tasks
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useMemo, useState, useCallback, useEffect } from 'react';
|
|
8
|
+
import {
|
|
9
|
+
Stack,
|
|
10
|
+
Text,
|
|
11
|
+
Card,
|
|
12
|
+
Button,
|
|
13
|
+
Grid,
|
|
14
|
+
Box,
|
|
15
|
+
Flex,
|
|
16
|
+
Switch,
|
|
17
|
+
Tooltip,
|
|
18
|
+
} from '@sanity/ui';
|
|
19
|
+
import { pluginConfig } from '../../adapter/core';
|
|
20
|
+
import { useTranslations } from '../TranslationsProvider';
|
|
21
|
+
import { LanguageStatus } from '../shared/LanguageStatus';
|
|
22
|
+
import { LocaleCheckbox } from '../shared/LocaleCheckbox';
|
|
23
|
+
import { DownloadIcon, LinkIcon } from '@sanity/icons';
|
|
24
|
+
|
|
25
|
+
export const TranslationView = () => {
|
|
26
|
+
const {
|
|
27
|
+
documents,
|
|
28
|
+
locales,
|
|
29
|
+
translationStatuses,
|
|
30
|
+
isBusy,
|
|
31
|
+
handleTranslateAll,
|
|
32
|
+
handleImportDocument,
|
|
33
|
+
handleRefreshAll,
|
|
34
|
+
isRefreshing,
|
|
35
|
+
importedTranslations,
|
|
36
|
+
setLocales,
|
|
37
|
+
handlePatchDocumentReferences,
|
|
38
|
+
} = useTranslations();
|
|
39
|
+
|
|
40
|
+
const [autoImport, setAutoImport] = useState(false);
|
|
41
|
+
const [isImporting, setIsImporting] = useState(false);
|
|
42
|
+
const [autoRefresh, setAutoRefresh] = useState(true);
|
|
43
|
+
|
|
44
|
+
// Get the single document (first document in single document mode)
|
|
45
|
+
const document = documents[0];
|
|
46
|
+
|
|
47
|
+
// Extract the current document's language from the language field
|
|
48
|
+
const currentDocumentLanguage = useMemo(() => {
|
|
49
|
+
if (!document) return null;
|
|
50
|
+
|
|
51
|
+
// Get the language from the document's language field
|
|
52
|
+
const languageField = pluginConfig.getLanguageField();
|
|
53
|
+
const documentLanguage = document[languageField];
|
|
54
|
+
|
|
55
|
+
// If no language field is set, assume it's the source language
|
|
56
|
+
return documentLanguage || pluginConfig.getSourceLocale();
|
|
57
|
+
}, [document]);
|
|
58
|
+
|
|
59
|
+
// Only show translation components if we're on a source language document
|
|
60
|
+
const shouldShowTranslationComponents = useMemo(() => {
|
|
61
|
+
if (!currentDocumentLanguage) return false;
|
|
62
|
+
return currentDocumentLanguage === pluginConfig.getSourceLocale();
|
|
63
|
+
}, [currentDocumentLanguage]);
|
|
64
|
+
|
|
65
|
+
// Get available locales (excluding source locale)
|
|
66
|
+
const availableLocales = useMemo(() => {
|
|
67
|
+
const sourceLocale = pluginConfig.getSourceLocale();
|
|
68
|
+
return locales.filter(
|
|
69
|
+
(locale) => locale.enabled !== false && locale.localeId !== sourceLocale
|
|
70
|
+
);
|
|
71
|
+
}, [locales]);
|
|
72
|
+
|
|
73
|
+
// Get document ID for status tracking
|
|
74
|
+
const documentId = useMemo(() => {
|
|
75
|
+
if (!document) return null;
|
|
76
|
+
return document._id?.replace('drafts.', '') || document._id;
|
|
77
|
+
}, [document]);
|
|
78
|
+
|
|
79
|
+
// Auto import functionality
|
|
80
|
+
const checkAndImportCompletedTranslations = useCallback(async () => {
|
|
81
|
+
if (!autoImport || isImporting || !documentId) return;
|
|
82
|
+
|
|
83
|
+
const completedTranslations = availableLocales.filter((locale) => {
|
|
84
|
+
const key = `${documentId}:${locale.localeId}`;
|
|
85
|
+
const status = translationStatuses.get(key);
|
|
86
|
+
return (
|
|
87
|
+
(status?.progress || 0) >= 100 &&
|
|
88
|
+
status?.isReady &&
|
|
89
|
+
!importedTranslations.has(key)
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (completedTranslations.length === 0) return;
|
|
94
|
+
|
|
95
|
+
setIsImporting(true);
|
|
96
|
+
try {
|
|
97
|
+
for (const locale of completedTranslations) {
|
|
98
|
+
await handleImportDocument(documentId, locale.localeId);
|
|
99
|
+
}
|
|
100
|
+
} finally {
|
|
101
|
+
setIsImporting(false);
|
|
102
|
+
}
|
|
103
|
+
}, [
|
|
104
|
+
autoImport,
|
|
105
|
+
isImporting,
|
|
106
|
+
documentId,
|
|
107
|
+
availableLocales,
|
|
108
|
+
translationStatuses,
|
|
109
|
+
importedTranslations,
|
|
110
|
+
handleImportDocument,
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
const handleImportAll = useCallback(async () => {
|
|
114
|
+
if (isImporting || !documentId) return;
|
|
115
|
+
|
|
116
|
+
setIsImporting(true);
|
|
117
|
+
try {
|
|
118
|
+
const readyTranslations = availableLocales.filter((locale) => {
|
|
119
|
+
const key = `${documentId}:${locale.localeId}`;
|
|
120
|
+
const status = translationStatuses.get(key);
|
|
121
|
+
return status?.isReady && !importedTranslations.has(key);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
for (const locale of readyTranslations) {
|
|
125
|
+
await handleImportDocument(documentId, locale.localeId);
|
|
126
|
+
}
|
|
127
|
+
} finally {
|
|
128
|
+
setIsImporting(false);
|
|
129
|
+
}
|
|
130
|
+
}, [
|
|
131
|
+
isImporting,
|
|
132
|
+
documentId,
|
|
133
|
+
availableLocales,
|
|
134
|
+
translationStatuses,
|
|
135
|
+
importedTranslations,
|
|
136
|
+
handleImportDocument,
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
// Check for completed translations on status updates
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
checkAndImportCompletedTranslations();
|
|
142
|
+
}, [checkAndImportCompletedTranslations]);
|
|
143
|
+
|
|
144
|
+
// Auto refresh functionality
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
if (!autoRefresh || !documentId || availableLocales.length === 0) return;
|
|
147
|
+
|
|
148
|
+
const interval = setInterval(async () => {
|
|
149
|
+
await handleRefreshAll();
|
|
150
|
+
await checkAndImportCompletedTranslations();
|
|
151
|
+
}, 10000);
|
|
152
|
+
|
|
153
|
+
return () => clearInterval(interval);
|
|
154
|
+
}, [
|
|
155
|
+
autoRefresh,
|
|
156
|
+
documentId,
|
|
157
|
+
availableLocales.length,
|
|
158
|
+
handleRefreshAll,
|
|
159
|
+
checkAndImportCompletedTranslations,
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
const initialRefresh = async () => {
|
|
164
|
+
await handleRefreshAll();
|
|
165
|
+
await checkAndImportCompletedTranslations();
|
|
166
|
+
};
|
|
167
|
+
initialRefresh();
|
|
168
|
+
}, []);
|
|
169
|
+
|
|
170
|
+
// Locale toggle functionality
|
|
171
|
+
const toggleLocale = useCallback(
|
|
172
|
+
(localeId: string, shouldEnable: boolean) => {
|
|
173
|
+
const updatedLocales = locales.map((locale) =>
|
|
174
|
+
locale.localeId === localeId
|
|
175
|
+
? { ...locale, enabled: shouldEnable }
|
|
176
|
+
: locale
|
|
177
|
+
);
|
|
178
|
+
setLocales(updatedLocales);
|
|
179
|
+
},
|
|
180
|
+
[locales, setLocales]
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const toggleAllLocales = useCallback(() => {
|
|
184
|
+
const sourceLocale = pluginConfig.getSourceLocale();
|
|
185
|
+
const nonSourceLocales = locales.filter(
|
|
186
|
+
(locale) => locale.localeId !== sourceLocale
|
|
187
|
+
);
|
|
188
|
+
const allEnabled = nonSourceLocales.every(
|
|
189
|
+
(locale) => locale.enabled === true || locale.enabled === undefined
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const updatedLocales = locales.map((locale) =>
|
|
193
|
+
locale.localeId === sourceLocale
|
|
194
|
+
? locale // Don't change source locale
|
|
195
|
+
: { ...locale, enabled: !allEnabled }
|
|
196
|
+
);
|
|
197
|
+
setLocales(updatedLocales);
|
|
198
|
+
}, [locales, setLocales]);
|
|
199
|
+
|
|
200
|
+
// Show message if we're not on a source language document
|
|
201
|
+
if (!shouldShowTranslationComponents) {
|
|
202
|
+
return (
|
|
203
|
+
<Card padding={4} tone='neutral' border>
|
|
204
|
+
<Text size={1} muted>
|
|
205
|
+
Translation tools are only available for{' '}
|
|
206
|
+
<code>{pluginConfig.getSourceLocale()}</code> documents.
|
|
207
|
+
</Text>
|
|
208
|
+
</Card>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<Stack space={6} padding={4}>
|
|
214
|
+
{/* Generate Translations Section */}
|
|
215
|
+
<Stack space={4}>
|
|
216
|
+
<Text as='h2' weight='semibold' size={2}>
|
|
217
|
+
Generate Translations
|
|
218
|
+
</Text>
|
|
219
|
+
|
|
220
|
+
{/* Locale Selection */}
|
|
221
|
+
<Stack space={3}>
|
|
222
|
+
<Flex align='center' justify='space-between'>
|
|
223
|
+
<Text weight='semibold' size={1}>
|
|
224
|
+
{availableLocales.length === 1
|
|
225
|
+
? 'Select locale'
|
|
226
|
+
: 'Select locales'}
|
|
227
|
+
</Text>
|
|
228
|
+
<Button
|
|
229
|
+
fontSize={1}
|
|
230
|
+
padding={2}
|
|
231
|
+
text='Toggle All'
|
|
232
|
+
onClick={toggleAllLocales}
|
|
233
|
+
/>
|
|
234
|
+
</Flex>
|
|
235
|
+
|
|
236
|
+
<Grid columns={[1, 1, 2, 3]} gap={1}>
|
|
237
|
+
{locales
|
|
238
|
+
.filter(
|
|
239
|
+
(locale) => locale.localeId !== pluginConfig.getSourceLocale()
|
|
240
|
+
)
|
|
241
|
+
.map((locale) => (
|
|
242
|
+
<LocaleCheckbox
|
|
243
|
+
key={locale.localeId}
|
|
244
|
+
locale={locale}
|
|
245
|
+
toggle={toggleLocale}
|
|
246
|
+
checked={
|
|
247
|
+
locale.enabled === true || locale.enabled === undefined
|
|
248
|
+
}
|
|
249
|
+
/>
|
|
250
|
+
))}
|
|
251
|
+
</Grid>
|
|
252
|
+
</Stack>
|
|
253
|
+
|
|
254
|
+
<Button
|
|
255
|
+
onClick={() => {
|
|
256
|
+
setAutoImport(true);
|
|
257
|
+
handleTranslateAll();
|
|
258
|
+
}}
|
|
259
|
+
disabled={isBusy || !availableLocales.length}
|
|
260
|
+
tone='positive'
|
|
261
|
+
text={isBusy ? 'Creating translations...' : 'Generate Translations'}
|
|
262
|
+
/>
|
|
263
|
+
</Stack>
|
|
264
|
+
|
|
265
|
+
{/* Translation Status Section */}
|
|
266
|
+
{documentId && availableLocales.length > 0 && (
|
|
267
|
+
<Stack space={4}>
|
|
268
|
+
<Flex align='center' justify='space-between'>
|
|
269
|
+
<Text as='h2' weight='semibold' size={2}>
|
|
270
|
+
Translation Status
|
|
271
|
+
</Text>
|
|
272
|
+
<Flex gap={3} align='center'>
|
|
273
|
+
<Flex gap={2} align='center'>
|
|
274
|
+
<Text size={1}>Auto-refresh</Text>
|
|
275
|
+
<Switch
|
|
276
|
+
checked={autoRefresh}
|
|
277
|
+
onChange={() => setAutoRefresh(!autoRefresh)}
|
|
278
|
+
/>
|
|
279
|
+
</Flex>
|
|
280
|
+
<Button
|
|
281
|
+
fontSize={1}
|
|
282
|
+
padding={2}
|
|
283
|
+
text='Refresh Status'
|
|
284
|
+
onClick={handleRefreshAll}
|
|
285
|
+
disabled={isRefreshing}
|
|
286
|
+
/>
|
|
287
|
+
</Flex>
|
|
288
|
+
</Flex>
|
|
289
|
+
|
|
290
|
+
<Box>
|
|
291
|
+
{availableLocales.map((locale) => {
|
|
292
|
+
const key = `${documentId}:${locale.localeId}`;
|
|
293
|
+
const status = translationStatuses.get(key);
|
|
294
|
+
const progress = status?.progress || 0;
|
|
295
|
+
const isImported = importedTranslations.has(key);
|
|
296
|
+
|
|
297
|
+
return (
|
|
298
|
+
<LanguageStatus
|
|
299
|
+
key={key}
|
|
300
|
+
title={locale.description}
|
|
301
|
+
progress={progress}
|
|
302
|
+
isImported={isImported}
|
|
303
|
+
importFile={async () => {
|
|
304
|
+
if (!isImported && status?.isReady) {
|
|
305
|
+
await handleImportDocument(documentId, locale.localeId);
|
|
306
|
+
}
|
|
307
|
+
}}
|
|
308
|
+
/>
|
|
309
|
+
);
|
|
310
|
+
})}
|
|
311
|
+
</Box>
|
|
312
|
+
|
|
313
|
+
{/* Import Controls */}
|
|
314
|
+
<Stack space={3}>
|
|
315
|
+
<Flex gap={3} align='center' justify='space-between'>
|
|
316
|
+
<Flex gap={2} align='center'>
|
|
317
|
+
<Button
|
|
318
|
+
mode='ghost'
|
|
319
|
+
onClick={handleImportAll}
|
|
320
|
+
text={isImporting ? 'Importing...' : 'Import All'}
|
|
321
|
+
icon={DownloadIcon}
|
|
322
|
+
disabled={
|
|
323
|
+
isImporting ||
|
|
324
|
+
availableLocales.every((locale) => {
|
|
325
|
+
const key = `${documentId}:${locale.localeId}`;
|
|
326
|
+
const status = translationStatuses.get(key);
|
|
327
|
+
return !status?.isReady || importedTranslations.has(key);
|
|
328
|
+
})
|
|
329
|
+
}
|
|
330
|
+
/>
|
|
331
|
+
<Text size={1} muted>
|
|
332
|
+
Imported{' '}
|
|
333
|
+
{
|
|
334
|
+
availableLocales.filter((locale) => {
|
|
335
|
+
const key = `${documentId}:${locale.localeId}`;
|
|
336
|
+
return importedTranslations.has(key);
|
|
337
|
+
}).length
|
|
338
|
+
}
|
|
339
|
+
/
|
|
340
|
+
{
|
|
341
|
+
availableLocales.filter((locale) => {
|
|
342
|
+
const key = `${documentId}:${locale.localeId}`;
|
|
343
|
+
const status = translationStatuses.get(key);
|
|
344
|
+
return status?.isReady;
|
|
345
|
+
}).length
|
|
346
|
+
}
|
|
347
|
+
</Text>
|
|
348
|
+
</Flex>
|
|
349
|
+
<Flex gap={2} align='center' style={{ whiteSpace: 'nowrap' }}>
|
|
350
|
+
<Text size={1}>Auto-import when complete</Text>
|
|
351
|
+
<Switch
|
|
352
|
+
checked={autoImport}
|
|
353
|
+
onChange={() => setAutoImport(!autoImport)}
|
|
354
|
+
disabled={isImporting}
|
|
355
|
+
/>
|
|
356
|
+
</Flex>
|
|
357
|
+
</Flex>
|
|
358
|
+
|
|
359
|
+
<Flex justify='flex-start'>
|
|
360
|
+
<Tooltip
|
|
361
|
+
placement='top'
|
|
362
|
+
content={`Replaces references to ${pluginConfig.getSourceLocale()} documents in this document with the corresponding translated document reference`}
|
|
363
|
+
>
|
|
364
|
+
<Button
|
|
365
|
+
mode='ghost'
|
|
366
|
+
tone='caution'
|
|
367
|
+
onClick={handlePatchDocumentReferences}
|
|
368
|
+
text={isBusy ? 'Patching...' : 'Patch Document References'}
|
|
369
|
+
icon={isBusy ? null : LinkIcon}
|
|
370
|
+
disabled={isBusy}
|
|
371
|
+
/>
|
|
372
|
+
</Tooltip>
|
|
373
|
+
</Flex>
|
|
374
|
+
</Stack>
|
|
375
|
+
</Stack>
|
|
376
|
+
)}
|
|
377
|
+
</Stack>
|
|
378
|
+
);
|
|
379
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { SanityDocument } from 'sanity';
|
|
3
|
+
import { BaseTranslationWrapper } from '../shared/BaseTranslationWrapper';
|
|
4
|
+
import { TranslationsProvider } from '../TranslationsProvider';
|
|
5
|
+
import { TranslationView } from './TranslationView';
|
|
6
|
+
|
|
7
|
+
type TranslationTabProps = {
|
|
8
|
+
document: {
|
|
9
|
+
displayed: SanityDocument;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const TranslationTab = (props: TranslationTabProps) => {
|
|
14
|
+
const { displayed } = props.document;
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<BaseTranslationWrapper showContainer={false}>
|
|
18
|
+
<TranslationsProvider singleDocument={displayed}>
|
|
19
|
+
<TranslationView />
|
|
20
|
+
</TranslationsProvider>
|
|
21
|
+
</BaseTranslationWrapper>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default TranslationTab;
|
|
@@ -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 '
|
|
4
|
+
import { BaseDocumentMerger } from '../../serialization';
|
|
5
5
|
|
|
6
6
|
import { findLatestDraft } from '../utils/findLatestDraft';
|
|
7
7
|
import { findDocumentAtRevision } from '../utils/findDocumentAtRevision';
|
|
@@ -9,7 +9,7 @@ import { createI18nDocAndPatchMetadata } from './helpers/createI18nDocAndPatchMe
|
|
|
9
9
|
import { getOrCreateTranslationMetadata } from './helpers/getOrCreateTranslationMetadata';
|
|
10
10
|
import { patchI18nDoc } from './helpers/patchI18nDoc';
|
|
11
11
|
import type { GTFile } from '../../types';
|
|
12
|
-
import {
|
|
12
|
+
import { pluginConfig } from '../../adapter/core';
|
|
13
13
|
|
|
14
14
|
export const documentLevelPatch = async (
|
|
15
15
|
docInfo: GTFile,
|
|
@@ -17,10 +17,9 @@ export const documentLevelPatch = async (
|
|
|
17
17
|
localeId: string,
|
|
18
18
|
client: SanityClient,
|
|
19
19
|
languageField: string = 'language',
|
|
20
|
-
mergeWithTargetLocale: boolean = false
|
|
21
|
-
publish: boolean = false
|
|
20
|
+
mergeWithTargetLocale: boolean = false
|
|
22
21
|
): Promise<void> => {
|
|
23
|
-
const baseLanguage =
|
|
22
|
+
const baseLanguage = pluginConfig.getSourceLocale();
|
|
24
23
|
//this is the document we use to merge with the translated fields
|
|
25
24
|
let baseDoc: SanityDocument | null = null;
|
|
26
25
|
|
|
@@ -100,8 +99,7 @@ export const documentLevelPatch = async (
|
|
|
100
99
|
baseDoc,
|
|
101
100
|
merged,
|
|
102
101
|
translatedFields,
|
|
103
|
-
client
|
|
104
|
-
publish
|
|
102
|
+
client
|
|
105
103
|
);
|
|
106
104
|
}
|
|
107
105
|
//otherwise, create a new document
|
|
@@ -114,8 +112,7 @@ export const documentLevelPatch = async (
|
|
|
114
112
|
client,
|
|
115
113
|
translationMetadata,
|
|
116
114
|
docInfo.documentId,
|
|
117
|
-
languageField
|
|
118
|
-
publish
|
|
115
|
+
languageField
|
|
119
116
|
);
|
|
120
117
|
}
|
|
121
118
|
};
|