/** * A localisation key may be a LEAF or a BRANCH, never both. * * Foundry expands dotted keys into a nested object, so shipping both * "RINGBRP.Authority.Merge" (a string) and "RINGBRP.Authority.Merge.0" makes it try * to assign a property to a string. That throws inside the loader, and the loader * abandons the WHOLE FILE — one bad pair silently un-localises the entire system and * every sheet starts printing raw keys. It cost an hour to find once. * * node tools/check-lang.mjs */ import { readFile, readdir } from "node:fs/promises"; const dir = new URL("../lang/", import.meta.url); let failures = 0; for (const file of (await readdir(dir)).filter(f => f.endsWith(".json"))) { const raw = await readFile(new URL(file, dir), "utf8"); let json; try { json = JSON.parse(raw); } catch (e) { console.error(`${file}: not valid JSON — ${e.message}`); failures++; continue; } const keys = Object.keys(json); const set = new Set(keys); for (const k of keys) { const parts = k.split("."); for (let i = 1; i < parts.length; i++) { const prefix = parts.slice(0, i).join("."); if (set.has(prefix)) { console.error(`${file}: "${prefix}" is a value AND a prefix of "${k}" — Foundry cannot load this file`); failures++; } } } // duplicate keys survive JSON.parse by silently overwriting; catch them in the text const seen = new Set(), dupes = new Set(); for (const m of raw.matchAll(/^\s*"([^"]+)"\s*:/gm)) { if (seen.has(m[1])) dupes.add(m[1]); else seen.add(m[1]); } for (const d of dupes) { console.error(`${file}: duplicate key "${d}"`); failures++; } if (!failures) console.log(`check-lang: OK — ${file}, ${keys.length} keys, no leaf/branch collisions`); } if (failures) { console.error(`\ncheck-lang: ${failures} failure(s)`); process.exit(1); }