an explicit per-line exemption, and command hq keyed (383 -> 344)

Adds `// i18n-exempt: <reason>`, which exempts the next line. The reason is mandatory
-- the regex will not match a bare marker -- so somebody has to write down why, and
`grep -rn i18n-exempt` lists every one for review.

It exists because CommandHQ's startBrief is a prompt written FOR THE MODEL: the person
sees a short displayText instead, and translating it would change how the agent
behaves. No syntactic rule can tell that from prose, and the alternative was a seventh
scanner heuristic. <Untranslated> covers JSX, this covers everything else, and together
they should mean the scanner needs no further special cases.

The new-commander guide was module-level data whose `body` paragraphs the scanner never
saw -- eight paragraphs of real user-facing prose. It carries titleKey and bodyKeys now
and the renderer resolves them.

Four implicit-return arrows needed bodies to hold the hook.
This commit is contained in:
2026-08-23 15:03:25 +00:00
parent d0904b2c84
commit 1e268d553d
7 changed files with 243 additions and 121 deletions
+17 -1
View File
@@ -126,6 +126,21 @@ const stripComments = (source: string): string =>
const stripCodeElements = (source: string): string =>
source.replace(/<(code|kbd|pre|samp)\b[^>]*>[\s\S]*?<\/\1>/g, '<$1/>');
/**
* An explicit, per-line opt-out: `// i18n-exempt: <reason>` exempts the NEXT line.
*
* For the cases no syntactic rule can reach — chiefly a prompt written for a model rather than a
* reader. `<Untranslated>` covers JSX; this covers the rest, and the two together mean the scanner does
* not need another heuristic every time a new shape of never-translate string turns up.
*
* A reason is mandatory: the regex will not match a bare `// i18n-exempt`. That is the whole safeguard —
* somebody has to write down why, and `grep -rn "i18n-exempt"` lists every one for review.
*/
const stripExemptLines = (source: string): string => {
const lines = source.split('\n');
return lines.map((line, i) => (/\/\/\s*i18n-exempt:\s*\S/.test(lines[i - 1] ?? '') ? '' : line)).join('\n');
};
/**
* `<Untranslated reason="…">` marks text that must stay in one language — today, a label quoted from
* another product's UI, which docs/localization.md classifies as an identifier.
@@ -169,7 +184,8 @@ function scan(root: string): { files: number; hits: Hit[] } {
files++;
let source: string;
try {
source = stripUntranslated(stripCodeElements(stripComments(readFileSync(file, 'utf8'))));
// Exempt lines go FIRST: stripComments would remove the marker they depend on.
source = stripUntranslated(stripCodeElements(stripComments(stripExemptLines(readFileSync(file, 'utf8')))));
} catch {
continue;
}