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

@wrnexus/i18n

Translation loading, locale resolution, and Intl formatting.

v0.8.8Private registryFrontend

Install the package

After WorkRoot approves private registry access, install the release-aligned package:

bun add @wrnexus/i18n@0.8.8

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

Recursive locale loading, fallback resolution, SSR/browser translations, locale formatting, and language UI blocks for WRNexusJS.

Locale files

Both layouts can be used together:

app/locales/en.json
app/locales/en/common.json
app/locales/en/auth.json
app/locales/mr/common.json

Namespaced files become keys such as common.save and auth.signIn.

import { loadLocales, makeT, resolveI18n, resolveLang } from "@wrnexus/i18n";

const i18n = resolveI18n(loadLocales("app/locales", { strict: true }), {
  default: "en",
  locales: ["en", "mr", "hi"],
  fallbacks: { "mr-IN": ["mr", "en"] },
  cookie: { name: "wire-lang", sameSite: "Lax", secure: true },
});

const lang = resolveLang(i18n, cookieValue, request.headers.get("accept-language"));
const t = makeT(i18n, lang);
t("common.hello", { name: "Ajay" });

Resolution behavior

  • normalized BCP-47-style locale names
  • cookie preference
  • weighted Accept-Language
  • wildcard language ranges
  • regional base fallback
  • explicit fallback chains
  • configured default language
  • automatic RTL for Arabic, Hebrew, Persian, Urdu, and related languages

Locale JSON is size-limited and rejects prototype-pollution keys. Recursive namespace collisions are resolved safely.

Views and runtime

<h1 data-t="dashboard.title">Dashboard</h1>
<input t:placeholder="search.placeholder" />

Text and translated attributes are resolved during SSR. Active/fallback messages are serialized safely for the language runtime, which rebinds data-t markers after client navigation.

Enable i18nPlugin() to use:

  • <LanguageSwitcher />
  • <LocaleStatus />

LanguageSwitcher renders a native select[data-wire-lang]. The packaged runtime validates the selection against the configured locales, writes the configured language cookie, updates the document lang/dir attributes, emits wrnexus:language-change, and reloads so the next SSR request uses the same cookie. No application-owned browser script is required.

Formatting

  • formatNumber
  • formatCurrency
  • formatDate
  • formatRelativeTime
  • plural
  • createLocaleFormatter
  • translationCoverage
  • Localization tooling can extract statically discoverable t("key"), i18n.t("key"), and data-i18n="key" usage, compare every locale with a reference, and create layout-stressing pseudo-locales:

import {
  auditLocaleKeys,
  createPseudoLocale,
  extractTranslationKeysFromFiles,
} from "@wrnexus/i18n";

const used = extractTranslationKeysFromFiles(sourceFiles);
const coverage = auditLocaleKeys(messages, "en");
const enXA = createPseudoLocale(messages.en);
const arXB = createPseudoLocale(messages.en, { rtl: true });

Pseudo-localization preserves interpolation placeholders and markup tags. RTL pseudo output uses Unicode direction controls, while runtime direction detection continues to derive rtl from Arabic and other RTL language subtags.

Complete TypeScript API

Generated from the exact installed package declarations.

import { TFunction } from '@wrnexus/core';
export { I18nPluginOptions, i18nComponentsDir, default as i18nPlugin } from './plugin.js';
import '@wrnexus/plugin';

/**
 * 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;

interface ExtractedTranslationKey {
    key: string;
    file?: string;
    offset: number;
}
declare function extractTranslationKeys(source: string, file?: string): ExtractedTranslationKey[];
declare function extractTranslationKeysFromFiles(files: Iterable<string>): ExtractedTranslationKey[];
declare function flattenMessageKeys(messages: Messages, prefix?: string): string[];
declare function auditLocaleKeys(messages: Record<string, Messages>, referenceLocale: string): Record<string, {
    missing: string[];
    extra: string[];
}>;
declare function pseudoLocalize(value: string, options?: {
    rtl?: boolean;
}): string;
declare function createPseudoLocale(messages: Messages, options?: {
    rtl?: boolean;
}): Messages;

declare function flattenMessages(messages: Messages, prefix?: string, output?: Record<string, string>): Record<string, string>;
declare function localeFallbacks(locale: string, fallback?: string): string[];
declare function translationCoverage(i18n: ResolvedI18n): Record<string, {
    translated: number;
    total: number;
    percentage: number;
    missing: string[];
    extra: string[];
}>;
interface LocaleFormatter {
    number(value: number, options?: Intl.NumberFormatOptions): string;
    currency(value: number, currency: string, options?: Omit<Intl.NumberFormatOptions, "style" | "currency">): string;
    date(value: Date | number | string, options?: Intl.DateTimeFormatOptions): string;
    relative(value: number, unit: Intl.RelativeTimeFormatUnit, options?: Intl.RelativeTimeFormatOptions): string;
    list(values: string[], options?: Intl.ListFormatOptions): string;
}
declare function createLocaleFormatter(locale: string, timeZone?: string, calendar?: string): LocaleFormatter;
/** ICU-style plural/select templates with exact values and recursive interpolation. */
declare function formatMessage(template: string, params: Record<string, string | number>, locale: string): string;

/**
 * @wrnexus/i18n — deterministic locale loading, fallback resolution, SSR
 * translation markers, browser translation helpers, and UI language controls.
 */

type Messages = Record<string, unknown>;
interface LocaleLoadOptions {
    /** Throw on invalid JSON instead of warning and continuing. */
    strict?: boolean;
    /** Maximum JSON file size. Default 1 MiB. */
    maxFileBytes?: number;
}
interface I18nCookieConfig {
    name?: string;
    maxAge?: number;
    path?: string;
    sameSite?: "Strict" | "Lax" | "None";
    secure?: boolean;
}
interface I18nConfig {
    default?: string;
    locales?: string[];
    /** Human-readable locale names used by package language controls. */
    labels?: Record<string, string>;
    /** Per-locale fallback override. Example: `{ "fr-CA": ["fr", "en"] }`. */
    fallbacks?: Record<string, string[]>;
    /** Locale direction overrides. Arabic/Hebrew/Persian/Urdu are RTL automatically. */
    direction?: Record<string, "ltr" | "rtl">;
    cookie?: I18nCookieConfig;
    strict?: boolean;
}
interface ResolvedI18n {
    default: string;
    langs: string[];
    messages: Record<string, Messages>;
    fallbacks: Record<string, string[]>;
    direction: Record<string, "ltr" | "rtl">;
    labels: Record<string, string>;
    cookie: Required<I18nCookieConfig>;
}
declare const LANG_COOKIE = "wire-lang";
declare const I18N_JS_HREF = "/__wrnexus/i18n.js";
declare function normalizeLocale(locale: string): string;
/**
 * Load both supported layouts:
 * - `locales/en.json`
 * - `locales/en/common.json`, `locales/en/auth.json`
 *
 * Namespaced files become `messages.en.common` and `messages.en.auth`.
 */
declare function loadLocales(dir: string, options?: LocaleLoadOptions): Record<string, Messages>;
declare function localeDirection(locale: string, overrides?: Record<string, "ltr" | "rtl">): "ltr" | "rtl";
declare function resolveI18n(messages: Record<string, Messages>, config?: I18nConfig): ResolvedI18n;
declare function lookupMessage(messages: Messages | undefined, key: string): string | undefined;
declare function interpolate(message: string, params?: Record<string, string | number>): string;
declare function translationChain(i18n: ResolvedI18n, locale: string): string[];
declare function makeT(i18n: ResolvedI18n, lang: string): TFunction;
/** Deeply apply tenant-specific translations without mutating the shared locale bundle. */
declare function withTenantMessages(i18n: ResolvedI18n, overrides: Record<string, Messages>): ResolvedI18n;
/** Load only common and route-specific messages for one locale. */
declare function loadRouteMessages(directory: string, locale: string, route: string): Messages;
declare function parseAcceptLanguage(value: string | null): string[];
declare function resolveLang(i18n: ResolvedI18n, cookieValue: string | undefined, acceptLanguage: string | null): string;
declare function translateHtml(html: string, t: TFunction): string;
declare function renderI18nData(i18n: ResolvedI18n, lang: string): string;
declare const I18N_RUNTIME: string;

export { type ExtractedTranslationKey, I18N_JS_HREF, I18N_RUNTIME, type I18nConfig, type I18nCookieConfig, LANG_COOKIE, type LocaleFormatter, type LocaleLoadOptions, type Messages, type ResolvedI18n, auditLocaleKeys, createLocaleFormatter, createPseudoLocale, extractTranslationKeys, extractTranslationKeysFromFiles, flattenMessageKeys, flattenMessages, formatCurrency, formatDate, formatMessage, formatNumber, formatRelativeTime, interpolate, loadLocales, loadRouteMessages, localeDirection, localeFallbacks, lookupMessage, makeT, normalizeLocale, parseAcceptLanguage, plural, pseudoLocalize, renderI18nData, resolveI18n, resolveLang, translateHtml, translationChain, translationCoverage, withTenantMessages };

Examples

Copy-ready examples from the installed package documentation.

Namespaced files become keys such as common.save and auth.signIn.

import { loadLocales, makeT, resolveI18n, resolveLang } from "@wrnexus/i18n";

const i18n = resolveI18n(loadLocales("app/locales", { strict: true }), {
  default: "en",
  locales: ["en", "mr", "hi"],
  fallbacks: { "mr-IN": ["mr", "en"] },
  cookie: { name: "wire-lang", sameSite: "Lax", secure: true },
});

const lang = resolveLang(i18n, cookieValue, request.headers.get("accept-language"));
const t = makeT(i18n, lang);
t("common.hello", { name: "Ajay" });

## Views and runtime

<h1 data-t="dashboard.title">Dashboard</h1>
<input t:placeholder="search.placeholder" />

reference, and create layout-stressing pseudo-locales

import {
  auditLocaleKeys,
  createPseudoLocale,
  extractTranslationKeysFromFiles,
} from "@wrnexus/i18n";

const used = extractTranslationKeysFromFiles(sourceFiles);
const coverage = auditLocaleKeys(messages, "en");
const enXA = createPseudoLocale(messages.en);
const arXB = createPseudoLocale(messages.en, { rtl: true });
} }