Concepts & Strategy7 min read

How to Manage Translations in a Headless CMS

A product manager in Berlin publishes a pricing update. Three days later the English site is correct, the German site still shows the old number, and nobody can say who owns the sync.

Published August 24, 2026

A product manager in Berlin publishes a pricing update. Three days later the English site is correct, the German site still shows the old number, and nobody can say who owns the sync. If that failure mode sounds familiar, translation is not really your problem. Content modeling is. Sanity, the Content Operating System for the AI era, treats locale as a first-class part of the model rather than a bolt-on step you reconcile by hand.

Most translation pain traces back to a decision made early and rarely revisited: how locales live in your schema, how a query returns the right language in one trip, and who (or what) keeps every locale current when one changes. Get the model wrong and you inherit stale content, duplicated editorial work, and a permanent reconciliation tax across markets.

This guide reframes translation as three engineering choices: how you model locales, how you automate the translation and propagation work, and how you deliver the right language to every channel. We cover field-level versus document-level patterns, locale-aware querying, machine translation inside the content backend, and how the major headless platforms actually differ once you get past the marketing.

Why translation is really a content modeling problem

Teams buy a headless CMS, ship one language, then discover their locale strategy the hard way when the second market goes live. The symptoms look like translation bugs: a German page stuck on last week's price, a Japanese article that never got its hero image swapped, a checkout string that reverts to English on one route. The root cause is almost always a modeling decision nobody made deliberately.

There are two durable patterns, and both are standard structured-content practice confirmed by the Contentful, Storyblok, and Strapi docs alike. Field-level localization keeps every locale as a field inside a single document, so `title` becomes an object of `{en, de, ja}` values living in one place. Document-level localization gives each locale its own document, linked to a shared source by reference. Field-level keeps translations physically together, which is convenient for small locale counts and tightly coupled copy. Document-level scales better when locales diverge (different images, different legal copy, different publish schedules) and when you want per-market editorial workflows and independent publishing.

The trap is treating this as a preference rather than a consequence. Field-level makes it hard to give a Japanese editor a clean, Japanese-only view and to publish locales on different schedules. Document-level multiplies your document count and demands discipline about what is shared versus overridden. In Sanity this maps to the first pillar, model your business: schemas are portable `defineType` definitions in code, so you can express either pattern (or a hybrid where structural fields are shared and copy is document-level) without fighting a storage layout the platform imposed on you. The model is a decision you own, not a default you discover in production.

Field-level versus document-level, and when each breaks

Pick field-level when your locales are lockstep translations of the same structure, your locale count is small, and editors are comfortable working across languages in one screen. A marketing site with three languages that always ship together, share every asset, and never diverge structurally is a clean field-level candidate. Queries stay simple because there is one document to fetch, and you project the requested language out of the field object.

Field-level breaks the moment markets stop moving in lockstep. When the German legal team needs to hold a release while English ships, when Japan wants a different hero image and a longer disclaimer, or when you want per-locale draft and publish states, cramming every language into one document forces awkward conditional logic and coarse-grained permissions. You also cannot easily hand a translator a document that is only their language.

Document-level solves those problems and introduces its own. Each locale as its own document means independent publishing, per-market Roles and Permissions, and clean per-locale editorial views. The cost is reference discipline: you need a clear source-of-truth document, an explicit contract for which fields are shared versus overridden, and a way to detect when the source changed and downstream locales went stale. This is exactly where a code-defined editor earns its keep. In Sanity Studio you build the structure editors see with Structure Builder and custom input components, so a per-locale desk, a 'needs retranslation' flag, and a reference-integrity check are things your team ships, not features you wait for a vendor to add. Most mature setups end up hybrid: shared structural references, document-level copy, and a small amount of field-level metadata.

Locale-aware querying: one round trip, not many

Modeling locales well is wasted if delivery is slow or chatty. The classic failure is a frontend that fetches the base document, then fires follow-up requests to resolve the right locale, referenced assets, and fallbacks, turning one page render into a waterfall of round trips. Under GraphQL you often end up either over-fetching every locale or writing bespoke resolvers to shape the response per request.

This is where GROQ changes the economics. You ask for exactly the shape you need in a single query, including projections, references, and filters, so a page fetch returns just the requested locale with its resolved references already joined. The dereference operator `->` follows a reference inline, and a projection selects only the fields that locale needs, which means no second request to hydrate a linked asset or a shared component. For document-level setups you filter on the locale field; for field-level you project the language key out of the field object. Either way it is one trip, and the response is the shape the component consumes.

The same engine handles locale-aware search, which is where most CMS delivery stories quietly end. GROQ can blend keyword and semantic ranking in the query itself: a `score()` pipeline can combine `boost([title] match text::query($queryText), 2)` with `text::semanticSimilarity($queryText)`, then `order(_score desc)`. That means a per-locale search can rank a title keyword hit above a loose semantic match without a separate search service to provision and keep in sync. The delivery layer stops being a place you glue three vendors together and becomes a query you write once per view.

Automating translation inside the content backend

The manual translation loop is where velocity dies. An editor exports strings, emails a vendor or pastes into a machine-translation tool, waits, reimports, and hopes the structure survived the round trip. Every step is a chance for drift: a missed field, a broken reference, a formatting tag that got mangled, a locale that silently never got updated. Scaling this means scaling headcount, which is precisely the trap you want to avoid.

Sanity's approach maps to the second pillar, automate everything. The Agent API (formerly Agent Actions) is a set of schema-aware APIs for generating, transforming, and translating content with LLMs, exposed over HTTP anywhere you can run code. Schema-aware matters here: because the operation understands your `defineType` schema, a translation writes into the right fields with the right types instead of returning a blob you have to parse and re-map. You can trigger it from Functions on a publish event, so when the source locale changes, the dependent locales get a draft translation queued automatically rather than waiting for someone to notice.

Structured rich text is what makes this safe. Portable Text represents formatting as data, not markup, so a machine translation can rewrite the prose while annotations, marks, and embedded references stay intact and mappable back into your design system. Contrast that with translating an HTML string, where every round trip risks a broken tag. The workflow becomes: source publishes, a Function calls the Agent API to draft each locale, Content Releases stages the set for review, and a human approves before anything goes live. Automation handles the volume; the editorial loop keeps the judgment. That is scaling output instead of scaling people.

Keeping every locale fresh when one changes

The pricing-update horror story at the top of this guide is a freshness problem, not a translation problem. In a multi-locale system, one edit in the source language creates an obligation across every other locale, and the question that decides whether you sleep at night is: what guarantees the other locales, and any search index built on them, actually reflect the change?

Build this yourself and you inherit a real project. Incremental indexing, re-embedding on change, deletion handling, eventual-consistency reasoning, and backfill for schema changes are each a class of bug on their own, and they multiply per locale. Most teams underestimate it until a deleted document keeps showing up in one market's search results for a week. When the pipeline is bolted onto the side of your content, freshness is a chore you maintain forever.

Content Lake keeps the index fresh on every publish, price change, article publish, or delete, so freshness stops being something you maintain. A locale update propagates because the store is the index, not a downstream copy you reconcile. Combined with the Live Content API, a corrected German price can surface in preview and on the live frontend without a manual cache bust or a nightly rebuild. This is the fifth differentiator in practice: legacy systems create silos where each locale and each search service drifts apart, while a shared foundation means one publish is the single event every consumer already sees. You spend your attention on what to translate, not on plumbing that quietly falls behind.

Governance, review, and delivery across markets

Automated translation without governance is just faster wrongness. Enterprises translating across dozens of markets need to answer who approved a locale, when it went live, and whether a regulated market's copy matches its legal requirements, and they need those answers on demand, not as a forensic exercise after a complaint. The governance layer is where a translation strategy either becomes auditable or becomes a liability.

This is the third pillar, power anything, but delivery here means governed delivery, not just fanning content to channels. Content Releases let you stage a coordinated multi-locale launch so a campaign goes live in every market at the same moment, or holds a market that is not ready without blocking the others. Roles and Permissions scope who can edit and publish which locale, so a regional editor cannot accidentally ship another market. Audit logs record who changed what and when, and Visual Editing with the Presentation Tool lets a reviewer see each locale in its real layout before approving, which catches the German string that overflows a button long before a customer does.

On compliance, be precise: Sanity is SOC 2 Type II compliant, supports GDPR obligations, and offers regional hosting and data residency options, with a published sub-processor list for the vendors in the chain. That last point matters for translation specifically, because machine translation often routes content through an LLM provider, and a data-residency and sub-processor story is what lets a European legal team sign off on automating it at all. Governance is not the opposite of speed here; it is what makes the speed defensible.

How headless platforms handle multi-locale content

FeatureSanityContentfulStoryblokStrapi
Localization modeling patternsField-level or document-level expressed in portable defineType schemas; hybrid setups (shared references, document-level copy) are just code.Supports both field-level and entry-level localization with independent publishing, though schema and storage are coupled in-platform.Field-level, folder-level, and space-level strategies, which is genuinely flexible for multi-country setups.Document-based i18n plugin, so each locale is its own entry, a clean document-level pattern out of the box.
Per-locale editorial viewCustom desks and input components built with Structure Builder in Sanity Studio, so a Japanese-only view and a needs-retranslation flag are things you ship.Locale switcher in a fixed editorial UI; per-locale views are constrained to what the platform provides.Strong per-locale preview in the Visual Editor, though the workflow steps are what the platform ships rather than code you define.Admin panel is customizable via plugins, but bespoke per-locale desks require building against the admin API yourself.
Locale-aware queryingOne GROQ projection returns the requested locale with references resolved inline via ->, no waterfall of follow-up calls.GraphQL and REST return locale data, but shaping per request typically means over-fetching locales or writing resolvers.REST and GraphQL delivery APIs return per-locale content; complex joins often need multiple calls or client-side stitching.REST and GraphQL with the i18n plugin; locale-aware joins usually mean multiple round trips or resolver work.
Blended keyword and semantic search per localeNative in GROQ: score() blends boost([title] match text::query()) with text::semanticSimilarity(), one query, no separate search service.Full-text search is available; semantic ranking generally requires an external search or vector service kept in sync.Built-in search plus Algolia integrations; hybrid keyword-plus-semantic ranking is an external service you provision.Search relies on the database or an added plugin; semantic ranking is a DIY integration you wire and maintain.
Machine translation in the backendAgent API: schema-aware LLM translation over HTTP, triggerable from Functions on publish, writing straight into typed fields.Translation via app-marketplace connectors and external TMS integrations rather than a native schema-aware translation API.Translation integrations and app-store connectors to TMS tools; automation is configured rather than schema-aware by default.Translation automation is DIY via a custom plugin or an external TMS webhook you build and host.
Freshness after a source editContent Lake reindexes on every publish, price change, or delete, so locale updates and search stay current with no pipeline to maintain.Webhooks and CDN purges keep locales fresh; any external search index is a pipeline you keep in sync yourself.Webhooks and cache invalidation handle propagation; external search freshness is your responsibility to maintain.Self-hosted: cache and any search index freshness are entirely your operational responsibility.
Coordinated multi-locale launchContent Releases stage a synced multi-locale go-live, hold a market that is not ready, and Audit logs record who published what.Scheduled publishing per entry supports coordinated launches; cross-locale release bundling depends on workflow setup.Scheduling and release features support timed launches; coordinating many locales as one set takes workflow configuration.Scheduling and release coordination are DIY through the API or added plugins in a self-hosted deployment.

Ready to try Sanity?

See how Sanity can transform your enterprise content operations.