diff --git a/README.md b/README.md index e6e4dc6..0dce0a9 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,8 @@ export const getDefaultDocumentNode = ({schemaType}) => { localeIdAdapter: (translationVendorId) => sanityId, /** - * For field-level translations, the key for the "source content" + * the key for the "source content" (for field level) or the code in the + * language field on the "base document" (for document level) * (e.g. "en" or "en_US"). */ baseLanguage: 'en_US', diff --git a/docs/00-faq.md b/docs/00-faq.md new file mode 100644 index 0000000..c4fd26c --- /dev/null +++ b/docs/00-faq.md @@ -0,0 +1,46 @@ +## Where should I place the Translations Tab? +The Translations Tab should be invoked as a view. This can be done in the `getDefaultDocumentNode` function in `schemas.js`: +```js +import {TranslationsTab, defaultDocumentLevelConfig, defaultFieldLevelConfig} from 'sanity-translations-tab' + +export const defaultDocumentNodeResolver = (S) => + S.document().views([ + // Give all documents the JSON preview, + // as well as the default form view + S.view.form(), + S.view.component(TranslationsTab).options(defaultDocumentLevelConfig) + ]) +``` +If using a document-level translation pattern, you should likely only include this view in your "base" language. Please see the document internationalization plugin on building different desk structure options for different locales. (example code coming soon) + +## What happens when I click the "Create Job" button? +The tab will do the following: +1. Fetch the latest draft of the document you're currently in. +2. Filter out (as best as it can) any fields that should not be translated (references, dates, numbers, etc.). It will also utilize options passed in to ignore certain fields and objects. See more in the Advanced Configuration docs. +3. Serialize the document into an HTML string. It will utilize options to serialize objects in particular ways, if provided. +4. Send the HTML string to the translation vendor's API, along with the locale code of the language(s) you want to translate to. +5. Look up the translation job ID in the response from the translation vendor (this will either match your document ID or be invoked by a custom job name resolver [coming soon]). +6. Show you the progress of the ongoing translation and a link to the job in the vendor, if available. + +## How am I seeing my progress? +On load, the tab fetches the total amount of strings that have reached the LAST stage of translation and are ready to be imported. That is divided over the total amount of strings in the document, and the progress bar is updated accordingly. + +## How do I import translations? +When the translation vendor has completed the translation, you can click the "Import Translations" button. This will do the following: +1. Deserialize the HTML string from the translation vendor into a Sanity document. +2. Fetch the revision of the document you're currently in at the time the translation was sent, if available. (This is to resolve the translation as smoothly as possible, in case the document has changed since it was sent to translation and cannot resolve conflicts) +3. Compare the two documents and merge the translated content with anything that was not sent for translation. +4. Issue a patch with the relevant merges to the relevant document. If using document internationalization, this will also update translation metadata. + +## How can I prevent certain fields from being sent to translation? +You can pass in a `stopTypes` parameter to name all objects you do not want translated. Alternately, the serializer also introspects your schema. You can set `localize: false` on any field you do not want translated. +```js + fields: [ + defineField({ + name: 'categories', + type: 'array', + //ts-ignore + localize: false, + ... + })] +``` \ No newline at end of file diff --git a/docs/01-advanced-configuration.md b/docs/01-advanced-configuration.md new file mode 100644 index 0000000..8740ef4 --- /dev/null +++ b/docs/01-advanced-configuration.md @@ -0,0 +1,60 @@ +## Advanced configuration + +This plugin provides defaults for most configuration options, but they can be overridden as needed. Please refer to the types to see what you should declare, but we also provide the type for all options, which we recommend using for quicker development. + +```typescript +//sanity.config.ts +import {TranslationsTab, defaultDocumentLevelConfig, defaultFieldLevelConfig, TranslationsTabConfigOptions} from 'sanity-translations-tab' + +const customConfig = { + ...defaultDocumentLevelConfig, + //baseLanguage is `en` by default, overwrite as needed. This is important for coalescing translations and creating accurate translation metadata. + baseLanguage: 'en_US', + //this is the field that will be used to determine the language of the document. It is `language` by default. + languageField: 'locale', + serializationOptions: { + //use this option to exclude objects that do not need to be translated. The plugin already uses defaults for references, dates, numbers, etc. + additionalStopTypes: ['colorTheme'], + //use this option to serialize objects in a particular way. The plugin will try to filter and serialize objects as best as it can, but you can override this behavior here. + //For now, you should also include these for annotations and inline objects in PortableText, if you want them to be translated. + //NOTE: it is VERY important to include the _type as a class and the _key as id for accurately deserializing and coalescing + additionalSerializers: { + testObject: ({value}) => { + return `${value.title}` + } + }, + //Create a method to deserialize any custom serialization rules + additonalDeserializers: { + testObject: ({node}) => { + return { + _type: 'testObject', + _key: node.id, + title: node.textContent + } + } + }, + //Block text requires a special deserialization format based on @sanity/block-tools. Use this option for any annotations or inline objects that need to be translated. + additionalBlockDeserializers: [ + { + deserialize(el, next): TypedObject | undefined { + if (!el.className || el.className?.toLowerCase() !== 'myannotation') { + return undefined + } + + const markDef = { + _key: el.id, + _type: 'myAnnotation', + } + + return { + _type: '__annotation', + markDef: markDef, + children: next(el.childNodes), + } + }, + }, + ] + } + //adapter, baseLanguage, secretsNamespace, importTranslation, exportForTranslation should likely not be touched unless you very much want to customize your plugin. +} satisfies TranslationsTabConfigOptions +``` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 77ce6d3..74d400c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,16 @@ { "name": "sanity-translations-tab", - "version": "4.0.0", + "version": "4.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "sanity-translations-tab", - "version": "4.0.0", + "version": "4.1.0", "license": "MIT", "dependencies": { + "@portabletext/to-html": "^2.0.3", + "@sanity/block-tools": "^3.16.4", "@sanity/incompatible-plugin": "^1.0.4", "@sanity/ui": "^1", "@sanity/util": "^3.16.4", @@ -3247,28 +3249,34 @@ } }, "node_modules/@portabletext/to-html": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@portabletext/to-html/-/to-html-1.0.4.tgz", - "integrity": "sha512-SOb/20OYEI5Tqy0St8mAGPqQuQ8uzJPpG4YQxusrQKby05NdD7vnPqDo6lkniD5kutevVoMbYrix0Kt0s6t6Nw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@portabletext/to-html/-/to-html-2.0.3.tgz", + "integrity": "sha512-X+Z/2HUpGJgUoaBAFI9u2oeWixg7iy8GIwYIQ0kX7yU1L/s6+WOBIPRsaPLJygyzoqDTqx1Z1btRvQclEhXm5g==", "dependencies": { - "@portabletext/toolkit": "^1.0.5", - "@portabletext/types": "^1.0.2" + "@portabletext/toolkit": "^2.0.8", + "@portabletext/types": "^2.0.6" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" } }, "node_modules/@portabletext/toolkit": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@portabletext/toolkit/-/toolkit-1.0.6.tgz", - "integrity": "sha512-u48kRSOyxbOmy0J//bLg1odTcL5dvPDmobSbiTgR11J/k9eIKCdWRiJtddpEyId/aWGP2bkX4ol2RMPCmAPCIg==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@portabletext/toolkit/-/toolkit-2.0.8.tgz", + "integrity": "sha512-MI3FKYZiL+/dYsClkkTDRjSvNS7K4j+U2LNZ5XIEoq67qCY0l7CYjvT0fn+lFBEUxjegtEmbxLk6T9nV/iXA+Q==", "dependencies": { - "@portabletext/types": "^1.0.3" + "@portabletext/types": "^2.0.6" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" } }, "node_modules/@portabletext/types": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@portabletext/types/-/types-1.0.3.tgz", - "integrity": "sha512-SDDsdury2SaTI2D5Ea6o+Y39SSZMYHRMWJHxkxYl3yzFP0n/0EknOhoXcoaV+bxGr2dTTqZi2TOEj+uWYuavSw==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@portabletext/types/-/types-2.0.6.tgz", + "integrity": "sha512-6iqorcsQ0Z1/4Y7PWLvoknyiUv0DngSPao+q5UIE0+0gT2cZwFdItUyLZRheG85AisSEvacHEEnvN+TT8mxXVg==", "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": "^14.13.1 || >=16.0.0 || >=18.0.0" } }, "node_modules/@rexxars/choosealicense-list": { @@ -14065,6 +14073,34 @@ "sanity": "^3" } }, + "node_modules/sanity-naive-html-serializer/node_modules/@portabletext/to-html": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@portabletext/to-html/-/to-html-1.0.4.tgz", + "integrity": "sha512-SOb/20OYEI5Tqy0St8mAGPqQuQ8uzJPpG4YQxusrQKby05NdD7vnPqDo6lkniD5kutevVoMbYrix0Kt0s6t6Nw==", + "dependencies": { + "@portabletext/toolkit": "^1.0.5", + "@portabletext/types": "^1.0.2" + } + }, + "node_modules/sanity-naive-html-serializer/node_modules/@portabletext/to-html/node_modules/@portabletext/types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@portabletext/types/-/types-1.0.3.tgz", + "integrity": "sha512-SDDsdury2SaTI2D5Ea6o+Y39SSZMYHRMWJHxkxYl3yzFP0n/0EknOhoXcoaV+bxGr2dTTqZi2TOEj+uWYuavSw==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/sanity-naive-html-serializer/node_modules/@portabletext/toolkit": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@portabletext/toolkit/-/toolkit-1.0.8.tgz", + "integrity": "sha512-SNO8at5crqySCeYa19/mdcZoZvGCINGc/eAX4FwYt02cEzb48hf013BuA9LbEQuTOgpMKxnyeRGpEzxmowmEug==", + "dependencies": { + "@portabletext/types": "^2.0.0" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" + } + }, "node_modules/sanity/node_modules/@esbuild/android-arm": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.2.tgz", @@ -18768,26 +18804,26 @@ } }, "@portabletext/to-html": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@portabletext/to-html/-/to-html-1.0.4.tgz", - "integrity": "sha512-SOb/20OYEI5Tqy0St8mAGPqQuQ8uzJPpG4YQxusrQKby05NdD7vnPqDo6lkniD5kutevVoMbYrix0Kt0s6t6Nw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@portabletext/to-html/-/to-html-2.0.3.tgz", + "integrity": "sha512-X+Z/2HUpGJgUoaBAFI9u2oeWixg7iy8GIwYIQ0kX7yU1L/s6+WOBIPRsaPLJygyzoqDTqx1Z1btRvQclEhXm5g==", "requires": { - "@portabletext/toolkit": "^1.0.5", - "@portabletext/types": "^1.0.2" + "@portabletext/toolkit": "^2.0.8", + "@portabletext/types": "^2.0.6" } }, "@portabletext/toolkit": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@portabletext/toolkit/-/toolkit-1.0.6.tgz", - "integrity": "sha512-u48kRSOyxbOmy0J//bLg1odTcL5dvPDmobSbiTgR11J/k9eIKCdWRiJtddpEyId/aWGP2bkX4ol2RMPCmAPCIg==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@portabletext/toolkit/-/toolkit-2.0.8.tgz", + "integrity": "sha512-MI3FKYZiL+/dYsClkkTDRjSvNS7K4j+U2LNZ5XIEoq67qCY0l7CYjvT0fn+lFBEUxjegtEmbxLk6T9nV/iXA+Q==", "requires": { - "@portabletext/types": "^1.0.3" + "@portabletext/types": "^2.0.6" } }, "@portabletext/types": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@portabletext/types/-/types-1.0.3.tgz", - "integrity": "sha512-SDDsdury2SaTI2D5Ea6o+Y39SSZMYHRMWJHxkxYl3yzFP0n/0EknOhoXcoaV+bxGr2dTTqZi2TOEj+uWYuavSw==" + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@portabletext/types/-/types-2.0.6.tgz", + "integrity": "sha512-6iqorcsQ0Z1/4Y7PWLvoknyiUv0DngSPao+q5UIE0+0gT2cZwFdItUyLZRheG85AisSEvacHEEnvN+TT8mxXVg==" }, "@rexxars/choosealicense-list": { "version": "1.1.2", @@ -26747,6 +26783,32 @@ "@sanity/schema": "3.3.1", "just-clone": "^6.2.0", "sanity": "^3" + }, + "dependencies": { + "@portabletext/to-html": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@portabletext/to-html/-/to-html-1.0.4.tgz", + "integrity": "sha512-SOb/20OYEI5Tqy0St8mAGPqQuQ8uzJPpG4YQxusrQKby05NdD7vnPqDo6lkniD5kutevVoMbYrix0Kt0s6t6Nw==", + "requires": { + "@portabletext/toolkit": "^1.0.5", + "@portabletext/types": "^1.0.2" + }, + "dependencies": { + "@portabletext/types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@portabletext/types/-/types-1.0.3.tgz", + "integrity": "sha512-SDDsdury2SaTI2D5Ea6o+Y39SSZMYHRMWJHxkxYl3yzFP0n/0EknOhoXcoaV+bxGr2dTTqZi2TOEj+uWYuavSw==" + } + } + }, + "@portabletext/toolkit": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@portabletext/toolkit/-/toolkit-1.0.8.tgz", + "integrity": "sha512-SNO8at5crqySCeYa19/mdcZoZvGCINGc/eAX4FwYt02cEzb48hf013BuA9LbEQuTOgpMKxnyeRGpEzxmowmEug==", + "requires": { + "@portabletext/types": "^2.0.0" + } + } } }, "saxes": { diff --git a/package.json b/package.json index f66dbae..e921794 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sanity-translations-tab", - "version": "4.0.1", + "version": "4.1.0", "description": "This is the base module for implementing common translation vendor tasks from a Studio, such as sending content to be translated in some specific languages, importing content back etc. Not useful on its own, but vendor-specific plugins will use this for its chrome.", "keywords": [ "sanity", @@ -53,6 +53,8 @@ "watch": "pkg-utils watch --strict" }, "dependencies": { + "@portabletext/to-html": "^2.0.3", + "@sanity/block-tools": "^3.16.4", "@sanity/incompatible-plugin": "^1.0.4", "@sanity/ui": "^1", "@sanity/util": "^3.16.4", diff --git a/src/components/TranslationsTab.tsx b/src/components/TranslationsTab.tsx index 0534365..ebdffae 100644 --- a/src/components/TranslationsTab.tsx +++ b/src/components/TranslationsTab.tsx @@ -17,27 +17,13 @@ import {TranslationContext} from './TranslationContext' import {TranslationView} from './TranslationView' import {useClient} from '../hooks/useClient' import {useSecrets} from '../hooks/useSecrets' -import { - Adapter, - ExportForTranslation, - ImportTranslation, - Secrets, - WorkflowIdentifiers, -} from '../types' +import {Secrets, TranslationsTabConfigOptions} from '../types' type TranslationTabProps = { document: { displayed: SanityDocument } - options: { - adapter: Adapter - baseLanguage: string - secretsNamespace: string | null - exportForTranslation: ExportForTranslation - importTranslation: ImportTranslation - workflowOptions?: WorkflowIdentifiers[] - localeIdAdapter?: (id: string) => string - } + options: TranslationsTabConfigOptions } const TranslationTab = (props: TranslationTabProps) => { @@ -49,6 +35,7 @@ const TranslationTab = (props: TranslationTabProps) => { displayed && displayed._id ? (displayed._id.split('drafts.').pop() as string) : '' const {errors, importTranslation, exportForTranslation} = useMemo(() => { + const {serializationOptions, baseLanguage, languageField} = props.options const ctx = { client, schema, @@ -69,8 +56,15 @@ const TranslationTab = (props: TranslationTabProps) => { } const contextImportTranslation = (localeId: string, doc: string) => { - const baseLanguage = props.options.baseLanguage - return importTranslationFunc(documentId, localeId, doc, ctx, baseLanguage) + return importTranslationFunc( + documentId, + localeId, + doc, + ctx, + baseLanguage, + serializationOptions, + languageField, + ) } const exportTranslationFunc = props.options.exportForTranslation @@ -86,7 +80,7 @@ const TranslationTab = (props: TranslationTabProps) => { } const contextExportForTranslation = (id: string) => { - return exportTranslationFunc(id, ctx) + return exportTranslationFunc(id, ctx, baseLanguage, serializationOptions, languageField) } return { diff --git a/src/configuration/baseDocumentLevelConfig/documentLevelPatch.ts b/src/configuration/baseDocumentLevelConfig/documentLevelPatch.ts index cadb2a9..fc6def6 100644 --- a/src/configuration/baseDocumentLevelConfig/documentLevelPatch.ts +++ b/src/configuration/baseDocumentLevelConfig/documentLevelPatch.ts @@ -15,6 +15,8 @@ export const documentLevelPatch = async ( localeId: string, client: SanityClient, baseLanguage: string = 'en', + languageField: string = 'language', + // eslint-disable-next-line max-params ): Promise => { let baseDoc: SanityDocument | null = null @@ -45,7 +47,7 @@ export const documentLevelPatch = async ( */ let translationMetadata = await getTranslationMetadata(documentId, client, baseLanguage) if (!translationMetadata) { - translationMetadata = await createTranslationMetadata(documentId, client, baseLanguage) + translationMetadata = await createTranslationMetadata(baseDoc, client, baseLanguage) } const i18nDocId = (translationMetadata.translations as Array>).find( @@ -61,6 +63,6 @@ export const documentLevelPatch = async ( //otherwise, create a new document //and add the document reference to the metadata document else if (translationMetadata) { - createI18nDocAndPatchMetadata(merged, localeId, client, translationMetadata) + createI18nDocAndPatchMetadata(merged, localeId, client, translationMetadata, languageField) } } diff --git a/src/configuration/baseDocumentLevelConfig/helpers/createI18nDocAndPatchMetadata.ts b/src/configuration/baseDocumentLevelConfig/helpers/createI18nDocAndPatchMetadata.ts index dc53f6a..36cf1e2 100644 --- a/src/configuration/baseDocumentLevelConfig/helpers/createI18nDocAndPatchMetadata.ts +++ b/src/configuration/baseDocumentLevelConfig/helpers/createI18nDocAndPatchMetadata.ts @@ -5,8 +5,9 @@ export const createI18nDocAndPatchMetadata = ( localeId: string, client: SanityClient, translationMetadata: SanityDocumentLike, + languageField: string = 'language', ): void => { - translatedDoc.language = localeId + translatedDoc[languageField] = localeId const translations = translationMetadata.translations as Record[] const existingLocaleKey = translations.find((translation) => translation._key === localeId) const operation = existingLocaleKey ? 'replace' : 'after' @@ -22,12 +23,12 @@ export const createI18nDocAndPatchMetadata = ( p.insert(operation, location, [ { _key: localeId, + _type: 'internationalizedArrayReferenceValue', value: { _type: 'reference', _ref, _weak: true, _strengthenOnPublish: { - _weak: false, type: doc._type, }, }, diff --git a/src/configuration/baseDocumentLevelConfig/helpers/createTranslationMetadata.ts b/src/configuration/baseDocumentLevelConfig/helpers/createTranslationMetadata.ts index 48c78d0..06e77ab 100644 --- a/src/configuration/baseDocumentLevelConfig/helpers/createTranslationMetadata.ts +++ b/src/configuration/baseDocumentLevelConfig/helpers/createTranslationMetadata.ts @@ -1,20 +1,38 @@ -import {SanityClient, SanityDocumentLike} from 'sanity' +import {KeyedObject, Reference, SanityClient, SanityDocumentLike} from 'sanity' + +type TranslationReference = KeyedObject & { + _type: 'internationalizedArrayReferenceValue' + value: Reference +} export const createTranslationMetadata = ( - documentId: string, + document: SanityDocumentLike, client: SanityClient, baseLanguage: string, ): Promise => { + const baseLangEntry: TranslationReference = { + _key: baseLanguage, + _type: 'internationalizedArrayReferenceValue', + value: { + _type: 'reference', + _ref: document._id.replace('drafts.', ''), + }, + } + + if (document._id.startsWith('drafts.')) { + baseLangEntry.value = { + ...baseLangEntry.value, + _weak: true, + //this should reflect doc i18n config when this + //plugin is able to take that as a config option + _strengthenOnPublish: { + type: document._type, + }, + } + } + return client.create({ _type: 'translation.metadata', - translations: [ - { - _key: baseLanguage, - value: { - _type: 'reference', - _ref: documentId, - }, - }, - ], + translations: [baseLangEntry], }) } diff --git a/src/configuration/baseDocumentLevelConfig/index.ts b/src/configuration/baseDocumentLevelConfig/index.ts index 96cb5b3..91d3639 100644 --- a/src/configuration/baseDocumentLevelConfig/index.ts +++ b/src/configuration/baseDocumentLevelConfig/index.ts @@ -7,6 +7,9 @@ import { SerializedDocument, BaseDocumentDeserializer, BaseDocumentSerializer, + defaultStopTypes, + customSerializers, + customBlockDeserializers, } from 'sanity-naive-html-serializer' import {DummyAdapter} from '../../adapter' @@ -14,30 +17,88 @@ export const baseDocumentLevelConfig = { exportForTranslation: async ( ...params: Parameters ): Promise => { - const [id, context] = params + const [ + id, + context, + baseLanguage = 'en', + serializationOptions = {}, + languageField = 'language', + ] = params const {client, schema} = context + const stopTypes = [...(serializationOptions.additionalStopTypes ?? []), ...defaultStopTypes] + const serializers = { + ...customSerializers, + types: { + ...customSerializers.types, + ...(serializationOptions.additionalSerializers ?? {}), + }, + } const doc = await findLatestDraft(id, client) - const serialized = BaseDocumentSerializer(schema).serializeDocument(doc, 'document') + delete doc[languageField] + const serialized = BaseDocumentSerializer(schema).serializeDocument( + doc, + 'document', + baseLanguage, + stopTypes, + serializers, + ) serialized.name = id return serialized }, - importTranslation: async (...params: Parameters): Promise => { - const [id, localeId, document, context, baseLanguage] = params + importTranslation: (...params: Parameters): Promise => { + const [ + id, + localeId, + document, + context, + baseLanguage = 'en', + serializationOptions = {}, + languageField = 'language', + ] = params const {client} = context - const deserialized = BaseDocumentDeserializer.deserializeDocument(document) as SanityDocument - await documentLevelPatch(id, deserialized, localeId, client, baseLanguage) + const deserializers = { + types: { + ...(serializationOptions.additionalDeserializers ?? {}), + }, + } + const blockDeserializers = [ + ...(serializationOptions.additionalBlockDeserializers ?? []), + ...customBlockDeserializers, + ] + + const deserialized = BaseDocumentDeserializer.deserializeDocument( + document, + deserializers, + blockDeserializers, + ) as SanityDocument + return documentLevelPatch(id, deserialized, localeId, client, baseLanguage, languageField) }, adapter: DummyAdapter, secretsNamespace: 'translationService', + baseLanguage: 'en', } export const legacyDocumentLevelConfig = { ...baseDocumentLevelConfig, - importTranslation: async (...params: Parameters): Promise => { - const [id, localeId, document, context] = params + importTranslation: (...params: Parameters): Promise => { + const [id, localeId, document, context, , serializationOptions = {}] = params const {client} = context - const deserialized = BaseDocumentDeserializer.deserializeDocument(document) as SanityDocument - await legacyDocumentLevelPatch(id, deserialized, localeId, client) + const deserializers = { + types: { + ...(serializationOptions.additionalDeserializers ?? {}), + }, + } + const blockDeserializers = [ + ...(serializationOptions.additionalBlockDeserializers ?? []), + ...customBlockDeserializers, + ] + + const deserialized = BaseDocumentDeserializer.deserializeDocument( + document, + deserializers, + blockDeserializers, + ) as SanityDocument + return legacyDocumentLevelPatch(id, deserialized, localeId, client) }, } diff --git a/src/configuration/baseFieldLevelConfig.ts b/src/configuration/baseFieldLevelConfig.ts index 8f2d38e..437d530 100644 --- a/src/configuration/baseFieldLevelConfig.ts +++ b/src/configuration/baseFieldLevelConfig.ts @@ -4,6 +4,9 @@ import { BaseDocumentDeserializer, BaseDocumentMerger, SerializedDocument, + defaultStopTypes, + customSerializers, + customBlockDeserializers, } from 'sanity-naive-html-serializer' import {DummyAdapter} from '../adapter' @@ -38,19 +41,48 @@ export const baseFieldLevelConfig = { exportForTranslation: async ( ...params: Parameters ): Promise => { - const [id, context] = params + const [id, context, baseLanguage = 'en', serializationOptions = {}] = params const {client, schema} = context + const stopTypes = [...(serializationOptions.additionalStopTypes ?? []), ...defaultStopTypes] + const serializers = { + ...customSerializers, + types: { + ...customSerializers.types, + ...(serializationOptions.additionalSerializers ?? {}), + }, + } const doc = await findLatestDraft(id, client) - const serialized = BaseDocumentSerializer(schema).serializeDocument(doc, 'field') + const serialized = BaseDocumentSerializer(schema).serializeDocument( + doc, + 'field', + baseLanguage, + stopTypes, + serializers, + ) serialized.name = id return serialized }, importTranslation: (...params: Parameters): Promise => { - const [id, localeId, document, context, , baseLanguage = 'en'] = params + const [id, localeId, document, context, baseLanguage = 'en', serializationOptions = {}] = params const {client} = context - const deserialized = BaseDocumentDeserializer.deserializeDocument(document) as SanityDocument + const deserializers = { + types: { + ...(serializationOptions.additionalDeserializers ?? {}), + }, + } + const blockDeserializers = [ + ...(serializationOptions.additionalBlockDeserializers ?? []), + ...customBlockDeserializers, + ] + + const deserialized = BaseDocumentDeserializer.deserializeDocument( + document, + deserializers, + blockDeserializers, + ) as SanityDocument return fieldLevelPatch(id, deserialized, localeId, client, baseLanguage) }, adapter: DummyAdapter, secretsNamespace: 'translationService', + baseLanguage: 'en', } diff --git a/src/index.ts b/src/index.ts index 5141ee0..b2b02b6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ import { ExportForTranslation, ImportTranslation, TranslationFunctionContext, + TranslationsTabConfigOptions, } from './types' import { baseDocumentLevelConfig, @@ -24,7 +25,14 @@ import { customSerializers, } from 'sanity-naive-html-serializer' -export type {Secrets, Adapter, ExportForTranslation, ImportTranslation, TranslationFunctionContext} +export type { + Secrets, + Adapter, + ExportForTranslation, + ImportTranslation, + TranslationFunctionContext, + TranslationsTabConfigOptions, +} export { TranslationsTab, DummyAdapter, diff --git a/src/types.ts b/src/types.ts index a813359..9fedc79 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,7 @@ -import {SanityClient, Schema} from 'sanity' +import {SanityClient, Schema, TypedObject} from 'sanity' import {SerializedDocument} from 'sanity-naive-html-serializer' +import {PortableTextTypeComponent} from '@portabletext/to-html' +import {DeserializerRule} from '@sanity/block-tools' export type TranslationTaskLocaleStatus = { localeId: string @@ -40,7 +42,7 @@ export interface Adapter { getLocales: (secrets: Secrets | null) => Promise getTranslationTask: (documentId: string, secrets: Secrets | null) => Promise createTask: ( - documentId: string, + taskName: string, document: Record, localeIds: string[], secrets: Secrets | null, @@ -57,6 +59,12 @@ export interface TranslationFunctionContext { export type ExportForTranslation = ( id: string, context: TranslationFunctionContext, + baseLanguage?: string, + serializationOptions?: { + additionalStopTypes?: string[] + additionalSerializers?: Record + }, + languageField?: string, ) => Promise export type ImportTranslation = ( @@ -65,4 +73,26 @@ export type ImportTranslation = ( document: string, context: TranslationFunctionContext, baseLanguage?: string, + serializationOptions?: { + additionalDeserializers?: Record TypedObject> + additionalBlockDeserializers?: DeserializerRule[] + }, + languageField?: string, ) => Promise + +export type TranslationsTabConfigOptions = { + adapter: Adapter + baseLanguage: string + secretsNamespace: string | null + exportForTranslation: ExportForTranslation + importTranslation: ImportTranslation + serializationOptions?: { + additionalStopTypes?: string[] + additionalSerializers?: Record + additionalDeserializers?: Record TypedObject> + additionalBlockDeserializers?: DeserializerRule[] + } + workflowOptions?: WorkflowIdentifiers[] + localeIdAdapter?: (id: string) => string + languageField?: string +}