Skip to main content
Urbicon UI

Localization

SSR-correct, package-scoped, type-safe internationalization for Svelte 5. A request-scoped locale lives in context (never a module-global singleton), reactive translations come from runes, and every Urbicon package plugs into one shared registry — all with zero runtime dependencies.

Overview

@urbicon-ui/i18n is the localization layer the design system runs on: <Pagination>, <Menu>, the data table, and the auth pages all resolve their text through it. You can also use it for your own app strings — it is a complete i18n solution, not just an internal helper.

It exists because the system is zero-dependency by design, and the established libraries each miss a requirement: svelte-i18n predates Svelte 5 runes, i18next ships a large generic runtime, and Paraglide compiles per app (so it can't ship as a reusable component-library locale source). This package gives exactly four things:

Architecture

The package splits state by lifetime. Static translation data is module-global — it is request-identical, so sharing it is safe. The mutable active locale is request-scoped, held in a Svelte context created by the provider. That split is what makes SSR correct: two requests rendering de and en at the same time each read their own locale, never a shared global.

Read-tolerant, write-strict

  • Reading without a provider → the constant base locale (en). A provider-less <Button> renders its ARIA strings out of the box, identical on server and client (no hydration mismatch). Zero-config.
  • Writing (setLocale) without a provider → throws. There is no request-scoped state to mutate — you forgot the provider. Loud by design.

Read tolerant, write strict: a component library must render before any consumer wires up i18n, but a locale switch with nowhere to store the choice is a real bug, not a default.

Quick Start

Two steps: mount a provider fed by a server-resolved locale, then read through a hook.

1. Mount the provider

<!-- +layout.svelte — mount ONE provider at the app root -->
<script>
  import { I18nProvider } from '@urbicon-ui/i18n';
  let { data, children } = $props();
</script>

<I18nProvider locale={data.locale}>
  {@render children()}
</I18nProvider>

2. Resolve the locale per request

resolveLocale reads the persisted cookie, then Accept-Language, then a default — so SSR and the first client render agree.

// +layout.server.ts — resolve the locale per request (cookie + Accept-Language)
import { resolveLocale } from '@urbicon-ui/i18n';

export const load = ({ request }) => ({ locale: resolveLocale(request) });

3. Read translations

<!-- Read translations through a hook — call during component init -->
<script>
  import { useI18n } from '@urbicon-ui/i18n';
  const i18n = useI18n();
</script>

<p>{i18n.t('greeting', { name: 'Ada' })}</p>
<p>{i18n.formatNumber(1234.5)}</p>

Switching language at runtime needs no reload — the built-in LocaleSwitcher calls setLocale for you. See Provider & SSR.

Guides

API Surface

Everything is a named export from the package root.

import {
  // Provider + hooks + server helper
  I18nProvider,      // <I18nProvider locale fallbackLocale? onLocaleChange?>
  provideI18n,       // provide from a component's own script (root layouts)
  useI18n,           // { locale, setLocale, availableLocales, isLoading, t, plural, exists, formatNumber, … }
  configureI18n,     // app-global error sink (onError)
  resolveLocale,     // server-side initial-locale resolution
  resolveDateLocale, // 'explicit prop → provider → base locale' for an Intl tag
  T,                 // <T key params? fallback? package? />
  BASE_LOCALE,       // 'en'
  SUPPORTED_LOCALES,
  isLocaleSupported,

  // Package integration
  createPackageI18n, // (name, { en }, { loaders }?) -> { useTranslate, t, exists, getLocales, … }
  validatePackageTranslations,

  // Deep-key utilities
  getDeepValue, hasDeepKey, collectDeepKeys
} from '@urbicon-ui/i18n';

Supported Locales

en and de ship translation data for every Urbicon package. fr, es, it, and nl are declared target locales (in the Locale union and SUPPORTED_LOCALES) — register your own bundles for them via createPackageI18n.

LocaleStatus
enShips data (base)
deShips data
frDeclared
esDeclared
itDeclared
nlDeclared