From 9c6d24f8e324f3902af494765cda7a74c15c843c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 19 Sep 2026 14:33:44 +0000 Subject: [PATCH] fix(i18n): catch keys read through a copyFor() key list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every theme's copy.ts batches its SlotCopy through a local copyFor(t, keys) helper that calls t.t(key) in a loop. Because the key reaches the translator through a variable, not a string literal, i18n-check's CALL regex never saw it, so a key listed there with no matching catalog entry passed silently — the exact shape of the "raw key leaks in the UI" report, even though every current copyFor() list in the repo does resolve. Scan the array literal passed to copyFor(t, [...]) directly and fail the same way the existing literal-call check does. --- scripts/i18n-check.mjs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/scripts/i18n-check.mjs b/scripts/i18n-check.mjs index ba63a133a..c357b434a 100644 --- a/scripts/i18n-check.mjs +++ b/scripts/i18n-check.mjs @@ -162,6 +162,7 @@ const source = files.filter( const CALL = /(?:\.\s*(?:t|has)|\bmsg)\(\s*'([\w.-]+)'/g const CARRIED = /\b(?:title|blurb|label|description|message|publicMessage)Key:\s*'([\w.-]+)'/g const KEY_SHAPE = /^[a-z][\w-]*(?:\.[\w-]+)+$/ +const KEY_LIST = /\w+\(\s*t\s*,\s*\[([\s\S]*?)\]\s*\)/g for (const { abs, rel } of source) { const text = await readFile(abs, 'utf8') @@ -183,6 +184,22 @@ for (const { abs, rel } of source) { ) } } + + for (const listMatch of text.matchAll(KEY_LIST)) { + for (const litMatch of listMatch[1].matchAll(new RegExp(STRING, 'g'))) { + const key = litMatch[0].slice(1, -1) + if (!KEY_SHAPE.test(key)) continue + + used.add(key) + if (allMessages[key] === undefined) { + fail( + `${rel} reads the message "${key}" through a key list passed to a translator ` + + 'helper, and no English catalog carries it. A missing message renders as its ' + + 'own key, in every language including English.', + ) + } + } + } } const orphans = messageKeys.filter((key) => !used.has(key))