[fix(web)] localize copied task-stats line instead of hardcoded Chinese (#8)

This commit is contained in:
Zhang Jason
2026-07-22 22:06:45 +08:00
committed by GitHub
parent 81d8c0045c
commit 1425b2fb5f
5 changed files with 104 additions and 29 deletions
@@ -71,7 +71,16 @@ export function TaskStatsLine({
const copy = () => {
const text =
assistantText?.trim() || stats === null ? (assistantText ?? "") : formatTaskStats(stats);
assistantText?.trim() || stats === null
? (assistantText ?? "")
: formatTaskStats(stats, {
stats: S.chat.statsLabel,
input: S.chat.statInput,
cached: S.chat.statCached,
output: S.chat.statOutput,
parenOpen: S.chat.statParenOpen,
parenClose: S.chat.statParenClose,
});
void navigator.clipboard?.writeText(text).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 1500);
+22 -5
View File
@@ -287,17 +287,34 @@ export function endTask(t: TaskStatsTracker, elapsedMs: number): TaskStats | nul
return stats;
}
/** Localized labels for {@link formatTaskStats} (supplied by the view layer's active dictionary). */
export interface TaskStatsLabels {
/** Row prefix, e.g. "Stats" / "统计信息". */
stats: string;
/** Input-tokens label, e.g. "Input tokens" / "输入 tokens". */
input: string;
/** Cached-amount label, e.g. "cached" / "已缓存". */
cached: string;
/** Output-tokens label, e.g. "Output tokens" / "输出 tokens". */
output: string;
/** Opening wrapper before the cached amount — keeps per-locale typography (e.g. " (" / "("). */
parenOpen: string;
/** Closing wrapper after the cached amount (e.g. ")" / ")"). */
parenClose: string;
}
/**
* Stats row text (copy fallback / used when there's no body text): same basis as the stats row —
* this round's usage "input tokens (including cached amount) · output tokens · output TPS", e.g.
* `[统计信息] 输入 tokens 4k(已缓存 3k) · 输出 tokens 1.2k · 42.5 tok/s`; label can be overridden
* per UI locale.
* `[Stats] Input tokens 4k (cached 3k) · Output tokens 1.2k · 42.5 tok/s`. Labels (including the
* parenthesis wrappers) come from the caller's active locale dictionary, so the copied text keeps
* per-locale typography — ASCII `(…)` in English, fullwidth `(…)` in Chinese.
*/
export function formatTaskStats(s: TaskStats, label = "统计信息"): string {
export function formatTaskStats(s: TaskStats, labels: TaskStatsLabels): string {
const b = s.tokensByBucket;
return (
`[${label}] 输入 tokens ${humanizeTokens(b.cacheRead + b.cacheWrite)}(已缓存 ${humanizeTokens(b.cacheRead)})` +
` · 输出 tokens ${humanizeTokens(b.output)}` +
`[${labels.stats}] ${labels.input} ${humanizeTokens(b.cacheRead + b.cacheWrite)}${labels.parenOpen}${labels.cached} ${humanizeTokens(b.cacheRead)}${labels.parenClose}` +
` · ${labels.output} ${humanizeTokens(b.output)}` +
` · ${formatTps(s.outputTps)}`
);
}
+4
View File
@@ -609,8 +609,12 @@ When done, open index.html in a browser and self-test once.`,
statCost: "Cost",
statElapsed: "Elapsed",
statInput: "Input tokens",
statCached: "cached",
statOutput: "Output tokens",
statTps: "Output TPS",
/** Copied-stats-line parenthesis wrappers around the cached amount (ASCII with a leading space for en). */
statParenOpen: " (",
statParenClose: ")",
noSessions: "No Sessions yet",
emptyStream: "Send a message to start the conversation",
historyLoadFailed: "Failed to load history",
+4
View File
@@ -589,8 +589,12 @@ Penguin 视觉风格(见 web-design 技能),深色/浅色主题(<html da
statCost: "成本",
statElapsed: "用时",
statInput: "输入 tokens",
statCached: "已缓存",
statOutput: "输出 tokens",
statTps: "输出 TPS",
/** Copied-stats-line parenthesis wrappers around the cached amount (fullwidth for zh typography). */
statParenOpen: "(",
statParenClose: ")",
noSessions: "还没有 Session",
emptyStream: "发送一条消息开始对话",
historyLoadFailed: "历史消息加载失败",
+64 -23
View File
@@ -195,33 +195,74 @@ describe("TaskStatsTracker", () => {
});
describe("formatTaskStats", () => {
it("the convention is this round's usage \"输入(已缓存)· 输出 · 输出 TPS\", taking this Task's three-bucket usage + this Task's TPS", () => {
const EN_LABELS = {
stats: "Stats",
input: "Input tokens",
cached: "cached",
output: "Output tokens",
parenOpen: " (",
parenClose: ")",
};
const ZH_LABELS = {
stats: "统计信息",
input: "输入 tokens",
cached: "已缓存",
output: "输出 tokens",
parenOpen: "(",
parenClose: ")",
};
it("the convention is this round's usage \"input (cached) · output · output TPS\", taking this Task's three-bucket usage + this Task's TPS", () => {
expect(
formatTaskStats({
context: 40000, // context usage isn't in the stats row (shown by the input-box ring instead)
contextDelta: 1000,
tokens: 60000,
tokensDelta: 1200,
elapsedMs: 5100,
elapsedDeltaMs: 2300,
tokensByBucket: { cacheRead: 3000, cacheWrite: 1000, output: 1200 },
outputTps: 42.5,
}),
).toBe("[统计信息] 输入 tokens 4k(已缓存 3k) · 输出 tokens 1.2k · 42.5 tok/s");
formatTaskStats(
{
context: 40000, // context usage isn't in the stats row (shown by the input-box ring instead)
contextDelta: 1000,
tokens: 60000,
tokensDelta: 1200,
elapsedMs: 5100,
elapsedDeltaMs: 2300,
tokensByBucket: { cacheRead: 3000, cacheWrite: 1000, output: 1200 },
outputTps: 42.5,
},
EN_LABELS,
),
).toBe("[Stats] Input tokens 4k (cached 3k) · Output tokens 1.2k · 42.5 tok/s");
});
it("TPS shows — without LLM timing", () => {
expect(
formatTaskStats({
context: 500,
contextDelta: 0,
tokens: 500,
tokensDelta: 500,
elapsedMs: 900,
elapsedDeltaMs: 900,
tokensByBucket: { cacheRead: 500, cacheWrite: 0, output: 0 },
outputTps: null,
}),
).toBe("[统计信息] 输入 tokens 500(已缓存 500) · 输出 tokens 0 · —");
formatTaskStats(
{
context: 500,
contextDelta: 0,
tokens: 500,
tokensDelta: 500,
elapsedMs: 900,
elapsedDeltaMs: 900,
tokensByBucket: { cacheRead: 500, cacheWrite: 0, output: 0 },
outputTps: null,
},
EN_LABELS,
),
).toBe("[Stats] Input tokens 500 (cached 500) · Output tokens 0 · —");
});
it("renders with the supplied locale labels (zh)", () => {
expect(
formatTaskStats(
{
context: 40000,
contextDelta: 1000,
tokens: 60000,
tokensDelta: 1200,
elapsedMs: 5100,
elapsedDeltaMs: 2300,
tokensByBucket: { cacheRead: 3000, cacheWrite: 1000, output: 1200 },
outputTps: 42.5,
},
ZH_LABELS,
),
).toBe("[统计信息] 输入 tokens 4k(已缓存 3k) · 输出 tokens 1.2k · 42.5 tok/s");
});
});