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.
<!-- +layout.svelte -->
<script>
import { I18nProvider } from '@urbicon-ui/i18n';
let { data, children } = $props();
</script>
<I18nProvider locale={data.locale} fallbackLocale="en">
{@render children()}
</I18nProvider>| 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.
<script>
import { useI18n } from '@urbicon-ui/i18n';
// Capture during component init; reads are reactive at call time.
const i18n = useI18n();
</script>
<p>{i18n.t('greeting', { name: 'Ada' })}</p>
<button onclick={() => i18n.setLocale('de')}>Deutsch</button>| 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:
<script>
import { useI18n } from '@urbicon-ui/i18n';
const i18n = useI18n();
</script>
<!-- setLocale mutates the request-scoped state and re-renders in place — no reload -->
<button onclick={() => i18n.setLocale('de')}>Deutsch</button>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:
<!-- Persist the choice so the NEXT SSR request renders it -->
<I18nProvider
locale={data.locale}
onLocaleChange={(l) =>
(document.cookie = `urbicon-locale=${l}; path=/; max-age=31536000; samesite=lax`)}
>
{@render children()}
</I18nProvider>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.
<!-- +layout.svelte — the SAME component both provides i18n and renders chrome -->
<script>
import { provideI18n, useI18n } from '@urbicon-ui/i18n';
let { data, children } = $props();
// A child <I18nProvider> can't serve the parent that mounts it (context only
// flows downward). Call provideI18n in this component's own script instead.
provideI18n(() => data.locale); // reactive getter → controlled by the load fn
const i18n = useI18n();
</script>
<header>{i18n.t('chrome.appTitle')}</header>
{@render children()}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).
// +layout.server.ts
import { resolveLocale } from '@urbicon-ui/i18n';
export const load = ({ request }) => ({
// Cookie → Accept-Language → default. Framework-agnostic: pass a Request
// or a { cookie, acceptLanguage } object.
locale: resolveLocale(request, {
supportedLocales: ['en', 'de'], // defaults to locales the registry has data for
defaultLocale: 'en',
cookieName: 'urbicon-locale'
})
});Fully prerendered (static) sites have no per-request server, so resolve on the client after mount instead:
// Fully prerendered (static) sites have no per-request server.
// Resolve on the client after mount instead — the provider's base-locale-first
// render keeps hydration stable, then setLocale swaps in the stored choice.
<script>
import { I18nProvider, useI18n } from '@urbicon-ui/i18n';
// ...read a cookie / localStorage on mount, then i18n.setLocale(stored)
</script>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).
// app entry / root setup — call ONCE at startup, never per-request
import { configureI18n } from '@urbicon-ui/i18n';
configureI18n({
onError: (e) => {
// e.type: 'load-failed' | 'load-failed-no-fallback' | 'unsupported-locale'
reportToSentry(e);
}
});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:
<!-- +layout.svelte — make Urbicon's provider FOLLOW the app's locale -->
<script>
import { I18nProvider } from '@urbicon-ui/i18n';
import { getLocale } from '$lib/paraglide/runtime'; // app's reactive locale
let { children } = $props();
</script>
<!-- getLocale() is reactive → the provider re-syncs when the app switches language -->
<I18nProvider locale={getLocale()}>
{@render children()}
</I18nProvider>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).