Concepts & Strategy7 min read

Can a Headless CMS Support Locale-Specific Content Variants?

A product page ships in twelve markets.

Published August 25, 2026

A product page ships in twelve markets. The German legal copy is three sentences longer than the English, the Japanese hero image needs different cropping, the French price displays in euros with a comma decimal, and the Arabic layout runs right to left. Then a translator "fixes" a string in the shared field, and the change silently overwrites the source everyone else builds from. This is where most content teams discover that "translated" and "localized" are not the same problem, and that their CMS only ever solved the first one.

Locale-specific variants are not just swapped strings. They are structurally different content: different field values, different media, different components present or absent, different publishing schedules, and different reviewers who must sign off per market. Sanity is the Content Operating System for the AI era, an intelligent backend that treats localization as a modeling decision you make deliberately rather than a plugin you bolt on after launch. A headless CMS can absolutely support locale-specific variants, but only if the content model, the query layer, and the editorial workflow all agree on where a locale lives. This article walks through the modeling choices, the query mechanics, and the governance controls that separate a localization stack that scales from one that quietly rots.

Why translation and localization are different problems

The failure mode is predictable. A team models a single "title" field, wires up a translation service, and assumes locale support is done. Six months later the Japanese market needs a different call to action, the German legal team needs a longer disclaimer, and the Brazilian site needs to hide a component that is illegal to advertise locally. None of that is translation. It is localization: the content itself differs in structure, presence, and meaning per market, not just in language.

Translation is a string operation. You take one canonical value and produce an equivalent in another language. Localization is a modeling operation. You accept that a "variant" may have different field values, different references, different media crops, different SEO metadata, and sometimes entirely different components. A locale is a first-class dimension of the content, not a lookup table applied at render time. When a CMS conflates the two, teams end up hacking around it with duplicated documents, naming conventions in slugs, or spreadsheet exports that drift out of sync within a release cycle.

The enterprise stakes are concrete. Locale variants touch legal compliance (market-specific disclaimers), regulatory display rules (pricing, allergen, or financial disclosure formats), brand governance (which markets may deviate from global copy), and revenue (a mistranslated checkout flow converts worse than no localization at all). Getting the model right up front is cheaper than every workaround that follows. The question is not whether a headless CMS can support variants. It is whether the one you pick lets you model the variant dimension the way your business actually works, or forces your business to work the way the tool ships.

Field-level versus document-level localization

There are two dominant patterns for modeling locales, and the right choice is rarely one or the other for the whole project. Field-level localization stores every translation inside a single document: the "title" field becomes an object keyed by locale, so one document carries the English, German, and Japanese values together. Document-level localization gives each locale its own document, linked to siblings through a shared reference or a translation metadata record.

Field-level shines when the structure is identical across markets and only the values change. One document is easy to query, easy to diff, and keeps the canonical relationship obvious. It struggles when locales diverge structurally, when one market needs an extra component, or when per-locale publishing schedules and reviewers enter the picture, because now a single document has to be partially published, which most systems handle badly.

Document-level localization is the honest choice for markets that genuinely diverge. Each locale document can carry different components, its own workflow state, its own reviewer, and its own publish time. The cost is coordination: you need a reliable way to know which documents are siblings and which source they derive from. Sanity supports both patterns rather than forcing one. Because a schema is just a portable defineType in sanity.config.ts, you can make a field localized by wrapping it in a locale object type, or you can model locale as a document-level field and let GROQ filter on it. Structure Builder then organizes the Studio so editors navigate by market instead of scrolling one giant document. The point is that the pattern is a decision you own, expressed in code you can version and change, not a fixed behavior you inherit.

Querying variants without N round trips

Modeling locales is half the problem. The other half is retrieval. A localized frontend has to ask for the right variant, fall back gracefully when a locale is missing, and often assemble a page from documents that live in different locale states, all without firing a dozen sequential requests that add latency to every page load.

This is where the query layer stops being a formality. With GraphQL, a localized query typically means either over-fetching every locale and filtering client-side, or stitching multiple queries together and resolving fallbacks in application code. Both push localization logic out of the content layer and into the frontend, where it has to be reimplemented for every consuming channel.

GROQ collapses that work into one round trip. You can project exactly the shape the page needs, select a single locale from a field-level object with a projection like `"title": title[$locale]`, and express a fallback in the same query with a coalesce so a missing German value drops to English without a second request. The `->` operator resolves references to sibling locale documents inline, so a document-level model returns its translated siblings in the same response. Filters like `*[_type == "page" && locale == $locale]` scope a whole market in one query, and because Content Lake is schema-aware and real-time, those queries stay live: a locale variant edited in the Studio propagates to preview without a rebuild. Ask for exactly the shape you need, including projections, references, and per-locale fallbacks, in a single request. That is the difference between a localization layer that adds milliseconds and one that adds round trips per component.

Governance: who can change which locale, and when

Localization at scale is a permissions problem before it is a translation problem. The global brand team owns the source. Regional teams own their markets. A translator should be able to edit the French variant without touching the canonical English, and a German legal reviewer must sign off before a market-specific disclaimer goes live. When everything lives in one undifferentiated space, the silent-overwrite failure from the introduction is not a bug, it is the default.

The governance requirement has three parts. First, scoping: reviewers and editors should see and change only the locales they own. Second, review: a variant should move through draft, review, and approved states independently, so France can ship while Japan is still in legal. Third, timing: markets rarely launch simultaneously, so each locale variant needs its own publish moment, sometimes coordinated into a single global release, sometimes staggered.

Sanity addresses each with a named surface. Roles & Permissions scope who can act on which content, so a regional editor is not one careless save away from rewriting the source of truth. Content Releases group a set of variant changes and schedule them, so a coordinated twelve-market launch publishes atomically rather than as twelve manual saves at midnight. Content Source Maps and Audit logs record what changed, by whom, and when, which is exactly the trail a legal or compliance reviewer needs when a market-specific claim is questioned after the fact. This is the pillar where a Content Operating System earns its name: it does not stop at publishing a translation, it operates the variant end to end, from who may touch it through when it goes live to what record survives the launch.

Structured content is what makes variants portable and reviewable

A locale variant is only as portable as the format it is stored in. If your German rich text is a blob of HTML with inline styles, every downstream channel (web, native app, in-store screen, voice) has to parse and reinterpret it, and every reviewer has to eyeball rendered output to catch a problem. Structured content is what turns a variant from a rendered artifact back into reviewable, reusable data.

Sanity stores rich text as Portable Text, a structured JSON representation rather than markup. For localization this matters in three ways. It is channel-agnostic, so the same French variant maps into a web design system, a mobile layout, and a headless commerce surface without a per-channel rewrite. It is annotatable, so a link, a legal footnote, or a market-specific term carries structured marks that a reviewer or an automated check can inspect field by field instead of scanning HTML. And it is machine-readable, so an automated glossary check or an enrichment step can read the structure directly.

That structure is also where automation lives without becoming a black box. Functions and the App SDK let you run serverless steps against content events, so a new source-locale document can trigger a draft translation, a terminology check, or a routing step that assigns the right regional reviewer, all landing as normal draft variants that a human still approves. This is the automate-everything pillar in practice: the machine scales the output, the structured format keeps every variant reviewable, and TypeGen generates TypeScript types from the schema so the frontend consuming those locale fields is type-safe rather than guessing at shapes. Portability and reviewability are not separate features here. They both fall out of storing the variant as structured data instead of rendered markup.

Common anti-patterns that quietly break localization

Most localization stacks do not fail loudly. They accumulate small compromises until adding a thirteenth market takes a quarter instead of a sprint. A few anti-patterns show up again and again, and each traces back to a modeling shortcut taken early.

Encoding locale in the slug or document ID ("/de/product", "product-de") feels convenient and works until you need to query across markets, rename a route, or ask "which documents are missing a Japanese variant." Locale becomes a string-parsing exercise instead of a queryable field. Duplicating whole documents per market with no shared source is the next trap: there is no canonical record, so drift is guaranteed and "update the global disclaimer everywhere" becomes a manual sweep. Handling fallbacks in frontend code scatters the same coalesce logic across every consuming app, so web and mobile disagree about what English-when-German-is-missing means. And treating translation as a fire-and-forget export to a third-party tool breaks the review trail the moment a legal claim is questioned, because the approved version lives in an email thread, not the content store.

The throughline is that each shortcut moves localization logic out of the content model and into places that cannot be governed or queried. The fix is the same in every case: make locale a modeled dimension, keep fallback logic in the query layer where GROQ can express it once, and keep the review trail in the system of record. When locale is a real field on real documents, "which markets are missing this variant," "who approved the German legal copy," and "publish these eight markets together" all become ordinary operations instead of custom tooling you have to build and maintain yourself.

Locale-specific variant support across headless platforms

FeatureSanityContentfulStoryblokStrapi
Localization modelBoth patterns you own in code: field-level via a locale object type, or document-level with locale as a filterable field, all in portable defineType schemas.Field-level locales configured per space with a defined default and fallback locale hierarchy set in space settings.Field-level translation plus folder-level localization; per-field or per-story translation depending on setup.Built-in i18n plugin adds locale entries per content type; field-level localization toggled per field.
Single-query variant + fallbackOne GROQ round trip: title[$locale] projection with coalesce fallback, plus -> to resolve sibling locale documents inline.GraphQL/REST return locale fields; cross-locale fallback and multi-document assembly often resolved in application code.REST/GraphQL Delivery API returns a story per locale; fallback logic typically handled by the frontend.REST/GraphQL with a locale param per request; fallbacks and cross-locale joins usually assembled in app code.
Per-locale scheduled releaseContent Releases group variant changes and schedule an atomic multi-market publish, with staggered or simultaneous timing.Scheduled publishing and Releases group entries for a coordinated go-live across a space.Pipelines and scheduling support staged releases across environments and stories.Draft and Publish plus community/plugin scheduling; atomic multi-locale release typically custom-built.
Per-locale review scopingRoles & Permissions scope who edits which content; regional editors constrained away from the canonical source.Roles and tasks scope editorial access; granular per-locale permissioning depends on plan tier.Roles and workflow stages assign approval; per-locale scoping configured through spaces and roles.Role-based access control on content types; locale-granular permissions usually require custom logic.
Structured rich text for variantsPortable Text stores each variant as annotatable JSON, channel-agnostic and inspectable field by field rather than rendered HTML.Rich Text field returns a structured JSON document model that renderers map per channel.Richtext returns a structured node tree; rendered via framework resolvers per channel.Rich text stored as blocks or markdown depending on editor configuration.
Automation on variant eventsFunctions plus App SDK run serverless steps on content events to trigger draft translation, terminology checks, or reviewer routing.App Framework and webhooks trigger external translation and enrichment workflows.Webhooks and the management API drive external automation and translation pipelines.Lifecycle hooks and webhooks let you script automation in the self-hosted server.
Type-safe locale fieldsTypeGen generates TypeScript types from the schema, so frontends consuming locale fields are typed rather than guessing shapes.TypeScript types available via community codegen against the GraphQL schema.TypeScript support via generated types and community tooling.Types generated from the schema for the self-hosted API and plugins.

Ready to try Sanity?

See how Sanity can transform your enterprise content operations.