From 34d6e16a7826ec7d921a6af97a84cc4da47a72d0 Mon Sep 17 00:00:00 2001 From: YAN YIXIN Date: Thu, 5 Nov 2026 22:11:42 +0800 Subject: [PATCH] [bugfix] i18n: localize the assistant trace page through the catalog The trace page hardcoded Chinese and English pairs in the component and picked the language from navigator.language, so it ignored the language chosen in the app and added 16 Chinese source lines that made npm run check:i18n fail on main (introduced in #96). Move every label and status message to assistant.trace.* catalog entries in zh-CN and en-US and read them with useT(), so the page follows the app locale like the rest of the UI. traceCategory now returns a typed category so each badge maps to a catalog key. Co-Authored-By: Claude Opus 5.5 --- src/canvas/AssistantTracePage.tsx | 70 +++++++++++++++------------- src/canvas/traceCategory.ts | 5 +- src/i18n/messages/en-US/assistant.ts | 21 +++++++++ src/i18n/messages/zh-CN/assistant.ts | 21 +++++++++ 4 files changed, 83 insertions(+), 34 deletions(-) diff --git a/src/canvas/AssistantTracePage.tsx b/src/canvas/AssistantTracePage.tsx index 5f871e01..feba5286 100644 --- a/src/canvas/AssistantTracePage.tsx +++ b/src/canvas/AssistantTracePage.tsx @@ -1,6 +1,7 @@ import { useEffect, useMemo, useState } from "react"; import { traceCategory } from "./traceCategory.ts"; +import { useT } from "../i18n/I18nProvider"; import { normalizeDeepseekTrace } from "./deepseekTrace.ts"; @@ -31,19 +32,21 @@ type TraceRecord = { }; }; -const labels: Record = { - system: ["系统", "System"], user: ["用户", "User"], context: ["上下文", "Context"], - assistant: ["助手", "Assistant"], tool: ["工具", "Tool"], model: ["模型", "Model"], - event: ["事件", "Event"], reasoning: ["思考", "Thinking"], -}; +const labels = { + system: "assistant.trace.kind.system", user: "assistant.trace.kind.user", + context: "assistant.trace.kind.context", assistant: "assistant.trace.kind.assistant", + tool: "assistant.trace.kind.tool", model: "assistant.trace.kind.model", + event: "assistant.trace.kind.event", reasoning: "assistant.trace.kind.reasoning", +} as const; -function TraceRow({ kind, summary, value, chinese }: { - kind: string; summary: string; value: unknown; chinese: boolean; +function TraceRow({ kind, summary, value }: { + kind: keyof typeof labels; summary: string; value: unknown; }) { + const t = useT(); return (
- {labels[kind][chinese ? 0 : 1]} + {t(labels[kind])} {summary}
{typeof value === "string" ? value : JSON.stringify(value, null, 2)}
@@ -51,7 +54,8 @@ function TraceRow({ kind, summary, value, chinese }: { ); } -function TraceEntries({ traces, chinese, rawModel }: { traces: string[]; chinese: boolean; rawModel: boolean }) { +function TraceEntries({ traces, rawModel }: { traces: string[]; rawModel: boolean }) { + const t = useT(); const entries = useMemo(() => { const parsed = traces.flatMap((trace, sourceIndex) => { let timestamp = 0; @@ -93,9 +97,9 @@ function TraceEntries({ traces, chinese, rawModel }: { traces: string[]; chinese message.role === "user" && message.content === "Generate a title for this conversation:"); if (rawModel) { request++; - const label = titleRequest ? (chinese ? "生成会话标题" : "Generate conversation title") : - (chinese ? `第 ${request} 次模型请求` : `Model request ${request}`); - return ; + const label = titleRequest ? t("assistant.trace.titleRequest") : + t("assistant.trace.modelRequest", { count: request }); + return ; } const thoughts = (payload.messages ?? []).flatMap((message, index) => { const text = message.reasoning_content?.trim(); @@ -104,7 +108,7 @@ function TraceEntries({ traces, chinese, rawModel }: { traces: string[]; chinese if (seenReasoning.has(identity)) return []; seenReasoning.add(identity); if (hasStreamedReasoning || firstModelRequest || titleRequest) return []; - return []; + return []; }); if (!titleRequest) firstModelRequest = false; const inputs = titleRequest ? [] : (payload.messages ?? []).flatMap((message, index) => { @@ -114,34 +118,34 @@ function TraceEntries({ traces, chinese, rawModel }: { traces: string[]; chinese seenInputs.add(identity); const content = typeof message.content === "string" ? message.content : identity; return []; + summary={t("assistant.trace.systemPrompt")} + value={content} />]; }); const toolsKey = payload.tools?.length ? JSON.stringify(payload.tools) : undefined; if (toolsKey && !seenInputs.has(toolsKey)) { seenInputs.add(toolsKey); inputs.push(); + summary={t("assistant.trace.toolDefinitions", { count: payload.tools?.length ?? 0 })} + value={payload.tools} />); } return
{thoughts}{inputs}
; } const type = record?.native?.event_type ?? record?.native?.type ?? record?.event ?? record?.type; const part = record?.native?.part; if (type === "model.reasoning" && record?.text) { - return ; + return ; } if (!rawModel && type?.startsWith("model.")) return null; if (type === "reasoning" && part?.text?.trim()) { if (hasStreamedReasoning) return null; - return ; + return ; } const assistantText = type === "text" || type === "reasoning" ? part?.text : type === "item.completed" && record?.native?.item?.type === "agent_message" ? record.native.item.text : type === "assistant" ? record?.native?.message?.content ?.filter((block) => block.type === "text").map((block) => block.text).join("\n") : undefined; if (assistantText?.trim()) { - return ; + return ; } if (type === "step_start" || type === "text" || type === "reasoning") return null; const kind = traceCategory( @@ -153,7 +157,7 @@ function TraceEntries({ traces, chinese, rawModel }: { traces: string[]; chinese part?.state?.status ?? part?.reason ?? record?.error ?? record?.status, record?.elapsedMs === undefined ? undefined : `${record.elapsedMs} ms`, ].filter(Boolean).join(" · ") || line; - return ; + return ; }); } @@ -167,7 +171,7 @@ export default function AssistantTracePage({ params }: { params: URLSearchParams const [showModel, setShowModel] = useState(false); const rawModel = runtime === "opencode" && showModel; const [status, setStatus] = useState(null); - const chinese = navigator.language.startsWith("zh"); + const t = useT(); useEffect(() => { const controller = new AbortController(); @@ -213,31 +217,31 @@ export default function AssistantTracePage({ params }: { params: URLSearchParams return (
-

{chinese ? "助手轨迹" : "Assistant trace"}

+

{t("assistant.trace.title")}

{turn}

{runtime === "opencode" && ( -
- {!rawModel && input !== null && } + {!rawModel && input !== null && } {status === 200 ? ( rawModel && !traces[0]?.trim() ? ( -

{chinese ? "该轮暂无模型请求记录。" : "No model requests recorded for this turn."}

+

{t("assistant.trace.noModelRequests")}

) : ( - + ) ) : ( -

{status === null ? (chinese ? "加载中…" : "Loading…") : - status === 404 && live ? (chinese ? "等待首条轨迹…" : "Waiting for trace events…") : - status === 404 ? (chinese ? "该轮没有此类轨迹。" : "No trace of this type for this turn.") : - (chinese ? "轨迹加载失败。" : "Could not load trace.")}

+

{status === null ? t("assistant.trace.loading") : + status === 404 && live ? t("assistant.trace.waiting") : + status === 404 ? t("assistant.trace.missing") : + t("assistant.trace.failed")}

)}
); diff --git a/src/canvas/traceCategory.ts b/src/canvas/traceCategory.ts index a8257f92..5e38d8c5 100644 --- a/src/canvas/traceCategory.ts +++ b/src/canvas/traceCategory.ts @@ -1,5 +1,8 @@ /** Semantic colors are shared by every harness, independent of its event names. */ -export function traceCategory(type?: string, role?: string): string { +export type TraceCategory = + | "system" | "user" | "context" | "reasoning" | "tool" | "assistant" | "model" | "event"; + +export function traceCategory(type?: string, role?: string): TraceCategory { if (role === "system" || role === "developer" || type === "system") return "system"; if (role === "user" || type === "user" || type === "user_message") return "user"; if (type === "plan" || type === "context" || type === "session_meta" || type === "turn_context") return "context"; diff --git a/src/i18n/messages/en-US/assistant.ts b/src/i18n/messages/en-US/assistant.ts index 81190e9b..a9145163 100644 --- a/src/i18n/messages/en-US/assistant.ts +++ b/src/i18n/messages/en-US/assistant.ts @@ -36,6 +36,27 @@ export const assistant = { "assistant.panel.welcomeTitle": "Start from an idea", "assistant.panel.welcomeLead": "Shape your next work with OpenDirector.", "assistant.panel.trace": "Trace", + // Assistant trace page (opened from a reply's trace link). + "assistant.trace.title": "Assistant trace", + "assistant.trace.views": "Trace views", + "assistant.trace.rawRequests": "Raw model requests", + "assistant.trace.noModelRequests": "No model requests recorded for this turn.", + "assistant.trace.loading": "Loading…", + "assistant.trace.waiting": "Waiting for trace events…", + "assistant.trace.missing": "No trace of this type for this turn.", + "assistant.trace.failed": "Could not load trace.", + "assistant.trace.titleRequest": "Generate conversation title", + "assistant.trace.modelRequest": "Model request {count}", + "assistant.trace.systemPrompt": "System prompt", + "assistant.trace.toolDefinitions": "Tool definitions · {count}", + "assistant.trace.kind.system": "System", + "assistant.trace.kind.user": "User", + "assistant.trace.kind.context": "Context", + "assistant.trace.kind.assistant": "Assistant", + "assistant.trace.kind.tool": "Tool", + "assistant.trace.kind.model": "Model", + "assistant.trace.kind.event": "Event", + "assistant.trace.kind.reasoning": "Thinking", "assistant.panel.welcomeBody": "Describe a story, or type @ to add image, video, audio and text references.", "assistant.panel.composer": "Chat input", diff --git a/src/i18n/messages/zh-CN/assistant.ts b/src/i18n/messages/zh-CN/assistant.ts index fa348e80..677bd595 100644 --- a/src/i18n/messages/zh-CN/assistant.ts +++ b/src/i18n/messages/zh-CN/assistant.ts @@ -35,6 +35,27 @@ export const assistant = { "assistant.panel.welcomeTitle": "从一个想法开始", "assistant.panel.welcomeLead": "和 OpenDirector 一起构思你的下一部作品。", "assistant.panel.trace": "轨迹", + // Assistant trace page (opened from a reply's trace link). + "assistant.trace.title": "助手轨迹", + "assistant.trace.views": "轨迹视图", + "assistant.trace.rawRequests": "模型原始请求", + "assistant.trace.noModelRequests": "该轮暂无模型请求记录。", + "assistant.trace.loading": "加载中…", + "assistant.trace.waiting": "等待首条轨迹…", + "assistant.trace.missing": "该轮没有此类轨迹。", + "assistant.trace.failed": "轨迹加载失败。", + "assistant.trace.titleRequest": "生成会话标题", + "assistant.trace.modelRequest": "第 {count} 次模型请求", + "assistant.trace.systemPrompt": "系统提示词", + "assistant.trace.toolDefinitions": "工具定义 · {count}", + "assistant.trace.kind.system": "系统", + "assistant.trace.kind.user": "用户", + "assistant.trace.kind.context": "上下文", + "assistant.trace.kind.assistant": "助手", + "assistant.trace.kind.tool": "工具", + "assistant.trace.kind.model": "模型", + "assistant.trace.kind.event": "事件", + "assistant.trace.kind.reasoning": "思考", "assistant.panel.welcomeBody": "描述故事,或输入 @ 添加图片、视频、音频与文本参考。", "assistant.panel.composer": "聊天输入框",