← Back to posts

Speaking Every Language: How Localization Works in This Starter

Localization isn't a feature you bolt on after launch. It's a decision you make at the architecture level — in your routing, your content model, and your Studio tooling. This starter makes that decision for you, wiring together next-intl and @tinloof/sanity-document-i18n so the entire stack speaks every language from day one.

Pedro Duque
Pedro Duque
localization

1. The Two Layers of i18n

There are two distinct problems to solve when building a multilingual site.

UI translations — labels, navigation copy, date formats, plurals. These live in your codebase.

Content translations — blog posts, pages, marketing copy. These live in your CMS.

Most starters solve one of them and leave the other as an exercise for the reader. This one solves both.

2. UI Layer: next-intl

next-intl handles everything that belongs in code:

  • Locale-prefixed routing — /en/about, /pt/sobre, /pl/o-nas
  • Typed message catalogues via JSON files per locale
  • Pluralisation, date and number formatting
  • First-class support for React Server Components
  • A single middleware that negotiates the locale, handles redirects, and wires it all up

The routing configuration lives in a single file and is shared between the middleware and the navigation APIs. Add a locale there and the routing, redirects, and alternate links update automatically.

Locale negotiation follows a clear order of priority: the URL prefix comes first, then a remembered cookie, then the browser's Accept-Language header, and finally the configured default. Users land in the right language on their first visit and stay there on every subsequent one.

Inside Server Components, translations are loaded per request and fully typed against your message catalogues. In Client Components, the same API is available synchronously. A provider in the root layout bridges the server and client boundary so messages are never duplicated in the bundle.

3. Content Layer: @tinloof/sanity-document-i18n

UI strings are only half the story. The content that editors write in Sanity Studio also needs to be translatable, reviewable, and publishable per locale.

@tinloof/sanity-document-i18n is a community fork of the official @sanity/document-internationalization plugin, enhanced with better template management and a more opinionated setup tailored for document-level translations.

The plugin adds:

  • A Translations tab in the document editor — one document per locale, linked via translation metadata
  • A language badge on every document so editors always know which locale they are editing
  • Fallback locale logic — missing translations gracefully fall back to the default locale
  • Per-locale preview in Sanity Studio — see exactly what a page looks like for each language before publishing
  • Locale-specific initial value templates so new documents start pre-wired to the correct language

Each translatable document type is registered in the plugin configuration alongside the list of supported languages. Each document then carries a hidden language field that GROQ queries use to filter content by locale — clean, explicit, and entirely predictable.

4. How the Two Layers Connect

The locale segment in the URL is the single source of truth.

next-intl middleware reads the incoming request, negotiates the locale, and makes it available to every Server Component in the tree.

The data layer uses that same locale to query Sanity for documents in the right language. The locale flows top-down — from the URL to the layout, to the UI translation catalogue, to the GROQ query that fetches the correct document. No hidden magic, no mismatches between what the router thinks and what the CMS returns.

5. Adding a New Locale

Extending the starter to a new language is a three-step process:

  • Add it to the routing config — the new URL prefix is live immediately
  • Add a new messages file — for example messages/de.json — for UI strings
  • Add it to the plugin's supported languages — editors can now create documents in that language inside Studio

No changes to layouts, no changes to page components, no changes to queries. The architecture absorbs new locales without touching application code.

Why This Matters

Most localization setups are split across two systems with no shared contract — the frontend handles routing, the CMS handles content, and the two never quite agree on locale identifiers or fallback rules.

This starter makes them agree by design:

  • The locale list is defined once in the routing config and mirrored in the Sanity plugin configuration
  • next-intl handles the routing and UI translation layer with zero custom code
  • @tinloof/sanity-document-i18n handles the content translation layer with per-locale previews and fallback logic
  • GROQ queries are locale-aware from the start — no retrofitting required

The result is a stack where adding a new language is additive, not invasive. Editors work in a Studio that shows them the right locale. Developers query a CMS that returns the right documents. Users land on URLs that speak their language.

That's the contract this starter enforces — and it holds at any scale.