mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-25 16:42:16 +03:00
Retargetado para release/v3.8.51 (release/v3.8.50 está congelada — freeze issue #11439). Validado em lote combinado (batch-0824h2, junto de #11435/#11436/#11437) contra o tip de release/v3.8.51: typecheck:core limpo, gates estáticos OK, 127/127 testes focados passando. Investigação sólida com repro real via container isolado, três causas independentes identificadas e corrigidas com testes de regressão dedicados para cada uma. Obrigado pela contribuição!
118 lines
3.9 KiB
JavaScript
118 lines
3.9 KiB
JavaScript
#!/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 zhLocales = ["zh-CN", "zh-TW"].map((n) => [n, loadJson(join(LOCALES_DIR, `${n}.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`);
|
|
}
|
|
|
|
// Check 3: zh-CN and zh-TW have full key parity with en.json
|
|
for (const [name, cat] of zhLocales) {
|
|
const catKeys = flattenKeys(cat);
|
|
const missingKeys = [...enKeys].filter((k) => !catKeys.has(k));
|
|
if (missingKeys.length > 0) {
|
|
console.error(`[cli-i18n] Keys in en.json missing from ${name}.json:`);
|
|
for (const k of missingKeys) console.error(` ✗ ${k}`);
|
|
errors += missingKeys.length;
|
|
} else {
|
|
console.log(`[cli-i18n] ✓ ${name}.json has full parity (${enKeys.size} keys)`);
|
|
}
|
|
}
|
|
|
|
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");
|
|
}
|