From ba768e5312d8d938de7f11f483b56100edb8b8bd Mon Sep 17 00:00:00 2001 From: Markus Hartung Date: Wed, 2 Sep 2026 04:37:27 -0300 Subject: [PATCH] test(i18n): derive locale count from config and guard every locale surface --- docs/i18n/README.md | 1 + .../g4f-space-gateway-truthful-copy.test.ts | 5 ++- .../unit/i18n-deno-relay-unclosed-tag.test.ts | 34 +++++--------- .../unit/i18n-locale-surfaces-parity.test.ts | 45 +++++++++++++++++++ tests/unit/modality-bridge-video-i18n.test.ts | 5 ++- tests/unit/radar-referrals-page-tab.test.ts | 25 ++++++----- 6 files changed, 77 insertions(+), 38 deletions(-) create mode 100644 tests/unit/i18n-locale-surfaces-parity.test.ts diff --git a/docs/i18n/README.md b/docs/i18n/README.md index 507933cbd7..276a42213c 100644 --- a/docs/i18n/README.md +++ b/docs/i18n/README.md @@ -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) diff --git a/tests/unit/g4f-space-gateway-truthful-copy.test.ts b/tests/unit/g4f-space-gateway-truthful-copy.test.ts index 7f248fe877..bc6b706419 100644 --- a/tests/unit/g4f-space-gateway-truthful-copy.test.ts +++ b/tests/unit/g4f-space-gateway-truthful-copy.test.ts @@ -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) { diff --git a/tests/unit/i18n-deno-relay-unclosed-tag.test.ts b/tests/unit/i18n-deno-relay-unclosed-tag.test.ts index 2444cc75fd..4b3de678a7 100644 --- a/tests/unit/i18n-deno-relay-unclosed-tag.test.ts +++ b/tests/unit/i18n-deno-relay-unclosed-tag.test.ts @@ -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, - prefix = "", -): Record { +function flatten(obj: Record, prefix = ""): Record { const out: Record = {}; for (const k of Object.keys(obj)) { const key = prefix ? `${prefix}.${k}` : k; diff --git a/tests/unit/i18n-locale-surfaces-parity.test.ts b/tests/unit/i18n-locale-surfaces-parity.test.ts new file mode 100644 index 0000000000..19b74f4efd --- /dev/null +++ b/tests/unit/i18n-locale-surfaces-parity.test.ts @@ -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); +}); diff --git a/tests/unit/modality-bridge-video-i18n.test.ts b/tests/unit/modality-bridge-video-i18n.test.ts index b5674de9e3..f4a5a57d35 100644 --- a/tests/unit/modality-bridge-video-i18n.test.ts +++ b/tests/unit/modality-bridge-video-i18n.test.ts @@ -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; }; diff --git a/tests/unit/radar-referrals-page-tab.test.ts b/tests/unit/radar-referrals-page-tab.test.ts index 2d2359966d..be93034230 100644 --- a/tests/unit/radar-referrals-page-tab.test.ts +++ b/tests/unit/radar-referrals-page-tab.test.ts @@ -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; 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",