# General Translation Integrations: Sanity plugin configuration URL: https://generaltranslation.com/en-US/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. | `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) | Auto-add `@sanity/document-internationalization`. | `boolean` | Yes | `true` | | [`internationalizedArray`](#internationalized-array) | Configure `sanity-plugin-internationalized-array` for field-level localization. | `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 localization 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 deserializers. | `CustomDeserializers` | Yes | `{}` | | [`additionalBlockDeserializers`](#additional-block-deserializers) | Custom Portable Text block deserializer 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-level translation's locale. Add a field with this name to document types translated with document-level localization, and query it to fetch a specific locale. Field-level localization does not use this field. ### `translateDocuments` [#translate-documents] **Type** `TranslateDocumentFilter[] | string[]` · **Optional** · **Default** `[]` Filters which documents can be translated. Accepts filter objects or shorthand type strings. Each string entry `'article'` is normalized 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 localized in place with internationalized arrays are excluded automatically. Set to `false` to manage internationalization 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 localization [#field-level] Field-level localization stores every locale's value in one document as an internationalized 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 internationalized-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 localization 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 localization. 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 localized 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'` localizes configured fields in place - `'mixed'` uses field-level localization for [`fieldLevelDocuments`](#field-level-documents) and document-level localization for everything else ### `fieldLevelDocuments` [#field-level-documents] **Type** `TranslateDocumentFilter[] | string[]` · **Optional** · **Default** `[]` Selects the document types that use internationalized 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', }); ``` ## Field matchers [#field-matchers] `ignoreFields`, `dedupeFields`, and `skipFields` each take an array of `FieldMatcher` objects. A matcher targets fields by a JSONPath `property` expression and, optionally, restricts to one source document by `documentId`. To exclude a field everywhere it appears, prefer marking it in the schema with 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 stay 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 serialization, 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 honored. `gt-sanity` augments Sanity's schema option types (the `GTSchemaFieldOptions` interface) so `options.gt` type-checks 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).* ## Serialization [#serialization] The plugin serializes documents to HTML for translation and deserializes the result back into Sanity fields. Most projects do not need these options. ### `additionalStopTypes` [#additional-stop-types] **Type** `string[]` · **Optional** · **Default** `[]` Extra 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 serializers merged with the defaults, following the `@portabletext/to-html` component shape (`types`, `marks`, `block`, `list`, `listItem`, and more). Most often used to serialize 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 deserializers 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** `[]` Extra Portable Text block deserializer 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* stopped 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 serialization 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 serializers. - `BaseDocumentSerializer`, `BaseDocumentDeserializer`, `BaseDocumentMerger` — the default serialize, deserialize, and merge implementations. - `defaultStopTypes`, `customSerializers` — the default stop types and serializer 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'), }, }, }), ], }); ```