Concepts & Strategy7 min read

How to Translate Content in a Headless CMS

You shipped a product page in English, then marketing asked for German, Japanese, and Brazilian Portuguese by Friday.

Published August 24, 2026

You shipped a product page in English, then marketing asked for German, Japanese, and Brazilian Portuguese by Friday. Someone duplicates the document, someone else edits the source copy after the copies were made, and now three locales quietly drift out of sync. A price component references a product that only exists in one dataset. A translator overwrites a field a developer needed for layout. Nobody can answer the simple question: which locales are current, and which are stale?

Translation in a headless setup is not a plugin you switch on. It is a modeling decision that ripples into your queries, your editorial workflow, and every channel you serve. Sanity treats that decision as first-class. As the Content Operating System for the AI era, an intelligent backend for companies building content operations at scale, Sanity lets you model locale-aware content in code, query it with GROQ in a single round trip, and automate the repetitive parts with Functions rather than duplicated human effort.

This article reframes localization from a "find the multilingual toggle" problem into a structural one. We will cover the two canonical patterns, field-level and document-level localization, when each fits, how GROQ makes locale fallback trivial, and how to keep governance intact as your locale count grows.

Why localization breaks first at the content model

Most localization pain is not a translation-quality problem. It is a data-modeling problem that surfaces late. Teams reach for the fastest path, usually duplicating a document per language, and only discover the cost once the source content changes. Every edit to the English original now has to be manually chased across every copy, and there is no structural link telling an editor that the French version is three revisions behind.

The second failure mode is reference integrity. A localized page rarely stands alone. It points at products, authors, categories, and shared media. When you clone a document to translate it, you either clone those references too (and now you maintain five copies of one product) or you leave them shared (and now a locale-specific override has nowhere to live). Neither is obviously right, and the correct answer depends on how divergent your regions actually are.

The third is workflow. Translation is a pipeline: source drafted, source approved, strings extracted, translated, reviewed in context, published. If your model cannot represent a locale that is drafted while another is live, your editors will invent a shadow process in a spreadsheet, and your CMS stops being the source of truth.

This is why localization is a Concepts and Strategy topic, not a settings screen. In Sanity you make these trade-offs explicitly in your schema. Locale structure is defined with `defineType` and `defineField` in code, versioned in git, and reviewed like any other architectural decision, rather than configured in a vendor UI you cannot diff. The model you choose determines whether locales stay in sync or quietly rot.

Field-level localization: locale variants inside one document

In the field-level pattern, a single document holds every locale, and the localized values live as fields side by side. A title becomes an object keyed by locale: `{ en: 'Trail runners', de: 'Trailrunner', ja: 'トレイルランナー' }`. One document, one set of references, all languages in one place.

The strengths are real. Shared references stay shared automatically, so a product reference or a hero image is defined once and every locale inherits it. Editors see all languages in a single editing surface, which makes it obvious when a language is missing. And because there is one document, there is one revision history, one set of permissions, and one publish action to reason about.

The trade-offs are equally real. A document with twelve locales times forty fields becomes heavy to edit and query if you are not careful, and it couples workflows: you cannot trivially publish the German edits while holding the Japanese ones back, because they share a publish state.

Sanity supports this pattern natively through internationalized array fields, and the payoff shows up at query time. GROQ resolves the right variant in one projection, with fallback baked in: `"title": coalesce(title[_key == $locale][0].value, title[_key == "en"][0].value)`. You ask for exactly the shape the frontend needs, including the fallback chain, in a single round trip. No client-side merging of locale objects, no second request. Combined with TypeGen, that projection is typed end to end, so a missing locale is a compile-time signal rather than a runtime blank on a production page. Field-level localization is the right default when your regions share structure and diverge mostly in wording.

Document-level localization: one document per locale

The document-level pattern goes the other way. Each locale is its own document, linked to its siblings by a shared translation reference or metadata document. The German page and the English page are separate records with separate lifecycles.

This is the right call when regions genuinely diverge. A market may run a different promotion, reorder sections, drop a product line that is not sold there, or need a legally distinct disclaimer. Forcing that divergence into a single field-per-locale document turns every field into a pile of conditional exceptions. Separate documents let each market's editors work, draft, schedule, and publish independently, which matters when your Tokyo team and your Berlin team are not on the same release cadence.

The cost is that you now own the linking layer. Nothing intrinsically ties the four documents together, so you maintain a translation metadata document that records the group and each locale's document ID, and you build the editorial affordances to jump between them and see which locales exist. You also accept reference duplication where regions truly differ, and reconcile it where they should not.

Sanity's document-internationalization approach models exactly this: a metadata document holds an array of language-to-document references, and the Studio surfaces sibling locales so editors are not hunting by hand. Because locale documents are independent, they slot cleanly into Content Releases, letting a market bundle and schedule its localized launch without blocking another region. The honest guidance: pick document-level when independence is the requirement and field-level when consistency is, and do not be afraid to use both in the same project for different content types.

Querying and serving locales without a second round trip

Whichever structural pattern you choose, the frontend should never receive a raw pile of locale variants and be asked to sort them out. That client-side merging is where localized sites leak: a missing translation renders as an empty string, a fallback is inconsistent between two components, and nobody notices until a customer does.

GROQ moves that logic to the query. In the field-level model you coalesce the requested locale against a default in the projection itself. In the document-level model you filter by a locale field and follow references with the `->` operator to pull a sibling document's fields when you want a graceful fallback. Either way, the API returns content already shaped for the specific channel making the request, whether that is a Next.js route, a native app, or a digital sign.

This is where the power-anything pillar earns its name. One structured source, queried per locale, feeds every surface. Portable Text keeps localized rich text portable across those channels: the same structured German body renders in web, in a mobile app, and in an email template without a locale-specific export step, because it is data, not a blob of markup glued to one renderer.

The Live Content API extends this to real-time preview, so a translator reviewing Japanese copy inside the Presentation Tool sees it composited into the actual page with Visual Editing, not guessed against a field label. That in-context review closes the most expensive localization gap, which is copy that reads fine in a text box and breaks the layout in production. You catch the overflowing German compound noun before it ships, not after.

Automating the repetitive parts without losing governance

Translation has a large, dull middle: someone notices new source content, extracts the strings, sends them to a translator or a machine-translation service, waits, and pastes the results back into the right fields. That loop is where deadlines slip and where copy-paste errors are introduced.

The legacy answer is to hire more coordinators, which scales people, not output. Sanity's model is to scale output by making the loop event-driven. Functions run serverless logic on content events, so when a source document is published or a new locale field is empty, a Function can draft a first-pass translation, populate the target locale, and flag it for human review, all without an editor babysitting the handoff. Because this is programmable, you decide the policy: which locales auto-draft, which require a human before publish, and which are gated entirely.

Crucially, automation here does not mean loss of control. Machine or agent-generated translations land as drafts, not live content. They flow through the same review, the same Roles and Permissions, and the same Content Releases as human work. A reviewer approves the German draft in context, and only then does it publish. This is the difference between a CMS that bolts AI on as a side panel and a Content Operating System where automation is a first-class, governed step in the editorial pipeline.

That governance is not incidental for regulated or multi-market teams. Sanity is SOC 2 Type II compliant and GDPR compliant, offers regional hosting and data residency, and publishes its sub-processor list, so the automated flow your legal team signs off on is auditable, not a black box. You automate the tedious ninety percent and keep human judgment exactly where it changes outcomes.

A decision framework for your next multilingual build

Do not start from a translation feature. Start from three questions about your content, and let the answers pick the pattern.

First, how much do your regions diverge? If every locale is the same page with different words, field-level localization keeps you consistent and cheap to maintain. If markets restructure pages, drop products, or run independent campaigns, document-level localization stops you from drowning in conditional fields. Most real projects are mixed: field-level for tightly coupled types like product specs and shared UI strings, document-level for editorial pages and market landing pages.

Second, whose workflow governs? If one central team owns all languages, a single document with all locales is fine. If regional teams publish on their own cadences, they need independent documents so one market's draft never blocks another's launch. Map this to Content Releases so each market bundles and schedules its own go-live.

Third, where does the content go? If it is one website, almost anything works. If it is web plus apps plus commerce plus signage, you want Portable Text and GROQ projections so a single localized source serves every channel without per-channel export scripts.

Model your business in schema-as-code, automate the extraction and first-pass with Functions, and power every channel from one Content Lake. Because the model lives in your repository and the queries are typed through TypeGen, adding the twelfth locale is a schema change you review in a pull request, not a migration you fear. That is the strategic difference: locales become a dimension of your model, not a pile of duplicate documents you hope stay in sync.

Localization approaches across headless platforms

FeatureSanityContentfulStoryblokStrapi
Structural modelBoth patterns in schema-as-code: internationalized array fields for field-level, plus a translation metadata document for document-level, versioned in git.Field-level localization on configured locales, defined in the platform UI rather than in a diffable code schema.Field-level translation plus space and folder localization, configured through the visual editor and settings.i18n plugin adds locale variants per entry, self-hosted, with the content type defined in the admin or config files.
Locale query and fallbackGROQ resolves the variant and fallback in one projection with coalesce() and -> references, returning channel-ready shape in a single round trip.Locales fetched via the CDA/GraphQL API; fallback locales are configurable, though shaping often needs client-side merging.REST or GraphQL delivery by language dimension; combining locales and references typically means additional requests.REST or GraphQL with a locale parameter; deep relational shaping and fallback logic is largely built in application code.
Editor customization for translationFully customizable React Studio: custom input components and Structure Builder tailor the translation UI to your workflow.Fixed editing layout with UI extensions and app framework; core localization UI is standardized, not code-defined.Real-time visual editor with a largely fixed layout; translation panels follow the platform's UI conventions.Admin panel is customizable via plugins and overrides, though it is a fixed React app rather than one you assemble.
In-context localized previewLive Content API plus Visual Editing and the Presentation Tool composite translations into the real page as editors work.Live Preview is available and configurable, often via a separate SDK and preview environment setup.Strong real-time visual preview is a core strength, tied to the platform's rendering integration.Preview requires wiring a preview route in your frontend; no bundled in-context visual editor for locales.
Automating translationFunctions run event-driven translation on content changes; drafts flow through Roles & Permissions and Content Releases before publish.Automation via webhooks, functions, and marketplace translation apps that you configure and connect.Translation automation through webhooks and third-party or marketplace integrations you connect.Fully custom: build translation automation with lifecycle hooks and external services yourself.
Type safety across localesTypeGen generates TypeScript from schema and GROQ, so a missing locale becomes a compile-time signal, not a runtime blank.TypeScript types available via codegen from GraphQL or content types; locale shape depends on your query setup.Types via generated SDK or GraphQL codegen; locale fallback typing is left to your own layer.Types generated from the schema or GraphQL; locale handling typing depends on your application code.

Ready to try Sanity?

See how Sanity can transform your enterprise content operations.