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);