jarrodwatts--claude-hud
a0debadd81
- Add src/i18n/zh-Hant.ts with full Traditional Chinese translations - Update Language type to include 'zh-Hant' and 'zh-TW' - Register zh-Hant locale and zh-TW alias in i18n index - Update validateLanguage() in config.ts to accept zh-Hant and zh-TW - Fix session-time.ts: replace getLanguage() === 'zh' with isCjkLanguage() so zh-Hant also gets correct no-space formatting (e.g. '5m前' not '5m 前') - Add test coverage for zh-Hant and zh-TW in i18n.test.js Co-authored-by: YutenC <yuten.c.tcg@tc168.cloud>
242 行
8.1 KiB
JavaScript
242 行
8.1 KiB
JavaScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { setLanguage, getLanguage, getCanonicalLanguage, isCjkLanguage, t } from "../dist/i18n/index.js";
|
|
import { en } from "../dist/i18n/en.js";
|
|
import { zhHant } from "../dist/i18n/zh-Hant.js";
|
|
import { mergeConfig } from "../dist/config.js";
|
|
import { renderSessionTokensLine } from "../dist/render/lines/session-tokens.js";
|
|
|
|
function stripAnsi(s) {
|
|
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
}
|
|
|
|
function makeCtx(overrides = {}) {
|
|
return {
|
|
stdin: {},
|
|
transcript: {
|
|
tools: [],
|
|
agents: [],
|
|
todos: [],
|
|
sessionTokens: {
|
|
inputTokens: 12345,
|
|
outputTokens: 6789,
|
|
cacheCreationTokens: 0,
|
|
cacheReadTokens: 4321,
|
|
},
|
|
},
|
|
claudeMdCount: 0,
|
|
rulesCount: 0,
|
|
mcpCount: 0,
|
|
hooksCount: 0,
|
|
sessionDuration: "",
|
|
gitStatus: null,
|
|
usageData: null,
|
|
memoryUsage: null,
|
|
config: {
|
|
display: { showSessionTokens: true },
|
|
colors: { label: "dim" },
|
|
},
|
|
extraLabel: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
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("mergeConfig defaults to English when no language is specified", () => {
|
|
const config = mergeConfig({});
|
|
assert.equal(config.language, "en");
|
|
});
|
|
|
|
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 English for invalid language", () => {
|
|
const config = mergeConfig({ language: "invalid" });
|
|
assert.equal(config.language, "en");
|
|
});
|
|
|
|
test("renderSessionTokensLine uses translated labels in English", () => {
|
|
setLanguage("en");
|
|
const line = stripAnsi(renderSessionTokensLine(makeCtx()) ?? "");
|
|
assert.ok(line.includes("Tokens"), `expected 'Tokens' in ${line}`);
|
|
assert.ok(line.includes("in:"), `expected 'in:' in ${line}`);
|
|
assert.ok(line.includes("out:"), `expected 'out:' in ${line}`);
|
|
assert.ok(line.includes("cache:"), `expected 'cache:' in ${line}`);
|
|
});
|
|
|
|
test("renderSessionTokensLine uses translated labels in Chinese", () => {
|
|
setLanguage("zh");
|
|
const line = stripAnsi(renderSessionTokensLine(makeCtx()) ?? "");
|
|
assert.ok(line.includes("词元"), `expected '词元' in ${line}`);
|
|
assert.ok(line.includes("输入:"), `expected '输入:' in ${line}`);
|
|
assert.ok(line.includes("输出:"), `expected '输出:' in ${line}`);
|
|
assert.ok(line.includes("缓存:"), `expected '缓存:' in ${line}`);
|
|
// No leftover English labels
|
|
assert.ok(!line.includes("in:"), `unexpected bare 'in:' label in zh output: ${line}`);
|
|
assert.ok(!line.includes("out:"), `unexpected bare 'out:' label in zh output: ${line}`);
|
|
assert.ok(!line.includes("cache:"), `unexpected bare 'cache:' label in zh output: ${line}`);
|
|
assert.ok(!line.includes("Tokens"), `unexpected bare 'Tokens' label in zh output: ${line}`);
|
|
setLanguage("en");
|
|
});
|
|
|
|
test("getCanonicalLanguage resolves zh alias to zh-Hans", () => {
|
|
setLanguage("zh");
|
|
assert.equal(getCanonicalLanguage(), "zh-Hans");
|
|
setLanguage("en");
|
|
});
|
|
|
|
test("getCanonicalLanguage returns zh-Hans for explicit zh-Hans", () => {
|
|
setLanguage("zh-Hans");
|
|
assert.equal(getCanonicalLanguage(), "zh-Hans");
|
|
setLanguage("en");
|
|
});
|
|
|
|
test("getCanonicalLanguage returns en for English", () => {
|
|
setLanguage("en");
|
|
assert.equal(getCanonicalLanguage(), "en");
|
|
});
|
|
|
|
test("isCjkLanguage returns true for zh", () => {
|
|
setLanguage("zh");
|
|
assert.equal(isCjkLanguage(), true);
|
|
setLanguage("en");
|
|
});
|
|
|
|
test("isCjkLanguage returns true for zh-Hans", () => {
|
|
setLanguage("zh-Hans");
|
|
assert.equal(isCjkLanguage(), true);
|
|
setLanguage("en");
|
|
});
|
|
|
|
test("isCjkLanguage returns false for en", () => {
|
|
setLanguage("en");
|
|
assert.equal(isCjkLanguage(), false);
|
|
});
|
|
|
|
test("t() resolves translations via canonical mapping for zh-Hans", () => {
|
|
setLanguage("zh-Hans");
|
|
assert.equal(t("label.context"), "上下文");
|
|
assert.equal(t("label.usage"), "用量");
|
|
setLanguage("en");
|
|
});
|
|
|
|
test("mergeConfig accepts zh-Hans as valid language", () => {
|
|
const config = mergeConfig({ language: "zh-Hans" });
|
|
assert.equal(config.language, "zh-Hans");
|
|
});
|
|
|
|
// zh-Hant / zh-TW
|
|
|
|
test("t() returns Traditional Chinese strings when language is zh-Hant", () => {
|
|
setLanguage("zh-Hant");
|
|
assert.equal(t("label.context"), "上下文");
|
|
assert.equal(t("label.usage"), "用量");
|
|
assert.equal(t("label.approxRam"), "記憶體");
|
|
assert.equal(t("label.promptCache"), "快取");
|
|
assert.equal(t("label.rules"), "規則");
|
|
assert.equal(t("label.hooks"), "Hook");
|
|
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"), "輸出");
|
|
assert.equal(t("format.absoluteTime"), "{time}");
|
|
assert.equal(t("format.relativeTime"), "{value} 前");
|
|
setLanguage("en");
|
|
});
|
|
|
|
test("Traditional Chinese locale defines every English message key", () => {
|
|
assert.deepEqual(Object.keys(zhHant).sort(), Object.keys(en).sort());
|
|
});
|
|
|
|
test("t() returns Traditional Chinese strings when language is zh-TW", () => {
|
|
setLanguage("zh-TW");
|
|
assert.equal(t("label.approxRam"), "記憶體");
|
|
assert.equal(t("label.promptCache"), "快取");
|
|
assert.equal(t("status.limitReached"), "已達上限");
|
|
setLanguage("en");
|
|
});
|
|
|
|
test("getCanonicalLanguage resolves zh-Hant to zh-Hant", () => {
|
|
setLanguage("zh-Hant");
|
|
assert.equal(getCanonicalLanguage(), "zh-Hant");
|
|
setLanguage("en");
|
|
});
|
|
|
|
test("getCanonicalLanguage resolves zh-TW alias to zh-Hant", () => {
|
|
setLanguage("zh-TW");
|
|
assert.equal(getCanonicalLanguage(), "zh-Hant");
|
|
setLanguage("en");
|
|
});
|
|
|
|
test("isCjkLanguage returns true for zh-Hant", () => {
|
|
setLanguage("zh-Hant");
|
|
assert.equal(isCjkLanguage(), true);
|
|
setLanguage("en");
|
|
});
|
|
|
|
test("isCjkLanguage returns true for zh-TW", () => {
|
|
setLanguage("zh-TW");
|
|
assert.equal(isCjkLanguage(), true);
|
|
setLanguage("en");
|
|
});
|
|
|
|
test("renderSessionTokensLine uses Traditional Chinese labels for zh-Hant", () => {
|
|
setLanguage("zh-Hant");
|
|
const line = stripAnsi(renderSessionTokensLine(makeCtx()) ?? "");
|
|
assert.ok(line.includes("Token"), `expected 'Token' in ${line}`);
|
|
assert.ok(line.includes("輸入:"), `expected '輸入:' in ${line}`);
|
|
assert.ok(line.includes("輸出:"), `expected '輸出:' in ${line}`);
|
|
assert.ok(line.includes("快取:"), `expected '快取:' in ${line}`);
|
|
assert.ok(!line.includes("in:"), `unexpected bare 'in:' in zh-Hant output: ${line}`);
|
|
assert.ok(!line.includes("out:"), `unexpected bare 'out:' in zh-Hant output: ${line}`);
|
|
assert.ok(!line.includes("cache:"), `unexpected bare 'cache:' in zh-Hant output: ${line}`);
|
|
setLanguage("en");
|
|
});
|
|
|
|
test("mergeConfig accepts zh-Hant as valid language", () => {
|
|
const config = mergeConfig({ language: "zh-Hant" });
|
|
assert.equal(config.language, "zh-Hant");
|
|
});
|
|
|
|
test("mergeConfig accepts zh-TW as valid language", () => {
|
|
const config = mergeConfig({ language: "zh-TW" });
|
|
assert.equal(config.language, "zh-TW");
|
|
});
|