From ce47b7e3b080619a9ea43d4dea85859397a1983b Mon Sep 17 00:00:00 2001 From: NekoPunch Date: Thu, 10 Sep 2026 22:11:14 -0700 Subject: [PATCH] fix(ci): widen the locale hygiene gate to its blind spots The gate did not open packages/runtime-host, so `const zh = locale !== 'en'` in host-handoff-copy.ts sat in the one package the rules never scanned. Two patterns also had no rule at all: a Han fallback handed to `generalizedErrorMessage`, which always renders `en` and so makes the surface switch language by error content, and a literal title in a native open/save dialog, where no catalog exists to route it. The gate is a ratchet against the merge base, so existing hits are grandfathered and only new ones fail. Generated-by: Claude Code --- scripts/check-locale-hygiene.mjs | 14 +++++++++++++- scripts/check-locale-hygiene.test.mjs | 24 ++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/scripts/check-locale-hygiene.mjs b/scripts/check-locale-hygiene.mjs index 461d719860..3a8c67ab1d 100644 --- a/scripts/check-locale-hygiene.mjs +++ b/scripts/check-locale-hygiene.mjs @@ -31,7 +31,12 @@ import { fileURLToPath, pathToFileURL } from 'node:url'; const repoRoot = fileURLToPath(new URL('..', import.meta.url)); -export const SCOPE = ['apps/desktop/src', 'packages/core/src', 'packages/ui/src']; +export const SCOPE = [ + 'apps/desktop/src', + 'packages/core/src', + 'packages/ui/src', + 'packages/runtime-host/src', +]; // Both quote styles: biome leaves apps/desktop and packages/ui unformatted, so // `"en"` is as permanent there as `'en'`. Every pattern is global so one line @@ -49,6 +54,13 @@ export const RULES = { 'cjk-sniff': /\\u3400-\\u9fff|\\u4e00-\\u9fff|\\u3400-\\u4dbf|\[一-龥\]/giu, // `'凭据已保存': '憑證已儲存'` — translating one locale's copy by string lookup. 'string-keyed-translation': /^\s*['"][㐀-鿿][^'"]*['"]:\s*['"]/gu, + // `generalizedErrorMessage(error, '处理失败')` — that helper always renders + // `en`, so a Han fallback makes the surface switch language by error content. + 'han-fallback-to-en-helper': /\bgeneralizedErrorMessage\([^)]*['"][^'"]*[㐀-鿿]/gu, + // `title: 'Import custom pet'` in a native dialog call — the open/save panels + // have no catalog of their own, so a literal here ships one language. + 'native-dialog-literal': + /\b(?:title|message|detail)\s*:\s*['"][A-Za-z][^'"]*['"](?=[\s\S]{0,400}?\bproperties\s*:\s*\[)/gu, }; const EXCLUDED = /(?:^|\/)(?:__tests__|stories)\/|\.(?:test|stories)\.tsx?$/u; diff --git a/scripts/check-locale-hygiene.test.mjs b/scripts/check-locale-hygiene.test.mjs index 5c8338fd62..7188b185b3 100644 --- a/scripts/check-locale-hygiene.test.mjs +++ b/scripts/check-locale-hygiene.test.mjs @@ -78,3 +78,27 @@ test('compare fails only on growth per rule', () => { ['locale-literal-compare 1->2', 'cjk-sniff 0->1'], ); }); + +test('flags a Han fallback handed to the en-only helper', () => { + assert.deepEqual(rules("generalizedErrorMessage(error, '处理失败')"), [ + 'han-fallback-to-en-helper', + ]); + // The same helper with an English fallback is how logs and CLI output use it. + assert.deepEqual(rules("generalizedErrorMessage(error, 'Runtime Host lifecycle failed')"), []); + // The locale-taking sibling is the localized path and stays clean. + assert.deepEqual(rules("generalizedErrorMessageForLocale(error, '处理失败', locale)"), []); +}); + +test('flags a literal title in a native dialog call', () => { + assert.deepEqual( + rules("showOpenDialog({ title: 'Import custom pet', properties: ['openDirectory'] })"), + ['native-dialog-literal'], + ); + // A catalog lookup in the same position is the shape this rule asks for. + assert.deepEqual( + rules("showOpenDialog({ title: copy.importCustomPet, properties: ['openDirectory'] })"), + [], + ); + // `title` outside a dialog call is not a native panel. + assert.deepEqual(rules("const meta = { title: 'Daily review', author: 'maka' };"), []); +});