Locale Strategies In The Example Matrix
The example matrix proves four authoritative locale strategies — cookie, route, subdomain, and tld — plus one derived host mapping behavior on top of the route strategy.
Cookie Strategy
Cookie examples follow this rule set:
- if no locale cookie is present, the server picks the best supported locale
from
Accept-Language - once the locale cookie exists, the cookie is authoritative
- there is no recurring mismatch banner once the cookie strategy has an explicit cookie value
- locale switches update the cookie and the next request renders with that locale
This is the verified path for apps that do not encode locale in the URL.
Route Strategy
Route examples use a mandatory locale prefix:
/:locale/...
Route examples follow this rule set:
- the locale in the URL is authoritative
Accept-Languageis advisory, not authoritative- if the browser prefers another supported locale, the page shows a small info bar with a switch CTA
- unsupported locale values fall back to the default locale in app-specific framework code
This is the verified path for SEO-oriented or shareable locale URLs.
Domain And Host Mapping
Domain-based locale selection is modeled as an extension of the route strategy.
The examples treat host mapping as:
- a host-to-locale map such as
de.lvh.me->de - an additional validation signal on top of
/:locale/... - a source of mismatch hints, not an automatic redirect
When host mapping disagrees with the rendered locale, the example shows the same
info bar used for Accept-Language mismatches, but the CTA points to the
canonical host plus path for the recommended locale.
Subdomain Strategy
Subdomain examples make the leftmost DNS label authoritative for the locale:
de.<app>-subdomain.examples.palamedes.dev->de
Subdomain examples follow this rule set:
- the leftmost host label is authoritative, resolved by
resolve({ strategy: "subdomain", requestHost }); there is no/:locale/...path prefix Accept-Languageis advisory, not authoritative: if the browser prefers another supported locale, the page shows the same info bar with a switch CTA- an unknown or missing leftmost label falls back to the best
Accept-Languagematch, then the default locale - switching locale loads a different host (the control swaps the leftmost label
via
canonicalUrl), so it is always a full document load — the live switching mechanism below does not apply
This is distinct from the host mapping above. Host mapping is a validation
signal on top of the route strategy, matched against fully configured per-locale
hosts. The subdomain strategy is a third authoritative resolution source
alongside cookie and route, and needs no per-locale host map: the same
hosts: { mode: "subdomain" } configuration works unchanged across lvh.me
locally and *.examples.palamedes.dev in production, because the locale is read
from the label rather than compared to a fixed host.
TLD Strategy
TLD examples make the top-level domain (the rightmost DNS label) authoritative for the locale:
<app>.examples.palamedes-i18n.de->de<app>.examples.palamedes-i18n.fr->fr<app>.examples.palamedes-i18n.com->en(explicit override)
TLD examples follow this rule set:
- the tld is resolved by
resolve({ strategy: "tld", requestHost }), three-tiered:- a tld label that already is a supported locale is authoritative
automatically (
.de->de,.es->es,.fr->fr), because the country code equals the language code - a tld whose label is not a language code is authoritative when listed in the
tldmap. These demos map the generic.comtoen(hosts: { mode: "tld", tld: { com: "en" } }); real deployments might add entries such as{ at: "de", uk: "en" } - anything else is deliberately not authoritative and falls back to the best
Accept-Languagematch (region tags expand, sode-CHyieldsde), then the default locale
- a tld label that already is a supported locale is authoritative
automatically (
- there is no
/:locale/...path prefix; the locale lives in the domain - switching locale swaps the top-level domain (the control swaps the rightmost
label via
canonicalUrl), so it is always a full document load. The default localeenswitches todefaultTld(hosts: { mode: "tld", defaultTld: "com" }, soen->.com), which the.com->enoverride above also serves authoritatively
Why real TLDs need an explicit policy
Unlike the subdomain label — which the operator fully controls, so the label can
simply be the locale — real top-level domains carry meaning the app does not
own. .de, .es, and .fr happen to match a language code and resolve
automatically. .com is generic and matches no language, so these demos map it
explicitly to en ({ com: "en" }), the natural default for a generic TLD.
.ch (Switzerland) has four official languages, so it cannot be authoritative
for one of them; left unmapped, it follows the browser's regional preference
(de-CH -> de) and then the default locale. Country codes whose language is
unambiguous but differs from the code (.at -> German, .uk -> English) are
opt-in through the same tld map. This is why the tld strategy resolves through
an explicit, three-tiered policy rather than the subdomain strategy's pure
label-is-locale rule.
Switching Mechanism: Reload vs. Live
Locale strategy (cookie vs. route) is independent from the switching mechanism — how a new locale reaches an already-running client. There are two mechanisms, and the choice matters more for robustness than the strategy does.
Reload (recommended default)
A switch triggers a full document load in the new locale: writing the cookie and
calling window.location.assign(...), or hard-loading the new locale URL.
- the whole app re-renders from a single, consistent server locale
- there is no client-side reactivity contract to uphold
- module-level caches, memoized
Intlformatters, and already-fetched data are discarded and rebuilt, so mixed-locale states cannot occur - the only cost is the reload UX: a brief flash, scroll reset, and refetch
The cookie examples use this mechanism.
Live (opt-in)
A switch swaps the active client i18n in place and re-renders reactively, with no document load. The route examples use this mechanism — client-side navigation via the router keeps the app mounted.
Live switching is a whole-app reactivity contract: every value derived from the locale must either live in the reactive graph or be keyed by locale. In practice the failure mode is rarely the translation components — it is the larger components with their own state or caches:
- memoized
Intl.NumberFormat/Intl.DateTimeFormatinstances not keyed by locale - data fetched server-side or cached client-side in the previous locale
- third-party components that snapshot strings on mount
Any of these silently goes stale and produces a mixed-locale UI. The library cannot enforce this discipline for you; it is an ongoing cost you accept per app.
Recommendation
Prefer reload. It is simpler, has no reactivity contract, and structurally rules out the mixed-locale class of bugs. Because locale changes are rare and deliberate, that robustness is almost always worth the small reload UX cost. Reach for live only when instant, state-preserving switches are a genuine product requirement and the team is prepared to key every locale-derived value.
Enabling live switching (Solid)
Reload needs nothing beyond a normal server render. Live switching in Solid has two opt-in pieces:
-
createClientLocaleEffect(localeAccessor, syncClientI18n)pushes locale changes into the client runtime -
point the macro transform at Solid's reactive runtime so
t/pluraloutput follows the switch:// app.config.ts palamedes({ runtimeModule: "@palamedes/solid/runtime" })
<Trans> / <Plural> / <Select> / <SelectOrdinal> track the active instance
on their own and follow a live switch even with the default runtimeModule. Only
the macro t / plural calls depend on the reactive runtimeModule — so wire
both, or use reload.
Why The Matrix Is Split Per Framework
Each framework family implements routing, request state, and server-side actions differently. The example matrix is therefore intentionally explicit:
- one cookie app per framework
- one route app per framework
This keeps the integration legible and gives Palamedes a real regression baseline for:
- tests
- docs
- benchmark-style verification artifacts
- future extraction of shared public helper APIs