test(i18n): derive locale count from config and guard every locale surface

This commit is contained in:
Markus Hartung
2026-09-02 04:37:27 -03:00
parent 87df0621b5
commit ba768e5312
6 changed files with 77 additions and 38 deletions

View File

@@ -20,6 +20,7 @@ Translations of documentation into 42 languages; together with the English sourc
- 🇮🇳 **हिन्दी** (`hi`): [Docs Root](./hi/README.md)
- 🇭🇺 **Magyar** (`hu`): [Docs Root](./hu/README.md)
- 🇮🇩 **Bahasa Indonesia** (`id`): [Docs Root](./id/README.md)
- 🇮🇩 **Bahasa Indonesia (Alt)** (`in`): [Docs Root](./in/README.md)
- 🇮🇹 **Italiano** (`it`): [Docs Root](./it/README.md)
- 🇯🇵 **日本語** (`ja`): [Docs Root](./ja/README.md)
- 🇰🇷 **한국어** (`ko`): [Docs Root](./ko/README.md)

View File

@@ -3,6 +3,7 @@ import assert from "node:assert/strict";
import { readFileSync, readdirSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import i18nConfig from "../../config/i18n.json" with { type: "json" };
interface G4fProviderMetadata {
hasFree?: boolean;
@@ -75,8 +76,8 @@ test("g4f.space metadata describes conditional access and the remote data bounda
test("every shipped locale gives both g4f.space access paths without a fixed quota", () => {
assert.ok(
localeFiles.length >= 43,
`expected the 43 shipped locales, found ${localeFiles.length}`
localeFiles.length >= i18nConfig.locales.length,
`expected the ${i18nConfig.locales.length} configured locales, found ${localeFiles.length}`
);
for (const file of localeFiles) {

View File

@@ -2,6 +2,7 @@ import { describe, it } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import i18nConfig from "../../config/i18n.json" with { type: "json" };
const MESSAGES_DIR = path.resolve("src/i18n/messages");
const KEY = "denoRelayOrgDomainHint";
@@ -28,17 +29,15 @@ const ENTITY_ORG_SLUG = "<org-slug>";
*/
describe("i18n — denoRelayOrgDomainHint UNCLOSED_TAG regression", () => {
const localeFiles = fs
.readdirSync(MESSAGES_DIR)
.filter((f) => f.endsWith(".json"));
const expectedCount = 43;
const localeFiles = fs.readdirSync(MESSAGES_DIR).filter((f) => f.endsWith(".json"));
const expectedCount = i18nConfig.locales.length;
// --- Test 1: JSON validity (no BOM, no parse errors) ---
it(`all ${expectedCount} locale JSON files are valid (no BOM, no parse errors)`, () => {
assert.equal(
localeFiles.length,
expectedCount,
`Expected ${expectedCount} locale files, found ${localeFiles.length}`,
`Expected ${expectedCount} locale files, found ${localeFiles.length}`
);
const invalid: string[] = [];
@@ -64,7 +63,7 @@ describe("i18n — denoRelayOrgDomainHint UNCLOSED_TAG regression", () => {
assert.deepEqual(
invalid,
[],
`${invalid.length} invalid JSON file(s). First few: ${invalid.slice(0, 5).join("; ")}`,
`${invalid.length} invalid JSON file(s). First few: ${invalid.slice(0, 5).join("; ")}`
);
});
@@ -73,9 +72,7 @@ describe("i18n — denoRelayOrgDomainHint UNCLOSED_TAG regression", () => {
const missingKey: string[] = [];
for (const file of localeFiles) {
const content = JSON.parse(
fs.readFileSync(path.join(MESSAGES_DIR, file), "utf8"),
);
const content = JSON.parse(fs.readFileSync(path.join(MESSAGES_DIR, file), "utf8"));
const flat = flatten(content);
// The key lives in a namespace (e.g. "settings.denoRelayOrgDomainHint")
// — match any path that ends with the target key name.
@@ -88,7 +85,7 @@ describe("i18n — denoRelayOrgDomainHint UNCLOSED_TAG regression", () => {
assert.deepEqual(
missingKey,
[],
`${missingKey.length} locale(s) missing key "${KEY}": ${missingKey.join(", ")}`,
`${missingKey.length} locale(s) missing key "${KEY}": ${missingKey.join(", ")}`
);
});
@@ -109,7 +106,7 @@ describe("i18n — denoRelayOrgDomainHint UNCLOSED_TAG regression", () => {
assert.deepEqual(
offenders,
[],
`${offenders.length} file(s) still carry raw angle-bracket placeholders: ${offenders.join(", ")}`,
`${offenders.length} file(s) still carry raw angle-bracket placeholders: ${offenders.join(", ")}`
);
});
@@ -118,9 +115,7 @@ describe("i18n — denoRelayOrgDomainHint UNCLOSED_TAG regression", () => {
const wrong: string[] = [];
for (const file of localeFiles) {
const content = JSON.parse(
fs.readFileSync(path.join(MESSAGES_DIR, file), "utf8"),
);
const content = JSON.parse(fs.readFileSync(path.join(MESSAGES_DIR, file), "utf8"));
const flat = flatten(content);
// Find the full dotted path (e.g. "settings.denoRelayOrgDomainHint")
@@ -145,16 +140,14 @@ describe("i18n — denoRelayOrgDomainHint UNCLOSED_TAG regression", () => {
}
// Double-check: the encoded value should contain the full URL pattern
if (!value.includes(`${ENTITY_APP_NAME}.${ENTITY_ORG_SLUG}`)) {
wrong.push(
`${file}: missing "${ENTITY_APP_NAME}.${ENTITY_ORG_SLUG}" sequence`,
);
wrong.push(`${file}: missing "${ENTITY_APP_NAME}.${ENTITY_ORG_SLUG}" sequence`);
}
}
assert.deepEqual(
wrong,
[],
`${wrong.length} locale(s) with wrong value for "${KEY}": ${wrong.slice(0, 10).join(", ")}`,
`${wrong.length} locale(s) with wrong value for "${KEY}": ${wrong.slice(0, 10).join(", ")}`
);
});
});
@@ -166,10 +159,7 @@ describe("i18n — denoRelayOrgDomainHint UNCLOSED_TAG regression", () => {
* E.g. { settings: { denoRelayOrgDomainHint: "..." } } →
* { "settings.denoRelayOrgDomainHint": "..." }
*/
function flatten(
obj: Record<string, unknown>,
prefix = "",
): Record<string, unknown> {
function flatten(obj: Record<string, unknown>, prefix = ""): Record<string, unknown> {
const out: Record<string, unknown> = {};
for (const k of Object.keys(obj)) {
const key = prefix ? `${prefix}.${k}` : k;

View File

@@ -0,0 +1,45 @@
import test from "node:test";
import assert from "node:assert/strict";
import { existsSync, readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import i18nConfig from "../../config/i18n.json" with { type: "json" };
/**
* Parity guard: every locale declared in config/i18n.json (the single source of
* truth) must exist on every in-repo surface — dashboard catalog, CLI catalog,
* docs mirror (README.md / llm.txt / CHANGELOG.md), README flag link and the
* docs/i18n/README.md index row — and the README headline count must match.
* Locales listed in `docsExcluded` (the English source) only need the two
* catalogs.
*/
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
const docsExcluded = new Set(i18nConfig.docsExcluded ?? ["en"]);
const readme = readFileSync(path.join(ROOT, "README.md"), "utf8");
const docsIndex = readFileSync(path.join(ROOT, "docs", "i18n", "README.md"), "utf8");
for (const { code } of i18nConfig.locales) {
test(`locale ${code} exists on every in-repo surface`, () => {
assert.ok(
existsSync(path.join(ROOT, "src", "i18n", "messages", `${code}.json`)),
"dashboard catalog"
);
assert.ok(existsSync(path.join(ROOT, "bin", "cli", "locales", `${code}.json`)), "CLI catalog");
if (docsExcluded.has(code)) return;
assert.ok(existsSync(path.join(ROOT, "docs", "i18n", code, "README.md")), "docs mirror README");
assert.ok(existsSync(path.join(ROOT, "docs", "i18n", code, "llm.txt")), "llm.txt mirror");
assert.ok(
existsSync(path.join(ROOT, "docs", "i18n", code, "CHANGELOG.md")),
"CHANGELOG mirror"
);
assert.ok(readme.includes(`docs/i18n/${code}/README.md`), "README flag link");
assert.ok(docsIndex.includes(`(\`${code}\`)`), "docs/i18n/README.md index row");
});
}
test("README headline count matches config/i18n.json", () => {
const m = readme.match(/In (\d+) languages/);
assert.ok(m, "README must carry the 'In N languages' headline");
assert.equal(Number(m[1]), i18nConfig.locales.length);
});

View File

@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
import { readdirSync, readFileSync } from "node:fs";
import path from "node:path";
import test from "node:test";
import i18nConfig from "../../config/i18n.json" with { type: "json" };
const messagesDirectory = path.resolve("src/i18n/messages");
const requiredKeys = [
@@ -18,11 +19,11 @@ const requiredKeys = [
"modalityBridgeVideoMaxVideos",
] as const;
test("all 43 UI locale catalogs contain non-placeholder Video Bridge settings", () => {
test(`all ${i18nConfig.locales.length} UI locale catalogs contain non-placeholder Video Bridge settings`, () => {
const catalogs = readdirSync(messagesDirectory)
.filter((file) => file.endsWith(".json"))
.sort();
assert.equal(catalogs.length, 43);
assert.equal(catalogs.length, i18nConfig.locales.length);
const english = JSON.parse(readFileSync(path.join(messagesDirectory, "en.json"), "utf8")) as {
settings: Record<string, string>;
};

View File

@@ -17,11 +17,9 @@ import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import i18nConfig from "../../config/i18n.json" with { type: "json" };
const PAGE_PATH = path.resolve(
process.cwd(),
"src/app/(dashboard)/dashboard/radar/page.tsx"
);
const PAGE_PATH = path.resolve(process.cwd(), "src/app/(dashboard)/dashboard/radar/page.tsx");
const PAGE_SRC = fs.readFileSync(PAGE_PATH, "utf-8");
test("radar page: fetches referrals only from the local /api/radar/referrals route", () => {
@@ -63,12 +61,9 @@ test("no new dashboard route was created for the free-credits feature (spec: reu
);
});
test("every t(\"...\") key referenced in the referrals tab section exists (non-empty) in en.json", () => {
test('every t("...") key referenced in the referrals tab section exists (non-empty) in en.json', () => {
const enMessages = JSON.parse(
fs.readFileSync(
path.resolve(process.cwd(), "src/i18n/messages/en.json"),
"utf-8"
)
fs.readFileSync(path.resolve(process.cwd(), "src/i18n/messages/en.json"), "utf-8")
);
const radarPage = enMessages.radarPage as Record<string, unknown>;
assert.ok(radarPage, "en.json must have a radarPage namespace");
@@ -87,16 +82,22 @@ test("every t(\"...\") key referenced in the referrals tab section exists (non-e
];
for (const key of referencedKeys) {
assert.ok(PAGE_SRC.includes(`t("${key}")` ) || PAGE_SRC.includes(`t("${key}",`), `page.tsx must reference t("${key}")`);
assert.ok(
PAGE_SRC.includes(`t("${key}")`) || PAGE_SRC.includes(`t("${key}",`),
`page.tsx must reference t("${key}")`
);
assert.equal(typeof radarPage[key], "string", `en.json radarPage.${key} must be a string`);
assert.ok((radarPage[key] as string).length > 0, `en.json radarPage.${key} must be non-empty`);
}
});
test("all 43 locale message files carry every new radarPage key with a non-empty value", () => {
test("every locale message file (config/i18n.json) carries every new radarPage key with a non-empty value", () => {
const messagesDir = path.resolve(process.cwd(), "src/i18n/messages");
const files = fs.readdirSync(messagesDir).filter((f) => f.endsWith(".json"));
assert.ok(files.length >= 40, `expected ~43 locale files, found ${files.length}`);
assert.ok(
files.length >= i18nConfig.locales.length,
`expected the ${i18nConfig.locales.length} configured locales, found ${files.length}`
);
const NEW_KEYS = [
"catalogTab",