Files
WRNexusJSDoc/app/pages/packages/i18n.wrn
T

259 lines
22 KiB
Plaintext

page wrnexusi18n {
seo {
title = "@wrnexus/i18n"
description = "Translation loading, locale resolution, and Intl formatting."
}
view {
<div class="docs-shell">
<SkipLink label="Skip to content" href="#main" class="docs-skip-link" />
<header class="topbar">
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
<nav aria-label="Primary"><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="https://component.wrnexusjs.dev/">Components</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
<div class="topbar-actions"><a class="preview-pill" href="/access">Private preview · v0.5.1</a><button data-wire-theme-toggle class="theme-button" aria-label="Toggle color theme" title="Toggle color theme">◐</button></div>
</header>
<div class="mobile-doc-nav"><details><summary>Browse documentation</summary><nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="https://component.wrnexusjs.dev/">Components</a><a href="/language">Language</a><a href="/architecture">Architecture</a><a href="/tutorial">Tutorial</a><a href="/guides/project-structure">Guides</a><a href="/examples">Examples</a><a href="/search">Search</a></nav></details></div>
<main class="portal-main docs-layout">
<article id="main" class="documentation prose standalone package-document"><nav class="breadcrumbs" aria-label="Breadcrumb"><a href="/">Home</a><span>/</span><a href="/packages">Packages</a><span>/</span><span aria-current="page">@wrnexus/i18n</span></nav><section class="doc-intro"><span class="eyebrow">Frontend · Package reference</span><h1>@wrnexus/i18n</h1><p>Translation loading, locale resolution, and Intl formatting.</p><div class="doc-meta"><span>v0.5.1</span><span>Private registry</span><span>Frontend</span></div><section id="access" class="access-callout"><h2>Install the package</h2><p>After WorkRoot approves private registry access, install the release-aligned package:</p><pre><code>bun add @wrnexus/i18n@0.5.1</code><button type="button" class="copy-button" aria-label="Copy installation command">Copy</button></pre><p><a href="/access">Request preview access</a>. Never put registry tokens in source control.</p></section></section><section id="guide"><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/&lt;lang&gt;.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>
<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 &amp; 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) =&gt; Record&lt;string, Messages&gt;</code></td><td>Reads every <code>&lt;lang&gt;.json</code> in <code>dir</code> into a <code>&#123; lang: messages &#125;</code> map. Missing dir → <code>&#123;&#125;</code>; a bad file is warned and skipped.</td></tr><tr><td><code>resolveI18n</code></td><td><code>(messages: Record&lt;string, Messages&gt;, config?: I18nConfig) =&gt; 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) =&gt; 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) =&gt; TFunction</code></td><td>Builds a translator resolving current language → default → the key itself, with <code>&#123;param&#125;</code> interpolation.</td></tr></tbody></table></div>
<h4 id="types-constants">Types &amp; 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&lt;string, unknown&gt;</code> — a locale's messages (supports nested/dotted keys).</td></tr><tr><td><code>I18nConfig</code></td><td><code>interface</code></td><td><code>&#123; default?: string; locales?: string[] &#125;</code>.</td></tr><tr><td><code>ResolvedI18n</code></td><td><code>interface</code></td><td><code>&#123; default: string; langs: string[]; messages: Record&lt;string, Messages&gt; &#125;</code>.</td></tr><tr><td><code>LANG_COOKIE</code></td><td><code>const</code></td><td><code>&quot;wire-lang&quot;</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>&quot;/__wrnexus/i18n.js&quot;</code> — URL the client runtime is served at.</td></tr></tbody></table></div>
<h4 id="html-client-runtime">HTML &amp; 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) =&gt; string</code></td><td>Rewrites markers in rendered HTML: <code>t:&lt;attr&gt;=&quot;key&quot;</code> → <code>&lt;attr&gt;=&quot;&lt;translation&gt;&quot;</code> (attribute-escaped) and <code>&lt;tag data-t=&quot;key&quot;&gt;…&lt;/tag&gt;</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) =&gt; string</code></td><td>JS snippet setting <code>window.__wireI18n = &#123; lang, langs, default &#125;</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=&quot;es&quot;]</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) =&gt; string</code></td><td><code>1234.5 → &quot;1,234.5&quot;</code></td></tr><tr><td><code>formatCurrency</code></td><td><code>(value: number, currency: string, lang: string) =&gt; string</code></td><td><code>9.99, &quot;USD&quot; → &quot;$9.99&quot;</code></td></tr><tr><td><code>formatDate</code></td><td>`(value: Date \</td><td>number \</td><td>string, lang: string, options?: Intl.DateTimeFormatOptions) =&gt; string`</td><td>defaults to <code>&#123; dateStyle: &quot;medium&quot; &#125;</code></td></tr><tr><td><code>formatRelativeTime</code></td><td><code>(value: number, unit: Intl.RelativeTimeFormatUnit, lang: string) =&gt; string</code></td><td><code>-3, &quot;day&quot; → &quot;3 days ago&quot;</code> (<code>numeric: &quot;auto&quot;</code>)</td></tr><tr><td><code>plural</code></td><td><code>(count: number, forms: Partial&lt;Record&lt;Intl.LDMLPluralRule, string&gt;&gt;, lang: string) =&gt; 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 &#123;
loadLocales,
resolveI18n,
resolveLang,
makeT,
translateHtml,
LANG_COOKIE,
&#125; from &quot;@wrnexus/i18n&quot;;
// app/locales/en.json, app/locales/es.json
const messages = loadLocales(&quot;app/locales&quot;);
const i18n = resolveI18n(messages, &#123; default: &quot;en&quot;, locales: [&quot;en&quot;, &quot;es&quot;] &#125;);
// Per request:
const lang = resolveLang(i18n, req.cookies?.[LANG_COOKIE], req.headers.get(&quot;accept-language&quot;));
const t = makeT(i18n, lang);
t(&quot;nav.home&quot;); // dotted key → &quot;Home&quot;
t(&quot;greeting&quot;, &#123; name: &quot;Ada&quot; &#125;); // &quot;Hello, &#123;name&#125;&quot; → &quot;Hello, Ada&quot;
// 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>&#123;
&quot;nav&quot;: &#123; &quot;home&quot;: &quot;Home&quot; &#125;,
&quot;greeting&quot;: &quot;Hello, &#123;name&#125;&quot;
&#125;</code></pre>
<h4 id="views-translation-markers">Views: translation markers</h4>
<pre data-language="html"><code>&lt;h1 data-t=&quot;nav.home&quot;&gt;Home&lt;/h1&gt;
&lt;input t:placeholder=&quot;search.placeholder&quot; /&gt;</code></pre>
<p><code>translateHtml</code> replaces the element text for <code>data-t</code> and the attribute value for any <code>t:&lt;attr&gt;</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 &#123; renderI18nData, I18N_RUNTIME, I18N_JS_HREF &#125; from &quot;@wrnexus/i18n&quot;;
// In the document &lt;head&gt;:
const head = `
&lt;script&gt;$&#123;renderI18nData(i18n, lang)&#125;&lt;/script&gt;
&lt;script src=&quot;$&#123;I18N_JS_HREF&#125;&quot;&gt;&lt;/script&gt;
`;
// Serve I18N_RUNTIME at I18N_JS_HREF; then in markup:
// &lt;button data-wire-lang-set=&quot;es&quot;&gt;Español&lt;/button&gt;
// &lt;select data-wire-lang&gt;…&lt;/select&gt;</code></pre>
<h4 id="formatting">Formatting</h4>
<pre data-language="ts"><code>import &#123;
formatNumber,
formatCurrency,
formatDate,
formatRelativeTime,
plural,
&#125; from &quot;@wrnexus/i18n&quot;;
formatNumber(1234.5, lang); // &quot;1,234.5&quot;
formatCurrency(9.99, &quot;USD&quot;, lang); // &quot;$9.99&quot;
formatDate(Date.now(), lang); // &quot;Jul 4, 2026&quot;
formatRelativeTime(-3, &quot;day&quot;, lang); // &quot;3 days ago&quot;
plural(2, &#123; one: &quot;# item&quot;, other: &quot;# items&quot; &#125;, lang); // &quot;2 items&quot;</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="api"><h2>Complete TypeScript API</h2><p>Generated from the exact installed package declarations.</p><pre data-language="typescript"><code>import &#123; TFunction &#125; 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 → &quot;1,234.5&quot;). */
declare function formatNumber(value: number, lang: string, options?: Intl.NumberFormatOptions): string;
/** Format a currency amount (e.g. 9.99, &quot;USD&quot; → &quot;$9.99&quot;). */
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 → &quot;3 days ago&quot; (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, &#123; one: &quot;1 item&quot;, other: &quot;# items&quot; &#125;, lang)` — &quot;#&quot; is replaced by n.
*/
declare function plural(count: number, forms: Partial&lt;Record&lt;Intl.LDMLPluralRule, string&gt;&gt;, lang: string): string;
declare function flattenMessages(messages: Messages, prefix?: string, output?: Record&lt;string, string&gt;): Record&lt;string, string&gt;;
declare function localeFallbacks(locale: string, fallback?: string): string[];
declare function translationCoverage(i18n: ResolvedI18n): Record&lt;string, &#123;
translated: number;
total: number;
percentage: number;
missing: string[];
extra: string[];
&#125;&gt;;
interface LocaleFormatter &#123;
number(value: number, options?: Intl.NumberFormatOptions): string;
currency(value: number, currency: string, options?: Omit&lt;Intl.NumberFormatOptions, &quot;style&quot; | &quot;currency&quot;&gt;): 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;
&#125;
declare function createLocaleFormatter(locale: string, timeZone?: string): LocaleFormatter;
/** Lightweight plural templates: `&#123;count, plural, one &#123;# item&#125; other &#123;# items&#125;&#125;`. */
declare function formatMessage(template: string, params: Record&lt;string, string | number&gt;, locale: string): string;
/**
* @wrnexus/i18n — translations for pages and API responses.
*
* Locales live in `app/locales/&lt;lang&gt;.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 `&#123;t:key&#125;` and
* `t:attr=&quot;key&quot;` markers are resolved by `translateHtml` before the HTML is sent.
*/
type Messages = Record&lt;string, unknown&gt;;
/** Load `&lt;dir&gt;/&lt;lang&gt;.json` files into a `&#123; lang: messages &#125;` map. */
declare function loadLocales(dir: string): Record&lt;string, Messages&gt;;
interface I18nConfig &#123;
/** 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[];
&#125;
interface ResolvedI18n &#123;
default: string;
langs: string[];
messages: Record&lt;string, Messages&gt;;
&#125;
declare const LANG_COOKIE = &quot;wire-lang&quot;;
declare const I18N_JS_HREF = &quot;/__wrnexus/i18n.js&quot;;
/** Merge loaded locale messages + config into a resolved i18n bundle. */
declare function resolveI18n(messages: Record&lt;string, Messages&gt;, 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:&lt;attr&gt;=&quot;key&quot; → &lt;attr&gt;=&quot;&lt;translation&gt;&quot; (e.g. t:placeholder, t:aria-label)
* &lt;tag data-t=&quot;key&quot;&gt;…&lt;/tag&gt; → element text becomes the translation
* Only runs when the HTML actually contains a marker.
*/
declare function translateHtml(html: string, t: TFunction): string;
/** `window.__wireI18n = &#123; lang, langs &#125;` for the client language switcher. */
declare function renderI18nData(i18n: ResolvedI18n, lang: string): string;
/**
* Client runtime: binds `[data-wire-lang-set=&quot;es&quot;]` elements to set the
* `wire-lang` cookie and reload, so the server re-renders in the new language.
*/
declare const I18N_RUNTIME: string;
export &#123; I18N_JS_HREF, I18N_RUNTIME, type I18nConfig, LANG_COOKIE, type LocaleFormatter, type Messages, type ResolvedI18n, createLocaleFormatter, flattenMessages, formatCurrency, formatDate, formatMessage, formatNumber, formatRelativeTime, loadLocales, localeFallbacks, makeT, plural, renderI18nData, resolveI18n, resolveLang, translateHtml, translationCoverage &#125;;
</code></pre></section><section id="examples" class="examples"><h2>Examples</h2><p>Copy-ready examples from the installed package documentation.</p><div class="example-grid"><article class="example-card"><h3>Server: load, resolve, translate</h3><pre data-language="ts"><code>import &#123;
loadLocales,
resolveI18n,
resolveLang,
makeT,
translateHtml,
LANG_COOKIE,
&#125; from &quot;@wrnexus/i18n&quot;;
// app/locales/en.json, app/locales/es.json
const messages = loadLocales(&quot;app/locales&quot;);
const i18n = resolveI18n(messages, &#123; default: &quot;en&quot;, locales: [&quot;en&quot;, &quot;es&quot;] &#125;);
// Per request:
const lang = resolveLang(i18n, req.cookies?.[LANG_COOKIE], req.headers.get(&quot;accept-language&quot;));
const t = makeT(i18n, lang);
t(&quot;nav.home&quot;); // dotted key → &quot;Home&quot;
t(&quot;greeting&quot;, &#123; name: &quot;Ada&quot; &#125;); // &quot;Hello, &#123;name&#125;&quot; → &quot;Hello, Ada&quot;
// After rendering a .wrn view, resolve translation markers in the HTML:
const finalHtml = translateHtml(renderedHtml, t);</code></pre></article><article class="example-card"><h3>Server: load, resolve, translate</h3><pre data-language="json"><code>&#123;
&quot;nav&quot;: &#123; &quot;home&quot;: &quot;Home&quot; &#125;,
&quot;greeting&quot;: &quot;Hello, &#123;name&#125;&quot;
&#125;</code></pre></article><article class="example-card"><h3>Views: translation markers</h3><pre data-language="html"><code>&lt;h1 data-t=&quot;nav.home&quot;&gt;Home&lt;/h1&gt;
&lt;input t:placeholder=&quot;search.placeholder&quot; /&gt;</code></pre></article><article class="example-card"><h3>Client: language switcher</h3><pre data-language="ts"><code>import &#123; renderI18nData, I18N_RUNTIME, I18N_JS_HREF &#125; from &quot;@wrnexus/i18n&quot;;
// In the document &lt;head&gt;:
const head = `
&lt;script&gt;$&#123;renderI18nData(i18n, lang)&#125;&lt;/script&gt;
&lt;script src=&quot;$&#123;I18N_JS_HREF&#125;&quot;&gt;&lt;/script&gt;
`;
// Serve I18N_RUNTIME at I18N_JS_HREF; then in markup:
// &lt;button data-wire-lang-set=&quot;es&quot;&gt;Español&lt;/button&gt;
// &lt;select data-wire-lang&gt;…&lt;/select&gt;</code></pre></article><article class="example-card"><h3>Formatting</h3><pre data-language="ts"><code>import &#123;
formatNumber,
formatCurrency,
formatDate,
formatRelativeTime,
plural,
&#125; from &quot;@wrnexus/i18n&quot;;
formatNumber(1234.5, lang); // &quot;1,234.5&quot;
formatCurrency(9.99, &quot;USD&quot;, lang); // &quot;$9.99&quot;
formatDate(Date.now(), lang); // &quot;Jul 4, 2026&quot;
formatRelativeTime(-3, &quot;day&quot;, lang); // &quot;3 days ago&quot;
plural(2, &#123; one: &quot;# item&quot;, other: &quot;# items&quot; &#125;, lang); // &quot;2 items&quot;</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="#api">API</a><a class="toc-level-4" href="#loading-resolving">Loading &amp; resolving</a><a class="toc-level-4" href="#types-constants">Types &amp; constants</a><a class="toc-level-4" href="#html-client-runtime">HTML &amp; 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><div class="footer-brand"><span class="footer-mark" aria-hidden="true">W</span><p><strong>WRNexusJS 0.5.1</strong><span>Complete API documentation generated from installed package declarations.</span></p></div><nav aria-label="Footer"><a href="/packages">All packages</a><a href="/getting-started">Get started</a><a href="/security">Security</a><a href="/support">Support</a><a href="/llms.txt">AI guide</a></nav><p class="footer-meta">Private Developer Preview · Bun-native</p></footer>
<BackToTop />
</div>
}
}