From 72a1771cf92d5513d9b72e920c297b887d8b3a89 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 3 Oct 2026 11:29:32 +0200 Subject: [PATCH] fix(i18n): keep the baseline intact on failed updates and strict audits `--update-baseline` now writes nothing while any locale is unreadable or has missing or extra keys, so an incomplete translation cannot reshape the baseline. `--fail-on-identical` no longer reads the baseline it ignores, so a damaged file cannot block a strict audit. Co-Authored-By: Claude Opus 5.5 --- docs/architecture/validation-map.md | 3 ++- tools/i18n/check-drift.mjs | 13 ++++++++----- tools/i18n/check-drift.test.mjs | 30 +++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/docs/architecture/validation-map.md b/docs/architecture/validation-map.md index 10e76410c..c380f3ee4 100644 --- a/docs/architecture/validation-map.md +++ b/docs/architecture/validation-map.md @@ -260,7 +260,8 @@ text changes, so copying reworded English into a locale fails as well. Baseline entries that are no longer English-identical (translated, removed, or reworded) are reported but do not fail. `pnpm run i18n:baseline:update` rewrites the baseline from the current locale files, dropping those entries -and printing every added one. Run it only after translating, or for a value +and printing every added one; it writes nothing while any locale is unreadable +or has missing or extra keys. Run it only after translating, or for a value that is legitimately identical in that language; review the diff, and never run it in CI. A new locale starts with no baseline entries, so it has to record its legitimate identical values the same way. diff --git a/tools/i18n/check-drift.mjs b/tools/i18n/check-drift.mjs index 9269a253c..e86c82ae4 100644 --- a/tools/i18n/check-drift.mjs +++ b/tools/i18n/check-drift.mjs @@ -269,7 +269,8 @@ export function run({ enLeaves = collectLeaves( readJson(resolve(i18nDir, `${EN_LOCALE}.json`)) ); - baseline = readBaseline(baselinePath); + // A strict audit ignores the baseline, so a damaged file cannot block it. + baseline = strict ? new Map() : readBaseline(baselinePath); } catch (error) { if (!enLeaves || !update) { log(`FAIL ${formatError(error)}`); @@ -283,7 +284,6 @@ export function run({ const identicalByLocale = new Map(); const rejected = []; let failed = false; - let unreadable = false; let staleTotal = 0; for (const locale of locales) { @@ -292,7 +292,7 @@ export function run({ try { result = checkLocale({ i18nDir, locale, enLeaves, baseline, mode }); } catch (error) { - failed = unreadable = true; + failed = true; log(`FAIL ${file}: ${formatError(error)}`); continue; } @@ -318,8 +318,11 @@ export function run({ reportRejected(log, rejected, enLeaves, mode); if (update) { - if (unreadable) { - log('FAIL baseline not written: a locale file could not be read.'); + // Only a complete, readable set of locales is a state worth recording. + if (failed) { + log( + 'FAIL baseline not written: fix the unreadable, missing or extra keys above first.' + ); return 1; } writeBaseline(log, baselinePath, baseline, identicalByLocale); diff --git a/tools/i18n/check-drift.test.mjs b/tools/i18n/check-drift.test.mjs index 9b63d2c77..45f249666 100644 --- a/tools/i18n/check-drift.test.mjs +++ b/tools/i18n/check-drift.test.mjs @@ -214,6 +214,36 @@ test('--update-baseline keeps the old baseline when a locale is unreadable', () assert.deepEqual(readBaselineFile(), { de: { PIN: 'PIN' } }); }); +test('--update-baseline keeps the old baseline while keys are missing or extra', () => { + writeLocale('en', EN); + writeLocale('de', { APP: { TITLE: 'IPTVnator', OLD: 'Alt' }, PIN: 'PIN' }); + writeBaseline({ de: { 'APP.SAVE': 'Save', PIN: 'PIN' } }); + + const { code, output } = check('--update-baseline'); + + assert.equal(code, 1); + assert.match(output, /baseline not written/); + assert.deepEqual(readBaselineFile(), { + de: { 'APP.SAVE': 'Save', PIN: 'PIN' }, + }); +}); + +test('--fail-on-identical audits even when the baseline is damaged', () => { + writeLocale('en', EN); + writeLocale('de', { + APP: { TITLE: 'IPTVnator', SAVE: 'Speichern' }, + PIN: 'PIN-Code', + }); + writeFileSync(baselinePath, '{ broken'); + + const { code, output } = check('--fail-on-identical'); + + assert.equal(code, 1); + assert.match(output, /^FAIL de\.json: missing=0 extra=0 identical_en=1$/m); + assert.match(output, /de\.json APP\.TITLE: "IPTVnator"/); + assert.equal(check().code, 1); +}); + test('rejects unknown and conflicting flags', () => { writeLocale('en', EN);