Overview

Voltro's i18n layer (@voltro/i18n) — an opinionated wrap over react-intl, auto-wired from a single app.config.ts field, with cookie + Accept-Language locale resolution.

Voltro ships internationalization in @voltro/i18n — an opinionated, thin wrap over react-intl plus framework auto-wiring. A web app gets a working <I18nProvider> from a single field in app.config.ts. You never write the provider, and you never import @voltro/i18n in your layout.

The public surface is intentionally small — <I18nProvider>, <T>, useT, useTFn, useLocale, useMessages, defineCatalog, defineLocale, pickCatalog. Power users who need an API the wrap doesn't expose (custom formatters, rich-text with React-element values, IntlProvider's timeZone / formats props) import { … } from 'react-intl' directly. The wrap is opt-in, not lock-in — the library is in your node_modules, the wrap is optional.

When to enable

Set locales in app.config.ts the moment your app ships more than one user-facing language, or expects to soon. The infrastructure has zero cost when locales is unset — no provider is generated, no bundle overhead. Enabling early avoids a painful retro-fit when the first translation request lands.

Don't enable just because the app could theoretically be translated. The framework's empty-i18n state is fine for English-only apps, and adding the wiring later is a one-config-field change.

Setup — auto-wiring

// apps/<project>/<app>/app.config.ts
export default {
  type:  'web' as const,
  name:  'myApp',
  port:  5191,
  // Codes the app SHIPS catalogs for. Every code listed here MUST
  // have a corresponding `src/locales/<code>.ts` file or boot fails.
  locales: ['en', 'de'] as const,
  // Fallback locale when no cookie / Accept-Language matches a
  // supported one. MUST be in `locales`. Pick your source-of-truth
  // language — typically English.
  defaultLocale: 'en' as const,
}

The framework's generated .framework/app.tsx wraps the Router in <I18nProvider> automatically. You don't write the provider yourself; you don't import @voltro/i18n in your layout. Each code in locales must have a matching src/locales/<code>.ts catalog file — a missing file fails boot, not silently at runtime.

Locale resolution order

Server-side, the active locale is determined by, in priority order:

  1. voltro:lang cookie — the user's explicit choice (written by @voltro/ui-shadcn's ProfileMenu).
  2. Accept-Language header — the browser/OS preference, q-weighted and sorted per RFC 4647.
  3. defaultLocale — last-resort fallback.

The resolved locale is guaranteed to be one of the codes in locales. Any unsupported value (a cookie pointing at a code you no longer ship, a browser asking for xx-YY) falls through to the next signal. RFC 4647 lookup strips subtags one segment at a time — de-CH-1996de-CHde — so a de catalog serves a de-CH browser.

The client side mirrors #1 (cookie) and #3 (default) for hydration safety. Accept-Language is server-only because navigator.languages can diverge from what the server saw, which would cause a hydration mismatch.

See Catalogs for the type-safe catalog convention and the component hooks, Plurals & formatting for CLDR plural selection and the Intl-backed date / number / relative-time hooks, and URL strategies for cookie-only vs URL-prefix routing.