From 1425b2fb5f0d6970ba404497a2716b5615f1655e Mon Sep 17 00:00:00 2001 From: Zhang Jason Date: Wed, 22 Jul 2026 22:06:45 +0800 Subject: [PATCH] [fix(web)] localize copied task-stats line instead of hardcoded Chinese (#8) --- .../web/src/features/chat/task-stats-line.tsx | 11 ++- packages/web/src/lib/omni/task-stats.ts | 27 ++++-- packages/web/src/lib/strings-en.ts | 4 + packages/web/src/lib/strings.ts | 4 + packages/web/test/task-stats.test.ts | 87 ++++++++++++++----- 5 files changed, 104 insertions(+), 29 deletions(-) diff --git a/packages/web/src/features/chat/task-stats-line.tsx b/packages/web/src/features/chat/task-stats-line.tsx index a457a22..9a21083 100644 --- a/packages/web/src/features/chat/task-stats-line.tsx +++ b/packages/web/src/features/chat/task-stats-line.tsx @@ -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); diff --git a/packages/web/src/lib/omni/task-stats.ts b/packages/web/src/lib/omni/task-stats.ts index 428cb73..d92195c 100644 --- a/packages/web/src/lib/omni/task-stats.ts +++ b/packages/web/src/lib/omni/task-stats.ts @@ -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)}` ); } diff --git a/packages/web/src/lib/strings-en.ts b/packages/web/src/lib/strings-en.ts index df18073..9709ab0 100644 --- a/packages/web/src/lib/strings-en.ts +++ b/packages/web/src/lib/strings-en.ts @@ -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", diff --git a/packages/web/src/lib/strings.ts b/packages/web/src/lib/strings.ts index 740f90b..21c94c7 100644 --- a/packages/web/src/lib/strings.ts +++ b/packages/web/src/lib/strings.ts @@ -589,8 +589,12 @@ Penguin 视觉风格(见 web-design 技能),深色/浅色主题( { }); 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"); }); });