Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion scripts/check-locale-hygiene.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down
24 changes: 24 additions & 0 deletions scripts/check-locale-hygiene.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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' };"), []);
});