release: WRNexusJS 0.8.0
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
createLocaleFormatter,
|
||||
formatMessage,
|
||||
loadRouteMessages,
|
||||
resolveI18n,
|
||||
withTenantMessages,
|
||||
} from "../src/index.ts";
|
||||
|
||||
test("formats ICU plural, exact and gender-aware select messages", () => {
|
||||
expect(
|
||||
formatMessage("{count, plural, =0 {None} one {# item} other {# items}}", { count: 2 }, "en"),
|
||||
).toBe("2 items");
|
||||
expect(
|
||||
formatMessage(
|
||||
"{gender, select, female {She} male {He} other {They}} approved",
|
||||
{ gender: "female" },
|
||||
"en",
|
||||
),
|
||||
).toBe("She approved");
|
||||
});
|
||||
test("supports calendars, tenant overrides and route-scoped loading", () => {
|
||||
const formatter = createLocaleFormatter("en-US", "UTC", "indian");
|
||||
expect(
|
||||
formatter.date(new Date("2026-08-02T00:00:00Z"), { year: "numeric", calendar: "indian" }),
|
||||
).toBeTruthy();
|
||||
const base = resolveI18n({ en: { title: "Default" } }, { default: "en" });
|
||||
expect(withTenantMessages(base, { en: { title: "Tenant" } }).messages.en?.title).toBe("Tenant");
|
||||
expect(base.messages.en?.title).toBe("Default");
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-i18n-route-"));
|
||||
mkdirSync(join(root, "en/routes/users"), { recursive: true });
|
||||
writeFileSync(join(root, "en/common.json"), JSON.stringify({ common: "Common" }));
|
||||
writeFileSync(join(root, "en/routes/users/index.json"), JSON.stringify({ title: "Users" }));
|
||||
expect(loadRouteMessages(root, "en", "/users/index")).toEqual({
|
||||
common: "Common",
|
||||
title: "Users",
|
||||
});
|
||||
});
|
||||
@@ -38,3 +38,13 @@ test("translateHtml is a no-op without markers", () => {
|
||||
const t = makeT(i18n, "en");
|
||||
expect(translateHtml("<p>plain</p>", t)).toBe("<p>plain</p>");
|
||||
});
|
||||
|
||||
test("translateHtml preserves authored fallback text when a key is unavailable", () => {
|
||||
const t = (key: string) => key;
|
||||
expect(translateHtml('<h1 data-t="missing.title">Readable fallback</h1>', t)).toBe(
|
||||
'<h1 data-t="missing.title">Readable fallback</h1>',
|
||||
);
|
||||
expect(translateHtml('<input t:placeholder="missing.placeholder" />', t)).toBe(
|
||||
'<input placeholder="" />',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
loadLocales,
|
||||
localeDirection,
|
||||
makeT,
|
||||
renderI18nData,
|
||||
resolveI18n,
|
||||
resolveLang,
|
||||
} from "../src/index.ts";
|
||||
|
||||
describe("i18n package kit", () => {
|
||||
test("loads top-level and namespaced locale files recursively", () => {
|
||||
const directory = mkdtempSync(join(tmpdir(), "wrn-i18n-"));
|
||||
try {
|
||||
writeFileSync(join(directory, "en.json"), JSON.stringify({ app: { title: "Portal" } }));
|
||||
mkdirSync(join(directory, "fr"));
|
||||
writeFileSync(
|
||||
join(directory, "fr", "common.json"),
|
||||
JSON.stringify({ hello: "Bonjour {name}" }),
|
||||
);
|
||||
const i18n = resolveI18n(loadLocales(directory, { strict: true }), {
|
||||
default: "en",
|
||||
locales: ["en", "fr"],
|
||||
});
|
||||
expect(makeT(i18n, "fr")("common.hello", { name: "Ajay" })).toBe("Bonjour Ajay");
|
||||
expect(resolveLang(i18n, undefined, "en;q=0.6, fr;q=0.9")).toBe("fr");
|
||||
expect(resolveLang(i18n, undefined, "*")).toBe("en");
|
||||
expect(renderI18nData(i18n, "fr")).toContain("Bonjour");
|
||||
expect(renderI18nData(i18n, "fr")).toContain('"directions"');
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("ships a cookie-backed language switcher that targets the native select", () => {
|
||||
const source = readFileSync(
|
||||
join(import.meta.dir, "../components/LanguageSwitcher.wrn"),
|
||||
"utf8",
|
||||
);
|
||||
expect(source).toContain("<select");
|
||||
expect(source).toContain("data-wire-lang");
|
||||
expect(source).toMatch(/<select[\s\S]*data-wire-lang/);
|
||||
expect(source).toContain("selected='{locale.value == current}'");
|
||||
expect(source).toContain('variant == "segmented"');
|
||||
expect(source).toContain("wire-language-switcher--compact");
|
||||
});
|
||||
|
||||
test("replaces primitive namespace collisions safely", () => {
|
||||
const directory = mkdtempSync(join(tmpdir(), "wrn-i18n-collision-"));
|
||||
try {
|
||||
writeFileSync(join(directory, "en.json"), JSON.stringify({ common: "legacy" }));
|
||||
mkdirSync(join(directory, "en"));
|
||||
writeFileSync(join(directory, "en", "common.json"), JSON.stringify({ save: "Save" }));
|
||||
const i18n = resolveI18n(loadLocales(directory, { strict: true }), { default: "en" });
|
||||
expect(makeT(i18n, "en")("common.save")).toBe("Save");
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("resolves RTL direction from language subtags", () => {
|
||||
expect(localeDirection("ar-IN")).toBe("rtl");
|
||||
expect(localeDirection("en-IN")).toBe("ltr");
|
||||
});
|
||||
|
||||
test("rejects prototype-polluting locale keys", () => {
|
||||
const directory = mkdtempSync(join(tmpdir(), "wrn-i18n-unsafe-"));
|
||||
try {
|
||||
writeFileSync(join(directory, "en.json"), '{"constructor":{"prototype":{"polluted":true}}}');
|
||||
expect(() => loadLocales(directory, { strict: true })).toThrow();
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import {
|
||||
auditLocaleKeys,
|
||||
createPseudoLocale,
|
||||
extractTranslationKeys,
|
||||
flattenMessageKeys,
|
||||
localeDirection,
|
||||
pseudoLocalize,
|
||||
} from "../src/index.ts";
|
||||
|
||||
test("extracts static translation keys from code and WRN attributes", () => {
|
||||
const keys = extractTranslationKeys(`
|
||||
const title = t("account.title")
|
||||
const copy = i18n.t('account.copy')
|
||||
const dynamic = t(key)
|
||||
<p data-i18n="account.help"></p>
|
||||
`);
|
||||
expect(keys.map(({ key }) => key)).toEqual(["account.copy", "account.help", "account.title"]);
|
||||
});
|
||||
|
||||
test("audits locale completeness against a reference locale", () => {
|
||||
const messages = {
|
||||
en: { common: { hello: "Hello", bye: "Bye" } },
|
||||
fr: { common: { hello: "Bonjour", extra: "Extra" } },
|
||||
};
|
||||
expect(flattenMessageKeys(messages.en)).toEqual(["common.bye", "common.hello"]);
|
||||
expect(auditLocaleKeys(messages, "en").fr).toEqual({
|
||||
missing: ["common.bye"],
|
||||
extra: ["common.extra"],
|
||||
});
|
||||
});
|
||||
|
||||
test("pseudo-locales preserve placeholders and exercise LTR and RTL layouts", () => {
|
||||
expect(pseudoLocalize("Hello {name} <strong>today</strong>")).toContain("{name}");
|
||||
const pseudo = createPseudoLocale({ greeting: "Hello {name}", nested: { save: "Save" } });
|
||||
expect(pseudo.greeting).toBe("[Hëllô {name}~~~]");
|
||||
expect((pseudo.nested as Record<string, string>).save).toBe("[Sàvë~~~]");
|
||||
expect(pseudoLocalize("Hello {name}", { rtl: true }).startsWith("\u202e[")).toBe(true);
|
||||
expect(localeDirection("ar-XB")).toBe("rtl");
|
||||
});
|
||||
Reference in New Issue
Block a user