iterate-plugin 3.6.0 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,517 @@
1
+ /**
2
+ * src/client/i18n.ts — the plugin UI's bilingual string layer.
3
+ *
4
+ * The panels used to hardcode Chinese copy while the project config (and the
5
+ * model-facing review output) already supported `language: zh | en` — an
6
+ * English reader following README.md got a UI they could not read. This module
7
+ * makes every piece of UI chrome bilingual:
8
+ *
9
+ * - `t('中文')` → returns the same string for zh, the `EN` entry for en.
10
+ * - `tf('模板 {a0}', { a0 })` → the same, plus `{name}` substitution, for the
11
+ * interpolated strings (round numbers, counts, phase names …).
12
+ *
13
+ * Design constraints:
14
+ * - **Pure / dependency-free**: no React, no DOM, no `require`. It is the
15
+ * one client module the unit tests can import directly, so the dictionary
16
+ * itself is testable (coverage, placeholder parity, no dead entries).
17
+ * - **Chinese is the key**: the call site stays readable (`t('已复制')`), an
18
+ * untranslated string degrades to its Chinese original instead of blank UI,
19
+ * and there is no second bookkeeping list of ids to drift out of sync.
20
+ * - **Persistence goes through an injected storage port** — the real
21
+ * localStorage wrapper lives in index.ts (it degrades to a Map in private
22
+ * mode); this module only asks for `get`/`set`.
23
+ *
24
+ * Model-facing copy — the `build*Instruction` payloads pasted back into the
25
+ * chat — deliberately stays Chinese: it is contract text that mirrors the
26
+ * SKILL.md workflow the model was primed with, and translating it would fork
27
+ * that contract. Only UI chrome is localized.
28
+ */
29
+
30
+ /** UI language. */
31
+ export type Lang = 'zh' | 'en'
32
+
33
+ /** localStorage key holding the user's explicit language choice. */
34
+ export const LANG_STORAGE_KEY = 'iterate-ui-lang'
35
+
36
+ /**
37
+ * Self-names, shown in the language switcher. A language is always listed in
38
+ * its OWN script (that is the point of the switch), so these deliberately do
39
+ * not go through `t()` — they are the one string per language that never
40
+ * translates.
41
+ */
42
+ export const LANG_NAMES: Record<Lang, string> = {
43
+ zh: '简体中文',
44
+ en: 'English',
45
+ }
46
+
47
+ /** Minimal storage port (the client's safe localStorage wrapper satisfies it). */
48
+ export interface LangStorage {
49
+ get(key: string): string | null
50
+ set(key: string, value: string): void
51
+ }
52
+
53
+ /**
54
+ * Chinese → English UI copy.
55
+ *
56
+ * Keys are the exact Chinese literals used at the call sites, and interpolated
57
+ * strings keep their `{aN}` placeholder names in both languages —
58
+ * `test/client-i18n.test.ts` enforces that every key used in the client has an
59
+ * entry here, that every entry is used, and that the placeholder sets match.
60
+ */
61
+ export const EN: Record<string, string> = {
62
+ // ── run state / dashboard ────────────────────────────────────────────────
63
+ 运行中: 'Running',
64
+ 已结束: 'Finished',
65
+ 'Round {a0}:{a1} 项': 'Round {a0}: {a1} findings',
66
+ '各轮发现数量趋势:{a0}': 'Findings per round: {a0}',
67
+ 完整迭代: 'Full iteration',
68
+ '复制启动指令:完整「审查 → 修复 → 验证 → 复盘」闭环(粘贴给模型即可)':
69
+ 'Copy the start instruction: the full "review → fix → validate → debrief" loop (paste it to the model)',
70
+ 仅评审: 'Review only',
71
+ '复制启动指令:只审查不修改(dry-run)(粘贴给模型即可)':
72
+ 'Copy the start instruction: review without changing anything (dry-run) — paste it to the model',
73
+ '第 {a0} 轮进行中{a1}——首轮汇总报告落地后,这里会显示收敛进度与发现统计。':
74
+ 'Round {a0} in progress{a1} — convergence progress and finding stats appear here once the first aggregated report lands.',
75
+ ' · 阶段「{a0}」': ' · phase "{a0}"',
76
+ '上次迭代在第 {a0} 轮后停止{a1},但没有可展示的汇总报告。':
77
+ 'The last iteration stopped at round {a0}{a1}, but there is no report to show.',
78
+ '运行一次评审后,这里会显示收敛进度与发现统计。':
79
+ 'Run a review and convergence progress and finding stats will show up here.',
80
+ 本次迭代从上一次中断的断点继续执行: 'This run resumed from the previous interrupted checkpoint',
81
+ '已中断恢复 ×{a0}': 'Resumed ×{a0}',
82
+ '会话中检测到用户附带的图片,评审将作为视觉证据参考':
83
+ 'Images attached by the user were detected in this session; the review treats them as visual evidence',
84
+ '附件图片 {a0}': 'Attached images {a0}',
85
+ '当前处于「{a0}」阶段(运行中)': 'Currently in the "{a0}" phase (running)',
86
+ '最近一次执行停留在「{a0}」阶段(已结束)': 'The last run stopped in the "{a0}" phase (finished)',
87
+ '代码模式:直接修改文件': 'Code mode: edits files directly',
88
+ '迭代模式:审查-修复循环': 'Iterate mode: review → fix loop',
89
+ '+{a0} 更多': '+{a0} more',
90
+ '本轮已应用的原子修复数(正常模式)': 'Atomic fixes applied this round (normal mode)',
91
+ '审查已收敛:最后一轮未发现新问题': 'Review converged: the final round found nothing new',
92
+ '✓ 已收敛': '✓ Converged',
93
+ '首轮 {a0} → 末轮 {a1} 项,降幅 {a2}%{a3}': 'Round {a0} → round {a1} findings, down {a2}%{a3}',
94
+ ',已收敛': ', converged',
95
+ 'Iterate · 收敛统计': 'Iterate · Convergence stats',
96
+ 收起历史: 'Collapse history',
97
+ '历史 / 趋势': 'History / trend',
98
+ 轮次: 'Round',
99
+ 发现: 'Findings',
100
+ 请继续执行下一轮迭代审查: 'Please continue with the next review round',
101
+ '请回滚上一轮迭代的修复:调用 `iterate_history` 查看本轮 fix id,再用 `iterate_rollback` 逐个撤销这些修复':
102
+ 'Please roll back the fixes from the last round: call `iterate_history` for this round’s fix ids, then `iterate_rollback` to undo them one by one',
103
+ 'y 修复': 'y fix',
104
+ '保留该 finding,进入修复': 'Keep this finding and move it to fixing',
105
+ 'n 跳过': 'n skip',
106
+ '跳过该 finding': 'Skip this finding',
107
+ 'a 已知有意': 'a known-intentional',
108
+ '标记为已知有意并写入 known_intentional': 'Mark as known-intentional and write it into known_intentional',
109
+
110
+ // ── triage panel ─────────────────────────────────────────────────────────
111
+ 'Iterate · Findings 分诊 ({a0}/{a1})': 'Iterate · Findings triage ({a0}/{a1})',
112
+ 'y=修复 · n=跳过 · a=已知有意 · ↑/↓ 选择': 'y=fix · n=skip · a=known-intentional · ↑/↓ to move',
113
+ 按严重度筛选: 'Filter by severity',
114
+ 全部严重度: 'All severities',
115
+ 按维度筛选: 'Filter by dimension',
116
+ 全部维度: 'All dimensions',
117
+ '搜索文件 / 摘要…': 'Search file / summary…',
118
+ 搜索文件或摘要: 'Search file or summary',
119
+ '清除筛选(显示 {a0})': 'Clear filters (showing {a0})',
120
+ '共 {a0} 项': '{a0} in total',
121
+ '批量作用于:': 'Batch applies to:',
122
+ '批量按钮仅作用于当前筛选可见的 findings': 'Batch buttons only apply to the findings visible under the current filter',
123
+ '可见 {a0}': 'Visible {a0}',
124
+ '批量按钮作用于全部 findings': 'Batch buttons apply to every finding',
125
+ '全部 {a0}': 'All {a0}',
126
+ '把所有判定恢复为默认 y(修复)': 'Reset every verdict to the default y (fix)',
127
+ 重置: 'Reset',
128
+ 'y {a0} · n {a1} · a {a2} · 待写回 known_intentional:{a3} 条':
129
+ 'y {a0} · n {a1} · a {a2} · pending known_intentional write-back: {a3}',
130
+ '当前没有标记为「已知有意」的 finding': 'No finding is marked known-intentional yet',
131
+ '复制应用指令:粘贴后模型会调用 iterate_triage apply(校验、去重、写入前备份、失败自动回滚)':
132
+ 'Copy the apply instruction: once pasted, the model calls iterate_triage apply (validates, dedupes, backs up before writing, rolls back on failure)',
133
+ '生成应用指令{a0}': 'Generate apply instruction{a0}',
134
+ '只复制 known_intentional YAML 片段(需自行粘到 iterate.config.yaml 的 personalization.known_intentional 下)':
135
+ 'Copy only the known_intentional YAML fragment (you must paste it under personalization.known_intentional in iterate.config.yaml yourself)',
136
+ '仅复制 YAML 片段': 'Copy YAML fragment only',
137
+ '复制 iterate_triage list 指令,核对已经写入的 known_intentional 条目(写回闭环确认)':
138
+ 'Copy the iterate_triage list instruction to check which known_intentional entries actually landed (write-back → readback)',
139
+ 回读已写入条目: 'Read back written entries',
140
+ '已复制应用指令——粘贴回会话,模型将调用 iterate_triage apply 并回报写入条数。':
141
+ 'Apply instruction copied — paste it back into the chat and the model will call iterate_triage apply and report how many entries were written.',
142
+ '已复制回读指令——粘贴回会话可列出当前已写入的 known_intentional。':
143
+ 'Readback instruction copied — paste it back to list the known_intentional entries currently written.',
144
+
145
+ // ── command actions ──────────────────────────────────────────────────────
146
+ 指挥操作: 'Command actions',
147
+ '批准架构修复(复制指令文本:iterate_fix force:true,含全部必填参数)':
148
+ 'Approve the architectural fix (copies the instruction text: iterate_fix with force:true and every required argument)',
149
+ 批准架构修复: 'Approve architectural fix',
150
+ 触发新一轮迭代审查: 'Trigger the next review round',
151
+ 触发新一轮: 'New round',
152
+ '指派全部 {a0} 个 findings 修复(逐个 iterate_fix)':
153
+ 'Assign all {a0} findings for fixing (one iterate_fix each)',
154
+ '指派当前可见 {a0} 个 findings 修复(逐个 iterate_fix)':
155
+ 'Assign the {a0} visible findings for fixing (one iterate_fix each)',
156
+ 指派修复: 'Assign fixes',
157
+ 回滚到上一个检查点: 'Roll back to the previous checkpoint',
158
+ 回滚检查点: 'Rollback to checkpoint',
159
+
160
+ // ── verdict banner / stats card ──────────────────────────────────────────
161
+ 报告已批准: 'Report approved',
162
+ 报告需修订: 'Report needs revision',
163
+ 项发现: ' findings',
164
+ 轮: ' rounds',
165
+ 项审查: ' reviews',
166
+ 报告通过全部一致性检查: 'The report passed every consistency check',
167
+ 处报告缺陷: ' report defects found',
168
+ 已收敛: 'Converged',
169
+ 未收敛: 'Not converged',
170
+ 'Round {a0} 完成 · 已收敛': 'Round {a0} complete · converged',
171
+ 'Round {a0} 完成': 'Round {a0} complete',
172
+
173
+ // ── settings ─────────────────────────────────────────────────────────────
174
+ '已清除 {a0} 条': 'Cleared {a0}',
175
+ 再次点击以确认清除全部判定: 'Click again to confirm clearing every verdict',
176
+ '清除所有分诊判定记录(需要二次确认)': 'Clear every triage verdict (requires a second click)',
177
+ '确认清除?': 'Confirm clear?',
178
+ 清除分诊: 'Clear triage',
179
+ 迭代主题: 'Iterate theme',
180
+ '启用暖琥珀配色的 iterate 专属皮肤,覆盖 dsh 默认主题令牌。':
181
+ 'Enables the warm-amber iterate skin, overriding the default dsh theme tokens.',
182
+ 已启用: 'Enabled',
183
+ 已关闭: 'Off',
184
+ '开关 iterate 主题': 'Toggle the iterate theme',
185
+ 分诊持久化: 'Triage persistence',
186
+ 'y / n / a 判定保存在本地浏览器(localStorage),刷新会话后仍保留。':
187
+ 'y / n / a verdicts are stored in your browser (localStorage) and survive a session refresh.',
188
+ 本地保存: 'Stored locally',
189
+ 配置管理: 'Configuration',
190
+ '目标 / 维度 / 最大轮数 / validation.commands 写在项目的 iterate.config.yaml,复制指引可让模型按需调整。':
191
+ 'Goal / dimensions / max rounds / validation.commands live in the project’s iterate.config.yaml; copy the guide to let the model adjust them on demand.',
192
+ 复制指引: 'Copy guide',
193
+ 收起: 'Collapse',
194
+ 展开: 'Expand',
195
+ 选择要修改的配置字段: 'Pick the config field to edit',
196
+ '生成并复制该字段的 iterate_config 写入指令(写入前自动备份、失败自动回滚)':
197
+ 'Build and copy the iterate_config write instruction for this field (backs up first, rolls back on failure)',
198
+ 生成写入指令: 'Build write instruction',
199
+ '剪贴板被拒绝——指令已展示在下方,请手动复制。':
200
+ 'The clipboard write was refused — the instruction is shown below, copy it manually.',
201
+ '写入指令(粘贴给模型)': 'Write instruction (paste to the model)',
202
+ '尚未回读当前配置——点「回读当前配置」复制指令发给模型,回显后即可在此看到当前值。':
203
+ 'No config read back yet — press “Read back current config”, send the instruction to the model, and the current value shows up here.',
204
+ '当前值 {a0}:{a1}': 'Current value for {a0}: {a1}',
205
+ '回读结果中没有 {a0}(该字段可能尚未写入配置文件)。':
206
+ 'The read-back does not contain {a0} (it may not have been written to the config file yet).',
207
+ '复制回读指令:让模型调用 iterate_config(operation:"read") 并回显结果':
208
+ 'Copy the read-back instruction: have the model call iterate_config(operation:"read") and echo the result',
209
+ 回读当前配置: 'Read back current config',
210
+ '回读指令(粘贴给模型)': 'Read-back instruction (paste to the model)',
211
+ 复制: 'Copy',
212
+ 'iterate.config.yaml 可编辑字段': 'Editable iterate.config.yaml fields',
213
+ '中文(粘贴给模型的契约文案)': 'Chinese (contract text to paste to the model)',
214
+ 状态概览: 'Runtime status',
215
+ '查看运行时产物布局与清理指引。iterate_status / iterate_history / iterate_prune 工具用于查看和管理。':
216
+ 'The runtime artifact layout and cleanup guide. Use iterate_status / iterate_history / iterate_prune to inspect and manage them.',
217
+ 拉取磁盘快照: 'Pull disk snapshot',
218
+ 运行时布局与清理: 'Runtime layout & cleanup',
219
+ '为每次代码评审生成 Review 报告与交互式分诊视图,专注 AI 自我审查与修正。':
220
+ 'Generates a review report and an interactive triage view for every code review, focused on AI self-review and correction.',
221
+ 就绪: 'Ready',
222
+ 'iterate 设置': 'iterate settings',
223
+ '剪贴板写入被拒绝,复制未生效——请展开对应区块手动选中文本复制。':
224
+ 'The clipboard write was refused, so nothing was copied — expand the section and copy the text manually.',
225
+ '界面语言': 'Interface language',
226
+ '\u9762\u677f\u6587\u6848\u7684\u663e\u793a\u8bed\u8a00\uff1b\u590d\u5236\u7ed9\u6a21\u578b\u7684\u6307\u4ee4\u4fdd\u6301\u4e2d\u6587\uff0c\u4e0e SKILL.md 的工作流契约一致。':
227
+ 'The language of the panel copy. Instructions copied for the model stay Chinese — they mirror the SKILL.md workflow contract.',
228
+ '切换界面语言': 'Switch the interface language',
229
+
230
+ // ── observatory shell / live feed ────────────────────────────────────────
231
+ '{a0} 页 · 按 1-9/0 切 Fn、- 回实时流': '{a0} tabs · keys 1-9/0 switch Fn, - returns to the live feed',
232
+ '复制拉取磁盘快照指令:让模型逐个调用 {a0} 并回显完整结果':
233
+ 'Copy the disk-snapshot instruction: have the model call {a0} and echo the full results',
234
+ 'iterate 观测台': 'iterate observatory',
235
+ '暂无运行时观测数据 · {a0}': 'No runtime observations yet · {a0}',
236
+ '复制拉取磁盘快照指令:磁盘上可能已有历史产物,回显进会话后观测台即可填充':
237
+ 'Copy the disk-snapshot instruction: older artifacts may already be on disk; once echoed back into the chat the observatory fills up',
238
+ '实时活动流仅在运行期间记录,当前已完成。':
239
+ 'The live activity stream is only recorded while a run is active; this run has finished.',
240
+ 查看审查线程: 'View review threads',
241
+ 查看发现: 'View findings',
242
+ 暂无实时活动: 'No live activity yet',
243
+ 实时活动: 'Live activity',
244
+ 按活动类型筛选: 'Filter by activity type',
245
+ 全部活动: 'All activity',
246
+ 无匹配的实时活动: 'No matching live activity',
247
+ 复制未成功: 'Copy failed',
248
+ '剪贴板写入被拒绝,请手动选中下方指令复制':
249
+ 'The clipboard write was refused — select the instruction below and copy it manually',
250
+ 关闭复制失败提示: 'Dismiss the copy-failure notice',
251
+ 关闭: 'Dismiss',
252
+ '将观测台全部数据导出为 JSON(优先下载,失败则复制)':
253
+ 'Export every observatory panel as JSON (downloads first, falls back to copying)',
254
+ 已导出: 'Exported',
255
+ '导出 JSON': 'Export JSON',
256
+ '观测台页签(快捷键 1-9 / 0 / -)': 'Observatory tabs (keys 1-9 / 0 / -)',
257
+ '{a0}(按 {a1} 切换)': '{a0} (press {a1})',
258
+ '{a0}(按 - 切换)': '{a0} (press -)',
259
+
260
+ // ── F1 review threads ────────────────────────────────────────────────────
261
+ 暂无审查线程: 'No review threads yet',
262
+ 审查线程: 'Review threads',
263
+ 展开全部线程: 'Expand all threads',
264
+ 全部展开: 'Expand all',
265
+ 收起全部线程: 'Collapse all threads',
266
+ 全部收起: 'Collapse all',
267
+ '{a0} · {a1} 轮': '{a0} · {a1} rounds',
268
+ 自由切换: 'Mixed',
269
+ 本轮无线程: 'No threads in this round',
270
+ 未命名维度: 'Unnamed dimension',
271
+ 收起线程: 'Collapse thread',
272
+ 展开线程: 'Expand thread',
273
+ 叙述: 'Narration',
274
+ '{a0} 条': '{a0}',
275
+ 无线索叙述: 'No narration recorded',
276
+ 读取文件: 'Files read',
277
+ 本线程无发现: 'This thread found nothing',
278
+ '{a0} 线程': '{a0} threads',
279
+
280
+ // ── F2 convergence trend ─────────────────────────────────────────────────
281
+ 暂无收敛数据: 'No convergence data yet',
282
+ 已修复: 'Fixed',
283
+ 严重度分布: 'Severity mix',
284
+ 验证: 'Validation',
285
+ '{a0}/{a1} 通过{a2}': '{a0}/{a1} passed{a2}',
286
+ '(含失败)': ' (failures included)',
287
+ '各轮发现数量:{a0}{a1}': 'Findings per round: {a0}{a1}',
288
+ ' · 当前 Round {a0}': ' · current round {a0}',
289
+ '跨轮对比(本会话)': 'Cross-round comparison (this session)',
290
+ '跨会话对比:本表只覆盖当前会话;点「拉取磁盘快照」让模型回显 iterate_history,再用「导出 JSON」并排比较两次运行':
291
+ 'Cross-session comparison: this table only covers the current session — click "Pull disk snapshot" to have the model echo iterate_history, then use "Export JSON" to compare two runs side by side',
292
+
293
+ // ── F3 finding locations ─────────────────────────────────────────────────
294
+ 暂无发现: 'No findings',
295
+ '{a0}({a1} 项)': '{a0} ({a1} findings)',
296
+ '搜索发现…': 'Search findings…',
297
+ 搜索发现: 'Search findings',
298
+ 清除全部筛选: 'Clear all filters',
299
+ 清除筛选: 'Clear filters',
300
+ 已确认: 'Acknowledged',
301
+ '复制该 finding 的建议修复文本': 'Copy this finding’s suggested fix text',
302
+ '复制 suggested_fix': 'Copy suggested_fix',
303
+ '复制 iterate_triage 指令文本(标记为 known_intentional)':
304
+ 'Copy the iterate_triage instruction text (mark as known_intentional)',
305
+ '复制 iterate_fix 指令文本': 'Copy the iterate_fix instruction text',
306
+ 复制修复指令: 'Copy fix instruction',
307
+ 无匹配的发现: 'No matching findings',
308
+
309
+ // ── F4 fixes & rollback ──────────────────────────────────────────────────
310
+ 暂无修复记录: 'No fixes recorded yet',
311
+ 成功: 'Success',
312
+ 失败: 'Failed',
313
+ '复制 iterate_rollback 指令文本': 'Copy the iterate_rollback instruction text',
314
+ 回滚: 'Rollback',
315
+
316
+ // ── F5 checkpoint ────────────────────────────────────────────────────────
317
+ 断点: 'Checkpoint',
318
+ '复制 iterate_checkpoint resume 指令文本(加载断点并计数一次恢复)':
319
+ 'Copy the iterate_checkpoint resume instruction (loads the checkpoint and counts one resume)',
320
+ 复制恢复指令: 'Copy resume instruction',
321
+ '复制 iterate_checkpoint clear 指令文本(清除当前断点,供重新开始)':
322
+ 'Copy the iterate_checkpoint clear instruction (drops the current checkpoint so you can start over)',
323
+ 清除断点: 'Clear checkpoint',
324
+
325
+ // ── F6 run console ───────────────────────────────────────────────────────
326
+ 询问用户: 'Ask the user',
327
+ 拒绝: 'Deny',
328
+ 直接执行: 'Run directly',
329
+ '请调用 `iterate_transcript` 写入 nudge 指令:\n\n```json\n{a0}\n```':
330
+ 'Please call `iterate_transcript` to write this nudge instruction:\n\n```json\n{a0}\n```',
331
+ '请调用 `iterate_transcript` 清除当前 nudge:\n\n```json\n{a0}\n```':
332
+ 'Please call `iterate_transcript` to clear the current nudge:\n\n```json\n{a0}\n```',
333
+ 通过: 'Passed',
334
+ 导致中止: ' aborted the run',
335
+ 验证结果: 'Validation results',
336
+ 本轮尚无验证记录: 'No validation was recorded in this round',
337
+ '{a0} 条命令': '{a0} commands',
338
+ 因验证失败中止: 'Stopped because validation failed',
339
+ '尚未拉取验证结果:数据在磁盘 transcript 里,点「拉取磁盘快照」复制指令发回会话即可回填。':
340
+ 'Validation results have not been pulled yet: they live in the transcript on disk — click "Pull disk snapshot", paste the instruction back into the chat, and this panel fills.',
341
+ 运行控制台: 'Run console',
342
+ '批准策略:{a0}': 'Approval policy: {a0}',
343
+ '当前 nudge': 'Current nudge',
344
+ '复制清除 nudge 指令(写 text:null),在运行控制台提示清除已持久化的 nudge':
345
+ 'Copy the clear-nudge instruction (writes text:null) to ask the model to drop the persisted nudge',
346
+ 已复制清除指令: 'Clear instruction copied',
347
+ 复制清除指令: 'Copy clear instruction',
348
+ '起草一条 nudge 方向指令…': 'Draft a nudge to steer the run…',
349
+ 'nudge 草稿': 'Nudge draft',
350
+ '复制 iterate_transcript nudge 指令文本': 'Copy the iterate_transcript nudge instruction text',
351
+ '复制 nudge 指令': 'Copy nudge instruction',
352
+
353
+ // ── F7 decision timeline ─────────────────────────────────────────────────
354
+ 暂无时间线条目: 'No timeline entries yet',
355
+ 时间线: 'Timeline',
356
+ 按时间线类型筛选: 'Filter the timeline by type',
357
+ 全部类型: 'All types',
358
+ 按轮次筛选: 'Filter by round',
359
+ 全部轮次: 'All rounds',
360
+ '搜索时间线…': 'Search timeline…',
361
+ 搜索时间线: 'Search timeline',
362
+ 清除: 'Clear',
363
+ 无匹配的时间线条目: 'No matching timeline entries',
364
+
365
+ // ── F8 quality gate ──────────────────────────────────────────────────────
366
+ 质量门禁: 'Quality gate',
367
+ '复制 iterate_quality_gate 查询指令(含 JSON 参数)':
368
+ 'Copy the iterate_quality_gate query instruction (JSON arguments included)',
369
+ 查询门禁: 'Query gate',
370
+ '复制 iterate_quality_gate clear 指令文本(重置陈旧的 FAIL 证书)':
371
+ 'Copy the iterate_quality_gate clear instruction (resets a stale FAIL certificate)',
372
+ 清除门禁: 'Clear gate',
373
+ '复制拉取磁盘快照指令:让模型回显 .iterate/ 下的门禁、经验、防御事件与 transcript':
374
+ 'Copy the disk-snapshot instruction: have the model echo the gate, experience, defense events and transcript artifacts under .iterate/',
375
+ 质量门禁视图: 'Quality gate view',
376
+ 质量门禁证书: 'Quality gate certificate',
377
+ '质量门禁显示各维度收敛度、验证通过率和整体 PASS/FAIL 状态。':
378
+ 'The quality gate shows per-dimension convergence, the validation pass rate and the overall PASS/FAIL state.',
379
+ 复制查询指令: 'Copy query instruction',
380
+ 复制拉取磁盘快照指令: 'Copy disk-snapshot instruction',
381
+ '整体评分 {a0} · 验证 {a1}/{a2} · 通过率 {a3}%': 'Overall score {a0} · validation {a1}/{a2} · pass rate {a3}%',
382
+ '{a0} · 收敛 {a1}% · 评分 {a2}': '{a0} · convergence {a1}% · score {a2}',
383
+ '评分 {a0} · 发现 {a1} · 已修复 {a2}': 'Score {a0} · findings {a1} · fixed {a2}',
384
+ '失败原因:{a0}': 'Failure reason: {a0}',
385
+
386
+ // ── F9 experience bank ───────────────────────────────────────────────────
387
+ '请调用 `iterate_experience` 搜索经验:\n\n```json\n{a0}\n```':
388
+ 'Please call `iterate_experience` to search the experience bank:\n\n```json\n{a0}\n```',
389
+ '请调用 `iterate_experience` 采纳经验并应用已验证修法:\n\n```json\n{a0}\n```':
390
+ 'Please call `iterate_experience` to adopt this entry and apply its verified fix:\n\n```json\n{a0}\n```',
391
+ '命中 {a0} 次': 'Hit {a0} times',
392
+ '复制采纳指令(查询该经验并应用到当前上下文)':
393
+ 'Copy the adopt instruction (queries this entry and applies it to the current context)',
394
+ 采纳: 'Adopt',
395
+ 经验银行: 'Experience bank',
396
+ '会话内 {a0} 条': '{a0} in this session',
397
+ '复制列出经验指令(含 JSON 参数)': 'Copy the list-experience instruction (JSON arguments included)',
398
+ 列出经验: 'List experiences',
399
+ '复制拉取磁盘快照指令:把 .iterate/ 下的经验银行等产物回显进会话':
400
+ 'Copy the disk-snapshot instruction: echo the experience bank and other .iterate/ artifacts back into the chat',
401
+ '搜索经验模式…': 'Search experience patterns…',
402
+ 搜索经验: 'Search experiences',
403
+ 复制搜索指令: 'Copy search instruction',
404
+ 搜索: 'Search',
405
+ '没有匹配「': 'No experience matches “',
406
+ '」的经验': '”',
407
+ '也可点「列出经验」查询磁盘上的全部历史经验。':
408
+ 'You can also click "List experiences" to query every historical entry on disk.',
409
+
410
+ // ── F10 defense events ───────────────────────────────────────────────────
411
+ '请调用 `iterate_defense_events` 查询防御事件:\n\n```json\n{a0}\n```':
412
+ 'Please call `iterate_defense_events` to query defense events:\n\n```json\n{a0}\n```',
413
+ 前置校验失败: 'Precondition failed',
414
+ 不变量违反: 'Invariant violated',
415
+ 假设被证伪: 'Assumption falsified',
416
+ 防御事件: 'Defense events',
417
+ '会话内 {a0} 条 / 累计 {a1}': '{a0} in this session / {a1} total',
418
+ '复制列出防御事件指令(含 JSON 参数)':
419
+ 'Copy the list-defense-events instruction (JSON arguments included)',
420
+ 列出事件: 'List events',
421
+ '复制拉取磁盘快照指令:把 .iterate/ 下的防御事件流回显进会话':
422
+ 'Copy the disk-snapshot instruction: echo the defense event stream under .iterate/ back into the chat',
423
+ '请调用 `iterate_defense_events` 查询事件统计:\n\n```json\n{a0}\n```':
424
+ 'Please call `iterate_defense_events` for the event counts:\n\n```json\n{a0}\n```',
425
+ '复制统计指令(含 JSON 参数)': 'Copy the counts instruction (JSON arguments included)',
426
+ 统计: 'Counts',
427
+ '请调用 `iterate_defense_events` 清除防御事件流:\n\n```json\n{a0}\n```':
428
+ 'Please call `iterate_defense_events` to clear the defense event stream:\n\n```json\n{a0}\n```',
429
+ '复制 iterate_defense_events clear 指令文本(重置陈旧事件流)':
430
+ 'Copy the iterate_defense_events clear instruction (resets a stale event stream)',
431
+ 清除事件: 'Clear events',
432
+ 防御事件流: 'Defense event stream',
433
+ '防御事件包括:前置校验失败、回滚、不变量违反、假设被证伪。':
434
+ 'Defense events cover failed preconditions, rollbacks, violated invariants and falsified assumptions.',
435
+ 按事件类型筛选: 'Filter by event type',
436
+ 清除类型筛选: 'Clear type filter',
437
+ '防线:{a0}': 'Defense: {a0}',
438
+ 没有匹配该类型的防御事件: 'No defense events of this type',
439
+
440
+ // ── copy feedback ────────────────────────────────────────────────────────
441
+ 已复制: 'Copied',
442
+ 复制失败: 'Copy failed',
443
+ }
444
+
445
+ let current: Lang = 'zh'
446
+ let storage: LangStorage | null = null
447
+ const listeners = new Set<(lang: Lang) => void>()
448
+
449
+ /** The active UI language. */
450
+ export function getLang(): Lang {
451
+ return current
452
+ }
453
+
454
+ /** Switch the UI language: persists the choice and repaints subscribers.
455
+ *
456
+ * `persist: false` is for the STARTUP resolution (an auto-detected language
457
+ * must not overwrite the user's explicit choice — or create one they never
458
+ * made); only an explicit switch writes the preference. */
459
+ export function setLang(next: Lang, persist = true): void {
460
+ if (next !== current) {
461
+ current = next
462
+ if (persist) {
463
+ try {
464
+ storage?.set(LANG_STORAGE_KEY, next)
465
+ } catch {
466
+ /* persistence is best-effort */
467
+ }
468
+ }
469
+ for (const listener of [...listeners]) {
470
+ try {
471
+ listener(next)
472
+ } catch {
473
+ /* one bad subscriber must not break the switch */
474
+ }
475
+ }
476
+ }
477
+ }
478
+
479
+ /** Subscribe to language changes; returns the unsubscribe function. */
480
+ export function subscribeLang(listener: (lang: Lang) => void): () => void {
481
+ listeners.add(listener)
482
+ return () => {
483
+ listeners.delete(listener)
484
+ }
485
+ }
486
+
487
+ /** Hand the module a storage port (called once from `apply()`). */
488
+ export function configureLangStorage(next: LangStorage | null): void {
489
+ storage = next
490
+ }
491
+
492
+ /**
493
+ * Resolve the startup language: an explicit stored choice always wins, then
494
+ * the browser locale (a zh* locale gets Chinese, everything else English —
495
+ * which is also what the default `language: 'en'` config assumes).
496
+ */
497
+ export function detectLang(stored: string | null | undefined, navigatorLanguage?: string | null): Lang {
498
+ if (stored === 'en' || stored === 'zh') return stored
499
+ return /^zh\b/i.test(navigatorLanguage ?? '') ? 'zh' : 'en'
500
+ }
501
+
502
+ /** Translate a static UI string. */
503
+ export function t(key: string): string {
504
+ return current === 'en' ? (EN[key] ?? key) : key
505
+ }
506
+
507
+ /**
508
+ * Translate an interpolated UI string, then substitute `{name}` placeholders.
509
+ * Unknown placeholders are left verbatim (so a translation typo degrades to a
510
+ * readable string rather than `undefined`).
511
+ */
512
+ export function tf(template: string, params: Record<string, unknown> = {}): string {
513
+ const translated = current === 'en' ? (EN[template] ?? template) : template
514
+ return translated.replace(/\{([A-Za-z0-9_]+)\}/g, (whole, name: string) =>
515
+ Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : whole,
516
+ )
517
+ }