Skip to main content
Urbicon UI

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>
PropType
localeLocale
fallbackLocaleLocale
onLocaleChange(locale: Locale) => void
childrenSnippet

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>
MemberSignature
localeLocale (readonly)
availableLocalesLocale[] (readonly)
isLoadingboolean (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).