Files
WRNexusJS/.publish/i18n
2026-07-22 12:56:07 +05:30
..
2026-07-22 12:56:07 +05:30
2026-07-12 15:55:18 +05:30

@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/<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.

Installation

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<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) => 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-USen) → 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<string, unknown> — a locale's messages (supports nested/dotted keys).
I18nConfig interface { default?: string; locales?: string[] }.
ResolvedI18n interface { default: string; langs: string[]; messages: Record<string, Messages> }.
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:<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) => 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<Record<Intl.LDMLPluralRule, string>>, lang: string) => string picks 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/coreTFunction (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.