From 2b0b6718953f5d591c91e8b128be47b2606cc027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sun, 23 Aug 2026 04:13:21 +0000 Subject: [PATCH] scanner: separate never-translate hits from convertible ones, and stop counting tailwind as prose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The --check guard compared an area's whole hit count against zero, including 'thrown errors' — which the doc's never-translate list forbids converting. An area holding one legitimate new Error() could therefore never be added to ZEROED, so the debt guard could never be switched on for it at all. Categories now carry neverTranslate, are still reported, and are excluded from --check. Also drops sonner's 'group-[.toast]:text-muted-foreground', matched as an object label. The filter tests per token rather than per string: the first attempt tested the whole string and ate a real label, 'Application type:'. --check now prints the offending strings and files instead of only a count. --- scripts/i18n/scan-hardcoded.ts | 59 +++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/scripts/i18n/scan-hardcoded.ts b/scripts/i18n/scan-hardcoded.ts index 07d9ffe7..c7b1f060 100755 --- a/scripts/i18n/scan-hardcoded.ts +++ b/scripts/i18n/scan-hardcoded.ts @@ -34,21 +34,36 @@ const SKIP = /node_modules|\.gen\.|[/\\]locales[/\\]|[/\\]vendor[/\\]|\.test\.|\ // A quoted literal, single or double. Kept as two groups because JS has no branch reset. const Q = String.raw`(?:"([^"\n]{2,120})"|'([^'\n]{2,120})')`; -const PATTERNS: { kind: string; rx: RegExp }[] = [ +/** + * `neverTranslate` marks a category the doc's "What must never be translated" list forbids converting — + * so it is reported (an agent should see it and confirm), but excluded from `--check`. + * + * Without that split the guard is unusable: an area holding one legitimate `new Error()` can never + * reach zero, so it could never be added to ZEROED, so the debt guard could never be switched on for + * it. Counting work that must NOT be done against a target of zero makes the target unreachable. + */ +const PATTERNS: { kind: string; rx: RegExp; neverTranslate?: true }[] = [ // >Some visible words< { kind: 'jsx text', rx: />\s*([A-Z][A-Za-z0-9][^<>{}\n]{2,100}?)\s* /^[a-z0-9:_\-[\]().#&/%!]+$/.test(tok); + if (tokens.every(utilityish) && tokens.some((tok) => /[:[]/.test(tok))) return false; return true; } @@ -118,7 +143,9 @@ const results = AREAS.map((area) => ({ ...area, ...scan(area.root) })); const kinds = PATTERNS.map((p) => p.kind); const pad = (s: string | number, n: number) => String(s).padStart(n); -console.log(`${'area'.padEnd(22)} ${pad('files', 5)} ${kinds.map((k) => pad(k.slice(0, 13), 14)).join('')} ${pad('total', 7)}`); +console.log( + `${'area'.padEnd(22)} ${pad('files', 5)} ${kinds.map((k) => pad(k.slice(0, 13), 14)).join('')} ${pad('total', 7)}`, +); for (const r of results) { const byKind = kinds.map((k) => pad(r.hits.filter((h) => h.kind === k).length, 14)).join(''); console.log(`${r.label.padEnd(22)} ${pad(r.files, 5)} ${byKind} ${pad(r.hits.length, 7)}`); @@ -128,9 +155,19 @@ const all = results.flatMap((r) => r.hits); const distinct = new Set(all.map((h) => h.value)); const interpolated = new Set(all.filter((h) => h.kind === 'template str').map((h) => h.value)); -console.log(`${'TOTAL'.padEnd(22)} ${pad(results.reduce((n, r) => n + r.files, 0), 5)} ${kinds.map((k) => pad(all.filter((h) => h.kind === k).length, 14)).join('')} ${pad(all.length, 7)}`); +const NEVER_KINDS = new Set(PATTERNS.filter((p) => p.neverTranslate).map((p) => p.kind)); +const convertible = (hits: Hit[]) => hits.filter((h) => !NEVER_KINDS.has(h.kind)); + +console.log( + `${'TOTAL'.padEnd(22)} ${pad( + results.reduce((n, r) => n + r.files, 0), + 5, + )} ${kinds.map((k) => pad(all.filter((h) => h.kind === k).length, 14)).join('')} ${pad(all.length, 7)}`, +); console.log(); console.log(`occurrences ${all.length}`); +console.log(` convertible ${convertible(all).length}`); +console.log(` never-translate ${all.length - convertible(all).length} (${[...NEVER_KINDS].join(', ')})`); console.log(`distinct strings ${distinct.size}`); console.log(` needing params ${interpolated.size}`); @@ -149,10 +186,16 @@ if (listPrefix) { } if (args.has('--check')) { - const regressed = results.filter((r) => ZEROED.includes(r.label) && r.hits.length > 0); + const regressed = results + .filter((r) => ZEROED.includes(r.label)) + .map((r) => ({ label: r.label, hits: convertible(r.hits) })) + .filter((r) => r.hits.length > 0); if (regressed.length > 0) { console.error('\nThese areas were converted and have regressed:'); - for (const r of regressed) console.error(` ${r.label}: ${r.hits.length} hardcoded strings`); + for (const r of regressed) { + console.error(` ${r.label}: ${r.hits.length} hardcoded strings`); + for (const h of r.hits) console.error(` [${h.kind}] ${h.value} — ${h.file}`); + } console.error('\nUse a catalog key. See docs/localization.md.'); process.exit(1); }