Provider & SSR
Mount one provider, read through useI18n(), and resolve the locale per request so server and client agree.
I18nProvider
One provider at the app root holds the single request-scoped locale state. Feed it a
server-resolved locale so the first client render matches the server.
| Prop | Type |
|---|---|
| locale | Locale |
| fallbackLocale | Locale |
| onLocaleChange | (locale: Locale) => void |
| children | Snippet |
useI18n()
The general hook for locale control and locale-aware formatting. Call it during component init
and capture the result; every member reads the context locale at call time, so wrapping a read
in markup or $derived re-renders on a locale switch.
| Member | Signature |
|---|---|
| locale | Locale (readonly) |
| availableLocales | Locale[] (readonly) |
| isLoading | boolean (readonly) |
| setLocale | (locale) => boolean |
| t | (key, params?, options?) => string |
| plural | (key, params, options?) => string |
| exists | (key, packageName?) => boolean |
| formatNumber | (value, options?) => string |
| formatDate | (date, options?) => string |
| formatRelativeTime | (value, unit) => string |
| formatTimeAgo | (date) => string |
Read-tolerant, write-strict
The contract that lets components ship before a consumer wires up i18n, while still catching a real bug:
- Reading (
locale,t, formatters) without a provider resolves against the constant base locale (en) — SSR-safe, identical on server and client. - Writing (
setLocale) without a provider throws. There is no request-scoped state to change — the error names the fix (mount<I18nProvider>).
setLocale returns false (and reports unsupported-locale) for a locale outside SUPPORTED_LOCALES, without
switching; otherwise it switches and returns true.
Switching & Persistence
setLocale mutates the request-scoped state and re-renders reactively in place —
no reload. The built-in LocaleSwitcher does this for you; programmatically:
An in-place switch lasts only for the current page session. To make the choice survive the
next SSR request, persist it where resolveLocale reads — the provider's onLocaleChange is the hook:
Root-layout chrome that itself is translated
A child <I18nProvider> can't serve the parent that mounts it — context only
flows downward. When the same root component both provides i18n and renders
translated chrome (header/footer), call provideI18n in its own script. Pass a
reactive getter (() => data.locale) to keep it controlled: a load change flows
in, while an in-place setLocale switch is never clobbered.
SSR — resolving the initial locale
resolveLocale derives the request's locale server-side from the persisted cookie,
then Accept-Language, then a default. It is framework-agnostic — pass a Request or a { cookie, acceptLanguage } object. Feed the result
to the provider so SSR and the first client render agree (no hydration mismatch, no navigator.language guess).
Fully prerendered (static) sites have no per-request server, so resolve on the client after mount instead:
Error Handling
Loader failures and unsupported-locale switches default to console.warn. Route
them to telemetry with an app-global handler — set once at startup (it lives on
the process-wide registry; a per-request assignment would race under concurrent SSR).
Coexisting with an app-level i18n (e.g. Paraglide)
If your app already uses Paraglide (or any other i18n) for its own strings, don't run two locale states — make Urbicon's provider follow the app's locale. Pass the app-i18n locale into the provider as a controlled (reactive) value:
When the app switches language, getLocale() updates, the provider's
controlled-sync pushes it into Urbicon's state, and every Urbicon component re-renders — one
switch, both layers. If you also expose an Urbicon <LocaleSwitcher>, route
its onLocaleChange back into the app's setLocale so the two never
diverge. Map locale codes if they differ (e.g. Paraglide en-US → Urbicon en).