gt-sanity 0.0.5 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +1 -8
- package/README.md +5 -5
- package/dist/index.d.mts +122 -95
- package/dist/index.d.ts +122 -95
- package/dist/index.js +9089 -1119
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +9099 -1100
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -4
- package/src/adapter/core.ts +111 -9
- package/src/adapter/createTask.ts +1 -1
- package/src/adapter/getLocales.ts +2 -2
- package/src/adapter/types.ts +9 -0
- package/src/components/TranslationsProvider.tsx +942 -0
- package/src/components/page/BatchProgress.tsx +27 -0
- package/src/components/page/ImportAllDialog.tsx +51 -0
- package/src/components/page/ImportMissingDialog.tsx +55 -0
- package/src/components/page/TranslateAllDialog.tsx +55 -0
- package/src/components/page/TranslationsTable.tsx +81 -0
- package/src/components/page/TranslationsTool.tsx +338 -0
- package/src/components/shared/BaseTranslationWrapper.tsx +82 -0
- package/src/components/{LanguageStatus.tsx → shared/LanguageStatus.tsx} +2 -0
- package/src/components/shared/LocaleCheckbox.tsx +47 -0
- package/src/components/{ProgressBar.tsx → shared/ProgressBar.tsx} +2 -0
- package/src/components/shared/SingleDocumentView.tsx +108 -0
- package/src/components/tab/TranslationView.tsx +379 -0
- package/src/components/tab/TranslationsTab.tsx +25 -0
- package/src/configuration/baseDocumentLevelConfig/documentLevelPatch.ts +21 -11
- package/src/configuration/baseDocumentLevelConfig/helpers/createI18nDocAndPatchMetadata.ts +57 -23
- package/src/configuration/baseDocumentLevelConfig/helpers/createTranslationMetadata.ts +2 -0
- package/src/configuration/baseDocumentLevelConfig/helpers/getOrCreateTranslationMetadata.ts +2 -0
- package/src/configuration/baseDocumentLevelConfig/helpers/getTranslationMetadata.ts +2 -0
- package/src/configuration/baseDocumentLevelConfig/helpers/patchI18nDoc.ts +31 -8
- package/src/configuration/baseDocumentLevelConfig/index.ts +18 -101
- package/src/configuration/baseFieldLevelConfig.ts +19 -51
- package/src/configuration/utils/checkSerializationVersion.ts +2 -0
- package/src/configuration/utils/findDocumentAtRevision.ts +2 -0
- package/src/configuration/utils/findLatestDraft.ts +2 -0
- package/src/hooks/useClient.ts +3 -1
- package/src/hooks/useSecrets.ts +2 -0
- package/src/index.ts +91 -67
- package/src/sanity-api/findDocuments.ts +44 -0
- package/src/sanity-api/publishDocuments.ts +49 -0
- package/src/sanity-api/resolveRefs.ts +146 -0
- package/src/serialization/BaseDocumentMerger.ts +138 -0
- package/src/serialization/BaseSerializationConfig.ts +220 -0
- package/src/serialization/__tests__/BaseDocumentDeserializer/__snapshots__/documentLevelDeserialization.test.ts.snap +189 -0
- package/src/serialization/__tests__/BaseDocumentDeserializer/__snapshots__/fieldLevelDeserialization.test.ts.snap +107 -0
- package/src/serialization/__tests__/BaseDocumentDeserializer/baseDeserialization.test.ts +397 -0
- package/src/serialization/__tests__/BaseDocumentDeserializer/documentLevelDeserialization.test.ts +107 -0
- package/src/serialization/__tests__/BaseDocumentDeserializer/fieldLevelDeserialization.test.ts +107 -0
- package/src/serialization/__tests__/BaseDocumentMerger/__snapshots__/documentLevelMerge.test.ts.snap +193 -0
- package/src/serialization/__tests__/BaseDocumentMerger/__snapshots__/fieldLevelMerge.test.ts.snap +97 -0
- package/src/serialization/__tests__/BaseDocumentMerger/baseMerge.test.ts +36 -0
- package/src/serialization/__tests__/BaseDocumentMerger/documentLevelMerge.test.ts +96 -0
- package/src/serialization/__tests__/BaseDocumentMerger/fieldLevelMerge.test.ts +142 -0
- package/src/serialization/__tests__/BaseDocumentMerger/utils.ts +52 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/__snapshots__/documentInlineMarks.test.ts.snap +39 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/__snapshots__/documentLevelSerialization.test.ts.snap +8 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/__snapshots__/fieldLevelSerialization.test.ts.snap +8 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/baseSerialization.test.ts +345 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/documentInlineMarks.test.ts +53 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/documentLevelSerialization.test.ts +120 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/fieldLevelSerialization.test.ts +153 -0
- package/src/serialization/__tests__/BaseDocumentSerializer/utils.ts +27 -0
- package/src/serialization/__tests__/README +2 -0
- package/src/serialization/__tests__/__fixtures__/annotationAndInlineBlocks.json +140 -0
- package/src/serialization/__tests__/__fixtures__/customStyles.json +62 -0
- package/src/serialization/__tests__/__fixtures__/documentInlineMarks.json +70 -0
- package/src/serialization/__tests__/__fixtures__/documentLevelArticle.json +185 -0
- package/src/serialization/__tests__/__fixtures__/fieldLevelArticle.json +107 -0
- package/src/serialization/__tests__/__fixtures__/inlineDocumentLevelArticle.json +134 -0
- package/src/serialization/__tests__/__fixtures__/inlineSchema.ts +270 -0
- package/src/serialization/__tests__/__fixtures__/messy-html.html +26 -0
- package/src/serialization/__tests__/__fixtures__/nestedLanguageFields.json +54 -0
- package/src/serialization/__tests__/__fixtures__/schema.ts +310 -0
- package/src/serialization/__tests__/global.setup.ts +40 -0
- package/src/serialization/__tests__/helpers.ts +132 -0
- package/src/serialization/data.ts +82 -0
- package/src/serialization/deserialize/BaseDocumentDeserializer.ts +171 -0
- package/src/serialization/deserialize/helpers.ts +42 -0
- package/src/serialization/helpers.ts +18 -0
- package/src/serialization/index.ts +11 -0
- package/src/serialization/serialize/fieldFilters.ts +124 -0
- package/src/serialization/serialize/index.ts +284 -0
- package/src/serialization/types.ts +41 -0
- package/src/translation/checkTranslationStatus.ts +42 -0
- package/src/translation/createJobs.ts +16 -0
- package/src/translation/downloadTranslations.ts +68 -0
- package/src/translation/importDocument.ts +23 -0
- package/src/translation/initProject.ts +61 -0
- package/src/translation/uploadFiles.ts +32 -0
- package/src/types.ts +7 -20
- package/src/utils/applyDocuments.ts +72 -0
- package/src/utils/batchProcessor.ts +111 -0
- package/src/utils/importUtils.ts +95 -0
- package/src/utils/serialize.ts +52 -0
- package/src/utils/shared.ts +1 -0
- package/src/adapter/index.ts +0 -13
- package/src/components/NewTask.tsx +0 -249
- package/src/components/TaskView.tsx +0 -255
- package/src/components/TranslationContext.tsx +0 -19
- package/src/components/TranslationView.tsx +0 -82
- package/src/components/TranslationsTab.tsx +0 -177
- package/src/configuration/baseDocumentLevelConfig/helpers/index.ts +0 -5
- package/src/configuration/baseDocumentLevelConfig/legacyDocumentLevelPatch.ts +0 -69
- package/src/configuration/index.ts +0 -18
- package/src/configuration/utils/index.ts +0 -3
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Flex, Text, Spinner } from '@sanity/ui';
|
|
3
|
+
|
|
4
|
+
interface BatchProgressProps {
|
|
5
|
+
isActive: boolean;
|
|
6
|
+
current: number;
|
|
7
|
+
total: number;
|
|
8
|
+
operationName: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const BatchProgress: React.FC<BatchProgressProps> = ({
|
|
12
|
+
isActive,
|
|
13
|
+
current,
|
|
14
|
+
total,
|
|
15
|
+
operationName,
|
|
16
|
+
}) => {
|
|
17
|
+
if (!isActive) return null;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Flex justify='center' align='center' gap={3}>
|
|
21
|
+
<Spinner size={1} />
|
|
22
|
+
<Text size={1}>
|
|
23
|
+
{operationName} {current} of {total}...
|
|
24
|
+
</Text>
|
|
25
|
+
</Flex>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Button, Dialog, Flex, Stack, Text } from '@sanity/ui';
|
|
3
|
+
import { useTranslations } from '../TranslationsProvider';
|
|
4
|
+
|
|
5
|
+
interface ImportAllDialogProps {
|
|
6
|
+
isOpen: boolean;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const ImportAllDialog: React.FC<ImportAllDialogProps> = ({
|
|
11
|
+
isOpen,
|
|
12
|
+
onClose,
|
|
13
|
+
}) => {
|
|
14
|
+
const { documents, handleImportAll } = useTranslations();
|
|
15
|
+
|
|
16
|
+
const handleConfirm = async () => {
|
|
17
|
+
onClose();
|
|
18
|
+
await handleImportAll();
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
if (!isOpen) return null;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Dialog
|
|
25
|
+
header='Confirm Import'
|
|
26
|
+
id='import-all-dialog'
|
|
27
|
+
onClose={onClose}
|
|
28
|
+
footer={
|
|
29
|
+
<Box padding={3}>
|
|
30
|
+
<Flex gap={2}>
|
|
31
|
+
<Button text='Cancel' mode='ghost' onClick={onClose} />
|
|
32
|
+
<Button text='Import All' tone='critical' onClick={handleConfirm} />
|
|
33
|
+
</Flex>
|
|
34
|
+
</Box>
|
|
35
|
+
}
|
|
36
|
+
>
|
|
37
|
+
<Box padding={4}>
|
|
38
|
+
<Stack space={3}>
|
|
39
|
+
<Text>
|
|
40
|
+
Are you sure you want to import translations for all{' '}
|
|
41
|
+
{documents.length} documents?
|
|
42
|
+
</Text>
|
|
43
|
+
<Text size={1} muted>
|
|
44
|
+
This will download and apply translated content to your documents.
|
|
45
|
+
Note that this will overwrite any existing translations!
|
|
46
|
+
</Text>
|
|
47
|
+
</Stack>
|
|
48
|
+
</Box>
|
|
49
|
+
</Dialog>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Button, Dialog, Flex, Stack, Text } from '@sanity/ui';
|
|
3
|
+
import { useTranslations } from '../TranslationsProvider';
|
|
4
|
+
|
|
5
|
+
interface ImportMissingDialogProps {
|
|
6
|
+
isOpen: boolean;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const ImportMissingDialog: React.FC<ImportMissingDialogProps> = ({
|
|
11
|
+
isOpen,
|
|
12
|
+
onClose,
|
|
13
|
+
}) => {
|
|
14
|
+
const { handleImportMissing } = useTranslations();
|
|
15
|
+
|
|
16
|
+
const handleConfirm = async () => {
|
|
17
|
+
onClose();
|
|
18
|
+
await handleImportMissing();
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
if (!isOpen) return null;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Dialog
|
|
25
|
+
header='Confirm Import Missing'
|
|
26
|
+
id='import-missing-dialog'
|
|
27
|
+
onClose={onClose}
|
|
28
|
+
footer={
|
|
29
|
+
<Box padding={3}>
|
|
30
|
+
<Flex gap={2}>
|
|
31
|
+
<Button text='Cancel' mode='ghost' onClick={onClose} />
|
|
32
|
+
<Button
|
|
33
|
+
text='Import Missing'
|
|
34
|
+
tone='primary'
|
|
35
|
+
onClick={handleConfirm}
|
|
36
|
+
/>
|
|
37
|
+
</Flex>
|
|
38
|
+
</Box>
|
|
39
|
+
}
|
|
40
|
+
>
|
|
41
|
+
<Box padding={4}>
|
|
42
|
+
<Stack space={3}>
|
|
43
|
+
<Text>
|
|
44
|
+
Import only the missing translations (translations that are ready
|
|
45
|
+
but haven't been imported to Sanity yet)?
|
|
46
|
+
</Text>
|
|
47
|
+
<Text size={1} muted>
|
|
48
|
+
This will check existing translation metadata and only import
|
|
49
|
+
translations that don't already exist in your dataset.
|
|
50
|
+
</Text>
|
|
51
|
+
</Stack>
|
|
52
|
+
</Box>
|
|
53
|
+
</Dialog>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Button, Dialog, Flex, Stack, Text } from '@sanity/ui';
|
|
3
|
+
import { useTranslations } from '../TranslationsProvider';
|
|
4
|
+
|
|
5
|
+
interface TranslateAllDialogProps {
|
|
6
|
+
isOpen: boolean;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const TranslateAllDialog: React.FC<TranslateAllDialogProps> = ({
|
|
11
|
+
isOpen,
|
|
12
|
+
onClose,
|
|
13
|
+
}) => {
|
|
14
|
+
const { documents, handleTranslateAll } = useTranslations();
|
|
15
|
+
|
|
16
|
+
const handleConfirm = async () => {
|
|
17
|
+
onClose();
|
|
18
|
+
await handleTranslateAll();
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
if (!isOpen) return null;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Dialog
|
|
25
|
+
header='Confirm Translation'
|
|
26
|
+
id='translate-all-dialog'
|
|
27
|
+
onClose={onClose}
|
|
28
|
+
footer={
|
|
29
|
+
<Box padding={3}>
|
|
30
|
+
<Flex gap={2}>
|
|
31
|
+
<Button text='Cancel' mode='ghost' onClick={onClose} />
|
|
32
|
+
<Button
|
|
33
|
+
text='Translate All'
|
|
34
|
+
tone='critical'
|
|
35
|
+
onClick={handleConfirm}
|
|
36
|
+
/>
|
|
37
|
+
</Flex>
|
|
38
|
+
</Box>
|
|
39
|
+
}
|
|
40
|
+
>
|
|
41
|
+
<Box padding={4}>
|
|
42
|
+
<Stack space={3}>
|
|
43
|
+
<Text>
|
|
44
|
+
Are you sure you want to create translations for all{' '}
|
|
45
|
+
{documents.length} documents?
|
|
46
|
+
</Text>
|
|
47
|
+
<Text size={1} muted>
|
|
48
|
+
This will submit all documents to General Translation for
|
|
49
|
+
processing.
|
|
50
|
+
</Text>
|
|
51
|
+
</Stack>
|
|
52
|
+
</Box>
|
|
53
|
+
</Dialog>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Card, Stack, Text, Flex, Spinner } from '@sanity/ui';
|
|
3
|
+
import { LanguageStatus } from '../shared/LanguageStatus';
|
|
4
|
+
import { useTranslations } from '../TranslationsProvider';
|
|
5
|
+
|
|
6
|
+
export const TranslationsTable: React.FC = () => {
|
|
7
|
+
const {
|
|
8
|
+
documents,
|
|
9
|
+
locales,
|
|
10
|
+
loadingDocuments,
|
|
11
|
+
translationStatuses,
|
|
12
|
+
downloadStatus,
|
|
13
|
+
importedTranslations,
|
|
14
|
+
handleImportDocument,
|
|
15
|
+
} = useTranslations();
|
|
16
|
+
|
|
17
|
+
if (loadingDocuments) {
|
|
18
|
+
return (
|
|
19
|
+
<Flex align='center' justify='center' padding={4}>
|
|
20
|
+
<Spinner />
|
|
21
|
+
</Flex>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Box style={{ maxHeight: '60vh', overflowY: 'auto' }}>
|
|
27
|
+
<Stack space={2}>
|
|
28
|
+
{documents.map((document) => (
|
|
29
|
+
<Card key={document._id} shadow={1} padding={3}>
|
|
30
|
+
<Stack space={3}>
|
|
31
|
+
<Flex justify='space-between' align='flex-start'>
|
|
32
|
+
<Box flex={1}>
|
|
33
|
+
<Text weight='semibold' size={1}>
|
|
34
|
+
{document._id?.replace('drafts.', '') || document._id}
|
|
35
|
+
</Text>
|
|
36
|
+
<Text size={0} muted style={{ marginTop: '2px' }}>
|
|
37
|
+
{document._type}
|
|
38
|
+
</Text>
|
|
39
|
+
</Box>
|
|
40
|
+
</Flex>
|
|
41
|
+
|
|
42
|
+
<Stack space={2}>
|
|
43
|
+
{locales.length > 0 ? (
|
|
44
|
+
locales
|
|
45
|
+
.filter((locale) => locale.enabled !== false)
|
|
46
|
+
.map((locale) => {
|
|
47
|
+
const documentId =
|
|
48
|
+
document._id?.replace('drafts.', '') || document._id;
|
|
49
|
+
const key = `${documentId}:${locale.localeId}`;
|
|
50
|
+
const status = translationStatuses.get(key);
|
|
51
|
+
const isDownloaded = downloadStatus.downloaded.has(key);
|
|
52
|
+
const isImported = importedTranslations.has(key);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<LanguageStatus
|
|
56
|
+
key={`${document._id}-${locale.localeId}`}
|
|
57
|
+
title={locale.description || locale.localeId}
|
|
58
|
+
progress={status?.progress || 0}
|
|
59
|
+
isImported={isImported || isDownloaded}
|
|
60
|
+
importFile={async () => {
|
|
61
|
+
await handleImportDocument(
|
|
62
|
+
documentId,
|
|
63
|
+
locale.localeId
|
|
64
|
+
);
|
|
65
|
+
}}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
})
|
|
69
|
+
) : (
|
|
70
|
+
<Text size={1} muted>
|
|
71
|
+
No locales configured
|
|
72
|
+
</Text>
|
|
73
|
+
)}
|
|
74
|
+
</Stack>
|
|
75
|
+
</Stack>
|
|
76
|
+
</Card>
|
|
77
|
+
))}
|
|
78
|
+
</Stack>
|
|
79
|
+
</Box>
|
|
80
|
+
);
|
|
81
|
+
};
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Box,
|
|
4
|
+
Button,
|
|
5
|
+
Container,
|
|
6
|
+
Flex,
|
|
7
|
+
Heading,
|
|
8
|
+
Stack,
|
|
9
|
+
Switch,
|
|
10
|
+
Text,
|
|
11
|
+
Spinner,
|
|
12
|
+
Tooltip,
|
|
13
|
+
} from '@sanity/ui';
|
|
14
|
+
import {
|
|
15
|
+
DownloadIcon,
|
|
16
|
+
CheckmarkCircleIcon,
|
|
17
|
+
LinkIcon,
|
|
18
|
+
PublishIcon,
|
|
19
|
+
} from '@sanity/icons';
|
|
20
|
+
import { Link } from 'sanity/router';
|
|
21
|
+
import { BaseTranslationWrapper } from '../shared/BaseTranslationWrapper';
|
|
22
|
+
import { TranslationsProvider, useTranslations } from '../TranslationsProvider';
|
|
23
|
+
import { TranslationsTable } from './TranslationsTable';
|
|
24
|
+
import { TranslateAllDialog } from './TranslateAllDialog';
|
|
25
|
+
import { ImportAllDialog } from './ImportAllDialog';
|
|
26
|
+
import { ImportMissingDialog } from './ImportMissingDialog';
|
|
27
|
+
import { BatchProgress } from './BatchProgress';
|
|
28
|
+
|
|
29
|
+
const TranslationsToolContent: React.FC = () => {
|
|
30
|
+
const [isTranslateAllDialogOpen, setIsTranslateAllDialogOpen] =
|
|
31
|
+
useState(false);
|
|
32
|
+
const [isImportAllDialogOpen, setIsImportAllDialogOpen] = useState(false);
|
|
33
|
+
const [isImportMissingDialogOpen, setIsImportMissingDialogOpen] =
|
|
34
|
+
useState(false);
|
|
35
|
+
|
|
36
|
+
const {
|
|
37
|
+
isBusy,
|
|
38
|
+
documents,
|
|
39
|
+
locales,
|
|
40
|
+
autoRefresh,
|
|
41
|
+
loadingDocuments,
|
|
42
|
+
importProgress,
|
|
43
|
+
importedTranslations,
|
|
44
|
+
isRefreshing,
|
|
45
|
+
setAutoRefresh,
|
|
46
|
+
handleRefreshAll,
|
|
47
|
+
handlePatchDocumentReferences,
|
|
48
|
+
handlePublishAllTranslations,
|
|
49
|
+
} = useTranslations();
|
|
50
|
+
|
|
51
|
+
// Track which specific operation is running
|
|
52
|
+
const [currentOperation, setCurrentOperation] = useState<string | null>(null);
|
|
53
|
+
|
|
54
|
+
const getOperationText = (
|
|
55
|
+
operationName: string | null,
|
|
56
|
+
isProcessing: boolean
|
|
57
|
+
) => {
|
|
58
|
+
if (!isProcessing || !operationName) return operationName;
|
|
59
|
+
|
|
60
|
+
switch (operationName) {
|
|
61
|
+
case 'Translate All':
|
|
62
|
+
return 'Translating...';
|
|
63
|
+
case 'Import All':
|
|
64
|
+
return 'Importing...';
|
|
65
|
+
case 'Import Missing':
|
|
66
|
+
return 'Importing...';
|
|
67
|
+
case 'Patch Document References':
|
|
68
|
+
return 'Patching...';
|
|
69
|
+
case 'Publish Translations':
|
|
70
|
+
return 'Publishing...';
|
|
71
|
+
default:
|
|
72
|
+
return 'Processing...';
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const getProgressOperationName = () => {
|
|
77
|
+
switch (currentOperation) {
|
|
78
|
+
case 'Import All':
|
|
79
|
+
return 'Importing';
|
|
80
|
+
case 'Import Missing':
|
|
81
|
+
return 'Importing missing';
|
|
82
|
+
case 'Patch Document References':
|
|
83
|
+
return 'Patching';
|
|
84
|
+
case 'Publish Translations':
|
|
85
|
+
return 'Publishing';
|
|
86
|
+
default:
|
|
87
|
+
return 'Processing';
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Reset current operation when operation completes
|
|
92
|
+
React.useEffect(() => {
|
|
93
|
+
if (!isBusy && !importProgress.isImporting) {
|
|
94
|
+
setCurrentOperation(null);
|
|
95
|
+
}
|
|
96
|
+
}, [isBusy, importProgress.isImporting]);
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<Container width={2}>
|
|
100
|
+
<Box padding={4} marginTop={5}>
|
|
101
|
+
<Stack space={4}>
|
|
102
|
+
<Flex align='center' justify='space-between'>
|
|
103
|
+
<Stack space={2}>
|
|
104
|
+
<Heading as='h2' size={3}>
|
|
105
|
+
Translations
|
|
106
|
+
</Heading>
|
|
107
|
+
<Text size={2}>
|
|
108
|
+
Manage your document translations from this centralized
|
|
109
|
+
location.
|
|
110
|
+
</Text>
|
|
111
|
+
</Stack>
|
|
112
|
+
|
|
113
|
+
<Flex gap={3} align='center'>
|
|
114
|
+
<Flex gap={2} align='center'>
|
|
115
|
+
<Text size={1}>Auto-refresh</Text>
|
|
116
|
+
<Switch
|
|
117
|
+
checked={autoRefresh}
|
|
118
|
+
onChange={() => setAutoRefresh(!autoRefresh)}
|
|
119
|
+
/>
|
|
120
|
+
</Flex>
|
|
121
|
+
<Button
|
|
122
|
+
fontSize={1}
|
|
123
|
+
padding={2}
|
|
124
|
+
text={isRefreshing ? 'Refreshing...' : 'Refresh Status'}
|
|
125
|
+
onClick={handleRefreshAll}
|
|
126
|
+
disabled={
|
|
127
|
+
isRefreshing ||
|
|
128
|
+
isBusy ||
|
|
129
|
+
loadingDocuments ||
|
|
130
|
+
documents.length === 0
|
|
131
|
+
}
|
|
132
|
+
/>
|
|
133
|
+
</Flex>
|
|
134
|
+
</Flex>
|
|
135
|
+
|
|
136
|
+
<Stack space={4}>
|
|
137
|
+
<Box>
|
|
138
|
+
<Text size={1} muted>
|
|
139
|
+
{loadingDocuments
|
|
140
|
+
? 'Loading documents...'
|
|
141
|
+
: `Found ${documents.length} documents available for translation`}
|
|
142
|
+
</Text>
|
|
143
|
+
</Box>
|
|
144
|
+
|
|
145
|
+
<Flex justify='center'>
|
|
146
|
+
<Button
|
|
147
|
+
style={{ width: '200px' }}
|
|
148
|
+
tone='critical'
|
|
149
|
+
text={getOperationText(
|
|
150
|
+
'Translate All',
|
|
151
|
+
isBusy && currentOperation === 'Translate All'
|
|
152
|
+
)}
|
|
153
|
+
onClick={() => {
|
|
154
|
+
setCurrentOperation('Translate All');
|
|
155
|
+
setIsTranslateAllDialogOpen(true);
|
|
156
|
+
}}
|
|
157
|
+
disabled={isBusy || loadingDocuments || documents.length === 0}
|
|
158
|
+
/>
|
|
159
|
+
</Flex>
|
|
160
|
+
|
|
161
|
+
<TranslationsTable />
|
|
162
|
+
|
|
163
|
+
<Stack space={3}>
|
|
164
|
+
<Flex gap={3} align='center' justify='space-between'>
|
|
165
|
+
<Flex gap={2} align='center'>
|
|
166
|
+
<Tooltip
|
|
167
|
+
placement='top'
|
|
168
|
+
content='Imports and overrides all translations'
|
|
169
|
+
>
|
|
170
|
+
<Button
|
|
171
|
+
mode='ghost'
|
|
172
|
+
onClick={() => {
|
|
173
|
+
setCurrentOperation('Import All');
|
|
174
|
+
setIsImportAllDialogOpen(true);
|
|
175
|
+
}}
|
|
176
|
+
text={getOperationText(
|
|
177
|
+
'Import All',
|
|
178
|
+
isBusy && currentOperation === 'Import All'
|
|
179
|
+
)}
|
|
180
|
+
icon={
|
|
181
|
+
isBusy && currentOperation === 'Import All'
|
|
182
|
+
? null
|
|
183
|
+
: DownloadIcon
|
|
184
|
+
}
|
|
185
|
+
disabled={
|
|
186
|
+
isBusy || loadingDocuments || documents.length === 0
|
|
187
|
+
}
|
|
188
|
+
/>
|
|
189
|
+
</Tooltip>
|
|
190
|
+
<Tooltip
|
|
191
|
+
placement='top'
|
|
192
|
+
content="Imports all translations that are not yet imported (according to the source document's translation.metadata document)"
|
|
193
|
+
>
|
|
194
|
+
<Button
|
|
195
|
+
mode='ghost'
|
|
196
|
+
tone='primary'
|
|
197
|
+
onClick={() => {
|
|
198
|
+
setCurrentOperation('Import Missing');
|
|
199
|
+
setIsImportMissingDialogOpen(true);
|
|
200
|
+
}}
|
|
201
|
+
text={getOperationText(
|
|
202
|
+
'Import Missing',
|
|
203
|
+
isBusy && currentOperation === 'Import Missing'
|
|
204
|
+
)}
|
|
205
|
+
icon={
|
|
206
|
+
isBusy && currentOperation === 'Import Missing'
|
|
207
|
+
? null
|
|
208
|
+
: DownloadIcon
|
|
209
|
+
}
|
|
210
|
+
disabled={
|
|
211
|
+
isBusy || loadingDocuments || documents.length === 0
|
|
212
|
+
}
|
|
213
|
+
/>
|
|
214
|
+
</Tooltip>
|
|
215
|
+
<Tooltip
|
|
216
|
+
placement='top'
|
|
217
|
+
content='Replaces references in documents with the corresponding translated document reference'
|
|
218
|
+
>
|
|
219
|
+
<Button
|
|
220
|
+
mode='ghost'
|
|
221
|
+
tone='caution'
|
|
222
|
+
onClick={() => {
|
|
223
|
+
setCurrentOperation('Patch Document References');
|
|
224
|
+
handlePatchDocumentReferences();
|
|
225
|
+
}}
|
|
226
|
+
text={getOperationText(
|
|
227
|
+
'Patch Document References',
|
|
228
|
+
isBusy &&
|
|
229
|
+
currentOperation === 'Patch Document References'
|
|
230
|
+
)}
|
|
231
|
+
icon={
|
|
232
|
+
isBusy &&
|
|
233
|
+
currentOperation === 'Patch Document References'
|
|
234
|
+
? null
|
|
235
|
+
: LinkIcon
|
|
236
|
+
}
|
|
237
|
+
disabled={
|
|
238
|
+
isBusy || loadingDocuments || documents.length === 0
|
|
239
|
+
}
|
|
240
|
+
/>
|
|
241
|
+
</Tooltip>
|
|
242
|
+
<Tooltip
|
|
243
|
+
placement='top'
|
|
244
|
+
content='Publishes all translations whose source document is published'
|
|
245
|
+
>
|
|
246
|
+
<Button
|
|
247
|
+
mode='ghost'
|
|
248
|
+
tone='positive'
|
|
249
|
+
onClick={() => {
|
|
250
|
+
setCurrentOperation('Publish Translations');
|
|
251
|
+
handlePublishAllTranslations();
|
|
252
|
+
}}
|
|
253
|
+
text={getOperationText(
|
|
254
|
+
'Publish Translations',
|
|
255
|
+
isBusy && currentOperation === 'Publish Translations'
|
|
256
|
+
)}
|
|
257
|
+
icon={
|
|
258
|
+
isBusy && currentOperation === 'Publish Translations'
|
|
259
|
+
? null
|
|
260
|
+
: PublishIcon
|
|
261
|
+
}
|
|
262
|
+
disabled={
|
|
263
|
+
isBusy || loadingDocuments || documents.length === 0
|
|
264
|
+
}
|
|
265
|
+
/>
|
|
266
|
+
</Tooltip>
|
|
267
|
+
{importedTranslations.size ===
|
|
268
|
+
documents.length *
|
|
269
|
+
locales.filter((l) => l.enabled !== false).length &&
|
|
270
|
+
documents.length > 0 &&
|
|
271
|
+
locales.length > 0 && (
|
|
272
|
+
<Flex gap={2} align='center' style={{ color: 'green' }}>
|
|
273
|
+
<CheckmarkCircleIcon />
|
|
274
|
+
<Text size={1}>All translations imported</Text>
|
|
275
|
+
</Flex>
|
|
276
|
+
)}
|
|
277
|
+
{importedTranslations.size > 0 &&
|
|
278
|
+
importedTranslations.size <
|
|
279
|
+
documents.length *
|
|
280
|
+
locales.filter((l) => l.enabled !== false).length && (
|
|
281
|
+
<Text size={1} style={{ color: '#666' }}>
|
|
282
|
+
{importedTranslations.size}/
|
|
283
|
+
{documents.length *
|
|
284
|
+
locales.filter((l) => l.enabled !== false)
|
|
285
|
+
.length}{' '}
|
|
286
|
+
imported
|
|
287
|
+
</Text>
|
|
288
|
+
)}
|
|
289
|
+
</Flex>
|
|
290
|
+
<Box />
|
|
291
|
+
</Flex>
|
|
292
|
+
|
|
293
|
+
<BatchProgress
|
|
294
|
+
isActive={importProgress.isImporting}
|
|
295
|
+
current={importProgress.current}
|
|
296
|
+
total={importProgress.total}
|
|
297
|
+
operationName={getProgressOperationName()}
|
|
298
|
+
/>
|
|
299
|
+
</Stack>
|
|
300
|
+
</Stack>
|
|
301
|
+
|
|
302
|
+
<Text size={2}>
|
|
303
|
+
For more information, see the{' '}
|
|
304
|
+
<Link href='https://dash.generaltranslation.com'>
|
|
305
|
+
General Translation Dashboard
|
|
306
|
+
</Link>
|
|
307
|
+
.
|
|
308
|
+
</Text>
|
|
309
|
+
</Stack>
|
|
310
|
+
</Box>
|
|
311
|
+
|
|
312
|
+
<TranslateAllDialog
|
|
313
|
+
isOpen={isTranslateAllDialogOpen}
|
|
314
|
+
onClose={() => setIsTranslateAllDialogOpen(false)}
|
|
315
|
+
/>
|
|
316
|
+
<ImportAllDialog
|
|
317
|
+
isOpen={isImportAllDialogOpen}
|
|
318
|
+
onClose={() => setIsImportAllDialogOpen(false)}
|
|
319
|
+
/>
|
|
320
|
+
<ImportMissingDialog
|
|
321
|
+
isOpen={isImportMissingDialogOpen}
|
|
322
|
+
onClose={() => setIsImportMissingDialogOpen(false)}
|
|
323
|
+
/>
|
|
324
|
+
</Container>
|
|
325
|
+
);
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const TranslationsTool: React.FC = () => {
|
|
329
|
+
return (
|
|
330
|
+
<BaseTranslationWrapper showContainer={false}>
|
|
331
|
+
<TranslationsProvider>
|
|
332
|
+
<TranslationsToolContent />
|
|
333
|
+
</TranslationsProvider>
|
|
334
|
+
</BaseTranslationWrapper>
|
|
335
|
+
);
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
export default TranslationsTool;
|
|
@@ -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
|
+
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// adapted from https://github.com/sanity-io/sanity-translations-tab. See LICENSE.md for more details.
|
|
2
|
+
|
|
1
3
|
import { useCallback, useState } from 'react';
|
|
2
4
|
import { Flex, Card, Text, Grid, Box, Button } from '@sanity/ui';
|
|
3
5
|
import { DownloadIcon, CheckmarkCircleIcon } from '@sanity/icons';
|