Spaces:
Runtime error
Runtime error
| /** | |
| * Validates that: | |
| * 1. All t("key") calls in bin/cli/commands/ resolve to existing keys in en.json. | |
| * 2. pt-BR.json has the same top-level shape as en.json (no missing top-level sections). | |
| * 3. No raw string literals are passed to .description() in commands without going | |
| * through t() β only warns, does not fail hard (many descriptions use || fallback). | |
| */ | |
| import { readFileSync, readdirSync, statSync } from "node:fs"; | |
| import { join, dirname } from "node:path"; | |
| import { fileURLToPath } from "node:url"; | |
| const __dirname = dirname(fileURLToPath(import.meta.url)); | |
| const ROOT = join(__dirname, "..", ".."); | |
| const COMMANDS_DIR = join(ROOT, "bin", "cli", "commands"); | |
| const LOCALES_DIR = join(ROOT, "bin", "cli", "locales"); | |
| // Paths that look like t() keys but are actually import paths β skip them. | |
| const IGNORE_AS_KEY = new Set([".", ".."]); | |
| const IMPORT_PATH_RE = /^(\.\.?\/|node:|\/)/; | |
| function walk(dir) { | |
| const results = []; | |
| for (const entry of readdirSync(dir)) { | |
| const full = join(dir, entry); | |
| if (statSync(full).isDirectory()) { | |
| results.push(...walk(full)); | |
| } else if (entry.endsWith(".mjs") || entry.endsWith(".js")) { | |
| results.push(full); | |
| } | |
| } | |
| return results; | |
| } | |
| function flattenKeys(obj, prefix = "") { | |
| const keys = new Set(); | |
| for (const [k, v] of Object.entries(obj)) { | |
| const full = prefix ? `${prefix}.${k}` : k; | |
| if (v !== null && typeof v === "object" && !Array.isArray(v)) { | |
| for (const sub of flattenKeys(v, full)) keys.add(sub); | |
| } else { | |
| keys.add(full); | |
| } | |
| } | |
| return keys; | |
| } | |
| function collectTKeys(files) { | |
| const used = new Set(); | |
| const re = /\bt\(\s*["']([^"']+)["']/g; | |
| for (const file of files) { | |
| const src = readFileSync(file, "utf8"); | |
| let m; | |
| re.lastIndex = 0; | |
| while ((m = re.exec(src)) !== null) { | |
| const key = m[1]; | |
| if (IGNORE_AS_KEY.has(key) || IMPORT_PATH_RE.test(key)) continue; | |
| used.add(key); | |
| } | |
| } | |
| return used; | |
| } | |
| function loadJson(file) { | |
| return JSON.parse(readFileSync(file, "utf8")); | |
| } | |
| const files = walk(COMMANDS_DIR); | |
| const usedKeys = collectTKeys(files); | |
| const en = loadJson(join(LOCALES_DIR, "en.json")); | |
| const ptBR = loadJson(join(LOCALES_DIR, "pt-BR.json")); | |
| const enKeys = flattenKeys(en); | |
| let errors = 0; | |
| // Check 1: all used keys exist in en.json | |
| const missingInEn = [...usedKeys].filter((k) => !enKeys.has(k)); | |
| if (missingInEn.length > 0) { | |
| console.error("[cli-i18n] Keys used in commands but missing in en.json:"); | |
| for (const k of missingInEn) console.error(` β ${k}`); | |
| errors += missingInEn.length; | |
| } else { | |
| console.log(`[cli-i18n] β All ${usedKeys.size} t() keys found in en.json`); | |
| } | |
| // Check 2: pt-BR.json has the same top-level sections as en.json | |
| const enTopLevel = Object.keys(en); | |
| const ptTopLevel = new Set(Object.keys(ptBR)); | |
| const missingTopLevel = enTopLevel.filter((k) => !ptTopLevel.has(k)); | |
| if (missingTopLevel.length > 0) { | |
| console.error("[cli-i18n] Top-level sections in en.json missing from pt-BR.json:"); | |
| for (const k of missingTopLevel) console.error(` β ${k}`); | |
| errors += missingTopLevel.length; | |
| } else { | |
| console.log(`[cli-i18n] β pt-BR.json has all ${enTopLevel.length} top-level sections`); | |
| } | |
| if (errors > 0) { | |
| console.error(`[cli-i18n] FAIL β ${errors} error(s) found`); | |
| process.exit(1); | |
| } else { | |
| console.log("[cli-i18n] PASS β CLI i18n is consistent"); | |
| } | |