gt-sanity@2.1.0
Overview
gt-sanity v2.1 adds field-level localization: instead of creating a separate document per locale, you can now store every language's value inside the same document using internationalized arrays. The plugin generates the schema types for you, ships an inline per-locale editing UI, and routes translation through the same GT workflow you already use — translated values are merged back into the source document in place.
The data shape matches sanity-plugin-internationalized-array ([{ _key, _type, language, value }]), so existing internationalized-array content works with zero migration.
What's new
Field-level (internationalized array) localization
Document-level translation remains the default: each locale gets its own document, linked via @sanity/document-internationalization. That model fits documents where everything varies by language.
The new field-level model keeps a single document and localizes individual fields in place:
{
_id: 'post-123',
_type: 'post',
title: [
{ _key: 'x1', _type: 'internationalizedArrayStringValue', language: 'en', value: 'Hello' },
{ _key: 'x2', _type: 'internationalizedArrayStringValue', language: 'es', value: 'Hola' },
],
}Enable it with the new internationalizedArray option (or its descriptive alias, fieldLevelLocalization):
import { defineConfig } from 'sanity';
import { gtPlugin } from 'gt-sanity';
export default defineConfig({
plugins: [
gtPlugin({
sourceLocale: 'en',
locales: ['es', 'fr', 'ja'],
translateDocuments: [{ type: 'post' }],
internationalizedArray: { enabled: true },
translationLevel: 'internationalizedArray',
}),
],
});Generated schema types
When enabled, the plugin generates internationalizedArray* schema types (e.g. internationalizedArrayString, internationalizedArrayText) from the same sourceLocale / locales you already pass to gtPlugin — locale identity is defined once. Use them in your schemas like any other type:
defineField({
name: 'title',
type: 'internationalizedArrayString',
});fieldTypes controls which types are generated. It defaults to ['string', 'text'], accepts 'block' for Portable Text, and accepts custom object definitions for arbitrary value shapes:
internationalizedArray: {
enabled: true,
fieldTypes: [
'string',
'text',
'block',
{ name: 'seo', type: 'seoFields' }, // generates internationalizedArraySeo
],
},You can also customize the generated type names with typePrefix (compatibility aliases under the standard internationalizedArray* names are kept by default) and the locale labels shown in the Studio with languageTitles or getLanguageTitle.
Inline per-locale editing UI
Generated types come with a purpose-built Studio input: each language renders as a labeled inline editor (no collapsed object rows or edit dialogs), with per-locale add buttons and remove buttons. The source language can't be removed.
If you'd rather bring your own UI, the components option lets you override the input, item, and field slots — or pass false to fall back to Sanity's default rendering. Translation is unaffected either way, since it operates on the stored data rather than the components.
translationLevel and mixed mode
Schema generation and translation routing are independent: enabling internationalizedArray only adds editable field types. The new translationLevel option controls how matched documents are translated:
'document'(default) — whole-document translation with per-locale documents, unchanged from v2.0.'internationalizedArray'— all matched documents are localized in place via internationalized arrays.'mixed'— the document types listed infieldLevelDocumentsuse the array strategy; everything else stays document-level.
gtPlugin({
sourceLocale: 'en',
locales: ['es', 'fr'],
translateDocuments: [{ type: 'post' }, { type: 'siteSettings' }],
internationalizedArray: { enabled: true },
translationLevel: 'mixed',
fieldLevelDocuments: [{ type: 'siteSettings' }], // localized in place
});Document types localized in place are automatically excluded from @sanity/document-internationalization, so they don't get language badges or per-locale document templates.
Importing in place
Field-level documents go through the same GT workflow you already use. On import, translated values are merged back into the same document: only the target locale is updated, and all other languages — including edits made while translation was running — are left untouched.
Translation statuses in the Studio also now stay accurate after in-place imports and page refreshes, instead of showing completed translations as "not started".