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
Provider & SSR
<I18nProvider>, the useI18n() API, locale switching &
persistence, resolveLocale, error handling, and coexisting with an app-level
i18n.Package Integration
createPackageI18n, namespaced keys, the use<Package>I18n hook pattern, the <T> component, and registering your own package.Type Safety
en bundle, eager vs. lazy parity, the DeepKeys utilities, and CI parity validation.Formatting & Plurals
Intl.PluralRules and locale-aware number, date, and relative-time
formatting.Lazy Loading
Locale Routing
/de/…) and drive it from the <LocaleSwitcher> — a SvelteKit reroute hook plus the
provider's onLocaleChange, with hreflang.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.
| Locale | Status |
|---|---|
| en | Ships data (base) |
| de | Ships data |
| fr | Declared |
| es | Declared |
| it | Declared |
| nl | Declared |