82 lines
3.7 KiB
JavaScript
82 lines
3.7 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFile } from "node:fs/promises";
|
|
import test from "node:test";
|
|
import vm from "node:vm";
|
|
|
|
const appSource = await readFile(new URL("../app.js", import.meta.url), "utf8");
|
|
const htmlSource = await readFile(new URL("../index.html", import.meta.url), "utf8");
|
|
const cssSource = await readFile(new URL("../styles.css", import.meta.url), "utf8");
|
|
|
|
function loadDictionaries() {
|
|
const start = appSource.indexOf("const I18N = ");
|
|
const end = appSource.indexOf("\n\nconst $ =", start);
|
|
assert.notEqual(start, -1, "I18N block should exist");
|
|
assert.notEqual(end, -1, "I18N block should have a stable end marker");
|
|
const expression = appSource.slice(start + "const I18N = ".length, end).trim().replace(/;$/, "");
|
|
return vm.runInNewContext(`(${expression})`, {
|
|
formatNumber: value => String(value)
|
|
});
|
|
}
|
|
|
|
const dictionaries = loadDictionaries();
|
|
const locales = Object.keys(dictionaries);
|
|
|
|
function placeholderNames(value) {
|
|
if (typeof value === "function") {
|
|
const match = value.toString().match(/\(\s*\{([^}]*)\}\s*\)/);
|
|
return match
|
|
? match[1].split(",").map(name => name.trim().split(/\s*=/)[0]).filter(Boolean).sort()
|
|
: [];
|
|
}
|
|
return [...String(value).matchAll(/\{(\w+)\}/g)].map(match => match[1]).sort();
|
|
}
|
|
|
|
test("all three locale dictionaries have identical keys", () => {
|
|
assert.deepEqual(locales, ["en", "zh", "th"]);
|
|
const expected = Object.keys(dictionaries.en).sort();
|
|
assert.equal(expected.length, 217);
|
|
for (const locale of locales) {
|
|
assert.deepEqual(Object.keys(dictionaries[locale]).sort(), expected, `${locale} key parity`);
|
|
}
|
|
});
|
|
|
|
test("parameter placeholders stay aligned across locales", () => {
|
|
for (const key of Object.keys(dictionaries.en)) {
|
|
const expected = placeholderNames(dictionaries.en[key]);
|
|
for (const locale of locales.slice(1)) {
|
|
assert.deepEqual(placeholderNames(dictionaries[locale][key]), expected, `${locale}.${key} placeholders`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("all literal translation references resolve in every locale", () => {
|
|
const referenced = new Set();
|
|
for (const match of appSource.matchAll(/\bt\(\s*["']([^"']+)["']/g)) referenced.add(match[1]);
|
|
for (const match of htmlSource.matchAll(/data-i18n(?:-placeholder|-aria-label)?="([^"]+)"/g)) referenced.add(match[1]);
|
|
for (const key of referenced) {
|
|
for (const locale of locales) assert.ok(key in dictionaries[locale], `${locale}.${key} is defined`);
|
|
}
|
|
assert.ok(referenced.size >= 100, `expected broad translation coverage, got ${referenced.size}`);
|
|
});
|
|
|
|
test("Thai locale metadata and three-way controls are present", () => {
|
|
assert.match(appSource, /th:\s*Object\.freeze\(\{ html: "th", intl: "th-TH-u-ca-gregory-nu-latn"/);
|
|
assert.equal((htmlSource.match(/data-login-language=/g) || []).length, 3);
|
|
assert.equal((htmlSource.match(/data-language=/g) || []).length, 3);
|
|
assert.match(cssSource, /grid-template-rows: repeat\(3, 1fr\)/);
|
|
assert.match(cssSource, /grid-template-columns: repeat\(3, 1fr\)/);
|
|
assert.match(cssSource, /\[data-locale="th"\][^{]*\{ transform: translateY\(200%\); \}/);
|
|
assert.match(cssSource, /\[data-locale="th"\][^{]*\{ transform: translateX\(200%\); \}/);
|
|
});
|
|
|
|
test("Thai messages do not fall back for representative runtime states", () => {
|
|
const keys = [
|
|
"signIn", "invalidCredentials", "sessionExpired", "runtimeApiLoadingMessage", "accountsShown",
|
|
"recordsShown", "checkoutLater", "insufficientPrivileges", "usageSaveNetwork", "usageSavedMessage"
|
|
];
|
|
for (const key of keys) {
|
|
assert.notEqual(dictionaries.th[key], undefined, `Thai key ${key}`);
|
|
if (typeof dictionaries.th[key] === "string") assert.notEqual(dictionaries.th[key], dictionaries.en[key], `Thai copy for ${key}`);
|
|
}
|
|
});
|