Guide: Localizing Sanity CMS with the Intl Input Plugin

I recently helped build a web site using Sanity CMS and Next.js where one of the requirements was support for multiple languages.

With this post I want to give you a head start in how to understand and use the sanity-plugin-intl-input plugin effectively. Furthermore I want this to be an experience report so that people can get an idea of what they should be expecting when working with this plugin and with localization in general.

Versions I used when I implemented localization:

  • Sanity CMS: 2.14.0
  • Sanity plugin intl input: 5.2.1

The bare essentials of localization #

When building any given feature in a technology it can be worthwhile to consider what this feature is in essence. When I went through my web site project I realized that I had to really think this through. Here, I’ll use the user story format to try and capture what most localized web sites require.

  1. As a visitor I want to be able to toggle between different locales (languages and/or countries) because I want to find content tailored for me that I can understand.

  2. As a visitor viewing a page I want to be able to change locale and be sent to the same page localized to the selected locale because I don’t want to get lost on the web site.

    • If the page isn’t available in the locale we have some options to consider: A quick fix is to send the user to the frontpage of the selected locale. Alternatively, we could give a customized 404 stating that the page hasn’t been localized and let the user decide if they want to go back to the page and the locale they were previously viewing or continue browsing in the selected locale.
  3. As a content author I want to be able to localize content because I want to communicate effectively with my audience.

  4. As a content author I want to get an overview of what content is translated and not so that I can effectively work with localization.

  5. As a content author I would like to have my editing experience localized because I might have difficulties understanding the default language.

  6. As a content author I want to localize all micro texts surrounding the main content because I don’t want to bewilder users by mixing different locales.

    • It depends whether you choose to build the ability to translate micro texts into the CMS so that content authors can change it themselves or instead maintain a hardcoded file in the codebase that mostly developers can work with. (Keep in mind that some content authors are comfortable with editing files in codebases using git version control.)

Localizing content in Sanity CMS #

The Sanity documentation on localization outlines two main approaches to localization: field level and document level.

Field level: With this approach you can translate individual fields per document by wrapping each field inside an object.

{
  "_id": "3eb915a9-5f87-4838-824e-d33cb37987c7",
  "_type": "article",
  "localizedTitle": {
    "en-US": "Buy our new mug",
    "no": "Kjøp vår nye mugge"
  }
}

The advantage to this approach is that we can more easily avoid having to localize content that does not need localization: like, author names or publication dates.

However, a downside to this approach is that it causes you to have more unique attributes in your dataset which can lead you to hit the attribute limit. And you will need to fix that before you can continue further. Thankfully, Sanity lets us effectively remodel content.

Document level: With this approach we create one document per localization.

{
  "_id": "ab05fcfb-3e30-4d67-8583-1f95e696ff02",
  "_type": "article",
  "locale": "no",
  "title": "Betre byrdi du ber’kje i bakken enn mannavit mykje. "
}

{
  "_id": "ab05fcfb-3e30-4d67-8583-1f95e696ff02",
  "_type": "article",
  "locale": "en-US",
  "title": "No finer load to carry uphill than wisdom in ample measures."
}

There are several advantages to this approach one is that we avoid multiplying the number of attributes in the dataset. Moreover, the editing experience in Sanity Studio can be made more effective by tailoring the desk structure to list out articles per locale and authors get to focus on one locale at the time when creating new documents. Also, when creating references to localized content it can be effective to have the localized versions appear as individual documents within the reference search field.

There are some downsides to this approach. With the document level approach you have no choice but to localize all fields in the document even though some fields will be the same for all locales. This can make articles burdensome to write and keep updated over time.

Finally, you might very well want to associate a document with all localized versions of it so that visitors can more easily jump between versions. So, you’ll need to find some way to effectively group the localized versions together. One way to solve this would be to create a translations field on the content where authors can create references to other versions. But that approach can become error-prone and cumbersome if authors forget to create these references or accidentally create such references on multiple documents each referring to the other.

Thankfully, the sanity-plugin-intl-input plugin offers a solution to this torny issue. Initially, the plugin was built to only enhance field level localization but now also offers advanced functionality for handling document level localization which we used for our web project.

Using the Intl Input plugin for document level localization #

Here are some notes about things I wish I had known when I decided to use this plugin for my project. This section is meant to complement the existing plugin documention.

The most important thing you need to know about how this plugin works with document level localization is that it defines a default locale document and then it refers to the other locale documents through automatically created references which are invisible to the content authors.

Gotcha #1: When setting up the document level configuration ensure that i18n.base is included in the i18n.languages array. Or you’ll get strange behavior. I was bit by this until I got the configuration right.

Gotcha #2: We setup a singleton document in our Sanity Studio desk structure that reused a page type which we also use for other pages. So, when we go to edit a locale version for this singleton we are redirected to where the desk structure lists out all the pages. And in the url we can see /desk/page;i18n.frontpage.no which is this plugin’s clever way of opening locale versions for editing. Sidenote: Id’s with punctuations in them like i18n.frontpage.no are normally hidden from users. In sum, this behavior is a little bewildering because it looks it’s sending you away from the document you were just looking at. We ended up solving this by documenting this behavior for our content authors.

screenshot of the sanity studio showing an overview of articles and editing one of the articles

Here an example of how the Sanity Studio looks after the plugin has been configured.

screenshot of the sanity studio where the translations tab for a document has been clicked.

Here’s how it looks like when you select the translation tab and can see the available locale versions of a given document.

In the screenshots above you can see the translations tab which lets us switch between locales. Also worth mentioning, is that I put some extra work into the list previews so that content authors can quickly see which of these articles have been translated.

After you get this plugin in place and start creating localized content you can inspect the data in Sanity Studio and you’ll see some hidden fields which have been automatically created: __i18n_lang and __i18n_refs.

{
  "title": "Example of English base document",
  "__i18n_lang": "en-US",
  "__i18n_refs": [
    {
      "_key": "i18n.19ef392d-ecfa-4c5f-83a4-7fff85e8601e.no",
      "lang": "no",
      "ref": {
        "_ref": "i18n.19ef392d-ecfa-4c5f-83a4-7fff85e8601e.no",
        "_type": "reference",
        "_weak": false
      }
    }
  ],
  "_id": "19ef392d-ecfa-4c5f-83a4-7fff85e8601e",
  "_type": "article",
}

{
  "title": "Example of an added Norwegian locale",
  "__i18n_lang": "no",
  "_id": "i18n.19ef392d-ecfa-4c5f-83a4-7fff85e8601e.no",
  "_type": "article",
}

Above you see an example of references created by the plugin. Note how the plugin also uses the document ID in a clever way to store information about the locale of the document. Also note, only the base document gets the __i18n_refs field.

With references in place we can tackle our user story #2 which is to show all versions of a given article with a GROQ query like so:

*[_type == 'article']{
  _id,
  title,
  slug,
  __i18n_refs,
  __i18n_lang,
  
  // Get translations from base document.
  !(_id match "i18n*") => {
    "translations": *[_id in path("i18n." + ^._id + ".*")]
  },

  // Get all translations from localized version.
  _id match "i18n*" => {
    "translations":
      // Find the base document.
      *[^._id in __i18n_refs[].ref._ref]{_id, __i18n_lang} + 
      // And combine it with all localized versions of it
      *[^._id in __i18n_refs[].ref._ref][0]{
        "matches": *[_id in path("i18n." + ^._id + ".*")]{_id, __i18n_lang}
      }.matches
  }
}

This query follows two paths two discovering all locale versions. 1) If we have the base document we can manipulate the ID to query for all versions. 2) Reversely, if we have a locale version we must first find the base document and then use that to find all locale versions.

Also, if you’re building functionality that for example lists out the five most recent articles you’ll need to filter by locale in the GROQ query like so:

*[_type == 'page' 
  && slug.current == $slug 
  && __i18n_lang == $locale ]{
  _id,
  title,
  slug,
  __i18n_refs,
  __i18n_lang,
  "recentArticles": *[
    _type == 'article' 
    && __i18n_lang == $locale
  ] | order(_updatedAt desc)[0...5]
}

So, there you have it. All my best tips for using this plugin optimally.

By the way if you’re looking for a full implementation that makes use of this plugin you should check out the Sanity localization starter.

Concluding thoughts #

I must admit that I was a bit wary about using this plugin for our project because it’s built and maintained by a single developer: Liam Martens has put in a tremendous effort creating this free resource for others to use.

There are no guarantees that Martens will maintain this plugin for the infinite future and the same goes for any open source project. Maintaining open source projects is hard. If in the future this plugin is left without maintainers we’ll have to fork it to make updates. Or, we’ll need to build something else. Those are decent contingency plans.

For our web project I’d say that this plugin has worked well (even considering the gotchas which I listed above). And I’m exited to see how well this localization functionality fares when the content authors really get into the swing of things by writing more content.