From b787670f8ea995ed16ec63100dbead6c99213832 Mon Sep 17 00:00:00 2001 From: Markus Hartung Date: Wed, 2 Sep 2026 04:49:02 -0300 Subject: [PATCH] test(i18n): derive remaining locale counts from config and add reverse surface parity --- .../unit/i18n-locale-surfaces-parity.test.ts | 47 ++++++++++++++++++- tests/unit/radar-claim-buttons.test.ts | 8 +++- tests/unit/radar-key-input.test.ts | 8 +++- tests/unit/radar-optin-rules-copy.test.ts | 7 ++- 4 files changed, 63 insertions(+), 7 deletions(-) diff --git a/tests/unit/i18n-locale-surfaces-parity.test.ts b/tests/unit/i18n-locale-surfaces-parity.test.ts index 19b74f4efd..4c9ae7ad60 100644 --- a/tests/unit/i18n-locale-surfaces-parity.test.ts +++ b/tests/unit/i18n-locale-surfaces-parity.test.ts @@ -1,6 +1,6 @@ import test from "node:test"; import assert from "node:assert/strict"; -import { existsSync, readFileSync } from "node:fs"; +import { existsSync, readdirSync, readFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import i18nConfig from "../../config/i18n.json" with { type: "json" }; @@ -11,7 +11,9 @@ import i18nConfig from "../../config/i18n.json" with { type: "json" }; * 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. + * catalogs. The reverse direction is guarded as well: every docs-surface entry + * (index row, README flag link, docs/i18n/ directory) must map back to a + * configured docs locale, so a retired locale cannot leave orphans behind. */ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", ".."); @@ -43,3 +45,44 @@ test("README headline count matches config/i18n.json", () => { assert.ok(m, "README must carry the 'In N languages' headline"); assert.equal(Number(m[1]), i18nConfig.locales.length); }); + +test("docs/i18n/README.md counts sentence matches config/i18n.json", () => { + const sentence = `into ${i18nConfig.locales.length - 1} languages; together with the English source, the UI supports ${i18nConfig.locales.length} locales`; + assert.ok(docsIndex.includes(sentence), `docs/i18n/README.md must contain "${sentence}"`); +}); + +test("no orphan locale surface outside config/i18n.json (reverse parity)", () => { + const docsLocales = new Set( + i18nConfig.locales.map(({ code }) => code).filter((code) => !docsExcluded.has(code)) + ); + const offenders: string[] = []; + + // docs/i18n/README.md rows: "- … (`code`): [Docs Root](./code/README.md)" + for (const line of docsIndex.split("\n")) { + const row = line.match(/^- .*\(`([^`]+)`\): \[Docs Root\]\(\.\/([^/]+)\/README\.md\)/); + if (!row) continue; + const [, code, linkCode] = row; + if (!docsLocales.has(code)) offenders.push(`docs/i18n/README.md row: ${code}`); + if (linkCode !== code) { + offenders.push(`docs/i18n/README.md row ${code} links to ./${linkCode}/README.md`); + } + } + + // Root README.md language block: docs/i18n//README.md flag links. + for (const [, code] of readme.matchAll(/docs\/i18n\/([^/\s"')]+)\/README\.md/g)) { + if (!docsLocales.has(code)) offenders.push(`README.md link: ${code}`); + } + + // Every directory under docs/i18n/ must be a configured docs locale. + for (const entry of readdirSync(path.join(ROOT, "docs", "i18n"), { withFileTypes: true })) { + if (entry.isDirectory() && !docsLocales.has(entry.name)) { + offenders.push(`docs/i18n/ directory: ${entry.name}`); + } + } + + assert.deepEqual( + offenders, + [], + `orphan locale surfaces that are not a configured docs locale (config/i18n.json minus docsExcluded) — retired locale left behind?: ${offenders.join("; ")}` + ); +}); diff --git a/tests/unit/radar-claim-buttons.test.ts b/tests/unit/radar-claim-buttons.test.ts index 7ae3d0ef31..3637055ca2 100644 --- a/tests/unit/radar-claim-buttons.test.ts +++ b/tests/unit/radar-claim-buttons.test.ts @@ -22,6 +22,7 @@ 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_SRC = fs.readFileSync(PAGE_PATH, "utf-8"); @@ -70,7 +71,7 @@ test("radar page: references the 5 new claim-section t(...) keys", () => { } }); -test("radar page + all 43 locale files: no price/monetary value in the claim section copy (D14)", () => { +test(`radar page + all ${i18nConfig.locales.length} locale files: no price/monetary value in the claim section copy (D14)`, () => { // D14: no pricing anywhere in the OSS repo, only a link to the plans page. const PRICE_PATTERN = /\$\s?\d|R\$\s?\d|\d+[.,]\d{2}\s?(USD|BRL|EUR)|\b(lifetime|life-time)\b.{0,20}\$/i; @@ -78,7 +79,10 @@ test("radar page + all 43 locale files: no price/monetary value in the claim sec 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}` + ); for (const file of files) { const data = JSON.parse(fs.readFileSync(path.join(messagesDir, file), "utf-8")); diff --git a/tests/unit/radar-key-input.test.ts b/tests/unit/radar-key-input.test.ts index 6e89d270b8..1ca3e202a7 100644 --- a/tests/unit/radar-key-input.test.ts +++ b/tests/unit/radar-key-input.test.ts @@ -26,6 +26,7 @@ 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_SRC = fs.readFileSync(PAGE_PATH, "utf-8"); @@ -114,14 +115,17 @@ test("radar page: references the 4 new key-input t(...) keys", () => { } }); -test("radar page + all 43 locale files: no price/monetary value in the key-input copy (D14)", () => { +test(`radar page + all ${i18nConfig.locales.length} locale files: no price/monetary value in the key-input copy (D14)`, () => { const PRICE_PATTERN = /\$\s?\d|R\$\s?\d|\d+[.,]\d{2}\s?(USD|BRL|EUR)|\b(lifetime|life-time)\b.{0,20}\$/i; assert.ok(!PRICE_PATTERN.test(PAGE_SRC), "page.tsx must not contain a price/monetary 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}` + ); for (const file of files) { const data = JSON.parse(fs.readFileSync(path.join(messagesDir, file), "utf-8")); diff --git a/tests/unit/radar-optin-rules-copy.test.ts b/tests/unit/radar-optin-rules-copy.test.ts index c3042782c0..3430aa91da 100644 --- a/tests/unit/radar-optin-rules-copy.test.ts +++ b/tests/unit/radar-optin-rules-copy.test.ts @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; import test from "node:test"; +import i18nConfig from "../../config/i18n.json" with { type: "json" }; function radarCopy(locale: "en" | "pt-BR"): Record { const file = path.resolve(process.cwd(), `src/i18n/messages/${locale}.json`); @@ -101,7 +102,11 @@ test("Brazilian Portuguese Radar opt-in copy states D32 without a PR-count grant test("every UI locale carries the D32 keys and none retains the superseded 5+ PR promise", () => { const messagesDir = path.resolve(process.cwd(), "src/i18n/messages"); const files = fs.readdirSync(messagesDir).filter((file) => file.endsWith(".json")); - assert.equal(files.length, 43, "expected the complete 43-locale UI catalog"); + assert.equal( + files.length, + i18nConfig.locales.length, + `expected the complete ${i18nConfig.locales.length}-locale UI catalog` + ); for (const file of files) { const messages = JSON.parse(fs.readFileSync(path.join(messagesDir, file), "utf8")) as {