Skip to main content

Sanity

A plugin that integrates Transifex with Sanity, simplifying document translation.

Written by Transifex

The Transifex plugin for Sanity enables in-studio translation workflows. Editors can send any document to Transifex with the click of a button, monitor ongoing translations, and import partial or complete translations back into the studio.

To maintain document structure, content is sent to Transifex as HTML and deserialized back on import. The plugin provides:

  • A translation tab inside Sanity Studio

  • An adapter that communicates with the Transifex file API

  • Customizable HTML serialization and deserialization tooling

  • Customizable document patching tooling


Supported versions

The plugin supports Sanity v5 and the current Sanity i18n plugin versions. Use the following package versions:

Package

Version

sanity

^5.0.0

react / react-dom

^19.2.0

styled-components

^6.1.15

sanity-plugin-transifex

latest

@sanity/document-internationalization

^6.0.0

sanity-plugin-internationalized-array

^5.0.0

@sanity/language-filter

^5.0.0

📝Note for projects upgrading from older versions

If your project used @sanity/document-internationalization v5 or below, the plugin reads and patches your existing translation.metadata documents in their original format automatically. However, any new metadata documents it creates will default to the v6 format (language field + random _key). If you need new documents to stay consistent with your existing data, set newMetadataFormat: 'legacy' in your config. For more details, see Upgrading from legacy format.


Prerequisites

  • An existing Sanity v5 project

  • A Transifex account with an API token (Editor or Administrator role since write access is required)

  • Node.js and npm installed


Setup Guide

Step 1: Install the plugin

npm install sanity-plugin-transifex

This automatically installs the required dependencies: sanity-translations-tab and sanity-naive-html-serializer.

⚠️Important: Do not register a transifex() function in the plugins array of your sanity.config.ts. The plugin does not export a plugin function. The integration is added exclusively via TranslationsTab in your desk structure (see Step 3).


Step 2: Store your Transifex credentials

The plugin reads credentials from a document stored in your Sanity dataset. Create a script to populate it:

// scripts/populateTransifexSecrets.js
// Do not commit this file to your repository.
import {getCliClient} from 'sanity/cli'

const client = getCliClient({apiVersion: '2023-02-15'})

client.createOrReplace({
// The '.' in this _id keeps the document private, even in a public dataset.
_id: 'transifex.secrets',
_type: 'transifexSettings',
organization: 'YOUR_TRANSIFEX_ORG_SLUG',
project: 'YOUR_TRANSIFEX_PROJECT_SLUG',
token: 'YOUR_TRANSIFEX_API_TOKEN',
})
.then(() => console.log('Transifex secrets saved'))
.catch(err => console.error('Error:', err))

Run it with:

sanity exec scripts/populateTransifexSecrets.js --with-user-token

sanity exec requires your logged-in Sanity account with write access. If this is not set up correctly, the command will exit without error, but the write will silently fail with a 403 error, and the transifex.secrets document will never be created. Confirm your role before running.

Verify the document was created by running the following query in the Vision tool in your Studio:

*[_id == 'transifex.secrets']

A successful result will look like this:

[
{
"_id": "transifex.secrets",
"_type": "transifexSettings",
"organization": "your-org-slug",
"project": "your-project-slug",
"token": "your-api-token"
}
]

If the result is an empty array [], the write most likely failed due to a lack of write access.

Once confirmed, delete populateTransifexSecrets.js so it isn't accidentally committed to version control to avoid exposing your API token.

📝Multiple datasets: If you use more than one dataset, run the script separately for each. Τhe credentials document is stored per-dataset.


Step 3: Add the Translations Tab to your desk structure

The Transifex tab is added via your Sanity desk structure, not via sanity.config.ts plugins. Choose the configuration that matches how your project stores translations.

Document-level translation (@sanity/document-internationalization)

Use this when each language is a separate Sanity document, linked together via a translation.metadata document.

// structure.ts
import {TranslationsTab, defaultDocumentLevelConfig} from 'sanity-plugin-transifex'

export function defaultDocumentNode(S, {schemaType}) {
if (schemaType === 'article') { // replace with your translatable schema type(s)
return S.document().views([
S.view.form(),
S.view
.component(TranslationsTab)
.title('Transifex')
.options(defaultDocumentLevelConfig),
])
}
return S.document().views([S.view.form()])
}

// sanity.config.ts
import {defineConfig} from 'sanity'
import {structureTool} from 'sanity/structure'
import {documentInternationalization} from '@sanity/document-internationalization'
import {defaultDocumentNode} from './structure'

export default defineConfig({
// ...
plugins: [
structureTool({defaultDocumentNode}),
documentInternationalization({
supportedLanguages: [
{id: 'en', title: 'English'},
{id: 'fr', title: 'French'},
],
schemaTypes: ['article'],
}),
// No transifex() plugin entry here
],
})

Field-level translation (sanity-plugin-internationalized-array)

Use this when each translatable field holds all language values within a single document.

The sanity-plugin-internationalized-array stores them internally as an array in the Content Lake, so you don't write this directly, but if you query your dataset in Vision and it looks like this:

"title": [
{"_key": "abc123", "language": "en", "value": "Hello"},
{"_key": "def456", "language": "fr", "value": "Bonjour"}
]

Then use the following configuration:

// structure.ts
import {TranslationsTab, defaultI18nArrayConfig} from 'sanity-plugin-transifex'

export function defaultDocumentNode(S, {schemaType}) {
if (schemaType === 'product') { // replace with your translatable schema type(s)
return S.document().views([
S.view.form(),
S.view
.component(TranslationsTab)
.title('Transifex')
.options(defaultI18nArrayConfig),
])
}
return S.document().views([S.view.form()])
}

// sanity.config.ts
import {defineConfig} from 'sanity'
import {structureTool} from 'sanity/structure'
import {internationalizedArray} from 'sanity-plugin-internationalized-array'
import {languageFilter} from '@sanity/language-filter'
import {defaultDocumentNode} from './structure'

export default defineConfig({
// ...
plugins: [
structureTool({defaultDocumentNode}),
internationalizedArray({
languages: [
{id: 'en', title: 'English'},
{id: 'fr', title: 'French'},
],
defaultLanguages: ['en'],
fieldTypes: ['string', 'text'],
}),
languageFilter({
supportedLanguages: [
{id: 'en', title: 'English'},
{id: 'fr', title: 'French'},
],
}),
// No transifex() plugin entry here
],
})

⚠️Important: defaultFieldLevelConfig (still exported by the plugin) is for an older, custom field-level setup that stored translations as an object ({en: "Hello", fr: "Bonjour"}). It is not compatible with sanity-plugin-internationalized-array. Use defaultI18nArrayConfig instead.


How translations work

Sending content to Transifex

  1. Open a document in Sanity Studio and click the Transifex tab.

  2. Select the target language(s) and click Send to Transifex.

  3. The document is serialized to HTML and uploaded to your Transifex project. Each document appears in Transifex under its Sanity _id as the resource name.

Monitoring and translating

Translation happens in Transifex. Strings must reach Reviewed status before they are available to import. Job progress reflects the percentage of reviewed strings and updates progressively.

Importing translations

  1. Return to the Transifex tab in Sanity Studio.

  2. Click Import for the desired language.

  3. The translated content is deserialized and written back to your dataset, either as a new language document (document-level) or as updated field values (field-level).


Upgrading from legacy format

If your project used @sanity/document-internationalization v5 or earlier, translation.metadata documents stored the language code directly in _key. The plugin reads both formats transparently, and no data migration is required.

When writing new translation.metadata documents, the plugin defaults to the v6 format (random _key + dedicated language field). To keep new metadata in the legacy format for consistency with existing data, pass the newMetadataFormat option:

S.view
.component(TranslationsTab)
.title('Transifex')
.options({
...defaultDocumentLevelConfig,
newMetadataFormat: 'legacy', // 'language-field' (default) or 'legacy'
})

Existing metadata documents are always written back in their detected format regardless of this setting.


Customizing translation behavior

defaultDocumentLevelConfig, defaultI18nArrayConfig, and defaultFieldLevelConfig all accept overrides. Below are the most common customization scenarios.

Excluding a specific field from translation

Set localize: false on any field in your schema to prevent it from being serialized and sent to Transifex:

// In your schema definition
fields: [
{
name: 'categories',
type: 'array',
localize: false, // This field will never be sent to Transifex
// ...
}
]

Excluding content types from serialization

The plugin ships with defaultStopTypes, a list of field types that are skipped during serialization (dates, numbers, and other non-linguistic content). You can extend it:

import {
TranslationsTab,
defaultDocumentLevelConfig,
defaultStopTypes,
BaseDocumentSerializer,
} from 'sanity-plugin-transifex'

const myStopTypes = [...defaultStopTypes, 'myCustomType']

const myCustomConfig = {
...defaultDocumentLevelConfig,
exportForTranslation: (id) =>
BaseDocumentSerializer.serializeDocument(id, 'document', 'en', myStopTypes),
}

// In your structure:
S.view.component(TranslationsTab).title('Transifex').options(myCustomConfig)

Overriding export and import behavior

Both exportForTranslation and importTranslation can be replaced with custom implementations. This is useful when your content model doesn't match the defaults, or when you need custom patching logic.

import {
TranslationsTab,
defaultDocumentLevelConfig,
BaseDocumentSerializer,
BaseDocumentDeserializer,
documentLevelPatch,
defaultStopTypes,
} from 'sanity-plugin-transifex'

const myCustomConfig = {
...defaultDocumentLevelConfig,

// Custom export: serialize with your own stop types or serializers
exportForTranslation: (id) =>
BaseDocumentSerializer.serializeDocument(id, 'document', 'en', defaultStopTypes),

// Custom import: deserialize then patch back to the dataset
importTranslation: (id, localeId, document) =>
BaseDocumentDeserializer.deserializeDocument(id, document).then((deserialized) =>
documentLevelPatch(deserialized, id, localeId)
),
}

S.view.component(TranslationsTab).title('Transifex').options(myCustomConfig)

📝Note: If objects in your document serialize or deserialize unexpectedly, first check that all types are declared at the top level of your schema (not anonymous inline objects). Named types give the serializer far more context about how to handle them. For advanced custom serialization rules beyond what is covered above, refer to the plugin README on GitHub.


Troubleshooting

The Transifex tab appears, but no strings are exported

Ensure you are using the latest version of sanity-plugin-transifex. Earlier versions had a query that failed silently against the data format introduced in @sanity/document-internationalization v6.

Import returns no results/translations don't appear in Studio

Strings must reach Reviewed status in Transifex before they can be imported. Unreviewed strings are not available for import regardless of translation progress.

403 error when running the credentials script

The Sanity token used with sanity exec must have write access. Please check the role associated with the token and ensure it is appropriate (Editor and Developer roles both have full read/write access).

Studio fails to build after installation

Ensure @sanity/language-filter is installed. It is a required peer dependency of sanity-plugin-internationalized-array v5 and must be listed explicitly in your package.json.

Run the following install:

npm install @sanity/language-filter

"transifex is not a function" error

Remove any transifex() call from the plugins array in sanity.config.ts. The plugin does not export a plugin function and does not belong in that array.

Credentials saved, but the tab can't connect

Check your Sanity dataset's access control settings. If your dataset has an explicit ACL that restricts read access, the Studio may not be able to retrieve the transifex.secrets document. Verify the document exists by running *[_id == 'transifex.secrets'] in the Vision tool.


💡Tip

Looking for more help? Get support from our Transifex Community Forum!

Find answers or post to get help from Transifex Support and our Community.

Did this answer your question?