🔮 Sanity Create is here. Writing is reinvented. Try now, no developer setup

Best practices for managing multi-language content in Sanity-based projects discussed

16 replies
Last updated: May 13, 2022
Hey everyone. I’m starting my second multi-language Sanity-based project and I’d like to have all the site data within, that means all navigation links, button labels, footer text, and every snippet of text possible. Is there a guide to best practices for this scenario? My previous project was hybrid, in which part of the site’s text strings were in JSON files and the rest on Sanity documents.
I want to create all the navigation structure and site data in Sanity Studio.
May 11, 2022, 6:33 PM
Hey Guillermo!
One approach to consider for globally translated labels is what I do on our Course Platform demo. It’s a singleton document with an array field of key-value pairs. The schema looks like this, the resultant field looks like the below.

May 12, 2022, 8:40 AM
user N
, I would say that when it comes to language (localisation) on your page the best practice depends on the scope / goals of your project.

Field level approach If you would like to have identical structure / content on the page regardless of the language then the field level approach is the best as you can create translations for the content that needs translation while keep the logical parts (e.g. show meny item on the page or not) shared between the languages.
Read more:

Document level approach If you would potentially have different structure of the content for every language then the doc level approach is good. Then you can also make it so some subpages would be only found in particular language but not found in other languages - that can be useful for marketing purposes. You can then also connect each language to different URL based on the localisation. Doc:

Separate Dataset for each language I you want to have 3 different webpage for each language (localisation) then it makes sense to use 3 different datasets for each language. In this case you also will have a lot of flexibility to customise studio (content / content structure) for each page.
May 12, 2022, 8:41 AM
user T
This is great! it’s very similar to my previous approach in which I stored the key:value pairs in JSON files. From what I gather it’s an array of objects, one for each label. Seems pretty simple.My other project used regular manual translations, something I took from the
official Sanity guide using field level translations. There wasn’t a ton of content and only two languages so it’s totally manageable. I see you’ve implemented automatic translations from Google… I’ll definitely look into that.
May 12, 2022, 11:34 AM
user M
I was considering the field level approach, but only because I’m familiar with it since I used it for my last project. It’s easier to manage and the site won’t be too big.
May 12, 2022, 11:37 AM
Yep Google Translate is a sneaky custom input plugin 🙂 https://www.sanity.io/plugins/sanity-plugin-google-translate
And yeah the idea of this labelGroup schema was to mirror the sort of translation common across sites that use JSON for content. But with GROQ you can query for just the right language across the entire array!

*[_id == "labelGroup"][0].labels{ key, "text": text[$lang] }
That demo course platform uses multiple approaches where it makes sense.

schema has document-level translations because there’s no common fields between languages
schema uses field-level because only some fields are localised
uses key/value pairs and field-level because they’re globally relevant content
May 12, 2022, 11:39 AM
Sorry to bother you, I’
May 13, 2022, 3:46 PM
Sorry to bother you again
user T
, I just have a quick question. I created a schema following you example to the translated labels. The schema is very similar to yours:

export default {
  name: "commonLabels",
  title: "Etiquetas y Textos",
  type: "document",
  fields: [
      name: "labels",
      title: "Etiquetas",
      description: `Etiquetas y textos comunes que requieran traducciones.`,
      type: "array",
      of: [
          name: "label",
          title: "Etiqueta",
          type: "object",
          fields: [
              name: "key",
              title: "Key",
              type: "string",
              description: `Identifica la etiqueta en el código`,
            { name: "value", title: "Value", type: "localeString" },
Running a simple QROQ query for it:

*[_type == "commonLabels"][0]{
Returns the following:

"result":{1 item
"labels":[1 item
0:{4 items
"value":{3 items
"en":"Welcome to EK roboter S.A."
"es":"Bienvenido a EK roboter S.A."
My dumb questing is, how do I display the
label? I’m using
in a test page to fetch all labels:

const commonLabelsQuery = groq`*[_type == "commonLabels"][0]{

export const getStaticProps: GetStaticProps = async () => {
  const commonLabels = await sanityClient.fetch(commonLabelsQuery)
  return {
    props: {
I’ve tried with
but doesn’t work. Probably becase my key is named
? I wanted to organized the labels by page or section, for instance all the strings for the homepage would begin with
this is how I did it before with JSON files.
May 13, 2022, 3:51 PM
You could either post-process array of

const labels = {}

commonLabels.forEach(label => labels[label.key] = label.value)
Then you could get


Or use a find method on your array

const label = commonLabels.find(label => label.key === 'home.introText')
May 13, 2022, 3:58 PM
Also if you don’t want all languages, filter your labels with GROQ and only get the language text you need 😄

*[_type == "commonLabels"][0]{
    "value": value[$lang]
May 13, 2022, 4:00 PM
Interesting. I only have two languages, so it’s not a lot of data. It also helps when switching languages instantaneously.The post-process approach sound logical, as I may have pages which take in strings and labels from different categories. It’s throwing me a TS error though:

TypeError: commonLabels.forEach is not a function. (In 'commonLabels.forEach(function(label) {
        return labels[label.key] = label.value;
    })', 'commonLabels.forEach' is undefined)
May 13, 2022, 4:03 PM
Target the array. Maybe it’s
May 13, 2022, 4:05 PM
Or extend your GROQ to only return the labels 😄

*[_type == "commonLabels"][0]{
    "value": value[$lang]
May 13, 2022, 4:05 PM
Awesome, I'll try it after lunch. Thanks so much for your help. I'm still learning a lot as you may notice. Took a look at your source code and there's still a ton to learn.
May 13, 2022, 4:11 PM
Every day’s a school day, there’s always more to learn!! 😅
May 13, 2022, 4:22 PM
Tell me about it! four months ago I didn’t know the first thing about React and NextJS and couldn’t even begin to try to do anything… now I can comfortably go through your code and understand almost everything in it. I returned to web dev after a few years and the landscape had changed enormously.
May 13, 2022, 7:10 PM
Yea my first experience with React was hacking together a gatsby site on Sanity … and the rest is history!
May 13, 2022, 8:08 PM

Sanity– build remarkable experiences at scale

Sanity is a modern headless CMS that treats content as data to power your digital business. Free to get started, and pay-as-you-go on all plans.

Was this answer helpful?