216 lines
18 KiB
Plaintext
216 lines
18 KiB
Plaintext
page wrnexusi18n {
|
|
seo {
|
|
title = "@wrnexus/i18n"
|
|
description = "Translation loading, locale resolution, and Intl formatting."
|
|
}
|
|
|
|
view {
|
|
<div class="docs-shell">
|
|
<header class="topbar">
|
|
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
|
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
|
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
|
</header>
|
|
<main class="page package-page">
|
|
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">Frontend</span><h1>@wrnexus/i18n</h1><p>Translation loading, locale resolution, and Intl formatting.</p><code>bun add @wrnexus/i18n@0.2.12</code><nav><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
|
|
<article class="documentation"><section class="doc-intro"><span class="eyebrow">Frontend</span><h1>@wrnexus/i18n</h1><p>Translation loading, locale resolution, and Intl formatting.</p><pre><code>bun add @wrnexus/i18n@0.2.12</code></pre></section><section id="guide" class="prose"><blockquote>Per-request translations plus locale-aware number, date, and currency formatting for WRNexusJS apps.</blockquote>
|
|
<p>Part of the <strong>WRNexusJS</strong> framework — an SSR-first, Bun-native full-stack web framework.</p>
|
|
<h3 id="overview">Overview</h3>
|
|
<p><code>@wrnexus/i18n</code> loads locale files from <code>app/locales/<lang>.json</code>, resolves the active language for each request (cookie → <code>Accept-Language</code> → default), and builds a <code>t(key, params)</code> translator used both in server code and in <code>.wrn</code> 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 <code>I18N_RUNTIME</code> snippet runs in the browser.</p>
|
|
<h3 id="installation">Installation</h3>
|
|
<pre data-language="bash"><code>bun add @wrnexus/i18n</code></pre>
|
|
<blockquote>Private package — the machine must be authenticated to the <code>wrnexus</code> npm org</blockquote>
|
|
<blockquote>(a read token in <code>~/.npmrc</code>). Requires <strong>Bun</strong> (Node is not supported).</blockquote>
|
|
<h3 id="api">API</h3>
|
|
<h4 id="loading-resolving">Loading & resolving</h4>
|
|
<div class="table-wrap"><table>
|
|
<thead><tr><th>Export</th><th>Signature</th><th>Description</th></tr></thead>
|
|
<tbody><tr><td><code>loadLocales</code></td><td><code>(dir: string) => Record<string, Messages></code></td><td>Reads every <code><lang>.json</code> in <code>dir</code> into a <code>{ lang: messages }</code> map. Missing dir → <code>{}</code>; a bad file is warned and skipped.</td></tr><tr><td><code>resolveI18n</code></td><td><code>(messages: Record<string, Messages>, config?: I18nConfig) => ResolvedI18n</code></td><td>Merges loaded messages + config into a resolved bundle (default lang, supported langs, messages).</td></tr><tr><td><code>resolveLang</code></td><td>`(i18n: ResolvedI18n, cookieValue: string \</td><td>undefined, acceptLanguage: string \</td><td>null) => string`</td><td>Picks the active language: matching cookie → best <code>Accept-Language</code> tag (falls back to base tag, e.g. <code>en-US</code> → <code>en</code>) → <code>i18n.default</code>.</td></tr><tr><td><code>makeT</code></td><td><code>(i18n: ResolvedI18n, lang: string) => TFunction</code></td><td>Builds a translator resolving current language → default → the key itself, with <code>{param}</code> interpolation.</td></tr></tbody></table></div>
|
|
<h4 id="types-constants">Types & constants</h4>
|
|
<div class="table-wrap"><table>
|
|
<thead><tr><th>Export</th><th>Kind</th><th>Notes</th></tr></thead>
|
|
<tbody><tr><td><code>Messages</code></td><td><code>type</code></td><td><code>Record<string, unknown></code> — a locale's messages (supports nested/dotted keys).</td></tr><tr><td><code>I18nConfig</code></td><td><code>interface</code></td><td><code>{ default?: string; locales?: string[] }</code>.</td></tr><tr><td><code>ResolvedI18n</code></td><td><code>interface</code></td><td><code>{ default: string; langs: string[]; messages: Record<string, Messages> }</code>.</td></tr><tr><td><code>LANG_COOKIE</code></td><td><code>const</code></td><td><code>"wire-lang"</code> — the cookie the language is read from / written to.</td></tr><tr><td><code>I18N_JS_HREF</code></td><td><code>const</code></td><td><code>"/__wrnexus/i18n.js"</code> — URL the client runtime is served at.</td></tr></tbody></table></div>
|
|
<h4 id="html-client-runtime">HTML & client runtime</h4>
|
|
<div class="table-wrap"><table>
|
|
<thead><tr><th>Export</th><th>Signature</th><th>Description</th></tr></thead>
|
|
<tbody><tr><td><code>translateHtml</code></td><td><code>(html: string, t: TFunction) => string</code></td><td>Rewrites markers in rendered HTML: <code>t:<attr>="key"</code> → <code><attr>="<translation>"</code> (attribute-escaped) and <code><tag data-t="key">…</tag></code> → element text becomes the translation (HTML-escaped). No-op unless a marker is present.</td></tr><tr><td><code>renderI18nData</code></td><td><code>(i18n: ResolvedI18n, lang: string) => string</code></td><td>JS snippet setting <code>window.__wireI18n = { lang, langs, default }</code> for the client switcher.</td></tr><tr><td><code>I18N_RUNTIME</code></td><td><code>const string</code></td><td>Browser IIFE that binds <code>[data-wire-lang-set="es"]</code> clicks and <code>select[data-wire-lang]</code> changes to set the <code>wire-lang</code> cookie and reload. Exposes <code>window.__wireLang.set(lang)</code>.</td></tr></tbody></table></div>
|
|
<h4 id="formatting-helpers-re-exported-from-format-ts">Formatting helpers (re-exported from <code>./format.ts</code>)</h4>
|
|
<div class="table-wrap"><table>
|
|
<thead><tr><th>Export</th><th>Signature</th><th>Example</th></tr></thead>
|
|
<tbody><tr><td><code>formatNumber</code></td><td><code>(value: number, lang: string, options?: Intl.NumberFormatOptions) => string</code></td><td><code>1234.5 → "1,234.5"</code></td></tr><tr><td><code>formatCurrency</code></td><td><code>(value: number, currency: string, lang: string) => string</code></td><td><code>9.99, "USD" → "$9.99"</code></td></tr><tr><td><code>formatDate</code></td><td>`(value: Date \</td><td>number \</td><td>string, lang: string, options?: Intl.DateTimeFormatOptions) => string`</td><td>defaults to <code>{ dateStyle: "medium" }</code></td></tr><tr><td><code>formatRelativeTime</code></td><td><code>(value: number, unit: Intl.RelativeTimeFormatUnit, lang: string) => string</code></td><td><code>-3, "day" → "3 days ago"</code> (<code>numeric: "auto"</code>)</td></tr><tr><td><code>plural</code></td><td><code>(count: number, forms: Partial<Record<Intl.LDMLPluralRule, string>>, lang: string) => string</code></td><td>picks CLDR form; <code>#</code> is replaced by <code>count</code></td></tr></tbody></table></div>
|
|
<h3 id="usage">Usage</h3>
|
|
<h4 id="server-load-resolve-translate">Server: load, resolve, translate</h4>
|
|
<pre data-language="ts"><code>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);</code></pre>
|
|
<p><code>app/locales/en.json</code>:</p>
|
|
<pre data-language="json"><code>{
|
|
"nav": { "home": "Home" },
|
|
"greeting": "Hello, {name}"
|
|
}</code></pre>
|
|
<h4 id="views-translation-markers">Views: translation markers</h4>
|
|
<pre data-language="html"><code><h1 data-t="nav.home">Home</h1>
|
|
<input t:placeholder="search.placeholder" /></code></pre>
|
|
<p><code>translateHtml</code> replaces the element text for <code>data-t</code> and the attribute value for any <code>t:<attr></code> (e.g. <code>t:placeholder</code>, <code>t:aria-label</code>).</p>
|
|
<h4 id="client-language-switcher">Client: language switcher</h4>
|
|
<pre data-language="ts"><code>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></code></pre>
|
|
<h4 id="formatting">Formatting</h4>
|
|
<pre data-language="ts"><code>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"</code></pre>
|
|
<h3 id="configuration">Configuration</h3>
|
|
<p><code>resolveI18n</code> accepts an <code>I18nConfig</code>:</p>
|
|
<ul>
|
|
<li><code>default</code> — fallback language; used when nothing else matches. Ignored if it has</li>
|
|
<p>no loaded messages, in which case the first supported language is used.</p>
|
|
<li><code>locales</code> — explicit supported-language list; defaults to the loaded locale names.</li>
|
|
</ul>
|
|
<p>Language resolution order at request time (<code>resolveLang</code>): a supported <code>wire-lang</code> cookie value → the first matching <code>Accept-Language</code> tag (or its base subtag) → the resolved default.</p>
|
|
<h3 id="requirements-notes">Requirements / Notes</h3>
|
|
<ul>
|
|
<li><strong>Bun-only.</strong> Locale loading uses <code>node:fs</code> (<code>existsSync</code>, <code>readdirSync</code>,</li>
|
|
<p><code>readFileSync</code>) and <code>node:path</code>; formatting relies on the platform <code>Intl</code> APIs.</p>
|
|
<li>Works with [<code>@wrnexus/core</code>](../core) — <code>TFunction</code> (the <code>t(key, params)</code> type)</li>
|
|
<p>comes from core, and the resolved translator is exposed as <code>ctx.t</code> / <code>ctx.lang</code> in request handling.</p>
|
|
<li>Nested message objects are supported: keys are looked up whole first, then split</li>
|
|
<p>on <code>.</code> to walk the object tree.</p>
|
|
</ul></section><section id="api" class="prose api"><h2>Complete TypeScript API</h2><p>This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>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 };
|
|
</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Copy-ready examples taken from this package's published documentation.</p><div class="example-grid"><article class="example-card"><h3>Example 1</h3><pre data-language="bash"><code>bun add @wrnexus/i18n</code></pre></article><article class="example-card"><h3>Example 2</h3><pre data-language="ts"><code>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);</code></pre></article><article class="example-card"><h3>Example 3</h3><pre data-language="json"><code>{
|
|
"nav": { "home": "Home" },
|
|
"greeting": "Hello, {name}"
|
|
}</code></pre></article><article class="example-card"><h3>Example 4</h3><pre data-language="html"><code><h1 data-t="nav.home">Home</h1>
|
|
<input t:placeholder="search.placeholder" /></code></pre></article></div></section></article>
|
|
<aside class="on-this-page"><h2>On this page</h2><nav><a class="toc-level-2" href="#guide">Guide</a><a class="toc-level-3" href="#overview">Overview</a><a class="toc-level-3" href="#installation">Installation</a><a class="toc-level-3" href="#api">API</a><a class="toc-level-4" href="#loading-resolving">Loading & resolving</a><a class="toc-level-4" href="#types-constants">Types & constants</a><a class="toc-level-4" href="#html-client-runtime">HTML & client runtime</a><a class="toc-level-4" href="#formatting-helpers-re-exported-from-format-ts">Formatting helpers (re-exported from ./format.ts)</a><a class="toc-level-3" href="#usage">Usage</a><a class="toc-level-4" href="#server-load-resolve-translate">Server: load, resolve, translate</a><a class="toc-level-4" href="#views-translation-markers">Views: translation markers</a><a class="toc-level-4" href="#client-language-switcher">Client: language switcher</a><a class="toc-level-4" href="#formatting">Formatting</a><a class="toc-level-3" href="#configuration">Configuration</a><a class="toc-level-3" href="#requirements-notes">Requirements / Notes</a><a class="toc-level-2" href="#api">Complete API</a><a class="toc-level-2" href="#examples">Examples</a></nav></aside>
|
|
</main>
|
|
<footer>WRNexusJS 0.2.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
|
</div>
|
|
}
|
|
}
|