Ctrl K
ComunidadGuías…

scripts/i18n-check.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/**
 * i18n drift guard.
 *
 * tsc proves every locale file has every KEY (shape parity). It cannot
 * prove the VALUES are still in sync with the English source — someone
 * edits en.ts, and fr.ts silently serves stale text.
 *
 * This script hashes en.ts's string values and compares against the
 * `lastSyncedHash:` comment each locale file must carry in its header.
 *
 * Usage:
 *   npm run i18n:check          → fail (exit 1) if any locale is stale
 *   npm run i18n:check -- --update   → rewrite the hash comments (run
 *                                      after syncing translations)
 *   npm run i18n:check -- --hash     → print the current en.ts hash only
 *
 * The hash is stable across formatting: it parses the AST-ish token stream
 * of quoted strings in key order, so reformatting en.ts does NOT bump it,
 * but any value/key change does.
 */
import fs from "node:fs";
import { fileURLToPath } from "node:url";
import path from "node:path";
import crypto from "node:crypto";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const LOCALES_DIR = path.join(__dirname, "..", "src", "locales");
const EN_FILE = path.join(LOCALES_DIR, "en.ts");
const HASH_RE = /lastSyncedHash:\s*([0-9a-f]+)/;

/** Extract a canonical string fingerprint from a .ts dictionary file. */
function fingerprint(file) {
  const src = fs.readFileSync(file, "utf8");
  // Grab every `key: "value"` pair (handles single/double quotes, template
  // literals without interpolation). ICU braces are kept verbatim.
  const pairs = [];
  const re = /([A-Za-z0-9_]+)\s*:\s*(["'`])((?:\\.|(?!\2).)*)\2/g;
  let m;
  while ((m = re.exec(src)) !== null) {
    // Skip the status marker and non-message keys like path/src comments.
    if (m[1] === "status") continue;
    pairs.push(`${m[1]}\u0000${m[3]}`);
  }
  return crypto
    .createHash("sha256")
    .update(pairs.join("\u0001"))
    .digest("hex")
    .slice(0, 12);
}

const enHash = fingerprint(EN_FILE);

if (process.argv.includes("--hash")) {
  console.log(enHash);
  process.exit(0);
}

const localeFiles = fs
  .readdirSync(LOCALES_DIR)
  .filter((f) => /^([a-z]{2}(-[A-Z]{2})?)\.ts$/.test(f) && f !== "en.ts");

const update = process.argv.includes("--update");
let stale = [];

for (const file of localeFiles) {
  const full = path.join(LOCALES_DIR, file);
  const src = fs.readFileSync(full, "utf8");
  const m = src.match(HASH_RE);

  if (!m) {
    console.error(`✗ ${file}: missing "lastSyncedHash:" header comment`);
    stale.push(file);
    continue;
  }
  if (m[1] !== enHash) {
    if (update) {
      fs.writeFileSync(
        full,
        src.replace(HASH_RE, `lastSyncedHash: ${enHash}`),
      );
      console.log(`↻ ${file}: hash updated to ${enHash}`);
    } else {
      console.error(
        `✗ ${file}: stale (has ${m[1]}, en.ts is ${enHash}) — re-sync translations, then run: npm run i18n:check -- --update`,
      );
      stale.push(file);
    }
  } else {
    console.log(`✓ ${file}: in sync`);
  }
}

if (stale.length && !update) {
  console.error(
    `\n${stale.length} locale file(s) out of sync with src/locales/en.ts.`,
  );
  process.exit(1);
}
if (update) console.log(`\nAll locale hashes now ${enHash}.`);