chore(i18n): full parity across all 8 locales + drop two-tier check

Backfill 276 missing translations across fr / it / ja / pt-BR so all
  8 shipped locales now match en. Most gaps come from features that
  landed with en+de+zh translations only:

    - login.resetPassword.* (12 × 3): fr, it, ja
    - printers.firmwareModal.* (7 × 4): all locales
    - settings.spoolbuddy.* (~40 × 3, plus 18 in ja): admin device-
      control block (unregister / reboot / shutdown / update /
      restart confirms)
    - spoolbuddy.settings.* (13 × 4): kiosk backend & auth + diagnostics
    - virtualPrinter.archiveNameSource.* (4 × 4): from this release

  Also fixes 27 ja and 1 fr placeholder-name mismatches that silently
  broke interpolation at runtime — e.g. printers.activeNozzle used
  {{side}} while the runtime passes nozzle, and several keys had
  {{count}} dropped entirely so the value would never render.

  Drop the STRICT / info two-tier machinery from check-i18n-parity.mjs:
  en is the reference, every other locale is checked identically,
  any drift fails CI. The previous tier was just deferred policy, no
  real distinction.

  All 8 locales now sit at 4492 leaves. Parity script, i18n test suite
  (11 tests), and full frontend build all green.
This commit is contained in:
maziggy
2026-04-29 10:45:43 +02:00
parent a34beaa599
commit 7bc4712480
8 changed files with 344 additions and 62 deletions
+12 -28
View File
@@ -161,11 +161,8 @@ export function compareLocales(locales) {
return { failed: reports.length > 0, reports };
}
// Strict locales fail CI when they drift from en. Everything else discovered
// in the locales directory is reported informationally — promote a locale to
// STRICT once its drift is caught up. en is implicitly the reference.
const STRICT = ['de', 'zh-CN', 'zh-TW'];
// en is the reference locale; every other locale discovered in the locales
// directory is checked identically and a drift in any of them fails CI.
// Skip file IO / process.exit when imported as a library (e.g. from tests).
const isMainModule = import.meta.url === url.pathToFileURL(process.argv[1] ?? '').href;
if (isMainModule) {
@@ -178,46 +175,33 @@ if (isMainModule) {
console.error(`No en.ts found in ${localesDir} — cannot run parity check without a reference locale.`);
process.exit(1);
}
const missingStrict = STRICT.filter((c) => !discovered.includes(c));
if (missingStrict.length) {
console.error(`STRICT locales declared but not found on disk: ${missingStrict.join(', ')}`);
process.exit(1);
}
const codes = ['en', ...discovered.filter((c) => c !== 'en')];
const locales = Object.fromEntries(
codes.map((c) => [c, loadLocale(path.join(localesDir, `${c}.ts`))]),
);
const MAX_REPORT = 20;
const strictSet = new Set(STRICT);
const printReports = (reports, header) => {
if (!reports.length) return;
console.error(`\n${header}`);
const { reports } = compareLocales(locales);
if (reports.length) {
console.error(`\n=== Locale parity failures (en is the reference) ===`);
for (const { label, items } of reports) {
console.error(`\n[${label}] (${items.length})`);
items.slice(0, MAX_REPORT).forEach((i) => console.error(` ${i}`));
if (items.length > MAX_REPORT) console.error(` ... and ${items.length - MAX_REPORT} more`);
}
};
// Label prefix is "${code}:" — route reports to strict vs informational.
const { reports } = compareLocales(locales);
const codeOf = (label) => label.split(':', 1)[0];
const strictReports = reports.filter((r) => strictSet.has(codeOf(r.label)));
const infoReports = reports.filter((r) => !strictSet.has(codeOf(r.label)));
printReports(strictReports, '=== STRICT locales (failures below fail CI) ===');
printReports(infoReports, '=== INFORMATIONAL locales (drift shown, does not fail CI) ===');
}
console.log('\nLocale leaf counts:');
for (const [code, map] of Object.entries(locales)) {
const tier = code === 'en' ? 'ref' : strictSet.has(code) ? 'strict' : 'info';
const tier = code === 'en' ? 'ref' : 'locale';
console.log(` ${code.padEnd(6)} ${String(map.size).padEnd(6)} [${tier}]`);
}
if (strictReports.length > 0) {
console.error(`\n❌ i18n parity check failed (strict: ${STRICT.join(', ')}).`);
if (reports.length > 0) {
console.error(`\n❌ i18n parity check failed.`);
process.exit(1);
}
console.log(`\n✓ Strict locales in parity (en / ${STRICT.join(' / ')}).`);
const others = codes.filter((c) => c !== 'en');
console.log(`\n✓ All locales in parity with en (${others.join(' / ')}).`);
}