已合并
fix(web): 清除中文界面残留硬编码英文与内部术语(Issue #98) #149
fix(web): 清除中文界面残留硬编码英文与内部术语(Issue #98) #149
已合并
hugoxk创建于 9月19日
共 16 个文件变更+387-76
Mapps/web/src/App.tsx+5-3文件内容审核中,请稍后刷新重试
@@ -33,6 +33,19 @@ const ACTIVITY_STATUS_KEYS: Record<string, MessageKey> = {
33 failed: "ideaResearch.activity.failed",33 failed: "ideaResearch.activity.failed",
34};34};
35 35 
36+const IDEA_TREE_STATUS_KEYS: Record<string, MessageKey> = {
37+ pending: "ideaTree.status.pending",
38+ running: "ideaTree.status.running",
39+ done: "ideaTree.status.done",
40+ needs_retry: "ideaTree.status.needs_retry",
41+ failed: "ideaTree.status.failed",
42+};
43+ 
44+const IDEA_TREE_SEARCH_STATUS_KEYS: Record<string, MessageKey> = {
45+ active: "ideaTree.search.active",
46+ pruned: "ideaTree.search.pruned",
47+};
48+ 
36/** An unknown value renders as itself: the engine may report a status this49/** An unknown value renders as itself: the engine may report a status this
37 * build does not know, and showing the raw token beats showing nothing. */50 * build does not know, and showing the raw token beats showing nothing. */
38export function ideaResearchStatusLabel(t: Translate, status: string): string {51export function ideaResearchStatusLabel(t: Translate, status: string): string {
@@ -45,6 +58,16 @@ export function ideaResearchActivityStatusLabel(t: Translate, status: string): s
45 return key ? t(key) : status;58 return key ? t(key) : status;
46}59}
47 60 
61+export function ideaTreeStatusLabel(t: Translate, status: string): string {
62+ const key = IDEA_TREE_STATUS_KEYS[status];
63+ return key ? t(key) : status;
64+}
65+ 
66+export function ideaTreeSearchStatusLabel(t: Translate, status: string): string {
67+ const key = IDEA_TREE_SEARCH_STATUS_KEYS[status];
68+ return key ? t(key) : status;
69+}
70+ 
48export function ideaResearchPhaseLabel(t: Translate, research: IdeaResearchState, role: string): string {71export function ideaResearchPhaseLabel(t: Translate, research: IdeaResearchState, role: string): string {
49 const key = ROLE_KEYS[role];72 const key = ROLE_KEYS[role];
50 if (key) return t(key);73 if (key) return t(key);
@@ -8,6 +8,7 @@ import type { PointerEvent as ReactPointerEvent, WheelEvent as ReactWheelEvent }
8 8 
9import type { IdeaTreeGraph, IdeaTreeNode } from "@sciencediscovery/schema";9import type { IdeaTreeGraph, IdeaTreeNode } from "@sciencediscovery/schema";
10 10 
11+import { ideaTreeStatusLabel } from "./IdeaResearchLabels.js";
11import { useLocale } from "./i18n/index.js";12import { useLocale } from "./i18n/index.js";
12 13 
13const NODE_WIDTH = 244;14const NODE_WIDTH = 244;
@@ -297,11 +298,11 @@ export function IdeaTreeCanvas({
297 x={NODE_WIDTH - 91}298 x={NODE_WIDTH - 91}
298 y="10"299 y="10"
299 />300 />
300- <text className="idea-tree-node-status" fill={IDEA_TREE_STATUS_COLORS[node.status]} textAnchor="middle" x={NODE_WIDTH - 52} y="24">{node.kind === "direction" ? t("ideaTree.direction") : node.status.replace("_", " ")}</text>301+ <text className="idea-tree-node-status" fill={IDEA_TREE_STATUS_COLORS[node.status]} textAnchor="middle" x={NODE_WIDTH - 52} y="24">{node.kind === "direction" ? t("ideaTree.direction") : ideaTreeStatusLabel(t, node.status)}</text>
301 <foreignObject height="40" width={NODE_WIDTH - 40} x="20" y="38">302 <foreignObject height="40" width={NODE_WIDTH - 40} x="20" y="38">
302 <div className="idea-tree-node-title" title={node.hypothesis}>{node.hypothesis}</div>303 <div className="idea-tree-node-title" title={node.hypothesis}>{node.hypothesis}</div>
303 </foreignObject>304 </foreignObject>
304- <text className="idea-tree-node-meta" x="20" y="94">Depth {node.depth}{node.score === null ? "" : ` · Score ${node.score}`}</text>305+ <text className="idea-tree-node-meta" x="20" y="94">{node.score === null ? t("ideaTree.canvas.depth", { depth: node.depth }) : t("ideaTree.canvas.depthScore", { depth: node.depth, score: node.score })}</text>
305 </g>;306 </g>;
306 })}307 })}
307 </g>308 </g>
@@ -2,11 +2,21 @@
2//2//
3// Licensed under the Apache License, Version 2.0 (the "License");3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.4// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
5 14 
6import { useEffect, useMemo, useState, type ReactNode } from "react";15import { useEffect, useMemo, useState, type ReactNode } from "react";
7 16 
8import type { IdeaTreeGraph, IdeaTreeNode } from "@sciencediscovery/schema";17import type { IdeaTreeGraph, IdeaTreeNode } from "@sciencediscovery/schema";
9 18 
19+import { ideaTreeSearchStatusLabel, ideaTreeStatusLabel } from "./IdeaResearchLabels.js";
10import { useLocale, type MessageKey } from "./i18n/index.js";20import { useLocale, type MessageKey } from "./i18n/index.js";
11import { CloseIcon } from "./icons.js";21import { CloseIcon } from "./icons.js";
12import { IDEA_TREE_STATUS_COLORS, IdeaTreeCanvas } from "./IdeaTreeCanvas.js";22import { IDEA_TREE_STATUS_COLORS, IdeaTreeCanvas } from "./IdeaTreeCanvas.js";
@@ -32,46 +42,46 @@ function formatValue(value: unknown): string {
32function IdeaTreeNodeDetail({ node, autonomous, onOpenArtifact, onOpenSubagent }: { node?: IdeaTreeNode; autonomous?: boolean; onOpenArtifact?: (id: string) => void; onOpenSubagent?: (id: string) => void }) {42function IdeaTreeNodeDetail({ node, autonomous, onOpenArtifact, onOpenSubagent }: { node?: IdeaTreeNode; autonomous?: boolean; onOpenArtifact?: (id: string) => void; onOpenSubagent?: (id: string) => void }) {
33 const { t } = useLocale();43 const { t } = useLocale();
34 if (!node) return <div className="idea-tree-detail-empty">44 if (!node) return <div className="idea-tree-detail-empty">
35- <strong>Select a tree node</strong>45+ <strong>{t("ideaTree.explorer.emptyTitle")}</strong>
36- <p>Click a node to inspect its hypothesis, result, score, execution state, and artifacts.</p>46+ <p>{t("ideaTree.explorer.emptyHelp")}</p>
37 </div>;47 </div>;
38 const fields: Array<[string, unknown]> = autonomous ? [48 const fields: Array<[string, unknown]> = autonomous ? [
39 [t("ideaTree.node.kind"), node.kind === "direction" ? t("ideaTree.kind.direction") : t("ideaTree.kind.candidate")],49 [t("ideaTree.node.kind"), node.kind === "direction" ? t("ideaTree.kind.direction") : t("ideaTree.kind.candidate")],
40 [t("ideaTree.node.depth"), node.depth], [t("ideaTree.node.score"), node.score], [t("ideaTree.node.parent"), node.parentId],50 [t("ideaTree.node.depth"), node.depth], [t("ideaTree.node.score"), node.score], [t("ideaTree.node.parent"), node.parentId],
41 [t("ideaTree.node.children"), node.childrenIds],51 [t("ideaTree.node.children"), node.childrenIds],
42 ] : [52 ] : [
43- ["Status", node.status],53+ [t("ideaTree.node.status"), ideaTreeStatusLabel(t, node.status)],
44- ["Search", node.searchStatus],54+ [t("ideaTree.node.search"), ideaTreeSearchStatusLabel(t, node.searchStatus)],
45- ["Depth", node.depth],55+ [t("ideaTree.node.depth"), node.depth],
46- ["Priority", node.priority],56+ [t("ideaTree.node.priority"), node.priority],
47- ["Score", node.score],57+ [t("ideaTree.node.score"), node.score],
48- ["Attempts", node.attemptCount],58+ [t("ideaTree.node.attempts"), node.attemptCount],
49- ["Parent", node.parentId],59+ [t("ideaTree.node.parent"), node.parentId],
50- ["Children", node.childrenIds],60+ [t("ideaTree.node.children"), node.childrenIds],
51- ["Artifacts", node.artifactRefs],61+ [t("ideaTree.node.artifacts"), node.artifactRefs],
52- ["Active execution", node.activeExecutionId],62+ [t("ideaTree.node.activeExecution"), node.activeExecutionId],
53- ["Last execution", node.lastExecutionId],63+ [t("ideaTree.node.lastExecution"), node.lastExecutionId],
54- ["Result handle", node.completedResultHandle],64+ [t("ideaTree.node.resultHandle"), node.completedResultHandle],
55- ["Updated", node.updatedAt],65+ [t("ideaTree.node.updated"), node.updatedAt],
56 ];66 ];
57 return <article className="idea-tree-detail">67 return <article className="idea-tree-detail">
58 <header>68 <header>
59 <span className="idea-tree-detail-id">{node.id}</span>69 <span className="idea-tree-detail-id">{node.id}</span>
60- <span className="idea-tree-status" style={{ borderColor: IDEA_TREE_STATUS_COLORS[node.status], color: IDEA_TREE_STATUS_COLORS[node.status] }}>{node.kind === "direction" ? t("ideaTree.kind.direction") : node.status}</span>70+ <span className="idea-tree-status" style={{ borderColor: IDEA_TREE_STATUS_COLORS[node.status], color: IDEA_TREE_STATUS_COLORS[node.status] }}>{node.kind === "direction" ? t("ideaTree.kind.direction") : ideaTreeStatusLabel(t, node.status)}</span>
61 </header>71 </header>
62 <section>72 <section>
63- <h3>Hypothesis</h3>73+ <h3>{t("ideaTree.section.hypothesis")}</h3>
64 <p>{node.hypothesis}</p>74 <p>{node.hypothesis}</p>
65 </section>75 </section>
66- {node.insight ? <section><h3>Insight</h3><p>{node.insight}</p></section> : null}76+ {node.insight ? <section><h3>{t("ideaTree.section.insight")}</h3><p>{node.insight}</p></section> : null}
67- {!autonomous && node.result ? <section><h3>Result</h3><p>{node.result}</p></section> : null}77+ {!autonomous && node.result ? <section><h3>{t("ideaTree.section.result")}</h3><p>{node.result}</p></section> : null}
68 {autonomous && Object.entries(node.stages ?? {}).map(([role, result]) => <section key={role}>78 {autonomous && Object.entries(node.stages ?? {}).map(([role, result]) => <section key={role}>
69 <h3>{ASSESSOR_KEYS[role] ? t(ASSESSOR_KEYS[role]!) : role}{result.score === undefined ? "" : ` · ${result.score}`}</h3>79 <h3>{ASSESSOR_KEYS[role] ? t(ASSESSOR_KEYS[role]!) : role}{result.score === undefined ? "" : ` · ${result.score}`}</h3>
70 <p>{result.text}</p>80 <p>{result.text}</p>
71 </section>)}81 </section>)}
72- {node.pruneReason ? <section><h3>Prune reason</h3><p>{node.pruneReason}</p></section> : null}82+ {node.pruneReason ? <section><h3>{t("ideaTree.section.pruneReason")}</h3><p>{node.pruneReason}</p></section> : null}
73- <section>{node.artifactRefs.map(id => <button key={id} type="button" onClick={() => onOpenArtifact?.(id)}>Artifact {id}</button>)}</section>83+ <section>{node.artifactRefs.map(id => <button key={id} type="button" onClick={() => onOpenArtifact?.(id)}>{t("ideaTree.explorer.artifactButton", { id })}</button>)}</section>
74- <section>{node.subagentIds?.map((id, index) => <button key={id} type="button" onClick={() => onOpenSubagent?.(id)}>Subagent {index + 1}</button>)}</section>84+ <section>{node.subagentIds?.map((id, index) => <button key={id} type="button" onClick={() => onOpenSubagent?.(id)}>{t("ideaTree.explorer.subagentButton", { index: index + 1 })}</button>)}</section>
75 <dl>85 <dl>
76 {fields.map(([label, value]) => <div key={label}><dt>{label}</dt><dd>{formatValue(value)}</dd></div>)}86 {fields.map(([label, value]) => <div key={label}><dt>{label}</dt><dd>{formatValue(value)}</dd></div>)}
77 </dl>87 </dl>
@@ -99,6 +109,7 @@ export function IdeaTreeExplorer({
99 onSelectTree: (treeId: string) => void;109 onSelectTree: (treeId: string) => void;
100 treeIds: string[];110 treeIds: string[];
101}) {111}) {
112+ const { t } = useLocale();
102 const [selectedId, setSelectedId] = useState<string>("ROOT");113 const [selectedId, setSelectedId] = useState<string>("ROOT");
103 const [query, setQuery] = useState("");114 const [query, setQuery] = useState("");
104 const [visibleStatuses, setVisibleStatuses] = useState<ReadonlySet<IdeaTreeNode["status"]>>(new Set(STATUSES));115 const [visibleStatuses, setVisibleStatuses] = useState<ReadonlySet<IdeaTreeNode["status"]>>(new Set(STATUSES));
@@ -133,23 +144,23 @@ export function IdeaTreeExplorer({
133 };144 };
134 145 
135 return <div className="idea-tree-explorer-backdrop" onMouseDown={(event) => { if (event.target === event.currentTarget) onClose(); }}>146 return <div className="idea-tree-explorer-backdrop" onMouseDown={(event) => { if (event.target === event.currentTarget) onClose(); }}>
136- <section aria-label="Idea Tree explorer" aria-modal={true} className="idea-tree-explorer-panel" role="dialog">147+ <section aria-label={t("ideaTree.explorer.aria")} aria-modal={true} className="idea-tree-explorer-panel" role="dialog">
137 <header className="idea-tree-explorer-header">148 <header className="idea-tree-explorer-header">
138 <div>149 <div>
139- <span className="eyebrow">Agent search</span>150+ <span className="eyebrow">{t("ideaTree.explorer.eyebrow")}</span>
140- <h2>Idea Tree</h2>151+ <h2>{t("ideaTree.explorer.title")}</h2>
141 <p title={graph.objective}>{graph.objective}</p>152 <p title={graph.objective}>{graph.objective}</p>
142 </div>153 </div>
143 <div className="idea-tree-explorer-controls">154 <div className="idea-tree-explorer-controls">
144 {treeIds.length > 1 ? <label>155 {treeIds.length > 1 ? <label>
145- <span>Tree</span>156+ <span>{t("ideaTree.explorer.tree")}</span>
146 <select disabled={loading} onChange={(event) => onSelectTree(event.target.value)} value={graph.treeId}>157 <select disabled={loading} onChange={(event) => onSelectTree(event.target.value)} value={graph.treeId}>
147- {treeIds.map((treeId, index) => <option key={treeId} value={treeId}>{index === 0 ? "Latest · " : ""}{treeId}</option>)}158+ {treeIds.map((treeId, index) => <option key={treeId} value={treeId}>{index === 0 ? t("ideaTree.explorer.latestTree") : ""}{treeId}</option>)}
148 </select>159 </select>
149 </label> : null}160 </label> : null}
150- <span>{graph.nodes.length} nodes</span>161+ <span>{t("ideaTree.explorer.nodeCount", { count: graph.nodes.length })}</span>
151- {!autonomous && <span>revision {graph.revision}</span>}162+ {!autonomous && <span>{t("ideaTree.explorer.revision", { revision: graph.revision })}</span>}
152- <button aria-label="Close Idea Tree" className="icon-button" onClick={onClose} type="button"><CloseIcon size={20} /></button>163+ <button aria-label={t("ideaTree.explorer.closeAria")} className="icon-button" onClick={onClose} type="button"><CloseIcon size={20} /></button>
153 </div>164 </div>
154 </header>165 </header>
155 {controls}166 {controls}
@@ -158,17 +169,17 @@ export function IdeaTreeExplorer({
158 <main className="idea-tree-explorer-graph">169 <main className="idea-tree-explorer-graph">
159 <div className="idea-tree-toolbar">170 <div className="idea-tree-toolbar">
160 <label className="idea-tree-search">171 <label className="idea-tree-search">
161- <span>Find</span>172+ <span>{t("ideaTree.explorer.find")}</span>
162- <input onChange={(event) => setQuery(event.target.value)} placeholder="Node id, hypothesis, insight…" value={query} />173+ <input onChange={(event) => setQuery(event.target.value)} placeholder={t("ideaTree.explorer.findPlaceholder")} value={query} />
163 </label>174 </label>
164- <div className="idea-tree-filter-chips" aria-label="Filter node statuses">175+ <div className="idea-tree-filter-chips" aria-label={t("ideaTree.explorer.filterAria")}>
165 {STATUSES.filter((status) => statusCounts.has(status)).map((status) => <button176 {STATUSES.filter((status) => statusCounts.has(status)).map((status) => <button
166 aria-pressed={visibleStatuses.has(status)}177 aria-pressed={visibleStatuses.has(status)}
167 className={visibleStatuses.has(status) ? "active" : ""}178 className={visibleStatuses.has(status) ? "active" : ""}
168 key={status}179 key={status}
169 onClick={() => toggleStatus(status)}180 onClick={() => toggleStatus(status)}
170 type="button"181 type="button"
171- ><i style={{ background: IDEA_TREE_STATUS_COLORS[status] }} />{status}<em>{statusCounts.get(status)}</em></button>)}182+ ><i style={{ background: IDEA_TREE_STATUS_COLORS[status] }} />{ideaTreeStatusLabel(t, status)}<em>{statusCounts.get(status)}</em></button>)}
172 </div>183 </div>
173 </div>184 </div>
174 <div className="idea-tree-canvas-host">185 <div className="idea-tree-canvas-host">
@@ -179,8 +190,8 @@ export function IdeaTreeExplorer({
179 selectedId={selectedId}190 selectedId={selectedId}
180 visibleStatuses={visibleStatuses}191 visibleStatuses={visibleStatuses}
181 />192 />
182- {loading ? <div className="idea-tree-loading">Loading tree…</div> : null}193+ {loading ? <div className="idea-tree-loading">{t("ideaTree.explorer.loading")}</div> : null}
183- <p className="idea-tree-canvas-hint">Auto layout · click to inspect · drag background to pan · scroll to zoom</p>194+ <p className="idea-tree-canvas-hint">{t("ideaTree.explorer.canvasHint")}</p>
184 </div>195 </div>
185 </main>196 </main>
186 </div>197 </div>
Mapps/web/src/IdeaTreeView.tsx+8-8文件内容审核中,请稍后刷新重试
Mapps/web/src/ReviewerPanel.tsx+1-1文件内容审核中,请稍后刷新重试
@@ -729,7 +729,7 @@ export function ArtifactModal({
729 setPoint(undefined);729 setPoint(undefined);
730 setNote("");730 setNote("");
731 } catch (error) {731 } catch (error) {
732- onError(error instanceof Error ? error : "Could not save annotation");732+ onError(error instanceof Error ? error : t("error.saveAnnotation"));
733 }733 }
734 }734 }
735 735 
@@ -836,7 +836,7 @@ export function ArtifactModal({
836 ? datasetPreview836 ? datasetPreview
837 : artifact?.kind === "dataset" && jsonSource837 : artifact?.kind === "dataset" && jsonSource
838 ? <JsonSourcePreview parsed={jsonSource.parsed} source={jsonSource.source} truncated={jsonSource.truncated} />838 ? <JsonSourcePreview parsed={jsonSource.parsed} source={jsonSource.source} truncated={jsonSource.truncated} />
839- : artifact?.kind === "dataset" && !(structureFormat && source) ? <pre className="artifact-source-preview">{source.slice(0, 100_000) || "Binary artifact: use the version content endpoint to inspect it."}</pre> : null}839+ : artifact?.kind === "dataset" && !(structureFormat && source) ? <pre className="artifact-source-preview">{source.slice(0, 100_000) || t("artifact.binaryNote")}</pre> : null}
840 {/* JSON: a record array still gets the table (with a raw-JSON switch);840 {/* JSON: a record array still gets the table (with a raw-JSON switch);
841 every other document is shown as formatted JSON or raw text, never841 every other document is shown as formatted JSON or raw text, never
842 an empty grid. */}842 an empty grid. */}
@@ -845,11 +845,11 @@ export function ArtifactModal({
845 ? datasetPreview845 ? datasetPreview
846 : jsonSource846 : jsonSource
847 ? <JsonSourcePreview parsed={jsonSource.parsed} source={jsonSource.source} truncated={jsonSource.truncated} />847 ? <JsonSourcePreview parsed={jsonSource.parsed} source={jsonSource.source} truncated={jsonSource.truncated} />
848- : <pre className="artifact-source-preview">{source.slice(0, 100_000) || "Loading JSON content…"}</pre>848+ : <pre className="artifact-source-preview">{source.slice(0, 100_000) || t("artifact.loadingJson")}</pre>
849 : null}849 : null}
850 {artifact?.kind === "notebook" && preview?.kind === "notebook" && preview.mode === "notebook-cells"850 {artifact?.kind === "notebook" && preview?.kind === "notebook" && preview.mode === "notebook-cells"
851 ? <NotebookCells cells={preview.cells} />851 ? <NotebookCells cells={preview.cells} />
852- : artifact?.kind === "notebook" && !(structureFormat && source) ? <pre className="artifact-source-preview">{source.slice(0, 100_000) || "Binary artifact: use the version content endpoint to inspect it."}</pre> : null}852+ : artifact?.kind === "notebook" && !(structureFormat && source) ? <pre className="artifact-source-preview">{source.slice(0, 100_000) || t("artifact.binaryNote")}</pre> : null}
853 {/* "other" is where every source file lands — the kind enum has no853 {/* "other" is where every source file lands — the kind enum has no
854 "code" — and it had no case at all, so a .py artifact opened to a854 "code" — and it had no case at all, so a .py artifact opened to a
855 blank body. A NUL byte in the head is the binary tell; those get855 blank body. A NUL byte in the head is the binary tell; those get
Mapps/web/src/UsagePage.tsx+1-1文件内容审核中,请稍后刷新重试
@@ -468,7 +468,9 @@ export class ArtifactsApiClient extends RunsApiClient {
468 );468 );
469 if (!response.ok) {469 if (!response.ok) {
470 this.reportAuthStatus(response.status);470 this.reportAuthStatus(response.status);
471- const reason = response.status === 404 ? "WebPage content not available" : "Could not load WebPage content";471+ const reason = response.status === 404
472+ ? translateActive("error.webPageContentUnavailable")
473+ : translateActive("error.webPageContentLoadFailed");
472 throw new ApiRequestError(reason, response.status);474 throw new ApiRequestError(reason, response.status);
473 }475 }
474 return await response.text();476 return await response.text();
Mapps/web/src/i18n/messages.ts+132-8文件内容审核中,请稍后刷新重试
Mapps/web/src/run-failure.ts+21-6文件内容审核中,请稍后刷新重试
Mapps/web/src/timeline/RunTimeline.tsx+5-5文件内容审核中,请稍后刷新重试
@@ -85,3 +85,53 @@ test("renders Chinese messages when the provider starts in zh-CN", () => {
85 const html = renderToStaticMarkup(createElement(LocaleProvider, { initialLocale: "zh-CN" }, createElement(Probe)));85 const html = renderToStaticMarkup(createElement(LocaleProvider, { initialLocale: "zh-CN" }, createElement(Probe)));
86 assert.match(html, /zh-CN:系统设置/);86 assert.match(html, /zh-CN:系统设置/);
87});87});
88+ 
89+test("skill library copy never exposes internal recall or git-head jargon", () => {
90+ // The settings picker and the library cards are user-facing in both
91+ // locales; “M1” pipeline names and a git “Head” ref are implementation
92+ // detail the reader cannot act on (issue #98).
93+ for (const key of ["settings.skillLibrariesPickerHint", "settings.skillLibraryHead", "skillLibrary.head"] as const) {
94+ assert.ok(zhCN[key], `${key} needs a zh-CN message`);
95+ for (const locale of ["en", "zh-CN"] as const) {
96+ const message = translate(locale, key);
97+ assert.doesNotMatch(message, /M1|Head\s|\bHead\b/i, `${locale} ${key} leaks internal jargon: ${message}`);
98+ }
99+ }
100+ assert.equal(translate("en", "settings.skillLibraryHead", { version: "8a87052b" }), "Latest version 8a87052b");
101+ assert.equal(translate("zh-CN", "settings.skillLibraryHead", { version: "8a87052b" }), "最新版本 8a87052b");
102+});
103+ 
104+test("the zh-CN catalogue covers every locale-sensitive chrome string this fix touched", () => {
105+ const keys = [
106+ "settings.loadingProxies",
107+ "error.saveIdeaTree",
108+ "error.saveAnnotation",
109+ "error.webPageContentUnavailable",
110+ "error.webPageContentLoadFailed",
111+ "timeline.replayTextTruncated",
112+ "timeline.replayStepTruncated",
113+ "timeline.historyTruncated",
114+ "timeline.runStatusFallback",
115+ "ideaTree.view.loadError",
116+ "ideaTree.view.retry",
117+ "ideaTree.view.stats",
118+ "ideaTree.view.running",
119+ "ideaTree.canvas.depth",
120+ "ideaTree.canvas.depthScore",
121+ "ideaTree.search.active",
122+ "ideaTree.search.pruned",
123+ "artifact.loadingJson",
124+ "reviewer.sourceIssuesAria",
125+ "usage.runLabel",
126+ "usage.standaloneInvocations",
127+ ] as const;
128+ for (const key of keys) {
129+ assert.ok(zhCN[key], `${key} needs a zh-CN message`);
130+ }
131+});
132+ 
133+test("idea tree statuses render as words instead of raw enum tokens", () => {
134+ assert.equal(translate("en", "ideaTree.status.needs_retry"), "Needs retry");
135+ assert.equal(translate("zh-CN", "ideaTree.status.needs_retry"), "需重试");
136+ assert.equal(translate("zh-CN", "ideaTree.status.running"), "执行中");
137+});
Mapps/web/tests/IdeaTreeCanvas.test.tsx+30-0文件内容审核中,请稍后刷新重试
@@ -0,0 +1,53 @@
1+// Copyright (C) 2026-2026 Huawei Technologies Co., Ltd
2+//
3+// Licensed under the Apache License, Version 2.0 (the "License");
4+// you may not use this file except in compliance with the License.
5+// You may obtain a copy of the License at
6+//
7+// http://www.apache.org/licenses/LICENSE-2.0
8+//
9+// Unless required by applicable law or agreed to in writing, software
10+// distributed under the License is distributed on an "AS IS" BASIS,
11+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+// See the License for the specific language governing permissions and
13+// limitations under the License.
14+ 
15+import assert from "node:assert/strict";
16+import test from "node:test";
17+ 
18+import { zhCN } from "../src/i18n/messages.js";
19+import { setActiveLocale } from "../src/i18n/locale.js";
20+import { formatRunFailure } from "../src/run-failure.js";
21+ 
22+test("a failure class reads as a plain cause with a recovery action, then the detail", () => {
23+ setActiveLocale("en");
24+ assert.match(
25+ formatRunFailure("rate-limited", "429 Too Many Requests"),
26+ /^The model service is limiting requests\. Wait a moment and try again\. · 429 Too Many Requests$/,
27+ );
28+ assert.match(
29+ formatRunFailure("transport-error", "fetch failed"),
30+ /^The model service could not be reached\./,
31+ );
32+});
33+ 
34+test("the Chinese locale renders the plain cause in Chinese", () => {
35+ setActiveLocale("zh-CN");
36+ assert.match(
37+ formatRunFailure("unauthorized", "401 invalid api key"),
38+ /^模型服务拒绝了当前凭据,请检查模型注册表中的 API Key 后重试。 · 401 invalid api key$/,
39+ );
40+});
41+ 
42+test("every stable failure class has a zh-CN message", () => {
43+ for (const code of ["rate-limited", "semantic-error", "server-error", "timeout", "transport-error", "unauthorized"] as const) {
44+ const key = `runFailure.${code}` as const;
45+ assert.ok(zhCN[key], `${key} needs a zh-CN message`);
46+ }
47+});
48+ 
49+test("a failure without a class degrades to the original error text", () => {
50+ setActiveLocale("en");
51+ assert.equal(formatRunFailure(undefined, "Gateway is unavailable"), "Gateway is unavailable");
52+ assert.equal(formatRunFailure("timeout", " "), "The request timed out. Retry, or split the task / raise the timeout budget.");
53+});
@@ -256,7 +256,7 @@ test("renders skill library cards with pinned head version metadata", async () =
256 256 
257 const text = JSON.stringify(renderer!.toJSON()).replace(/","/g, "");257 const text = JSON.stringify(renderer!.toJSON()).replace(/","/g, "");
258 assert.match(text, /Evaluation Skills/);258 assert.match(text, /Evaluation Skills/);
259- assert.match(text, /Head version/);259+ assert.match(text, /Latest version/);
260 assert.match(text, /2 skills/);260 assert.match(text, /2 skills/);
261 assert.match(text, /abcdef123456/);261 assert.match(text, /abcdef123456/);
262 assert.match(text, /Pending proposals/);262 assert.match(text, /Pending proposals/);