Files
RingBRP/tools/check-lang.mjs
T
slaguru666andClaude Opus 5 d0144f744e The Custodians v0.6.1 — full system
A Foundry VTT game system: Basic Roleplaying d100 in a drab British agency that
deals with crossings, where reality keeps a record of what belongs where and can be
made to get it wrong.

Contents:
- rules.mjs as the single authority for every rule formula, with 40 rules and 132
  spot-checked values. Nothing may restate a formula it defines.
- 25 postings, each with a key characteristic, trained skills, talents and its own
  kit; a 175-item catalogue across four eras; 231 generated icons.
- Reaction Lanes initiative, re-rolled every round including the first.
- Coherence, and the Quiet Arts, which only ever arrive from a crossing gone wrong.
- Graded defences: only a critical stops a critical.
- Borrowed Authority — access as a record rather than a hacking roll.
- A case generator, and a rules journal generated from rules.mjs so the book cannot
  drift from the code.

Built compendia are committed so a remote Foundry needs only the files.
node_modules is not; `npm install` restores it for rebuilding content.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:09:07 +01:00

45 lines
1.8 KiB
JavaScript

/**
* 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); }