# 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. --- Register General Translation in your Sanity config with the `gtPlugin` function. Pass it 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. Source-locale and duplicate entries are removed. | `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 `sanity-plugin-internationalized-array` for field-level localisation. | `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 | `[]` | | [`preserveExistingTranslations`](#preserve-existing-translations) | Initial state of the **Save local edits** toggle. | `boolean` | Yes | `false` | | [`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 serialisers 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']`. The plugin removes duplicate entries and any entry equal to the resolved source locale before configuring translation or Sanity's localisation plugins. | Version | Changes | | ------- | ------------------------------------------------------------------------------------------------------------------------------- | | `3.1.1` | Removes duplicate target locales and the resolved source locale before configuring translation and Sanity localisation plugins. | ### `customMapping` [#custom-mapping] **Type** [`CustomMapping`](/docs/platform/core/reference/types/custom-mapping) · **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-level translation's locale. Add a field with this name to document types translated with document-level localisation, and query it to fetch a specific locale. Field-level localisation does not use this field. ### `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 the source locale followed by the normalised target 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. If you already register `@sanity/document-internationalization` in your Studio, keep your setup and set this option to `false` so the plugin (and its `translation.metadata` document type) is only registered once — two registrations produce a duplicate schema type error. Translation works with your instance unchanged: the plugin reads and writes documents through the [`languageField`](#language-field) and `translation.metadata` documents, regardless of which registration added them. Use the same language IDs and language field in both configurations. ## Field-level localisation [#field-level] Field-level localisation stores the value for each locale in a single document as an internationalised array. It is powered by [`sanity-plugin-internationalized-array`](https://github.com/sanity-io/sanity-plugin-internationalized-array): `gtPlugin` configures the native plugin, which registers the `internationalizedArray*` schema types and the Studio editing UI. The stored data uses the `{ _key, _type, language, value }` item shape, so existing internationalised-array content does not need a migration — and translation works the same whether the types were registered by `gtPlugin` or by your own `internationalizedArray()` registration. **Changed in v3:** field-level localisation is provided by `sanity-plugin-internationalized-array` instead of GT-generated types and components. `createInternationalizedArrayTypes`, `FieldLevelUIComponents`, and the `typePrefix`, `includeCompatibilityTypes`, and `components` options were removed; passing a removed option logs a warning and it is ignored. ### `internationalizedArray` [#internationalized-array] **Type** `GTFieldLevelLocalizationConfig` · **Optional** Configures `sanity-plugin-internationalized-array` for field-level localisation. Locale identity always comes from `sourceLocale` and `locales`; leave this option unset if you register the native plugin yourself, so the schema types are only registered once. ```ts type GTFieldLevelLocalizationConfig = { enabled?: boolean; // default: false fieldTypes?: FieldLevelFieldType[]; // default: ['string', 'text'] languageTitles?: Record; getLanguageTitle?: (locale: string) => string; defaultLanguages?: string[]; // default: [sourceLocale] // Passed through to sanity-plugin-internationalized-array apiVersion?: string; buttonLocations?: ('field' | 'unstable__fieldAction' | 'document')[]; buttonAddAll?: boolean; languageDisplay?: 'titleOnly' | 'codeOnly' | 'titleAndCode'; }; type FieldLevelFieldType = | string | { name: string; type: string; title?: string; of?: unknown[]; fields?: unknown[]; options?: Record; }; ``` The `fieldTypes` entries accept a Sanity type name (`'string'`, `'text'`), the `'block'` shortcut (a Portable Text array), or an object entry defining a custom wrapped field; an object entry named `seo` registers `internationalizedArraySeo`. `getLanguageTitle` overrides `languageTitles` when both are set. `defaultLanguages` controls which locales are pre-populated on empty localised fields and defaults to the source locale. `apiVersion`, `buttonLocations`, `buttonAddAll`, and `languageDisplay` are passed through to the native plugin unchanged. ### `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'`. Each entry is a type filter such as `{ type: 'siteSettings' }` or the shorthand string `'siteSettings'`. Document ID filters are not supported here. ```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', }); ``` ## Translation workflow options [#workflow-options] ### `preserveExistingTranslations` [#preserve-existing-translations] **Type** `boolean` · **Optional** · **Default** `false` Sets the initial state of the **Save local edits** toggle. When the toggle is on, the translations currently in Sanity are sent to General Translation before a translation run, so content whose source text has not changed retains its existing wording instead of being translated again. ```ts title="sanity.config.ts" gtPlugin({ sourceLocale: 'en', locales: ['es', 'fr'], translateDocuments: [{ type: 'post' }], preserveExistingTranslations: true, }); ``` This option sets the toggle’s initial state when the Studio loads. Editors can turn it on or off for their session using the **Translations** tool or the document tab, and that choice takes precedence for the rest of the session. With the toggle on, what is in Sanity replaces whatever General Translation holds for that version of the document, including a translation that has finished but has not yet been imported. Import any pending translations before enabling it. See [Keep edits to translations](/docs/integrations/sanity/guides/translating-content#preserve-edits). ## 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`. To exclude a field wherever it appears, prefer marking it in the schema using the [schema exclusion options](#schema-exclusion). ```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' }] }, ], }); ``` ## Schema exclusion options [#schema-exclusion] Fields and types can be excluded from translation directly in the schema, without a plugin-config entry. During serialisation, the plugin checks each schema definition's `options` for these namespaces and drops matches from the content sent for translation: | Option | Source plugin | | ---------------------------------------------- | --------------------------------------- | | `options.gt.exclude` | `gt-sanity` | | `options.documentInternationalization.exclude` | `@sanity/document-internationalization` | | `options.aiAssist.exclude` | `@sanity/assist` | Each accepts `boolean`; only an explicit `true` excludes. Exclusion applies at any nesting depth. Excluded content is never sent for translation, so translated documents keep the source value unchanged. ```ts defineField({ name: 'internalNotes', type: 'string', options: { gt: { exclude: true } }, }); ``` Setting an exclusion option on a custom type definition's `options` excludes every occurrence of that type, matching the native plugins' "field or type" semantics. The legacy `localize: false` field property is also still honoured. `gt-sanity` augments Sanity's schema option types (the `GTSchemaFieldOptions` interface), so `options.gt` is type-checked on any field definition. *Note: schema exclusion marks a field by name and type; for rules that target a specific document by ID or transform values per locale, use [`ignoreFields`, `skipFields`, and `dedupeFields`](#field-matchers).* ## 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`. * `internationalizedArray`, `internationalizedArrayLanguageFilter`, and `isInternationalizedArrayItemType`, plus the `InternationalizedArrayPluginConfig` and `InternationalizedArrayLanguage` types — re-exported from `sanity-plugin-internationalized-array`. * `GTSchemaFieldOptions` — the schema options interface behind `options.gt`. See [Schema exclusion options](#schema-exclusion). ## 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'), }, }, }), ], }); ```