File size: 3,425 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#!/usr/bin/env node
/**
 * 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");
}