From 4ecfbd25a5e745d67b568a4dc5283fa39ccdcd65 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Mon, 31 Aug 2026 12:12:39 -0700 Subject: [PATCH] Register Dutch, and catch a catalogue nobody can select nl.ts shipped without an entry in UI_LANGUAGES, so the language was never offered: the catalogue built, every test passed, the coverage check reported 98%, and the picker did not list it. The entry was added by a text replacement anchored on the French line, which does not exist on a branch cut from main, so the replacement was a silent no-op. A catalogue and a picker entry are two halves of one thing and either half alone is dead weight, so the checker now verifies both directions -- a catalogue with no entry, and an entry with no catalogue. Reverting the one-line fix makes it fail, which is the only way to know a check works. --- scripts/i18n-catalog-check.mjs | 25 +++++++++++++++++++++++++ web/src/lib/languages.ts | 1 + 2 files changed, 26 insertions(+) diff --git a/scripts/i18n-catalog-check.mjs b/scripts/i18n-catalog-check.mjs index 695857a..289aa8d 100644 --- a/scripts/i18n-catalog-check.mjs +++ b/scripts/i18n-catalog-check.mjs @@ -56,7 +56,32 @@ for (const file of globSync("web/src/**/*.{ts,tsx}").filter((f) => !f.includes(" visit(src); } +/* + * A catalogue and a picker entry are two halves of one thing, and either half + * alone is dead weight. A catalogue with no entry in UI_LANGUAGES never + * reaches a reader -- it builds, it passes every test, and the language simply + * is not offered. That happened to Dutch: the entry was added by a text + * replacement anchored on a line that did not exist on that branch, so it was + * a silent no-op and nothing anywhere complained. + */ +const languagesSrc = readFileSync("web/src/lib/languages.ts", "utf8"); +const registered = new Set([...languagesSrc.matchAll(/tag:\s*"([\w-]+)"/g)].map((m) => m[1])); +const catalogues = new Set(globSync("web/src/locales/*.ts").map((f) => f.split("/").pop().replace(".ts", ""))); + let failed = false; +for (const tag of catalogues) { + if (!registered.has(tag)) { + failed = true; + console.log(`!! ${tag}.ts exists but is not in UI_LANGUAGES — the language is never offered\n`); + } +} +for (const tag of registered) { + if (tag !== "en" && !catalogues.has(tag)) { + failed = true; + console.log(`!! UI_LANGUAGES offers ${tag} but there is no ${tag}.ts — it would fall back to English\n`); + } +} + for (const file of globSync("web/src/locales/*.ts")) { const tag = file.split("/").pop().replace(".ts", ""); const src = ts.createSourceFile(file, readFileSync(file, "utf8"), ts.ScriptTarget.Latest, true, ts.ScriptKind.TS); diff --git a/web/src/lib/languages.ts b/web/src/lib/languages.ts index 87cbca3..80e07e6 100644 --- a/web/src/lib/languages.ts +++ b/web/src/lib/languages.ts @@ -38,6 +38,7 @@ export interface UiLanguage { export const UI_LANGUAGES: readonly UiLanguage[] = [ { tag: "en", name: "English" }, { tag: "de", name: "Deutsch", beta: true }, + { tag: "nl", name: "Nederlands", beta: true }, ]; /** Where to report a bad translation. Beta languages depend on it. */