release: WRNexusJS 0.8.0
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-02 23:18:51 +05:30
parent 87507edf59
commit 586a6db8ff
625 changed files with 243608 additions and 11210 deletions
+14
View File
@@ -0,0 +1,14 @@
# WRNexus i18n showcase
A complete English, Hindi, and Marathi example covering SSR translations, translated attributes,
interpolation, plural rules, numbers, INR currency, percentages, dates, relative time, lists,
fallback chains, browser language negotiation, and cookie-persisted language selection.
```bash
bun install
bun run --cwd examples/i18n-showcase dev
```
Open `http://localhost:3000`, change the language with the packaged `LanguageSwitcher`, and inspect
the document `<html lang>` attribute and `wire-lang` cookie. The `/api/formats` endpoint demonstrates
request-aware `Intl` formatting using the same selected locale.
+22
View File
@@ -0,0 +1,22 @@
import type { Context } from "@wrnexus/core";
import { createLocaleFormatter, formatMessage, plural } from "@wrnexus/i18n";
export const GET = async (ctx: Context) => {
const locale = ctx.lang || "en";
const format = createLocaleFormatter(locale, "Asia/Kolkata");
const count = 3;
return Response.json({
locale,
direction: ["ar", "fa", "he", "ur"].includes(locale.split("-")[0]!) ? "rtl" : "ltr",
translated: ctx.t("api.greeting", { name: "Asha" }),
interpolated: formatMessage(ctx.t("api.inbox"), { name: "Asha", count }, locale),
plural: plural(count, { one: ctx.t("api.itemOne"), other: ctx.t("api.itemOther") }, locale),
number: format.number(1_234_567.89),
currency: format.currency(1_234.5, "INR"),
percent: format.number(0.78, { style: "percent" }),
date: format.date("2026-08-15T09:30:00+05:30", { dateStyle: "full" }),
relativeTime: format.relative(-3, "day", { numeric: "auto" }),
list: format.list(["Mumbai", "Pune", "Nagpur"], { style: "long", type: "conjunction" }),
});
};
@@ -0,0 +1,14 @@
layout Document {
props {
language: string = "en"
}
view {
<html>
<head></head>
<body>
<div id="app"><slot /></div>
</body>
</html>
}
}
@@ -0,0 +1,69 @@
{
"header": {
"eyebrow": "WRNexus i18n",
"title": "Internationalization showcase"
},
"hero": {
"kicker": "Three languages, one page",
"title": "Everything needed for an international product",
"description": "The server chooses a language from the cookie or Accept-Language header and renders translated HTML."
},
"sections": { "content": "Translated content" },
"switcher": {
"label": "Language",
"placeholder": "Choose language",
"helper": "Saved securely in your language cookie",
"variantsTitle": "Responsive switcher variants",
"variantsDescription": "Every variant reads locales from configuration and the active language from the request cookie.",
"compactTitle": "Compact",
"segmentedTitle": "Segmented"
},
"cards": {
"text": {
"title": "Text translation",
"body": "Headings, paragraphs, buttons, and labels are rendered on the server."
},
"attributes": {
"title": "Attribute translation",
"label": "Search label",
"placeholder": "Search in English"
},
"interpolation": {
"title": "Interpolation",
"body": "Parameters such as a person's name are inserted safely.",
"action": "Open the dynamic API"
},
"plural": {
"title": "Plural rules",
"body": "One item and many items follow locale-aware CLDR rules."
},
"numbers": { "title": "Numbers and currency" },
"time": { "title": "Dates and relative time" },
"list": { "title": "List formatting" },
"fallback": {
"title": "Fallback chains",
"body": "Missing regional content falls back to the base language and then English."
},
"document": {
"title": "Document language",
"body": "The same cookie sets the server-rendered html lang and dir attributes."
}
},
"samples": {
"number": "1,234,567.89",
"currency": "₹1,234.50",
"percent": "78%",
"date": "15 August 2026",
"relative": "3 days ago",
"list": "Mumbai, Pune, and Nagpur"
},
"api": {
"title": "Try request-aware formatting",
"description": "The JSON endpoint uses ctx.lang, translations, Intl formatters, interpolation, plurals, and lists.",
"action": "View localized JSON",
"greeting": "Hello, {name}!",
"inbox": "{name}, you have {count, plural, one {# message} other {# messages}}.",
"itemOne": "# item",
"itemOther": "# items"
}
}
@@ -0,0 +1,69 @@
{
"header": {
"eyebrow": "WRNexus अंतर्राष्ट्रीयकरण",
"title": "बहुभाषी उदाहरण"
},
"hero": {
"kicker": "तीन भाषाएँ, एक पृष्ठ",
"title": "एक अंतर्राष्ट्रीय उत्पाद के लिए आवश्यक सब कुछ",
"description": "सर्वर कुकी या Accept-Language हेडर से भाषा चुनता है और अनुवादित HTML प्रस्तुत करता है।"
},
"sections": { "content": "अनुवादित सामग्री" },
"switcher": {
"label": "भाषा",
"placeholder": "भाषा चुनें",
"helper": "आपकी भाषा कुकी में सुरक्षित रूप से सहेजा गया",
"variantsTitle": "रेस्पॉन्सिव स्विचर प्रकार",
"variantsDescription": "हर प्रकार कॉन्फ़िगरेशन से भाषाएँ और अनुरोध कुकी से सक्रिय भाषा पढ़ता है।",
"compactTitle": "कॉम्पैक्ट",
"segmentedTitle": "खंडित"
},
"cards": {
"text": {
"title": "पाठ अनुवाद",
"body": "शीर्षक, अनुच्छेद, बटन और लेबल सर्वर पर प्रस्तुत होते हैं।"
},
"attributes": {
"title": "एट्रिब्यूट अनुवाद",
"label": "खोज लेबल",
"placeholder": "हिन्दी में खोजें"
},
"interpolation": {
"title": "इंटरपोलेशन",
"body": "व्यक्ति के नाम जैसे पैरामीटर सुरक्षित रूप से जोड़े जाते हैं।",
"action": "डायनेमिक API खोलें"
},
"plural": {
"title": "बहुवचन नियम",
"body": "एक और अनेक वस्तुएँ भाषा के CLDR नियमों का पालन करती हैं।"
},
"numbers": { "title": "संख्याएँ और मुद्रा" },
"time": { "title": "तिथियाँ और सापेक्ष समय" },
"list": { "title": "सूची स्वरूपण" },
"fallback": {
"title": "फॉलबैक श्रृंखला",
"body": "अनुपलब्ध क्षेत्रीय सामग्री पहले मूल भाषा और फिर अंग्रेज़ी में मिलती है।"
},
"document": {
"title": "दस्तावेज़ की भाषा",
"body": "यही कुकी सर्वर द्वारा प्रस्तुत html lang और dir एट्रिब्यूट तय करती है।"
}
},
"samples": {
"number": "12,34,567.89",
"currency": "₹1,234.50",
"percent": "78%",
"date": "15 अगस्त 2026",
"relative": "3 दिन पहले",
"list": "मुंबई, पुणे और नागपुर"
},
"api": {
"title": "अनुरोध के अनुसार स्वरूपण आज़माएँ",
"description": "JSON एंडपॉइंट ctx.lang, अनुवाद, Intl फॉर्मैटर, इंटरपोलेशन, बहुवचन और सूचियों का उपयोग करता है।",
"action": "स्थानीयकृत JSON देखें",
"greeting": "नमस्ते, {name}!",
"inbox": "{name}, आपके पास {count, plural, one {# संदेश} other {# संदेश}} हैं।",
"itemOne": "# वस्तु",
"itemOther": "# वस्तुएँ"
}
}
@@ -0,0 +1,69 @@
{
"header": {
"eyebrow": "WRNexus आंतरराष्ट्रीयीकरण",
"title": "बहुभाषिक उदाहरण"
},
"hero": {
"kicker": "तीन भाषा, एक पृष्ठ",
"title": "आंतरराष्ट्रीय उत्पादनासाठी आवश्यक असलेले सर्व काही",
"description": "सर्व्हर कुकी किंवा Accept-Language हेडरवरून भाषा निवडतो आणि भाषांतरित HTML प्रस्तुत करतो."
},
"sections": { "content": "भाषांतरित मजकूर" },
"switcher": {
"label": "भाषा",
"placeholder": "भाषा निवडा",
"helper": "तुमच्या भाषा कुकीमध्ये सुरक्षितपणे जतन केले",
"variantsTitle": "प्रतिसादक्षम स्विचर प्रकार",
"variantsDescription": "प्रत्येक प्रकार कॉन्फिगरेशनमधून भाषा आणि विनंती कुकीमधून सक्रिय भाषा घेतो.",
"compactTitle": "संक्षिप्त",
"segmentedTitle": "विभाजित"
},
"cards": {
"text": {
"title": "मजकूर भाषांतर",
"body": "शीर्षके, परिच्छेद, बटणे आणि लेबले सर्व्हरवर प्रस्तुत होतात."
},
"attributes": {
"title": "गुणधर्म भाषांतर",
"label": "शोध लेबल",
"placeholder": "मराठीत शोधा"
},
"interpolation": {
"title": "इंटरपोलेशन",
"body": "व्यक्तीच्या नावासारखे पॅरामीटर सुरक्षितपणे जोडले जातात.",
"action": "डायनॅमिक API उघडा"
},
"plural": {
"title": "अनेकवचन नियम",
"body": "एक आणि अनेक वस्तू स्थानिक CLDR नियमांचे पालन करतात."
},
"numbers": { "title": "संख्या आणि चलन" },
"time": { "title": "दिनांक आणि सापेक्ष वेळ" },
"list": { "title": "यादी स्वरूपण" },
"fallback": {
"title": "फॉलबॅक साखळी",
"body": "प्रादेशिक मजकूर उपलब्ध नसल्यास मूळ भाषा आणि नंतर इंग्रजी वापरली जाते."
},
"document": {
"title": "दस्तावेजाची भाषा",
"body": "हीच कुकी सर्व्हरने प्रस्तुत केलेले html lang आणि dir गुणधर्म ठरवते."
}
},
"samples": {
"number": "12,34,567.89",
"currency": "₹1,234.50",
"percent": "78%",
"date": "15 ऑगस्ट 2026",
"relative": "3 दिवसांपूर्वी",
"list": "मुंबई, पुणे आणि नागपूर"
},
"api": {
"title": "विनंतीनुसार स्वरूपण वापरून पहा",
"description": "JSON एंडपॉइंट ctx.lang, भाषांतरे, Intl फॉर्मॅटर, इंटरपोलेशन, अनेकवचन आणि याद्या वापरतो.",
"action": "स्थानिकीकरण केलेला JSON पहा",
"greeting": "नमस्कार, {name}!",
"inbox": "{name}, तुमच्याकडे {count, plural, one {# संदेश} other {# संदेश}} आहेत.",
"itemOne": "# वस्तू",
"itemOther": "# वस्तू"
}
}
@@ -0,0 +1,64 @@
import LanguageSwitcher from "@wrnexus/i18n/components/LanguageSwitcher.wrn"
page InternationalizationShowcase {
seo {
title = "Internationalization Showcase"
description = "English, Hindi, and Marathi localization with WRNexusJS."
}
view {
<main class="min-h-screen bg-[var(--wire-color-bg)] text-[var(--wire-color-text)]">
<header class="border-b border-[var(--wire-color-border)] bg-[var(--wire-color-surface)]">
<div class="mx-auto flex max-w-6xl flex-wrap items-center justify-between gap-5 px-6 py-5">
<div>
<p class="m-0 text-xs font-semibold uppercase tracking-[0.2em] text-[var(--wire-color-primary)]" data-t="header.eyebrow">WRNexus i18n</p>
<h1 class="m-0 mt-1 text-xl font-bold" data-t="header.title">Internationalization showcase</h1>
</div>
<LanguageSwitcher
label="Language / भाषा"
placeholder="Choose language"
helperText="Saved in your language cookie"
/>
</div>
</header>
<div class="mx-auto grid max-w-6xl gap-8 px-6 py-10">
<section class="rounded-3xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-8 shadow-sm">
<p class="m-0 text-sm font-medium text-[var(--wire-color-primary)]" data-t="hero.kicker">Three languages, one page</p>
<h2 class="mb-3 mt-2 text-4xl font-bold tracking-tight" data-t="hero.title">Everything needed for an international product</h2>
<p class="m-0 max-w-3xl text-lg leading-8 text-[var(--wire-color-muted)]" data-t="hero.description">The server chooses a language from the cookie or Accept-Language header and renders translated HTML.</p>
</section>
<section>
<h2 class="mb-2 text-2xl font-bold" data-t="switcher.variantsTitle">Responsive switcher variants</h2>
<p class="mb-5 mt-0 text-[var(--wire-color-muted)]" data-t="switcher.variantsDescription">Every variant reads locales from configuration and the active language from the request cookie.</p>
<div class="grid items-start gap-5 rounded-3xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-6 md:grid-cols-2">
<div class="grid gap-2"><strong data-t="switcher.compactTitle">Compact</strong><LanguageSwitcher variant="compact" showHelper="false" label="Language / भाषा" /></div>
<div class="grid gap-2"><strong data-t="switcher.segmentedTitle">Segmented</strong><LanguageSwitcher variant="segmented" showHelper="false" label="Language / भाषा" /></div>
</div>
</section>
<section>
<h2 class="mb-4 text-2xl font-bold" data-t="sections.content">Translated content</h2>
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<article class="demo-card"><span class="icon-[lucide--languages] demo-icon"></span><h3 data-t="cards.text.title">Text translation</h3><p data-t="cards.text.body">Headings, paragraphs, buttons, and labels are rendered on the server.</p></article>
<article class="demo-card"><span class="icon-[lucide--text-cursor-input] demo-icon"></span><h3 data-t="cards.attributes.title">Attribute translation</h3><label class="mt-3 grid gap-1 text-sm"><span data-t="cards.attributes.label">Search label</span><input class="rounded-xl border border-[var(--wire-color-border)] bg-transparent px-3 py-2" t:placeholder="cards.attributes.placeholder" /></label></article>
<article class="demo-card"><span class="icon-[lucide--message-square-more] demo-icon"></span><h3 data-t="cards.interpolation.title">Interpolation</h3><p data-t="cards.interpolation.body">Parameters such as a person's name are inserted safely.</p><a class="demo-link" href="/api/formats" data-t="cards.interpolation.action">Open the dynamic API</a></article>
<article class="demo-card"><span class="icon-[lucide--copy-plus] demo-icon"></span><h3 data-t="cards.plural.title">Plural rules</h3><p data-t="cards.plural.body">One item and many items follow locale-aware CLDR rules.</p></article>
<article class="demo-card"><span class="icon-[lucide--indian-rupee] demo-icon"></span><h3 data-t="cards.numbers.title">Numbers and currency</h3><p data-t="samples.number">12,34,567.89</p><p data-t="samples.currency">₹1,234.50</p><p data-t="samples.percent">78%</p></article>
<article class="demo-card"><span class="icon-[lucide--calendar-days] demo-icon"></span><h3 data-t="cards.time.title">Dates and relative time</h3><p data-t="samples.date">15 August 2026</p><p data-t="samples.relative">3 days ago</p></article>
<article class="demo-card"><span class="icon-[lucide--list] demo-icon"></span><h3 data-t="cards.list.title">List formatting</h3><p data-t="samples.list">Mumbai, Pune, and Nagpur</p></article>
<article class="demo-card"><span class="icon-[lucide--route] demo-icon"></span><h3 data-t="cards.fallback.title">Fallback chains</h3><p data-t="cards.fallback.body">Missing regional content falls back to the base language and then English.</p></article>
<article class="demo-card"><span class="icon-[lucide--file-code-2] demo-icon"></span><h3 data-t="cards.document.title">Document language</h3><p data-t="cards.document.body">The same cookie sets the server-rendered html lang and dir attributes.</p></article>
</div>
</section>
<section class="rounded-3xl bg-[var(--wire-color-primary)] p-8 text-[var(--wire-color-primary-contrast)]">
<h2 class="m-0 text-2xl font-bold" data-t="api.title">Try request-aware formatting</h2>
<p class="mb-5 mt-2 opacity-85" data-t="api.description">The JSON endpoint uses ctx.lang, translations, Intl formatters, interpolation, plurals, and lists.</p>
<a href="/api/formats" class="inline-flex rounded-xl bg-white px-4 py-2 font-semibold text-slate-900" data-t="api.action">View localized JSON</a>
</section>
</div>
</main>
}
}
+55
View File
@@ -0,0 +1,55 @@
// AUTO-GENERATED by `wrnexus dev` - do not edit.
// Typed routes support required, optional, and catch-all parameters.
export interface Routes {
"/": Record<string, never>;
}
export type RoutePath = keyof Routes;
type RouteValue = string | readonly string[] | undefined;
function encodeRouteValue(value: RouteValue, catchAll: boolean): string {
if (value === undefined) return "";
const values = Array.isArray(value) ? value : catchAll ? String(value).split("/") : [String(value)];
return values.map((part) => encodeURIComponent(part)).join("/");
}
export function href<P extends RoutePath>(
path: P,
...args: keyof Routes[P] extends never
? []
: Record<string, never> extends Routes[P]
? [params?: Routes[P]]
: [params: Routes[P]]
): string {
const params = (args[0] ?? {}) as Record<string, RouteValue>;
const output: string[] = [];
for (const segment of String(path).split("/").filter(Boolean)) {
let name: string | undefined;
let optional = false;
let catchAll = false;
if (segment.startsWith("[[") && segment.endsWith("]]")) {
optional = true;
name = segment.slice(2, -2);
} else if (segment.startsWith("[") && segment.endsWith("]")) {
name = segment.slice(1, -1);
if (name.endsWith("?")) {
optional = true;
name = name.slice(0, -1);
}
}
if (!name) {
output.push(segment);
continue;
}
if (name.startsWith("...")) {
catchAll = true;
name = name.slice(3);
}
const value = params[name];
if (value === undefined && optional) continue;
if (value === undefined) throw new Error(`WRN-ROUTE-MISSING-PARAM: Missing route parameter '${name}'.`);
output.push(encodeRouteValue(value, catchAll));
}
return "/" + output.filter(Boolean).join("/");
}
@@ -0,0 +1,59 @@
@import "tailwindcss";
@plugin "@iconify/tailwind4";
@source "../**/*.wrn";
@source "../../../packages/i18n/components/*.wrn";
html {
color-scheme: light dark;
}
body {
margin: 0;
font-family: "Plus Jakarta Sans", "Noto Sans Devanagari", ui-sans-serif, system-ui, sans-serif;
}
/* The server sets <html lang> from the language cookie, so each script can use
a typography stack designed for its glyph shapes and metrics. */
html:lang(hi) body,
html:lang(mr) body {
font-family: "Noto Sans Devanagari", "Nirmala UI", Mangal, sans-serif;
}
html[dir="rtl"] body {
text-align: start;
}
.demo-card {
display: flex;
min-height: 12rem;
flex-direction: column;
border: 1px solid var(--wire-color-border);
border-radius: var(--wire-radius-xl);
background: var(--wire-color-surface);
padding: 1.25rem;
}
.demo-card h3 {
margin: 0.75rem 0 0.35rem;
font-size: 1rem;
}
.demo-card p {
margin: 0.2rem 0;
color: var(--wire-color-muted);
line-height: 1.6;
}
.demo-icon {
width: 1.4rem;
height: 1.4rem;
color: var(--wire-color-primary);
}
.demo-link {
margin-top: auto;
padding-top: 1rem;
color: var(--wire-color-primary);
font-size: 0.875rem;
font-weight: 600;
}
+25
View File
@@ -0,0 +1,25 @@
{
"name": "i18n-showcase",
"version": "0.8.0",
"private": true,
"type": "module",
"scripts": {
"dev": "bun run ../../packages/cli/src/index.ts dev .",
"build": "bun run ../../packages/cli/src/index.ts build .",
"test": "bun test test",
"typecheck": "tsc --noEmit -p tsconfig.json",
"check": "bun run typecheck && bun run test && bun run build"
},
"dependencies": {
"@wrnexus/core": "workspace:*",
"@wrnexus/i18n": "workspace:*",
"@wrnexus/ui": "workspace:*"
},
"devDependencies": {
"@iconify-json/lucide": "^1.2.118",
"@iconify/tailwind4": "^1.2.3",
"@tailwindcss/cli": "^4.0.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.9.2"
}
}
@@ -0,0 +1,41 @@
import { expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { loadLocales, makeT, resolveI18n, resolveLang, translationCoverage } from "@wrnexus/i18n";
const localeDir = join(import.meta.dir, "../app/locales");
const i18n = resolveI18n(loadLocales(localeDir, { strict: true }), {
default: "en",
locales: ["en", "hi", "mr"],
strict: true,
});
test("ships complete English, Hindi, and Marathi translations", () => {
expect(i18n.langs).toEqual(["en", "hi", "mr"]);
const coverage = translationCoverage(i18n);
expect(coverage.en?.percentage).toBe(100);
expect(coverage.hi?.percentage).toBe(100);
expect(coverage.mr?.percentage).toBe(100);
expect(makeT(i18n, "hi")("header.title")).toBe("बहुभाषी उदाहरण");
expect(makeT(i18n, "mr")("header.title")).toBe("बहुभाषिक उदाहरण");
});
test("resolves browser negotiation and persisted cookie preferences", () => {
expect(resolveLang(i18n, undefined, "mr-IN,hi;q=0.8,en;q=0.5")).toBe("mr");
expect(resolveLang(i18n, "hi", "mr;q=1")).toBe("hi");
});
test("uses the packaged language switcher and document shell", () => {
const page = readFileSync(join(import.meta.dir, "../app/pages/index.wrn"), "utf8");
const document = readFileSync(join(import.meta.dir, "../app/layouts/document.wrn"), "utf8");
expect(page).toContain(
'import LanguageSwitcher from "@wrnexus/i18n/components/LanguageSwitcher.wrn"',
);
expect(page).toContain("<LanguageSwitcher");
expect(page).not.toContain("locales=");
expect(page).not.toContain("current=");
expect(page).toContain('variant="compact"');
expect(page).toContain('variant="segmented"');
expect(page).toContain('t:placeholder="cards.attributes.placeholder"');
expect(document).toContain("<html>");
});
+5
View File
@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": { "noEmit": true },
"include": ["app/**/*.ts", "test/**/*.ts", "wrnexus.config.ts"]
}
+41
View File
@@ -0,0 +1,41 @@
import type { AppConfig } from "@wrnexus/styles";
const config = {
seo: {
title: "WRNexus i18n Showcase",
description: "English, Hindi, and Marathi internationalization examples.",
robots: "noindex,nofollow",
},
theme: { palette: "violet", default: "light" },
fonts: {
google: [
{ family: "Plus Jakarta Sans", weights: [400, 500, 600, 700] },
{ family: "Noto Sans Devanagari", weights: [400, 500, 600, 700] },
],
display: "swap",
sans: '"Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif',
},
// Keep localhost development deterministic. A service-worker navigation cache
// is URL-based and cannot distinguish pages rendered from different cookies.
pwa: { serviceWorker: false },
i18n: {
default: "en",
locales: ["en", "hi", "mr"],
labels: { en: "English", hi: "हिन्दी", mr: "मराठी" },
fallbacks: { hi: ["en"], mr: ["en"] },
cookie: { name: "wire-lang", path: "/", maxAge: 31_536_000, sameSite: "Lax" },
strict: true,
},
styles: {
entry: "app/styles/global.css",
failureMode: "throw",
process: async ({ entryPath, appRoot, mode }) => {
if (!entryPath) throw new Error("The i18n showcase stylesheet was not resolved.");
const args = ["@tailwindcss/cli", "-i", entryPath];
if (mode === "production") args.push("--minify");
return await Bun.$.cwd(appRoot)`bunx ${args}`.text();
},
},
} satisfies AppConfig;
export default config;