Localization
Localize your content
Sanity is flexible in allowing you to model translated or localized content in the way it makes the most sense to you. Here we will outline two main ways - field level and document level. Which method you prefer will depend on your use case and publishing workflow, and we will discuss concerns that can help you inform that decision. And Sanity being a flexible platform you may of course mix both methods or draw inspiration from these approaches and come up with your own preferred solution.
When determining how to model translations it is often helpful to do some discovery on your current or desired publishing workflows. Is the content and its translations published at the same time? Then it could make sense to go with field level translations. You'll always have one document representing a piece of content and any translated fields are available on that document. On the other hand, if the different locales have their own workflows or should be published as they become available, then a document level approach could suit you better.
Changing your document fields from scalar values to a collection of values allows you to add any number of languages to a field with ease. Generalizing these object values into their own named types also makes them easily reusable across different documents.
Gotcha
The video example below shows code implemented in a project using Sanity Studio v2, while the latest version is v3! The example being shown is still valid, but some things that are not the main focus of the video may look strange to you if you are used to working with Sanity Studio v3.
Since Sanity schema definitions are written in JavaScript, you can programmatically define the language properties for these localized versions of fields. Adding languages then simply becomes a task of expanding the list of languages, and all fields will be expanded to include them. In the following code example we are also using the fieldset feature of objects to group every language value except a "base language" into a collapsible group in order to tidy up the Studio document editor a bit.
// ./sanity.config.js
import {defineConfig} from 'sanity'
// Since schemas are code, we can programmatically build
// fields to hold translated values. We'll use this array
// of languages to determine which fields to define.
const supportedLanguages = [
{ id: 'en', title: 'English', isDefault: true },
{ id: 'no', title: 'Norwegian' },
{ id: 'fr', title: 'French' }
]
const baseLanguage = supportedLanguages.find(l => l.isDefault)
const localeString = {
title: 'Localized string',
name: 'localeString',
type: 'object',
// Fieldsets can be used to group object fields.
// Here we omit a fieldset for the "default language",
// making it stand out as the main field.
fieldsets: [
{
title: 'Translations',
name: 'translations',
options: { collapsible: true }
}
],
// Dynamically define one field per language
fields: supportedLanguages.map(lang => ({
title: lang.title,
name: lang.id,
type: 'string',
fieldset: lang.isDefault ? null : 'translations'
}))
}
const article = {
title: 'Article',
name: 'article',
type: 'document',
fields: [
{
title: 'Title',
name: 'title',
type: 'localeString'
}
],
preview: {
select: {
title: `title.${baseLanguage.id}`
}
}
}
export default defineConfig({
// ...rest of config
schema: {
types: [
localeString,
article,
])
}
})
Protip
The IANA language tags can be helpful when selecting identifiers for your supported languages.
Another big plus with the field level approach is the ability to translate a section of a document - not every field necessarily needs to be translated. Consider a document representing a presenter at a conference. The name and profile picture are values that probably does not warrant translation, but a biography field might.
// presenter.js
export default {
title: 'Presenter',
name: 'presenter',
type: 'document',
fields: [
{
title: 'Name',
name: 'name',
type: 'string',
},
{
title: 'Profile picture',
type: 'image',
name: 'image'
},
{
title: 'Biography',
type: 'localeString',
name: 'bio'
}
]
}
Here is an example of content structured according to this schema, as it appears fetched from our query API
* [_type == "presenter"] {
_type,
name,
image,
bio
}
{
"_type": "presenter",
"name": "Rune Botten",
"image": {
"asset": {
"_ref": "image-ea6db6c70f7330629346b5f04ad0181dcc615608-800x749-png",
"_type": "reference"
}
},
"bio": {
"en": "Rune is a solution architect at Sanity.io",
"es": "Rune trabaja como arquitecto de soluciones en Sanity.io",
"no": "Rune jobber som løsningsarkitekt hos Sanity.io"
}
}
When making use of this content in a front end you can either fetch the full document as exemplified above, or you can selectively return a specific bio depending on which locale you are interested in displaying. This example fetches only the English bio and returns that value in the bio
property
* [_type == "presenter"] {
name,
image,
"bio": bio.en
}
This will return the following JSON from our previous content example
{
"name": "Rune Botten",
"image": {
"asset": {
"_ref": "image-ea6db6c70f7330629346b5f04ad0181dcc615608-800x749-png",
"_type": "reference"
}
},
"bio": "Rune is a solution architect at Sanity.io"
}
If you are making a query for the bio
in a language that might not have a value set yet, say Swedish, you can use the coalesce function in GROQ to provide a fallback. In this case the bio.sv
property won't have a value (since no 'sv' language value is set in our example), so the English value is used instead, returning the same JSON as the example above. Had it not had an English value set either, it would have fallen further back to the string Missing translation
.
* [_type == "presenter"] {
name,
image,
"bio": coalesce(bio.sv, bio.en, "Missing translation")
}
To clean up the UI of a document with localized fields, the langugage filter-plugin can be used to allow editors to hide languages they won’t need to interact with.
You might have more complex publishing workflows that field level translations are too simple to solve for. You could be working in a base language and want to publish that content as soon as it is ready, then publish translations as they become available from other editors or external translation services. Or you may have content that exists only in a certain locale. It might then make the most sense to model localized content as separate documents.
The simplest way to achieve this is to have a language field on documents and set this to which ever language the document's contents correspond to.
// article.js
export default {
title: "Article",
name: "article",
type: "document",
fields: [
{
title: "Language",
type: "string",
name: "language",
options: {
list: [
{title: 'English', value: 'en'},
{title: 'Spanish', value: 'es'}
]
}
},
{
title: "Title",
type: "string",
name: "title",
},
{
title: "Body",
type: "array",
of: [{type: 'block'}],
name: "body"
}
]
}
You can then filter queries for specific locales thus only present the relevant localized content in your front ends.
* [_type == "article" && language == "es"]
Using our Document Actions API you can further add actions for duplicating a document into another locale and then translate the content manually, or send the document off to a third party translation service through their API for automated or professional translation. Once the translation is complete you can re-import it to your Sanity dataset via for example a webhook triggered by the translation service.
You can also use the Structure Builder API to provide segmented navigation to find and organize localized content in the Desk tool, if you wish.
A more integrated solution is to use a plugin which provides some of this for you out of the box and lets you create content in a base language and easily switch your view to other locales in the Studio.
Another nice feature of this plugin is that the base language document contains references to its translated sibling documents, so you can query for the base language content, then follow references to its published translations.