page wrnexusi18n { seo { title = "@wrnexus/i18n" description = "Translation loading, locale resolution, and Intl formatting." } view {
W WRNexusJS
Browse documentation
Frontend · Preview

@wrnexus/i18n

Translation loading, locale resolution, and Intl formatting.

Private registry access required

This package is not available from the public npm registry. After WorkRoot approves access and supplies private registry instructions, install the release-aligned package:

bun add @wrnexus/i18n@0.2.45

Request preview access. Never put registry tokens in source control.

Per-request translations plus locale-aware number, date, and currency formatting for WRNexusJS apps.

Part of the WRNexusJS framework — an SSR-first, Bun-native full-stack web framework.

Overview

@wrnexus/i18n loads locale files from app/locales/<lang>.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.

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

ExportSignatureDescription
loadLocales(dir: string) => Record<string, Messages>Reads every <lang>.json in dir into a { lang: messages } map. Missing dir → {}; a bad file is warned and skipped.
resolveI18n(messages: Record<string, Messages>, config?: I18nConfig) => ResolvedI18nMerges 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-USen) → i18n.default.
makeT(i18n: ResolvedI18n, lang: string) => TFunctionBuilds a translator resolving current language → default → the key itself, with {param} interpolation.

Types & constants

ExportKindNotes
MessagestypeRecord<string, unknown> — a locale's messages (supports nested/dotted keys).
I18nConfiginterface{ default?: string; locales?: string[] }.
ResolvedI18ninterface{ default: string; langs: string[]; messages: Record<string, Messages> }.
LANG_COOKIEconst"wire-lang" — the cookie the language is read from / written to.
I18N_JS_HREFconst"/__wrnexus/i18n.js" — URL the client runtime is served at.

HTML & client runtime

ExportSignatureDescription
translateHtml(html: string, t: TFunction) => stringRewrites markers in rendered HTML: t:<attr>="key"<attr>="<translation>" (attribute-escaped) and <tag data-t="key">…</tag> → element text becomes the translation (HTML-escaped). No-op unless a marker is present.
renderI18nData(i18n: ResolvedI18n, lang: string) => stringJS snippet setting window.__wireI18n = { lang, langs, default } for the client switcher.
I18N_RUNTIMEconst stringBrowser 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)

ExportSignatureExample
formatNumber(value: number, lang: string, options?: Intl.NumberFormatOptions) => string1234.5 → "1,234.5"
formatCurrency(value: number, currency: string, lang: string) => string9.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<Record<Intl.LDMLPluralRule, string>>, lang: string) => stringpicks CLDR form; # is replaced by count

Usage

Server: load, resolve, translate

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:

{
  "nav": { "home": "Home" },
  "greeting": "Hello, {name}"
}

Views: translation markers

<h1 data-t="nav.home">Home</h1>
<input t:placeholder="search.placeholder" />

translateHtml replaces the element text for data-t and the attribute value for any t:<attr> (e.g. t:placeholder, t:aria-label).

Client: language switcher

import { renderI18nData, I18N_RUNTIME, I18N_JS_HREF } from "@wrnexus/i18n";

// In the document <head>:
const head = `
  <script>${renderI18nData(i18n, lang)}</script>
  <script src="${I18N_JS_HREF}"></script>
`;

// Serve I18N_RUNTIME at I18N_JS_HREF; then in markup:
// <button data-wire-lang-set="es">Español</button>
// <select data-wire-lang>…</select>

Formatting

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.

Complete TypeScript API

This declaration comes from the exact installed package and lists its exported functions, classes, interfaces, and types.

import { TFunction } from '@wrnexus/core';

/**
 * Locale-aware formatting helpers (Intl-based) + pluralization. Pair with the
 * request language (`ctx.lang`) so numbers, dates, and currencies render right
 * for each user.
 */
/** Format a number for a locale (e.g. 1234.5 → "1,234.5"). */
declare function formatNumber(value: number, lang: string, options?: Intl.NumberFormatOptions): string;
/** Format a currency amount (e.g. 9.99, "USD" → "$9.99"). */
declare function formatCurrency(value: number, currency: string, lang: string): string;
/** Format a date/timestamp for a locale. */
declare function formatDate(value: Date | number | string, lang: string, options?: Intl.DateTimeFormatOptions): string;
/** Relative time, e.g. -3 days → "3 days ago" (localized). */
declare function formatRelativeTime(value: number, unit: Intl.RelativeTimeFormatUnit, lang: string): string;
/**
 * Pick a plural form for `count` in `lang` using CLDR rules, e.g.
 * `plural(n, { one: "1 item", other: "# items" }, lang)` — "#" is replaced by n.
 */
declare function plural(count: number, forms: Partial<Record<Intl.LDMLPluralRule, string>>, lang: string): string;

/**
 * @wrnexus/i18n — translations for pages and API responses.
 *
 * Locales live in `app/locales/<lang>.json`. Per request the active language is
 * resolved from the `wire-lang` cookie, then Accept-Language, then the default.
 * `ctx.t(key, params)` translates on the server; in `.wrn` views `{t:key}` and
 * `t:attr="key"` markers are resolved by `translateHtml` before the HTML is sent.
 */

type Messages = Record<string, unknown>;
/** Load `<dir>/<lang>.json` files into a `{ lang: messages }` map. */
declare function loadLocales(dir: string): Record<string, Messages>;
interface I18nConfig {
    /** Default language, used as the fallback and when nothing else matches. */
    default?: string;
    /** Explicit set of supported languages (defaults to the loaded locale names). */
    locales?: string[];
}
interface ResolvedI18n {
    default: string;
    langs: string[];
    messages: Record<string, Messages>;
}
declare const LANG_COOKIE = "wire-lang";
declare const I18N_JS_HREF = "/__wrnexus/i18n.js";
/** Merge loaded locale messages + config into a resolved i18n bundle. */
declare function resolveI18n(messages: Record<string, Messages>, config?: I18nConfig): ResolvedI18n;
/** Build a `t()` for a language: current → default → the key itself. */
declare function makeT(i18n: ResolvedI18n, lang: string): TFunction;
/** Resolve the active language from a cookie, Accept-Language, then default. */
declare function resolveLang(i18n: ResolvedI18n, cookieValue: string | undefined, acceptLanguage: string | null): string;
/**
 * Resolve translation markers in rendered HTML:
 *   t:<attr>="key"        → <attr>="<translation>"   (e.g. t:placeholder, t:aria-label)
 *   <tag data-t="key">…</tag> → element text becomes the translation
 * Only runs when the HTML actually contains a marker.
 */
declare function translateHtml(html: string, t: TFunction): string;
/** `window.__wireI18n = { lang, langs }` for the client language switcher. */
declare function renderI18nData(i18n: ResolvedI18n, lang: string): string;
/**
 * Client runtime: binds `[data-wire-lang-set="es"]` elements to set the
 * `wire-lang` cookie and reload, so the server re-renders in the new language.
 */
declare const I18N_RUNTIME: string;

export { I18N_JS_HREF, I18N_RUNTIME, type I18nConfig, LANG_COOKIE, type Messages, type ResolvedI18n, formatCurrency, formatDate, formatNumber, formatRelativeTime, loadLocales, makeT, plural, renderI18nData, resolveI18n, resolveLang, translateHtml };

Examples

Examples are taken from this package's installed documentation and must be evaluated with its requirements and stability notes.

Server: load, resolve, translate

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);

Server: load, resolve, translate

{
  "nav": { "home": "Home" },
  "greeting": "Hello, {name}"
}

Views: translation markers

<h1 data-t="nav.home">Home</h1>
<input t:placeholder="search.placeholder" />

Client: language switcher

import { renderI18nData, I18N_RUNTIME, I18N_JS_HREF } from "@wrnexus/i18n";

// In the document <head>:
const head = `
  <script>${renderI18nData(i18n, lang)}</script>
  <script src="${I18N_JS_HREF}"></script>
`;

// Serve I18N_RUNTIME at I18N_JS_HREF; then in markup:
// <button data-wire-lang-set="es">Español</button>
// <select data-wire-lang>…</select>

Formatting

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"
} }