83 lines
3.0 KiB
TypeScript
83 lines
3.0 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import ts from "typescript";
|
|
import russian from "../shared/locales/ru.json";
|
|
import { detectLocale, resolveLocale, translator } from "../shared/i18n";
|
|
|
|
test("browser language preferences support regional tags and weighted fallbacks", () => {
|
|
assert.equal(detectLocale("ru-RU,ru;q=0.9,en;q=0.8"), "ru");
|
|
assert.equal(detectLocale("en-US;q=0.3,ru;q=0.9"), "ru");
|
|
assert.equal(detectLocale("de-DE,ru;q=0.7,en;q=0.5"), "ru");
|
|
assert.equal(detectLocale("ru;q=0,en-GB;q=0.8"), "en");
|
|
assert.equal(detectLocale("ru;q=invalid,en"), "en");
|
|
assert.equal(detectLocale(null), "en");
|
|
assert.equal(detectLocale("fr-FR"), "en");
|
|
});
|
|
|
|
test("an explicit language choice overrides automatic detection", () => {
|
|
assert.equal(resolveLocale("en", "ru-RU"), "en");
|
|
assert.equal(resolveLocale("ru", "en-US"), "ru");
|
|
assert.equal(resolveLocale("auto", "ru"), "ru");
|
|
assert.equal(resolveLocale("unexpected", "ru"), "ru");
|
|
});
|
|
|
|
test("translations interpolate values without changing player content", () => {
|
|
const ru = translator("ru"),
|
|
en = translator("en");
|
|
assert.equal(ru("Log in"), "Войти");
|
|
assert.equal(en("Log in"), "Log in");
|
|
assert.equal(
|
|
ru("Keep {location}", { location: "Steve's Castle" }),
|
|
"Оставить: Steve's Castle",
|
|
);
|
|
assert.equal(ru("Map authored by Steve"), "Map authored by Steve");
|
|
assert.equal(ru("Keep {location}"), "Оставить: {location}");
|
|
});
|
|
|
|
test("all literal interface messages have Russian translations with matching placeholders", () => {
|
|
const missing: string[] = [],
|
|
mismatches: string[] = [];
|
|
const dictionary = russian as Record<string, string>;
|
|
const placeholders = (s: string) =>
|
|
[...s.matchAll(/\{(\w+)\}/g)].map((match) => match[1]).sort();
|
|
function walk(directory: string) {
|
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
const file = path.join(directory, entry.name);
|
|
if (entry.isDirectory()) walk(file);
|
|
else if (file.endsWith(".tsx")) {
|
|
const source = ts.createSourceFile(
|
|
file,
|
|
fs.readFileSync(file, "utf8"),
|
|
ts.ScriptTarget.Latest,
|
|
true,
|
|
ts.ScriptKind.TSX,
|
|
);
|
|
function visit(node: ts.Node) {
|
|
if (
|
|
ts.isCallExpression(node) &&
|
|
node.expression.getText(source) === "t" &&
|
|
node.arguments[0] &&
|
|
ts.isStringLiteral(node.arguments[0])
|
|
) {
|
|
const message = node.arguments[0].text;
|
|
if (!dictionary[message]) missing.push(`${file}: ${message}`);
|
|
else if (
|
|
JSON.stringify(placeholders(message)) !==
|
|
JSON.stringify(placeholders(dictionary[message]))
|
|
)
|
|
mismatches.push(message);
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
}
|
|
visit(source);
|
|
}
|
|
}
|
|
}
|
|
walk("app");
|
|
walk("components");
|
|
assert.deepEqual(missing, []);
|
|
assert.deepEqual(mismatches, []);
|
|
});
|