@wrnexus/i18n
Translation loading, locale resolution, and Intl formatting.
Install the package
After WorkRoot approves private registry access, install the release-aligned package:
bun add @wrnexus/i18n@0.8.8Request 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
formatNumberformatCurrencyformatDateformatRelativeTimepluralcreateLocaleFormattertranslationCoverage
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 });