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 |
| ^5.0.0 |
| ^19.2.0 |
| ^6.1.15 |
| latest |
| ^6.0.0 |
| ^5.0.0 |
| ^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
Open a document in Sanity Studio and click the Transifex tab.
Select the target language(s) and click Send to Transifex.
The document is serialized to HTML and uploaded to your Transifex project. Each document appears in Transifex under its Sanity
_idas 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
Return to the Transifex tab in Sanity Studio.
Click Import for the desired language.
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.


