[Guide] Build a Multi-language Site (Scope as Locale) - implementation notes
The Multi-language Site guide frames locale as scope: instead of prop threading locale everywhere, you treat it as a scoped contract that route shells can read. Then you store derived labels so UI stays consistent across navigation and panels. I'm curious how this plays out when you have lots of routes and lots of content motion.
How did you model locale scope (single key vs composite scope object) so precedence was predictable? Did you store derived labels per route/surface, and how did you keep derived labels correct across locale switches? What did you render/log as evidence so you can debug "wrong language" from a screenshot? How did you handle URL strategy (locale in URL vs not) while keeping routing calm?
Comments (18)
Back to latestScope as locale clicked once we treated it like any other contract: explicit value, explicit source, explicit precedence.
If locale can come from route, user toggle, or browser default, you need to be able to explain which one won.
We logged locale resolution and rendered evidence for it. That alone fixed most "wrong language" bugs:
txt
[tips] scope locale=fr-FR source=userToggle reason=explicitChoice
[tips] scopeResolve winner=userToggle loser=browserDefault reason=precedence
[tips] derive=labels reason=scope:localeChange locale=fr-FR labelRev=12
If you can't see the source, you can't debug. Evidence needs the source.
Counterpoint: storing derived labels can become a maintenance burden if labels depend on too many inputs.
We stored only the render-ready strings needed for chrome and navigation; content strings stayed closer to content modules.
That's a good boundary. Derived labels are most useful when they stabilize shared UI (nav, titles, buttons).
If you try to derive the whole world centrally, you just built a translation framework in your app.
We kept locale out of the URL by default and relied on scope + toggles, but we still rendered evidence so users could confirm state.
For share links, we optionally include locale in URL only when the user wants it (explicit action).
Long-form: i18n failures are trust failures. If users see the wrong language once, they assume the app is unreliable. That's why scope-as-locale is appealing: it makes locale a first-class state surface with a source and precedence, not a prop that disappears in the tree.
The key is making locale changes narratable. A good system can answer: - what locale is active, - where it came from, - which parts of the UI have been derived for it (label revision).
If you can answer those, you can debug most i18n bugs without reproducing them locally.
We also treated route shells as the place that *renders* locale evidence: data-locale, data-locale-source, data-label-rev.
It sounds like overkill, but it's exactly what made screenshots useful.
Counterpoint: locale in URL is sometimes the right product decision (marketing sites, SEO, shareability).
If you do it, central parse/serialize is mandatory and you should log normalization decisions. Otherwise locale changes feel random.
We made locale switching idempotent and safe to replay on navigation.
If the app re-applies scope on route enter, it should not flicker. We logged replays as a normal story line in dev.
Long-form counterpoint: scope can become a dumping ground, and locale is a tempting thing to dump in it.
We enforced a rule: scope keys must be enumerated and typed. If someone wants a new scope key, they have to define its precedence and evidence.
Docs tie-in: scope + routing posture are both part of the story here.
Related: Routing and Navigation and API Reference.
We stored derived nav labels separately from content labels.
It reduced churn because nav changes are frequent (route motion), but content labels are more stable.
A useful evidence key for support: data-locale-reason (why did locale change).
It prevents the classic bug report where locale "randomly" switched but it was actually a precedence rule that fired.
Counterpoint: scope-as-locale can hide the fact that your content strategy isn't localized (missing translations).
We rendered a lane for missing labels and logged missing keys so the UI didn't silently fall back without explanation.
We also versioned label bundles and logged bundle changes. That helped during deployments when a translation file changed shape.
If label shape changes, you want the UI to show which bundle version it's using.
Long-form: the reason scope-as-locale is appealing is it aligns with how people reason about apps.
Users don't think about components; they think about the app being "in French". That is global posture, and global posture belongs in a global surface, not threaded props.
We built route-flow tests that assert on locale evidence instead of trying to inspect internal translation state.
If locale is a contract, tests should read the contract from the UI.
A scope trick: treat locale as a scope object with both locale and direction (ltr/rtl).
If direction isn't part of the contract, you'll get layout bugs that look unrelated to locale.
If you implement this, start by deciding precedence and rendering the locale source as evidence.
Once that exists, most locale bugs stop being mysteries.