mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:41:09 +02:00
feat(scripts): validate:i18n-hardcoded — ratcheting baseline check
Stoppt das Wachsen des 1877-String-Backlogs hardgecodeter deutscher User-facing Strings in .svelte Files. Per-file Count vs. committed Baseline; Datei darf NIE über ihrer Baseline liegen, neue Files müssen mit 0 Verstößen starten. - Erkennt: placeholder/title/aria-label/label/alt mit Umlauten, Text-Content `>Großbuchstabe…<` (ohne Interpolation). - Aktuelle Baseline: 1877 Verstöße in 428 Files; jeder Fix ratchet't den erlaubten Wert nach unten. - Lokales Update nach gewolltem Wachstum: `pnpm run validate:i18n-hardcoded -- --update`. - In validate:all + CI verdrahtet. - Drift-Test bestätigt: ein zusätzlicher umlaut-Placeholder lässt die Datei "2 (was 1, +1)" failen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d8feef1149
commit
8b9fbd2e1c
3 changed files with 557 additions and 1 deletions
125
scripts/validate-no-hardcoded-strings.mjs
Normal file
125
scripts/validate-no-hardcoded-strings.mjs
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Ratcheting validator for hardcoded German user-facing strings in
|
||||
* apps/mana/apps/web Svelte components. Looks for German-looking text
|
||||
* in attribute values (placeholder, title, aria-label, label, alt) and
|
||||
* in text content, and compares per-file counts against a committed
|
||||
* baseline.
|
||||
*
|
||||
* Every file's current count must be ≤ its baseline count. New files
|
||||
* (not in baseline) must have 0 violations. The baseline can only
|
||||
* shrink — fixing strings is rewarded, adding new ones fails CI.
|
||||
*
|
||||
* The validator is intentionally coarse: the goal is to stop the 1877-
|
||||
* string backlog from growing while it's being whittled down, not to
|
||||
* catch every translation miss perfectly.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/validate-no-hardcoded-strings.mjs # check
|
||||
* node scripts/validate-no-hardcoded-strings.mjs --update # rewrite baseline
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join, relative } from 'node:path';
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const REPO_ROOT = join(__dirname, '..');
|
||||
const BASELINE_PATH = join(__dirname, 'i18n-hardcoded-baseline.json');
|
||||
const SCAN_GLOB = 'apps/mana/apps/web/src/**/*.svelte';
|
||||
|
||||
const PATTERNS = [
|
||||
/placeholder="([^"{}]*[äöüÄÖÜß][^"{}]*)"/g,
|
||||
/title="([^"{}]*[äöüÄÖÜß][^"{}]*)"/g,
|
||||
/aria-label="([^"{}]*[äöüÄÖÜß][^"{}]*)"/g,
|
||||
/label="([^"{}]*[äöüÄÖÜß][^"{}]*)"/g,
|
||||
/alt="([^"{}]*[äöüÄÖÜß][^"{}]*)"/g,
|
||||
/>([A-ZÄÖÜ][a-zäöüß][a-zäöüßÄÖÜA-Z ,.!?]{2,40})</g,
|
||||
];
|
||||
|
||||
function scan() {
|
||||
const files = execSync(`git ls-files '${SCAN_GLOB}'`, { cwd: REPO_ROOT })
|
||||
.toString()
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter(Boolean);
|
||||
|
||||
const counts = {};
|
||||
for (const f of files) {
|
||||
let src;
|
||||
try {
|
||||
src = readFileSync(join(REPO_ROOT, f), 'utf8');
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
let n = 0;
|
||||
for (const p of PATTERNS) for (const _ of src.matchAll(p)) n++;
|
||||
if (n > 0) counts[f] = n;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
function loadBaseline() {
|
||||
if (!existsSync(BASELINE_PATH)) return {};
|
||||
return JSON.parse(readFileSync(BASELINE_PATH, 'utf8'));
|
||||
}
|
||||
|
||||
function main() {
|
||||
const update = process.argv.includes('--update');
|
||||
const current = scan();
|
||||
const currentTotal = Object.values(current).reduce((a, b) => a + b, 0);
|
||||
|
||||
if (update) {
|
||||
const sorted = Object.fromEntries(
|
||||
Object.entries(current).sort(([a], [b]) => a.localeCompare(b))
|
||||
);
|
||||
writeFileSync(BASELINE_PATH, JSON.stringify(sorted, null, 2) + '\n');
|
||||
console.log(
|
||||
`✓ Baseline updated: ${currentTotal} violations across ${Object.keys(current).length} files.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const baseline = loadBaseline();
|
||||
const baselineTotal = Object.values(baseline).reduce((a, b) => a + b, 0);
|
||||
const violations = [];
|
||||
|
||||
for (const [file, n] of Object.entries(current)) {
|
||||
const b = baseline[file] ?? 0;
|
||||
if (n > b) violations.push({ file, current: n, baseline: b, delta: n - b });
|
||||
}
|
||||
|
||||
if (violations.length > 0) {
|
||||
console.error(
|
||||
`\n✗ Hardcoded-string check FAILED — ${violations.length} file(s) exceed baseline:\n`
|
||||
);
|
||||
for (const v of violations.slice(0, 20)) {
|
||||
console.error(` ${v.file}: ${v.current} (was ${v.baseline}, +${v.delta})`);
|
||||
}
|
||||
if (violations.length > 20) console.error(` … +${violations.length - 20} more`);
|
||||
console.error(
|
||||
`\nYou added user-facing German strings to .svelte files without\n` +
|
||||
`going through \$_('namespace.key'). Move them into locales/ or\n` +
|
||||
`translate them inline, then re-run validate:i18n-hardcoded.\n` +
|
||||
`If the additions are intentional (e.g. an untranslated dev-only\n` +
|
||||
`page), run: pnpm run validate:i18n-hardcoded -- --update\n`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Dropped below baseline? Tell the user so they can ratchet.
|
||||
const shrunk = Object.keys(baseline).filter((f) => (current[f] ?? 0) < baseline[f]).length;
|
||||
const cleaned = Object.keys(baseline).filter((f) => !(f in current)).length;
|
||||
|
||||
console.log(
|
||||
`✓ Hardcoded strings: ${currentTotal} violations across ${Object.keys(current).length} files ` +
|
||||
`(baseline ${baselineTotal}).` +
|
||||
(shrunk || cleaned
|
||||
? `\n ${shrunk} file(s) shrunk, ${cleaned} file(s) fully cleaned — ` +
|
||||
`run 'pnpm run validate:i18n-hardcoded -- --update' to ratchet.`
|
||||
: '')
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
||||
Loading…
Add table
Add a link
Reference in a new issue