# @wrnexus/i18n > Per-request translations plus locale-aware number, date, and currency formatting for WrNexus apps. Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework. ## Overview `@wrnexus/i18n` loads locale files from `app/locales/.json`, resolves the active language for each request (cookie → `Accept-Language` → default), and builds a `t(key, params)` translator used both in server code and in `.wrn` views. It also ships Intl-based formatting helpers and a tiny client runtime that wires up a language switcher. Translation lookup, language resolution, and HTML marker rewriting run server-side; only the small `I18N_RUNTIME` snippet runs in the browser. ## Installation ```bash bun add @wrnexus/i18n ``` > Private package — the machine must be authenticated to the `wrnexus` npm org > (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported). ## API ### Loading & resolving | Export | Signature | Description | | ------------- | -------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `loadLocales` | `(dir: string) => Record` | Reads every `.json` in `dir` into a `{ lang: messages }` map. Missing dir → `{}`; a bad file is warned and skipped. | | `resolveI18n` | `(messages: Record, config?: I18nConfig) => ResolvedI18n` | Merges loaded messages + config into a resolved bundle (default lang, supported langs, messages). | | `resolveLang` | `(i18n: ResolvedI18n, cookieValue: string \| undefined, acceptLanguage: string \| null) => string` | Picks the active language: matching cookie → best `Accept-Language` tag (falls back to base tag, e.g. `en-US` → `en`) → `i18n.default`. | | `makeT` | `(i18n: ResolvedI18n, lang: string) => TFunction` | Builds a translator resolving current language → default → the key itself, with `{param}` interpolation. | ### Types & constants | Export | Kind | Notes | | -------------- | ----------- | ------------------------------------------------------------------------------ | | `Messages` | `type` | `Record` — a locale's messages (supports nested/dotted keys). | | `I18nConfig` | `interface` | `{ default?: string; locales?: string[] }`. | | `ResolvedI18n` | `interface` | `{ default: string; langs: string[]; messages: Record }`. | | `LANG_COOKIE` | `const` | `"wire-lang"` — the cookie the language is read from / written to. | | `I18N_JS_HREF` | `const` | `"/__wrnexus/i18n.js"` — URL the client runtime is served at. | ### HTML & client runtime | Export | Signature | Description | | ---------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `translateHtml` | `(html: string, t: TFunction) => string` | Rewrites markers in rendered HTML: `t:="key"` → `=""` (attribute-escaped) and `` → element text becomes the translation (HTML-escaped). No-op unless a marker is present. | | `renderI18nData` | `(i18n: ResolvedI18n, lang: string) => string` | JS snippet setting `window.__wireI18n = { lang, langs, default }` for the client switcher. | | `I18N_RUNTIME` | `const string` | Browser IIFE that binds `[data-wire-lang-set="es"]` clicks and `select[data-wire-lang]` changes to set the `wire-lang` cookie and reload. Exposes `window.__wireLang.set(lang)`. | ### Formatting helpers (re-exported from `./format.ts`) | Export | Signature | Example | | -------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------------------------- | | `formatNumber` | `(value: number, lang: string, options?: Intl.NumberFormatOptions) => string` | `1234.5 → "1,234.5"` | | `formatCurrency` | `(value: number, currency: string, lang: string) => string` | `9.99, "USD" → "$9.99"` | | `formatDate` | `(value: Date \| number \| string, lang: string, options?: Intl.DateTimeFormatOptions) => string` | defaults to `{ dateStyle: "medium" }` | | `formatRelativeTime` | `(value: number, unit: Intl.RelativeTimeFormatUnit, lang: string) => string` | `-3, "day" → "3 days ago"` (`numeric: "auto"`) | | `plural` | `(count: number, forms: Partial>, lang: string) => string` | picks CLDR form; `#` is replaced by `count` | ## Usage ### Server: load, resolve, translate ```ts import { loadLocales, resolveI18n, resolveLang, makeT, translateHtml, LANG_COOKIE, } from "@wrnexus/i18n"; // app/locales/en.json, app/locales/es.json const messages = loadLocales("app/locales"); const i18n = resolveI18n(messages, { default: "en", locales: ["en", "es"] }); // Per request: const lang = resolveLang(i18n, req.cookies?.[LANG_COOKIE], req.headers.get("accept-language")); const t = makeT(i18n, lang); t("nav.home"); // dotted key → "Home" t("greeting", { name: "Ada" }); // "Hello, {name}" → "Hello, Ada" // After rendering a .wrn view, resolve translation markers in the HTML: const finalHtml = translateHtml(renderedHtml, t); ``` `app/locales/en.json`: ```json { "nav": { "home": "Home" }, "greeting": "Hello, {name}" } ``` ### Views: translation markers ```html

Home

``` `translateHtml` replaces the element text for `data-t` and the attribute value for any `t:` (e.g. `t:placeholder`, `t:aria-label`). ### Client: language switcher ```ts import { renderI18nData, I18N_RUNTIME, I18N_JS_HREF } from "@wrnexus/i18n"; // In the document : const head = ` `; // Serve I18N_RUNTIME at I18N_JS_HREF; then in markup: // // ``` ### Formatting ```ts import { formatNumber, formatCurrency, formatDate, formatRelativeTime, plural, } from "@wrnexus/i18n"; formatNumber(1234.5, lang); // "1,234.5" formatCurrency(9.99, "USD", lang); // "$9.99" formatDate(Date.now(), lang); // "Jul 4, 2026" formatRelativeTime(-3, "day", lang); // "3 days ago" plural(2, { one: "# item", other: "# items" }, lang); // "2 items" ``` ## Configuration `resolveI18n` accepts an `I18nConfig`: - `default` — fallback language; used when nothing else matches. Ignored if it has no loaded messages, in which case the first supported language is used. - `locales` — explicit supported-language list; defaults to the loaded locale names. Language resolution order at request time (`resolveLang`): a supported `wire-lang` cookie value → the first matching `Accept-Language` tag (or its base subtag) → the resolved default. ## Requirements / Notes - **Bun-only.** Locale loading uses `node:fs` (`existsSync`, `readdirSync`, `readFileSync`) and `node:path`; formatting relies on the platform `Intl` APIs. - Works with [`@wrnexus/core`](../core) — `TFunction` (the `t(key, params)` type) comes from core, and the resolved translator is exposed as `ctx.t` / `ctx.lang` in request handling. - Nested message objects are supported: keys are looked up whole first, then split on `.` to walk the object tree.