fix: address code review findings for i18n

- Fix detectLanguage() to follow POSIX priority: LC_ALL > LC_MESSAGES > LANG
- Fix Chinese format.resetsIn from broken wrap-around grammar to prefix form
- Fix loadConfig() to call mergeConfig({}) when no config file exists,
  ensuring detectLanguage() runs for auto-detection
- Add setLanguage('en') guard to render.test.js for locale-independent tests
- Add dedicated i18n test suite (tests/i18n.test.js) covering t(),
  detectLanguage(), and mergeConfig language handling

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
xiaodream
2026-04-01 17:03:53 +08:00
parent c8d9e809ed
commit 7895941b8b
4 changed files with 1003 additions and 549 deletions

View File

@@ -96,8 +96,9 @@ export interface HudConfig {
}
export function detectLanguage(): Language {
// POSIX priority: LC_ALL overrides everything, then LC_MESSAGES, then LANG
const envLang =
process.env.LANG || process.env.LC_ALL || process.env.LC_MESSAGES || "";
process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || "";
if (envLang.startsWith("zh")) return "zh";
return "en";
}
@@ -458,13 +459,13 @@ export async function loadConfig(): Promise<HudConfig> {
try {
if (!fs.existsSync(configPath)) {
return DEFAULT_CONFIG;
return mergeConfig({});
}
const content = fs.readFileSync(configPath, "utf-8");
const userConfig = JSON.parse(content) as Partial<HudConfig>;
return mergeConfig(userConfig);
} catch {
return DEFAULT_CONFIG;
return mergeConfig({});
}
}

View File

@@ -14,7 +14,7 @@ export const zh: Messages = {
// Format
"format.resets": "重置于",
"format.resetsIn": "将在...后重置",
"format.resetsIn": "重置剩余",
"format.in": "输入",
"format.cache": "缓存",
"format.out": "输出",

126
tests/i18n.test.js Normal file
View File

@@ -0,0 +1,126 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { setLanguage, getLanguage, t } from "../dist/i18n/index.js";
import { detectLanguage, mergeConfig } from "../dist/config.js";
test("t() returns English strings by default", () => {
setLanguage("en");
assert.equal(t("label.context"), "Context");
assert.equal(t("label.usage"), "Usage");
assert.equal(t("label.approxRam"), "Approx RAM");
assert.equal(t("status.limitReached"), "Limit reached");
assert.equal(t("status.allTodosComplete"), "All todos complete");
});
test("t() returns Chinese strings when language is zh", () => {
setLanguage("zh");
assert.equal(t("label.context"), "上下文");
assert.equal(t("label.usage"), "用量");
assert.equal(t("label.approxRam"), "内存");
assert.equal(t("label.rules"), "规则");
assert.equal(t("label.hooks"), "钩子");
assert.equal(t("status.limitReached"), "已达上限");
assert.equal(t("status.allTodosComplete"), "全部完成");
assert.equal(t("format.in"), "输入");
assert.equal(t("format.cache"), "缓存");
assert.equal(t("format.out"), "输出");
// Restore
setLanguage("en");
});
test("setLanguage and getLanguage round-trip", () => {
setLanguage("zh");
assert.equal(getLanguage(), "zh");
setLanguage("en");
assert.equal(getLanguage(), "en");
});
test("detectLanguage respects LC_ALL over LANG (POSIX priority)", () => {
const origLang = process.env.LANG;
const origLcAll = process.env.LC_ALL;
const origLcMsg = process.env.LC_MESSAGES;
try {
process.env.LANG = "en_US.UTF-8";
process.env.LC_ALL = "zh_CN.UTF-8";
delete process.env.LC_MESSAGES;
assert.equal(detectLanguage(), "zh");
process.env.LC_ALL = "en_US.UTF-8";
process.env.LANG = "zh_CN.UTF-8";
assert.equal(detectLanguage(), "en");
delete process.env.LC_ALL;
assert.equal(detectLanguage(), "zh");
} finally {
process.env.LANG = origLang;
process.env.LC_ALL = origLcAll;
process.env.LC_MESSAGES = origLcMsg;
}
});
test("detectLanguage returns en for unknown locales", () => {
const origLang = process.env.LANG;
const origLcAll = process.env.LC_ALL;
const origLcMsg = process.env.LC_MESSAGES;
try {
delete process.env.LC_ALL;
delete process.env.LC_MESSAGES;
process.env.LANG = "fr_FR.UTF-8";
assert.equal(detectLanguage(), "en");
process.env.LANG = "C";
assert.equal(detectLanguage(), "en");
delete process.env.LANG;
assert.equal(detectLanguage(), "en");
} finally {
process.env.LANG = origLang;
process.env.LC_ALL = origLcAll;
process.env.LC_MESSAGES = origLcMsg;
}
});
test("mergeConfig uses detectLanguage when no language specified", () => {
const origLcAll = process.env.LC_ALL;
const origLang = process.env.LANG;
const origLcMsg = process.env.LC_MESSAGES;
try {
process.env.LC_ALL = "zh_CN.UTF-8";
delete process.env.LC_MESSAGES;
const config = mergeConfig({});
assert.equal(config.language, "zh");
} finally {
process.env.LC_ALL = origLcAll;
process.env.LANG = origLang;
process.env.LC_MESSAGES = origLcMsg;
}
});
test("mergeConfig preserves explicit language from config", () => {
const config = mergeConfig({ language: "zh" });
assert.equal(config.language, "zh");
const config2 = mergeConfig({ language: "en" });
assert.equal(config2.language, "en");
});
test("mergeConfig falls back to detection for invalid language", () => {
const origLcAll = process.env.LC_ALL;
const origLang = process.env.LANG;
const origLcMsg = process.env.LC_MESSAGES;
try {
delete process.env.LC_ALL;
delete process.env.LC_MESSAGES;
process.env.LANG = "C";
const config = mergeConfig({ language: "invalid" });
assert.equal(config.language, "en");
} finally {
process.env.LC_ALL = origLcAll;
process.env.LANG = origLang;
process.env.LC_MESSAGES = origLcMsg;
}
});

File diff suppressed because it is too large Load Diff