release: WRNexusJS 0.8.0
This commit is contained in:
@@ -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.
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>");
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": { "noEmit": true },
|
||||
"include": ["app/**/*.ts", "test/**/*.ts", "wrnexus.config.ts"]
|
||||
}
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user