Lazy Loading
By default a package registers all its locale bundles eagerly. Opt into code-splitting to keep non-base languages out of the initial bundle until they're activated.
When to use it
Eager registration ships every locale's strings in the initial bundle. That's simplest and
right for a handful of languages (en/de). Past that, the inactive
locales are dead weight on first paint — register them as dynamic-import loaders so only the
active language is in the initial bundle, and the rest load on activation.
Registering loaders
Pass options.loaders to createPackageI18n. The bundle in translations (typically en) stays eager as the base/fallback; each
listed locale becomes a dynamic import that Vite/Rollup splits into its own chunk.
export const blocksI18n = createPackageI18n(
'blocks',
{ en }, // eager base — stays in the initial bundle, is the fallback
{
loaders: {
de: () => import('../translations/de').then((m) => m.default),
fr: () => import('../translations/fr').then((m) => m.default)
}
}
);Compile-time key parity is not checked for lazy locales — the chunk isn't
visible to the type-checker. Guard parity at runtime with validatePackageTranslations in a test.
Load lifecycle
The provider drives loading automatically:
- On mount it loads the active locale and the fallback. (Effects don't run during SSR, so a lazy non-base initial locale renders the fallback on the server and the first client paint, then re-resolves reactively once its chunk lands.)
- For SSR, register the bundle eagerly once at
server start so the first server render already resolves that locale instead of flashing the
fallback:
packageI18n.registerLocale(locale, bundle)(blocks re-exports it asregisterBlocksLocale) with the per-locale subpath import (@urbicon-ui/blocks/i18n/de). Additive — it keeps the eager base — and safe on the module-global registry (static, request-identical data). - On switch,
setLocaletriggers the target's loader (not awaited) and flips the locale; the$derivedreads re-resolve when the chunk arrives. - If a load fails and no data exists for that
locale,
load-failed-no-fallbackis reported to the error sink — the loud signal that "the language you switched to can't be rendered."
useI18n().isLoading is true while a lazy load is in flight — wire it to
a spinner if a switch is perceptible:
<script>
import { useI18n } from '@urbicon-ui/i18n';
const i18n = useI18n();
</script>
{#if i18n.isLoading}
<Spinner aria-label="Loading translations…" />
{/if}Trade-offs
Eager (default)
- Simplest — no extra config.
- Compile-time key parity across all locales.
- No loading state, no flash of fallback.
- Best for a few locales (
en/de).
Lazy (opt-in)
- Smaller initial bundle — inactive locales excluded.
- Parity is a runtime/CI check, not compile-time.
- A lazy non-base initial locale flashes the fallback first (unless eager-registered for SSR).
- Worth it past a handful of locales.