# General Translation Integrations: Sanity plugin configuration URL: https://generaltranslation.com/en-GB/docs/integrations/sanity/reference/plugin-configuration.mdx --- title: Sanity plugin configuration description: Configure the General Translation gt-sanity plugin for Sanity Studio. API reference for gtPlugin. --- Configure General Translation in Sanity Studio with the `gtPlugin` function. Pass a single options object. ```ts title="sanity.config.ts" import { gtPlugin } from 'gt-sanity'; gtPlugin({ sourceLocale: 'en', locales: ['es', 'fr'], translateDocuments: [{ type: 'article' }], }); ``` ## Options [#options] | Option | Description | Type | Optional | Default | | ----------------------------------------------------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------- | -------- | ------------------------------------- | | [`sourceLocale`](#source-locale) | Source language code, such as `en`. | `string` | Yes | `defaultLocale`, then library default | | [`defaultLocale`](#default-locale) | Alias for `sourceLocale`, for spreading `gt.config.json`. | `string` | Yes | — | | [`locales`](#locales) | Target locale codes. | `string[]` | No | — | | [`customMapping`](#custom-mapping) | Custom locale code mappings and property overrides. | [`CustomMapping`](/docs/platform/core/reference/types/custom-mapping) | Yes | — | | [`apiKey`](#api-key) | General Translation API key. | `string` | Yes | Secrets document | | [`projectId`](#project-id) | General Translation project ID. | `string` | Yes | Secrets document | | [`secretsNamespace`](#secrets-namespace) | `_id` of the private credentials document. | `string` | Yes | `generaltranslation.secrets` | | [`languageField`](#language-field) | Document field that stores the locale. | `string` | Yes | `language` | | [`translateDocuments`](#translate-documents) | Filter which documents can be translated. | `TranslateDocumentFilter[] \| string[]` | Yes | `[]` | | [`singletons`](#singletons) | Document IDs treated as singletons. | `string[]` | Yes | `[]` | | [`singletonMapping`](#singleton-mapping) | Maps a source ID and locale to a translated singleton ID. | `(sourceDocumentId: string, locale: string) => string` | Yes | `` `${sourceDocumentId}-${locale}` `` | | [`showDocumentInternationalization`](#show-doc-i18n) | Automatically adds `@sanity/document-internationalization`. | `boolean` | Yes | `true` | | [`internationalizedArray`](#internationalized-array) | Configure field-level localisation with internationalised arrays. | `GTFieldLevelLocalizationConfig` | Yes | — | | [`fieldLevelLocalization`](#field-level-localization) | Alias for `internationalizedArray`. | `GTFieldLevelLocalizationConfig` | Yes | — | | [`translationLevel`](#translation-level) | Choose document-level, field-level, or mixed translation. | `'document' \| 'internationalizedArray' \| 'mixed'` | Yes | `'document'` | | [`fieldLevelDocuments`](#field-level-documents) | Document types that use field-level localisation in mixed mode. | `TranslateDocumentFilter[] \| string[]` | Yes | `[]` | | [`ignoreFields`](#ignore-fields) | Fields copied from the source without translating. | `FieldMatcher[]` | Yes | `[]` | | [`dedupeFields`](#dedupe-fields) | Fields copied from the source and made unique per locale. | `FieldMatcher[]` | Yes | `[]` | | [`skipFields`](#skip-fields) | Fields removed from translated documents. | `FieldMatcher[]` | Yes | `[]` | | [`additionalStopTypes`](#additional-stop-types) | Extra schema types to preserve without translating. | `string[]` | Yes | `[]` | | [`additionalSerializers`](#additional-serializers) | Custom HTML serializers for marks and block types. | `Partial` | Yes | `{}` | | [`additionalDeserializers`](#additional-deserializers) | Custom HTML deserialisers. | `CustomDeserializers` | Yes | `{}` | | [`additionalBlockDeserializers`](#additional-block-deserializers) | Custom Portable Text block deserialiser rules. | `unknown[]` | Yes | `[]` | ## Locale options [#locale-options] ### `sourceLocale` [#source-locale] **Type** `string` · **Optional** · **Default** `defaultLocale`, then the library default The source language code, such as `en`. The plugin resolves the source locale in this order: `sourceLocale`, then `defaultLocale`, then the `generaltranslation` library default. ### `defaultLocale` [#default-locale] **Type** `string` · **Optional** Alias for `sourceLocale`, accepted so you can spread a `gt.config.json` directly into the plugin. If both are set, `sourceLocale` takes precedence. ```ts import gtConfig from './gt.config.json'; gtPlugin({ ...gtConfig }); ``` ### `locales` [#locales] **Type** `string[]` · **Required** The target locale codes to translate into, such as `['es', 'fr', 'ja']`. ### `customMapping` [#custom-mapping] **Type** `CustomMapping` · **Optional** Custom mappings of locale codes to names, or overrides of locale properties. Passed through to the `generaltranslation` library. See [CustomMapping](/docs/platform/core/reference/types/custom-mapping). ## Credentials [#credentials] By default, the plugin reads credentials from a private Sanity document. See [Store credentials](/docs/integrations/sanity/guides/configuring-sanity#store-credentials). ### `apiKey` [#api-key] **Type** `string` · **Optional** · **Default** read from the secrets document Your General Translation API key. If set, it is passed to the library at startup. When a secrets document is present, its `secret` field takes precedence at runtime. Prefer the secrets document so keys stay out of source control. ### `projectId` [#project-id] **Type** `string` · **Optional** · **Default** read from the secrets document Your General Translation project ID. When a secrets document is present, its `project` field takes precedence at runtime. ### `secretsNamespace` [#secrets-namespace] **Type** `string` · **Optional** · **Default** `generaltranslation.secrets` The `_id` of the private Sanity document that holds credentials. The plugin fetches this document by `_id` and reads its `secret` field as the API key and its `project` field as the project ID. A leading `.` in the `_id` keeps the document private, even in a public dataset. ```js title="populateSecrets.js" import { getCliClient } from 'sanity/cli'; const client = getCliClient({ apiVersion: '2026-04-06' }); client.createOrReplace({ _id: 'generaltranslation.secrets', _type: 'generaltranslation.secrets', secret: process.env.GT_API_KEY, project: process.env.GT_PROJECT_ID, }); ``` ## Document options [#document-options] ### `languageField` [#language-field] **Type** `string` · **Optional** · **Default** `language` The document field used to store each document's locale. Add a field with this name to every translatable document type, and query it to fetch a specific locale. ### `translateDocuments` [#translate-documents] **Type** `TranslateDocumentFilter[] | string[]` · **Optional** · **Default** `[]` Specifies which documents can be translated. Accepts filter objects or shorthand type strings. Each string entry `'article'` is normalised to `{ type: 'article' }`, and entries without a `documentId` or `type` are dropped. ```ts gtPlugin({ sourceLocale: 'en', locales: ['es'], translateDocuments: [ { type: 'article' }, { documentId: 'homepage' }, 'page', // shorthand for { type: 'page' } ], }); ``` `TranslateDocumentFilter` has this shape: ```ts type TranslateDocumentFilter = { documentId?: string; // match a specific document by _id type?: string; // match all documents of a schema type }; ``` The `type` entries also determine which schema types [`showDocumentInternationalization`](#show-doc-i18n) enables. ### `singletons` [#singletons] **Type** `string[]` · **Optional** · **Default** `[]` Document IDs treated as singletons, such as site settings or navigation. Their translated document IDs are derived by [`singletonMapping`](#singleton-mapping). ### `singletonMapping` [#singleton-mapping] **Type** `(sourceDocumentId: string, locale: string) => string` · **Optional** · **Default** `` `${sourceDocumentId}-${locale}` `` Maps a singleton's source document ID and a locale to the translated singleton's document ID. The default is deterministic, so `siteSettings` becomes `siteSettings-es` for Spanish. ```ts gtPlugin({ sourceLocale: 'en', locales: ['es'], singletons: ['siteSettings'], singletonMapping: (sourceDocumentId, locale) => `${sourceDocumentId}_${locale}`, }); ``` ### `showDocumentInternationalization` [#show-doc-i18n] **Type** `boolean` · **Optional** · **Default** `true` When `true`, the plugin adds the `@sanity/document-internationalization` plugin with language badges, a translation menu and per-language document templates. It uses the `type` entries in [`translateDocuments`](#translate-documents) as the schema types and `[sourceLocale, ...locales]` as the supported languages, so it only takes effect when `translateDocuments` includes document types. Document types localised in place with internationalised arrays are excluded automatically. Set this to `false` to manage internationalisation yourself. ## Field-level localisation [#field-level] Field-level localisation stores each locale's value in a single document as an internationalized array. The stored data uses the same `{ _key, _type, language, value }` item shape as `sanity-plugin-internationalized-array`, so existing internationalized-array content does not require migration. ### `internationalizedArray` [#internationalized-array] **Type** `GTFieldLevelLocalizationConfig` · **Optional** Configures the schema types and Studio input components used for field-level localisation. Locale identity always comes from `sourceLocale` and `locales`. ```ts type GTFieldLevelLocalizationConfig = { enabled?: boolean; // default: false fieldTypes?: FieldLevelFieldType[]; // default: ['string', 'text'] languageTitles?: Record; getLanguageTitle?: (locale: string) => string; typePrefix?: string; // default: 'internationalizedArray' includeCompatibilityTypes?: boolean; // default: true components?: { input?: ComponentType | false; item?: ComponentType | false; field?: ComponentType | false; }; }; type FieldLevelFieldType = | 'string' | 'text' | 'block' | { name: string; type: string; title?: string; of?: unknown[]; fields?: unknown[]; options?: Record; }; ``` The built-in `fieldTypes` shortcuts are `string`, `text`, and `block` (Portable Text). An object entry defines a custom generated type. `getLanguageTitle` overrides `languageTitles` when both are set. Each `components` slot accepts a Studio component or `false` to use Sanity's default rendering. ### `fieldLevelLocalization` [#field-level-localization] **Type** `GTFieldLevelLocalizationConfig` · **Optional** A descriptive alias for [`internationalizedArray`](#internationalized-array). ### `translationLevel` [#translation-level] **Type** `'document' | 'internationalizedArray' | 'mixed'` · **Optional** · **Default** `'document'` Controls how matched documents are translated: * `'document'` creates one document per locale * `'internationalizedArray'` localises configured fields in place * `'mixed'` uses field-level localisation for [`fieldLevelDocuments`](#field-level-documents) and document-level localisation for everything else ### `fieldLevelDocuments` [#field-level-documents] **Type** `TranslateDocumentFilter[] | string[]` · **Optional** · **Default** `[]` Selects the document types that use internationalised arrays when `translationLevel` is `'mixed'`. It accepts the same filter and shorthand string forms as [`translateDocuments`](#translate-documents). ```ts gtPlugin({ sourceLocale: 'en', locales: ['es', 'fr'], translateDocuments: [{ type: 'post' }, { type: 'siteSettings' }], internationalizedArray: { enabled: true, fieldTypes: ['string', 'text', 'block'], languageTitles: { es: 'Español', fr: 'Français' }, }, translationLevel: 'mixed', fieldLevelDocuments: [{ type: 'siteSettings' }], }); ``` Then use the generated types in your schemas: ```ts defineField({ name: 'title', type: 'internationalizedArrayString', }); ``` ## Field matchers [#field-matchers] `ignoreFields`, `dedupeFields`, and `skipFields` each take an array of `FieldMatcher` objects. A matcher targets fields using a JSONPath `property` expression and can optionally be limited to a single source document by `documentId`. ```ts type FieldMatcher = { documentId?: string | null; // restrict to a source document by _id fields?: { property: string; // JSONPath expression, such as $.slug type?: string; // optional schema type hint, such as slug }[]; }; ``` ### `ignoreFields` [#ignore-fields] **Type** `FieldMatcher[]` · **Optional** · **Default** `[]` Fields copied from the source document to the translated document without being sent to the translation API. Use for values that should remain identical across locales, such as categories or tags. ```ts gtPlugin({ sourceLocale: 'en', locales: ['es'], ignoreFields: [ // Copy category unchanged for all documents { fields: [{ property: '$.category' }] }, // Copy tags unchanged only for one document { documentId: 'homepage', fields: [{ property: '$.tags' }] }, ], }); ``` ### `dedupeFields` [#dedupe-fields] **Type** `FieldMatcher[]` · **Optional** · **Default** `[]` Fields copied from the source value and made unique by appending the locale when the translated document is first created. Commonly used for slugs. ```ts gtPlugin({ sourceLocale: 'en', locales: ['es', 'fr'], // "about" becomes "about-es" and "about-fr" dedupeFields: [{ fields: [{ property: '$.slug', type: 'slug' }] }], }); ``` For a slug field, the plugin updates the slug object's `current` value. If an editor later changes the translated slug, future imports preserve that edited value. ### `skipFields` [#skip-fields] **Type** `FieldMatcher[]` · **Optional** · **Default** `[]` Fields removed entirely from translated documents. ```ts gtPlugin({ sourceLocale: 'en', locales: ['es'], skipFields: [ { fields: [{ property: '$.slug', type: 'slug' }] }, { documentId: 'homepage', fields: [{ property: '$.debugInfo' }] }, ], }); ``` ## Serialisation [#serialization] The plugin serialises documents to HTML for translation and deserialises the result back into Sanity fields. Most projects do not need these options. ### `additionalStopTypes` [#additional-stop-types] **Type** `string[]` · **Optional** · **Default** `[]` Additional schema types to preserve without translating, added to the [default stop types](#stop-types). ```ts gtPlugin({ sourceLocale: 'en', locales: ['es'], additionalStopTypes: ['codeBlock', 'mux.video', 'mux.videoAsset'], }); ``` ### `additionalSerializers` [#additional-serializers] **Type** `Partial` · **Optional** · **Default** `{}` Custom serialisers merged with the defaults, following the `@portabletext/to-html` component shape (`types`, `marks`, `block`, `list`, `listItem`, and more). Most often used to serialise custom `marks`. For custom marks, wrap the output with `attachGTData` so the mark's data survives translation. ```ts title="sanity.config.ts" import { attachGTData, gtPlugin } from 'gt-sanity'; gtPlugin({ sourceLocale: 'en', locales: ['es'], additionalSerializers: { marks: { link: ({ value, children }) => attachGTData(`${children}`, value, 'markDef'), inlineMath: ({ value, children }) => attachGTData(`${children}`, value, 'markDef'), }, }, }); ``` `attachGTData(html, data, 'markDef')` base64-encodes `data` into a `data-gt-internal` attribute on the first element of `html` and returns the updated HTML. On import, the plugin reads that attribute to rebuild the mark definition. ```ts function attachGTData( html: string, data: Record, type: 'markDef' ): string; ``` ### `additionalDeserializers` [#additional-deserializers] **Type** `CustomDeserializers` · **Optional** · **Default** `{}` Custom deserialisers that convert translated HTML elements back into Sanity objects, keyed by type. ```ts type CustomDeserializers = { types?: Record< string, (element: HTMLElement) => Record | unknown[] >; } & Record; ``` ### `additionalBlockDeserializers` [#additional-block-deserializers] **Type** `unknown[]` · **Optional** · **Default** `[]` Additional Portable Text block deserialiser rules, appended to the plugin's built-in rules. Each rule is an object with a `deserialize(node, next)` method, matching the `@portabletext/block-tools` rule shape. ## Default stop types [#stop-types] These schema types are preserved and never sent for translation. Add more with [`additionalStopTypes`](#additional-stop-types). ```ts const defaultStopTypes = [ 'reference', 'date', 'datetime', 'file', 'geopoint', 'image', 'number', 'crop', 'hotspot', 'boolean', 'url', 'color', 'code', ]; ``` Slug fields are *not* skipped by default, so a slug's `current` string is translated unless you use [`dedupeFields`](#dedupe-fields) or [`skipFields`](#skip-fields). ## Exported helpers [#helpers] `gt-sanity` also exports building blocks for advanced serialisation and custom document nodes. Most projects do not need them. * `TranslationsTab` — the document tab component for `structureTool`. See [Configure Sanity](/docs/integrations/sanity/guides/configuring-sanity#translations-tab). * `attachGTData` / `detachGTData` — attach and read the encoded mark data used by custom serialisers. * `BaseDocumentSerializer`, `BaseDocumentDeserializer`, `BaseDocumentMerger` — the default serialise, deserialise, and merge implementations. * `defaultStopTypes`, `customSerializers` — the default stop types and serialiser set. * `documentInternationalization` and its types (`DocumentInternationalizationConfig`, `Language`, `Metadata`, `TranslationReference`) — re-exported from `@sanity/document-internationalization`. ## Complete example [#complete-example] ```ts title="sanity.config.ts" import { defineConfig } from 'sanity'; import { attachGTData, gtPlugin } from 'gt-sanity'; export default defineConfig({ plugins: [ gtPlugin({ // Required sourceLocale: 'en', locales: ['es', 'fr', 'de', 'ja'], // Documents and fields languageField: 'language', translateDocuments: [{ type: 'article' }, { type: 'page' }], singletons: ['siteSettings', 'navigation'], singletonMapping: (id, locale) => `${id}_${locale}`, // Field behavior ignoreFields: [{ fields: [{ property: '$.category' }] }], dedupeFields: [{ fields: [{ property: '$.slug', type: 'slug' }] }], skipFields: [{ fields: [{ property: '$.internalNotes' }] }], // Serialization — only if the defaults do not handle your schema additionalSerializers: { marks: { link: ({ value, children }) => attachGTData(`${children}`, value, 'markDef'), }, }, }), ], }); ```