This plugin provides an in-studio integration with Transifex. It allows your editors to 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, send your documents to Transifex as HTML fragments are most accessible, then deserialize them upon import. This plugin provides the following:
A new tab in your studio for the documents you want to translate
An adapter that communicates with the Transifex file API
Customizable HTML serialization and deserialization tooling
Customizable document patching tooling
Quickstart
Prerequisites
Package manager "npm" installed.
Sanity Project Created "npm install -g @sanity/cli && sanity init".
Once your environment is ready, please continue to install our Sanity-Transifex plugin.
In your studio folder, run
npm install sanity-plugin-transifex
.Ensure the plugin has access to your Transifex secrets. You'll want to create a document that includes your project name, organization name, and a token with appropriate access. Please refer to our documentation on creating a token if you don't have one already.
In your studio, create a file called
populateTransifexSecrets.js
.Place the following in the file and fill out the correct values (those in all caps):
// ./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 will ensure the document is private
// even in a public dataset!
_id: 'transifex.secrets',
_type: 'transifexSettings',
// Replace these with your values
organization: 'YOUR_TRANSIFEX_ORG_HERE',
project: 'YOUR_TRANSIFEX_PROJECT_HERE',
token: 'YOUR_TRANSIFEX_TOKEN_HERE'
})
On the command line, run the file:
npx sanity exec populateTransifexSecrets.js --with-user-token
Verify that the document was created using the Vision Tool in the studio:
Execute the following query:
*[_id == 'transifex.secrets']
📝 Note: If you have multiple datasets, you'll have to do this across all of them since it's a document!
If everything looks good, delete
populateTransifexSecrets.js
so you don't commit it. Because the document's_id
is on a path (transifex
), it won't be exposed to the outside world, even in a public dataset. If you have concerns about this being exposed to authenticated users of your studio, you can control access to this path with role-based access control.
3. Get the Transifex tab on your desired document type, using whatever pattern you like. You'll use the desk structure for this. The options for translation will be nested under this desired document type's views.
Here's a base sample example:
import {DefaultDocumentNodeResolver} from 'sanity/desk'
//...your other desk structure imports...
import {TranslationsTab, defaultDocumentLevelConfig} from 'sanity-plugin-transifex'
//if you are using field-level translations, you can import the field-level config instead:
//import {TranslationsTab, defaultFieldLevelConfig} from 'sanity-plugin-studio-smartling'
//if you're not sure which, please look at the document-level and field-level sections below
export const defaultDocumentNode: DefaultDocumentNodeResolver = (S, {schemaType}) => {
if (schemaType === 'myTranslatableDocumentType') {
return S.document().views([
S.view.form(),
//...my other views -- for example, live preview, document pane, etc.,
S.view.component(TranslationsTab).title('Transifex').options(defaultDocumentLevelConfig)
//again, if you're using field-level translations, you can use the field-level config instead:
])
}
}
For our setup, we will need the following files set as outlined below:
📝 Note: The files mentioned below follow (sanity.config.ts and structure.js) the file structures outlined in this article "Migrating Custom Structure and Default Document Node" on the Sanity site. This will also help with migrating from Sanity V2 to Sanity V3.
You should have a sanity.config.ts that should look as follows:
// sanity.config.ts
import {defineConfig} from 'sanity'
import {deskTool} from 'sanity/desk'
import {structure, defaultDocumentNode} from './structure'
import { schemaTypes } from './schemas'
export default defineConfig({
name: 'default',
title: 'YOUR-SANITY-PROJECT-TITLE-GOES-HERE',
projectId: 'YOUR-SANITY-PROJECT-ID-GOES-HERE',
dataset: 'production',
plugins: [
deskTool({
structure,
defaultDocumentNode,
}),
],
schema: {
types: schemaTypes
}
})
You should have a structure.js (.ts) that should look as follows:
// ./structure.js (.ts)
import {TranslationsTab, defaultDocumentLevelConfig} from 'sanity-plugin-transifex'
// note: context includes `currentUser` and the client
export const structure = (S, context) =>
S.list()
.title('Content')
.items([
S.listItem()
.title('Settings')
.child(
S.document()
.schemaType('siteSettings')
.documentId('siteSettings')
),
...S.documentTypeListItems()
])
export const defaultDocumentNode = (S, {schemaType}) => {
// Conditionally return a different configuration based on the schema type
if (schemaType === "post") {
return S.document().views([
S.view.form(),
S.view.component(TranslationsTab).title('Transifex').options(defaultDocu$
])
}
return S.document().views([
S.view.form(),
S.view.component(JsonView).title('JSON')
])
}
📝 Note: The difference between Sanity V2 and Sanity V3:
The main difference is that your custom structures are now passed in on the deskTool's structure property. It allows for a function with the Structure Builder API (aka S) as its first argument and context with the current user and the studio client as its second.
This means that if you use import client from "part:@sanity/base/client" in your structure builder definition, you have to move that to use the client being exposed through the call function.
Similarly, getDefaultDocumentNode from v2 is now the property defaultDocumentNode in the deskTool configuration object. It receives Structure Builder (aka S) as its first argument and the context with the current schemaType as its second.
For further information, please see the following article.
And that should do it! Go into your studio, click around, and check the document in Transifex (it should be under its Sanity _id
). Once translated, check the import by clicking the Import
button on your Transifex tab!
defaultDocumentLevelConfig
and defaultFieldLevelConfig
make a few assumptions that can be overridden (see below). These assumptions are based on Sanity's existing recommendations on localization:
defaultDocumentLevelConfig
:You want any fields containing text or text arrays to be translated.
You're storing documents in different languages along a path pattern like
i18n.{id-of-base-language-document}.{locale}
.
defaultFieldLevelConfig
:Your base language is English.
Any fields you want to be translated exist in the multi-locale object form we recommend. For example, on a document you don't want to be translated, you may have a "title" field that's a flat string:
title: 'My title is here.'
For a field you want to include many languages, your title may look like{ title: { en: 'My title is here.', es: 'Mi título está aquí.', etc... } }
This config will look for the English values on all similar fields and place translated values into their appropriate fields.
⚠️Warning: If your content models don't look like this, you can still run the defaults as an experiment, though you'll likely get some funky results on import.
Uploading content
Once the plugin is installed, you can go to the Sanity dashboard, choose a content type, and create a post, article, etc. Next to the Editor tab, you will see the Transifex tab.
The target languages from the Transifex-linked project will also be there. Click on the chosen target language and then Create Job to send the source content to Transifex to be translated.
Downloading translations
In order for translated strings to be available for translation, the strings need to have a reviewed status on the Transifex side. This is also reflected by the job progress, which will display the review percentage of your strings in Transifex.
The updates are progressive, meaning each string marked as reviewed will cause your Job Progress % to update after refreshing.
Here's an example of an active translation job:
And here is the corresponding resource on the Transifex project for the same target language:
After strings are translated and reviewed on Transifex, you may return to your Sanity project and click on the Import button.
Overriding defaults, customizing serialization, and more!
To truly fit your documents and layout, you have much power over how exporting, importing, serializing, and patching work. Below are some everyday use cases/situations and how to resolve them.
Scenario: Some fields or objects in my document are serializing /deserializing strangely
First, this is often caused by not declaring types at the top level of your schema. Serialization introspects your schema files and can get a much better sense of what to do when objects are not "anonymous" (this is similar to how our GraphQL functions work -- more info on "strict" schemas here). You can save yourself some development time by trying this first.
If that's still not doing the trick, you can add to the serializer to ensure you have a complete say over how an object gets serialized and deserialized. Under the hood, serialization uses Sanity's blocks-to-html; the same principles apply here. We strongly recommend you check that documentation to understand how to use these serialization rules. Here's how you might declare and use some custom serialization.
First, write your serialization rules:
import { h } from '@sanity/block-content-to-html'
import { customSerializers } from 'sanity-plugin-transifex'
const myCustomSerializerTypes = {
...customSerializers.types,
myType: (props) => { const innerElements =
//do things with the props //className and id is VERY important!! don't forget them!!
return h('div', { className: props.node._type, id: props.node._key }, innerElements)
}
}
const myCustomSerializers = customSerializers myCustomSerializers.types = myCustomSerializerTypes const myCustomDeserializer = {
types: {
myType: (htmlString) => {
//parse it back out!
}
}
}
If your object is inline, you may need to use the deserialization rules in Sanity's block tools (also used in deserialization. So you might declare something like this:
const myBlockDeserializationRules = [
{
deserialize(el, next, block) {
if (el.className.toLowerCase() != myType.toLowerCase()) {
return undefined
}
//do stuff with the HTML string
return { _type:
'myType',
//all my other fields
})
}
]
Now, to bring it all together:
import { TranslationTab, defaultDocumentLevelConfig, BaseDocumentSerializer, BaseDocumentDeserializer, BaseDocumentPatcher, defaultStopTypes } from "sanity-plugin-transifex"
const myCustomConfig = {
...defaultDocumentLevelConfig, exportForTranslation: (id) =>
BaseDocumentSerializer.serializeDocument (
id,
'document',
'en',
defaultStopTypes,
myCustomSerializers),
importTranslation: (id, localeId, document) => {
return BaseDocumentDeserializer.deserializeDocument(
id,
document,
myCustomDeserializer,
myBlockDeserializationRules).then( deserialized =>
BaseDocumentPatcher.documentLevelPatch(deserialized, id,
localeId)
)
}
}
Then, in your document structure, feed the config into your TranslationTab
.
S.view.component(TranslationTab).title('Transifex').options( myCustomConfig )
import { TranslationTab, defaultDocumentLevelConfig, BaseDocumentDeserializer } from "sanity-plugin-transifex"
const myCustomConfig = {
...defaultDocumentLevelConfig,
importTranslation: (
id,
localeId,
document) => {
return BaseDocumentDeserializer.deserializeDocument(id,document).then(
deserialized =>
//you should have an object of translated values here.
//Do things with them!
)
}
}
Scenario: I want more granular control over how my documents get patched back to my dataset
If all the serialization works to your liking, but you have a different setup for how your document works, you can overwrite that patching logic.
Scenario: I want to ensure specific fields never get sent to my translators
The serializer introspects your schema files. You can set localize: false
it on a schema; that field should not be sent off. Example:
fields: [{ name: 'categories', type: 'array', localize: false, ... }]
Scenario: I want to ensure certain types of objects never get serialized or sent to my translators
This plugin ships with a specification called stopTypes
. By default, it ignores fields that don't have helpful linguistic information -- dates, numbers, etc. You can add to it easily.
import { TranslationTab, defaultDocumentLevelConfig, defaultStopTypes, BaseDocumentSerializer } from "sanity-plugin-transifex"
const myCustomStopTypes = [ }
...defaultStopTypes,
'listItem' ]
const myCustomConfig = {
...defaultDocumentLevelConfig,
exportForTranslation: (id) => BaseDocumentSerializer.serializeDocument (
id,
'document',
'en',
myCustomStopTypes
)
}
As mentioned above, feed the config into your TranslationTab
.
S.view.component(TranslationTab).title('Transifex').options( myCustomConfig )
There are several possibilities here. Pretty much every interface provided can be partially or fully overwritten. If something doesn't work how you expect it, or if you'd like a more streamlined way of doing things, please let us know. We always strive to improve Transifex and appreciate any feedback you might have.
💡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.