Files
MineGeo/tests/i18n.test.ts

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, []);
});