Skip to main content
Urbicon UI

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 as registerBlocksLocale) 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, setLocale triggers the target's loader (not awaited) and flips the locale; the $derived reads re-resolve when the chunk arrives.
  • If a load fails and no data exists for that locale, load-failed-no-fallback is 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.