From cb010fac2f0644f5bbc34416f9866b08057aa16a Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Thu, 12 Nov 2026 02:40:39 +0800 Subject: [PATCH] test(i18n): fail on keys a locale catalog defines twice (#2804) JSON.parse keeps only the last of two equal keys, so a catalog could carry duplicate entries (one of them silently unused) without any test noticing. The text catalog test now parses every message and source-text catalog with the TypeScript JSON parser and lists keys defined more than once. --- src/platform/locales/textCatalog.test.ts | 35 ++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/platform/locales/textCatalog.test.ts b/src/platform/locales/textCatalog.test.ts index 30ccc2d3a..5ba1d274f 100644 --- a/src/platform/locales/textCatalog.test.ts +++ b/src/platform/locales/textCatalog.test.ts @@ -620,6 +620,26 @@ const catalogs = Object.fromEntries( .map((f) => [f.slice(0, -5), JSON.parse(readFileSync(new URL(f, TEXT_DIR), "utf8")) as Record]), ); +/** Keys a JSON file defines more than once (at any depth); `JSON.parse` keeps only the last. */ +function duplicateKeys(url: URL): string[] { + const file = ts.parseJsonText(url.pathname, readFileSync(url, "utf8")); + const duplicates: string[] = []; + const visit = (node: ts.Node, path: string) => { + if (ts.isObjectLiteralExpression(node)) { + const seen = new Set(); + for (const property of node.properties) { + if (!ts.isPropertyAssignment(property) || !ts.isStringLiteral(property.name)) continue; + const key = path + property.name.text; + if (seen.has(key)) duplicates.push(key); + seen.add(key); + visit(property.initializer, `${key}.`); + } + } + }; + file.statements.forEach((statement) => visit(statement.expression, "")); + return duplicates; +} + /** `{name}` templates of a catalog, as `txTemplate` matches them. */ function templatePatterns(catalog: Record): RegExp[] { return Object.keys(catalog) @@ -658,6 +678,21 @@ it("uses only message keys of the English catalog", () => { expect(report(scan.keys.filter(({ text }) => !Object.hasOwn(en, text)))).toEqual([]); }); +const CATALOG_DIRS: [prefix: string, dir: URL][] = [ + ["", new URL("./", import.meta.url)], + ["text/", TEXT_DIR], +]; + +it.each( + CATALOG_DIRS.flatMap(([prefix, dir]) => + readdirSync(dir) + .filter((f) => f.endsWith(".json")) + .map((f) => ({ name: `${prefix}${f}`, url: new URL(f, dir) })), + ), +)("defines each key once in $name", ({ url }) => { + expect(duplicateKeys(url)).toEqual([]); +}); + describe.each(Object.keys(catalogs))("source-text catalog %s", (locale) => { const catalog = catalogs[locale]!; const patterns = templatePatterns(catalog);