A missing translation is not an empty field
Empty-field fallbacks keep a single-language page from breaking when an optional image is cleared. Multi-locale work is a different problem. A French route with no French document is not "empty copy." It is an unpublished translation. How you handle that absence — coalesce to English, hold the route, or show a partial page with a language banner — is a product contract, and it belongs in the query and the publish checklist, not in a one-off ternary in a component.
Absence is not emptiness
On a monolingual site we already treat optional fields as optional. The hero image can be missing; the page still renders with a gradient. That rule does not translate cleanly to locales. If the Spanish document does not exist, the Spanish URL should not invent Spanish by reading the English title into a Spanish shell. And it should not render a page whose every field is blank because the query returned null and the UI treated null like an empty string.
Document-level localization makes the distinction structural. Each language is its own document, linked through a shared translation set, with its own publishedAt. The English page can ship while French is still a draft. Field-level arrays blur that: one publish pushes every language at once, and a missing French string looks like any other empty field.
For marketing sites and clinical content we tend to prefer document-level translations — Provale-style pages where copy is reviewed before it goes live. Field-level still makes sense for short shared strings (nav labels, button verbs) that must stay in lockstep. Mixing both in one project is fine; pretending they are the same failure mode is not.
coalesce is a product choice
GROQ coalesce(localizedTitle, defaultTitle) is the usual move. It is also how you accidentally publish English on /fr/product without anyone noticing. The query "succeeds." The page looks finished. The html lang says fr. The reader is halfway down before the language switches in their head.
We only coalesce when the brief says mixed-language is acceptable — usually for markets where the audience is bilingual and a late translation is worse than English. Even then the page should admit what happened: a small "Showing English — French in progress" marker, or at least lang attributes that match the string that actually rendered, not the route segment.
When the brief says each locale must be complete before it is public — regulated medical copy, legal pages, anything a clinician will quote — coalesce is the wrong tool. The route should 404 or redirect to the default locale until the translation document is published. That is harsher in staging demos and correct in production.
Put the policy in the query
If every page component invents its own fallback, you get three policies on one site. Centralize it. A single helper — loadPage(locale, slug) — either returns the localized document, returns the default with a fallback flag, or returns null so the App Router can notFound() or redirect.
For field-level arrays the same discipline applies at the field: coalesce(title[_key == $locale][0].value, title[_key == $default][0].value) is fine only when the caller also receives which key won. Without that, generateMetadata will happily emit an English title on a French canonical.
Scheduled publishing compounds this. A French document with publishedAt next Tuesday must not appear in the French sitemap on Monday, and the English fallback must not inherit the French schedule. Filter on the document you actually intend to show, not on the translation set as a whole.
Tell the browser what language it got
html lang, Open Graph locale, and hreflang only help if they describe the bytes on the wire. After a fallback, the rendered language and the URL language have diverged. Either update the signals to the language you served, or do not serve the page on that URL.
Search consoles are unforgiving here. A /es/ URL that always returns English gets treated as a soft duplicate of /en/. That is worse than a temporary 404 while the translation is unfinished, because the bad association sticks after the Spanish document finally publishes.
Editor tooling should make the gap visible too. In Studio, a translation set with English published and Spanish missing should read as "unpublished locale," not as a green document with blank fields. The Presentation preview for /es/... should fail closed the same way the public route does — otherwise editors approve a page the site will never show.
What we check before a second locale ships
Publish English. Confirm /es/slug 404s or redirects per policy — not a blank layout, not silent English. Publish Spanish. Confirm the Spanish document alone controls the Spanish route, and that clearing one Spanish field uses the empty-field fallback from the Spanish document, not a cross-locale coalesce you did not ask for.
Crawl the hreflang set and the sitemap. Every alternate must resolve to content in that language, or must be omitted until it can. Spot-check one Portable Text field and one string field; rich text is where field-level localization usually collapses, which is why we keep long-form on document-level translations.
Empty fields and missing translations are both absences. Only one of them should be filled in by the frontend. The other waits for an editor.
Questions
- Should every missing translation fall back to English?
- Only when the product brief accepts mixed-language pages — and even then mark what was served. For reviewed or regulated copy, hold or redirect the locale route until that translation document is published.
- Document-level or field-level localization?
- Document-level for pages and Portable Text that publish on their own schedule. Field-level for short shared strings that must stay in lockstep. Do not model a long-form page as an array of languages inside one document if editors need independent publish.
- How is this different from empty-field fallbacks?
- Empty-field fallbacks keep one locale from breaking when an optional image or aside is cleared. Missing-translation handling decides whether a locale URL may exist at all, and whether another language may stand in — a publish and SEO decision, not a layout one.
Sources
- Sanity — Localization
- Sanity — Document internationalization plugin
- Sanity — Internationalized array plugin
- GROQ coalesce() — Common pattern for locale field fallback — pair with an explicit policy.