dsh-recall-plugin 2.4.5 → 2.4.7

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.
package/lib/client.js CHANGED
@@ -77,11 +77,11 @@
77
77
  // 变色、不套 ring。替换 V2-5 的 border-l3 环方案(当时核验的是旧版写法)。
78
78
  ".dsh-recall-btn:focus-visible,.dsh-recall-ex-chip:focus-visible,.dsh-recall-tree-toggle:focus-visible,.dsh-recall-cfg-switch:focus-visible,.dsh-recall-icon-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}",
79
79
  ".dsh-recall-cardbtn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}",
80
- ".dsh-recall-ex-input:focus-visible,.dsh-recall-cfg-input:focus-visible,.dsh-recall-ex-area:focus-visible,.dsh-recall-cfg-area:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}",
80
+ ".dsh-recall-ex-input:focus-visible,.dsh-recall-cfg-input:focus-visible,.dsh-recall-cfg-select:focus-visible,.dsh-recall-ex-area:focus-visible,.dsh-recall-cfg-area:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}",
81
81
  // 输入类 hover 中间态:rest l4 → hover l3 → focus brand 三级递进,与按钮的
82
82
  // 描边 hover 逻辑同语言;:not(:focus-visible) 收口是防 hover 伪类(特异性更
83
83
  // 高)把 focus 的品牌色描边刷回 l3
84
- ".dsh-recall-ex-input:hover:not(:disabled):not(:focus-visible),.dsh-recall-cfg-input:hover:not(:disabled):not(:focus-visible),.dsh-recall-ex-area:hover:not(:disabled):not(:focus-visible),.dsh-recall-cfg-area:hover:not(:disabled):not(:focus-visible){border-color:var(--dsw-alias-border-l3)}",
84
+ ".dsh-recall-ex-input:hover:not(:disabled):not(:focus-visible),.dsh-recall-cfg-input:hover:not(:disabled):not(:focus-visible),.dsh-recall-cfg-select:hover:not(:disabled):not(:focus-visible),.dsh-recall-ex-area:hover:not(:disabled):not(:focus-visible),.dsh-recall-cfg-area:hover:not(:disabled):not(:focus-visible){border-color:var(--dsw-alias-border-l3)}",
85
85
  ".dsh-recall-toast{position:fixed;top:18px;left:50%;transform:translateX(-50%);z-index:10000;max-width:min(560px,86vw);box-sizing:border-box;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:10px 16px;font-size:13px;line-height:1.5;box-shadow:0 8px 28px rgba(0,0,0,.22);display:flex;align-items:baseline;gap:8px;opacity:0;transition:opacity .25s ease;pointer-events:auto}",
86
86
  ".dsh-recall-toast.dsh-recall-toast-in{opacity:1}",
87
87
  ".dsh-recall-toast-tag{flex:none;font-weight:600;color:var(--dsw-alias-state-error-primary)}",
@@ -244,13 +244,17 @@
244
244
  ".dsh-recall-cfg-input{flex:none;width:var(--dsh-recall-input-w);box-sizing:border-box;height:34px;font:inherit;font-size:13px;text-align:right;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;padding:0 12px}",
245
245
  // disabled 随官方 input:文字降 tertiary,不动透明度(整框变淡会让「框还在
246
246
  // 只是不可写」的语义变含糊)
247
- ".dsh-recall-cfg-input:disabled,.dsh-recall-cfg-area:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}",
247
+ ".dsh-recall-cfg-input:disabled,.dsh-recall-cfg-select:disabled,.dsh-recall-cfg-area:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}",
248
248
  // 数字输入框隐藏原生加减微调按钮(spinner):34px 高的定宽框里 spinner 挤占
249
249
  // 右侧数字区、跨引擎渲染不一(Chromium 上下箭头 / Firefox 无),与「右对齐
250
250
  // 数字」的纵向扫描相冲;隐藏后键盘 ↑↓ 与直接输入仍可用,步进语义不丢。
251
251
  // scope 到 cfg-input,不外溢影响宿主或其他插件的 number 输入。
252
252
  ".dsh-recall-cfg-input::-webkit-inner-spin-button,.dsh-recall-cfg-input::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}",
253
253
  ".dsh-recall-cfg-input{-moz-appearance:textfield;appearance:textfield}",
254
+ // 语言下拉(A4):与数字输入框同一套描边/底色/高度配方,差异只在文本左对齐
255
+ // 与宽度自适应——select 的选项文本长度不可控(Follow system (auto) 最长),
256
+ // 定宽会截断;min-width 与输入框同宽保住「控件列」的视觉对齐基线
257
+ ".dsh-recall-cfg-select{flex:none;box-sizing:border-box;height:34px;min-width:var(--dsh-recall-input-w);font:inherit;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;padding:0 8px 0 10px}",
254
258
  ".dsh-recall-cfg-area{font-family:inherit;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;padding:6px 8px;min-height:64px;box-sizing:border-box;width:100%;grid-column:2}",
255
259
  // 布尔开关:官方设置表单的布尔字段用 role=switch 滑钮而非原生 checkbox
256
260
  //(vCGm7G_switch 逐字配方:36×20 轨道 border-l3 底、开=brand-primary、
@@ -303,6 +307,460 @@
303
307
  "@keyframes dsh-recall-pulse{0%,100%{opacity:1}50%{opacity:.45}}"
304
308
  ].join("");
305
309
 
310
+ // src/client/locales/zh.ts
311
+ var zh = {
312
+ // ---- 通用(跨模块复用的动作与连接词)----
313
+ "common.close": "\u5173\u95ED",
314
+ "common.cancel": "\u53D6\u6D88",
315
+ "common.confirm": "\u786E\u8BA4",
316
+ "common.save": "\u4FDD\u5B58",
317
+ "common.discard": "\u653E\u5F03\u4FEE\u6539",
318
+ "common.retry": "\u91CD\u8BD5",
319
+ "common.errorPrefix": "\u9519\u8BEF\uFF1A",
320
+ "common.unknownReason": "\u672A\u77E5\u539F\u56E0",
321
+ "common.opFailed": "\u64CD\u4F5C\u5931\u8D25",
322
+ // 列表连接符:中文顿号 / 英文逗号+空格(分隔符本身也是语言事实)
323
+ "common.listSep": "\u3001",
324
+ // 括号包裹模板:中英标点族不同((x) / (x),英文含前导空格),不写死在调用点
325
+ "common.paren": "\uFF08{text}\uFF09",
326
+ // ---- 变更类型(KIND_INFO 单表引用)----
327
+ "kind.modified": "\u4FEE\u6539",
328
+ "kind.restored": "\u6062\u590D",
329
+ "kind.added": "\u5220\u9664",
330
+ // ---- 用户消息内的文件卡片 ----
331
+ "file.unnamed": "\u672A\u547D\u540D\u6587\u4EF6",
332
+ // ---- 快照管理树的节点文案 ----
333
+ "tree.unknownWorkspace": "\u672A\u77E5\u5DE5\u4F5C\u533A",
334
+ "tree.deletedSession": "\uFF08\u5DF2\u5220\u9664\u4F1A\u8BDD\uFF09",
335
+ "tree.expand": "\u5C55\u5F00\uFF1A{label}",
336
+ "tree.collapse": "\u6536\u8D77\uFF1A{label}",
337
+ "tree.snapCount": "{n} \u6761",
338
+ "tree.wsCount": "{n} \u4F1A\u8BDD / {m} \u5FEB\u7167",
339
+ "tree.family.title": "\u7248\u672C\u5BB6\u65CF\uFF1A{chain}",
340
+ "tree.switch": "\u5207\u6362",
341
+ "tree.switch.title": "\u5207\u6362\u5230\u8BE5\u7248\u672C\u4F1A\u8BDD",
342
+ "tree.delete.snapshot.title": "\u5220\u9664\u8BE5\u5FEB\u7167\uFF08tag \u4E0E\u7D22\u5F15\u6761\u76EE\uFF09",
343
+ "tree.delete.snapshot.confirm": "\u786E\u8BA4\u5220\u9664\u8BE5\u5FEB\u7167\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002",
344
+ "tree.delete.session.title": "\u5220\u9664\u8BE5\u4F1A\u8BDD\u5168\u90E8\u5FEB\u7167",
345
+ "tree.delete.session.confirm": "\u786E\u8BA4\u5220\u9664\u8BE5\u4F1A\u8BDD\u5168\u90E8\u5FEB\u7167\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002",
346
+ "tree.delete.workspace.title": "\u5220\u9664\u8BE5\u5DE5\u4F5C\u533A\u5168\u90E8\u5FEB\u7167",
347
+ "tree.delete.workspace.confirm": "\u786E\u8BA4\u5220\u9664\u8BE5\u5DE5\u4F5C\u533A\u5168\u90E8\u5FEB\u7167\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002",
348
+ // ---- 撤回确认面板 ----
349
+ "recall.loading": "\u6B63\u5728\u8BA1\u7B97\u53D8\u66F4\u2026",
350
+ "recall.error.title": "\u65E0\u6CD5\u56DE\u9000",
351
+ "recall.confirm.title": "\u6574\u6BB5\u56DE\u9000",
352
+ "recall.truncated.files": "\u2026\u4EC5\u663E\u793A\u524D {shown} \u6761\uFF0C\u5171 {total} \u4E2A\u6587\u4EF6\u5C06\u53D8\u66F4",
353
+ "recall.truncated.chat": "\u2026\u4EC5\u663E\u793A\u524D {shown} \u6761\uFF0C\u5171 {total} \u5904\u5DEE\u5F02",
354
+ "recall.confirm.sessionOnly": "\u9879\u76EE\u6587\u4EF6\u4FDD\u6301\u5F53\u524D\u72B6\u6001\uFF0C\u4E0D\u4F1A\u88AB\u56DE\u9000\u6216\u5220\u9664\uFF1B\u5BF9\u8BDD\u56DE\u9000\u5230\u8BE5\u6D88\u606F\u4E4B\u524D\u3002",
355
+ "recall.confirm.rollbackAt": "\u5C06\u9879\u76EE\u6062\u590D\u5230 {time} \u53D1\u9001\u8BE5\u6D88\u606F\u65F6\u7684\u72B6\u6001\u3002",
356
+ "recall.confirm.rollbackNoTime": "\u5C06\u9879\u76EE\u6062\u590D\u5230\u53D1\u9001\u8BE5\u6D88\u606F\u65F6\u7684\u72B6\u6001\u3002",
357
+ "recall.confirm.files": "\u5171 {total} \u4E2A\u6587\u4EF6\u5C06\u53D8\u66F4{summary}\u3002\u6B64\u64CD\u4F5C\u4F1A\u8986\u76D6\u5F53\u524D\u6587\u4EF6\u5185\u5BB9\uFF1B\u56DE\u9000\u524D\u4F1A\u81EA\u52A8\u4FDD\u5B58\u4E00\u4EFD\u5F53\u524D\u72B6\u6001\u7684\u5B89\u5168\u5FEB\u7167\uFF08\u4E0D\u542B\u5728\u4E0B\u65B9\u6E05\u5355\u5185\uFF09\u3002",
358
+ "recall.confirm.chatBoth": "\u5BF9\u8BDD\u5C06\u4E00\u5E76\u56DE\u9000\u5230\u8BE5\u6D88\u606F\u4E4B\u524D\uFF1A\u8BE5\u6D88\u606F\u53CA\u4E4B\u540E\u7684\u5168\u90E8\u5BF9\u8BDD\u4F1A\u4ECE\u5F53\u524D\u89C6\u56FE\u79FB\u9664\uFF0C\u539F\u4F1A\u8BDD\u5F52\u6863\u4FDD\u5B58\uFF08\u53EF\u4ECE\u5F52\u6863\u627E\u56DE\uFF09\u3002",
359
+ "recall.confirm.chatFirst": "\u8BE5\u6D88\u606F\u662F\u672C\u4F1A\u8BDD\u4E2D\u7B2C\u4E00\u6761\u7528\u6237\u6D88\u606F\uFF0C\u65E0\u6CD5\u56DE\u9000\u5BF9\u8BDD\uFF1B\u786E\u8BA4\u540E\u4EC5\u56DE\u9000\u9879\u76EE\u6587\u4EF6\u3002",
360
+ "recall.confirm.refNote": "\u4EE5\u4E0B\u5DEE\u5F02\u4EC5\u4F5C\u53C2\u8003\uFF0C\u6240\u9009\u6A21\u5F0F\u4E0D\u4F1A\u6539\u52A8\u6587\u4EF6\u3002",
361
+ "recall.scope.aria": "\u64A4\u56DE\u8303\u56F4",
362
+ "recall.scope.both": "\u56DE\u9000\u6587\u4EF6\u4E0E\u5BF9\u8BDD",
363
+ "recall.scope.sessionOnly": "\u4EC5\u64A4\u56DE\u5BF9\u8BDD",
364
+ "recall.confirm.submit.both": "\u786E\u8BA4\u56DE\u9000",
365
+ "recall.confirm.submit.sessionOnly": "\u786E\u8BA4\u64A4\u56DE\u5BF9\u8BDD",
366
+ "recall.executing.both": "\u6B63\u5728\u56DE\u9000\u2026",
367
+ "recall.executing.sessionOnly": "\u6B63\u5728\u64A4\u56DE\u5BF9\u8BDD\u2026",
368
+ "recall.done.title": "\u56DE\u9000\u5B8C\u6210",
369
+ "recall.done.both.ok": "\u9879\u76EE\u6587\u4EF6\u4E0E\u5BF9\u8BDD\u5DF2\u56DE\u9000\u5230\u8BE5\u6D88\u606F\u4E4B\u524D\u3002\u65B0\u4F1A\u8BDD\u5DF2\u6253\u5F00\uFF0C\u539F\u4F1A\u8BDD\u5DF2\u5F52\u6863\uFF08\u53EF\u4ECE\u5F52\u6863\u627E\u56DE\uFF09\u3002",
370
+ "recall.done.both.partial": "\u9879\u76EE\u5DF2\u6062\u590D\u5230\u53D1\u9001\u8BE5\u6D88\u606F\u65F6\u7684\u72B6\u6001\u3002",
371
+ "recall.done.both.failChat": " \u5BF9\u8BDD\u56DE\u9000\u5931\u8D25\uFF1A{error}",
372
+ "recall.done.sessionOnly.ok": "\u5BF9\u8BDD\u5DF2\u56DE\u9000\u5230\u8BE5\u6D88\u606F\u4E4B\u524D\uFF0C\u9879\u76EE\u6587\u4EF6\u4FDD\u6301\u5F53\u524D\u72B6\u6001\u3002\u65B0\u4F1A\u8BDD\u5DF2\u6253\u5F00\uFF0C\u539F\u4F1A\u8BDD\u5DF2\u5F52\u6863\uFF08\u53EF\u4ECE\u5F52\u6863\u627E\u56DE\uFF09\u3002",
373
+ "recall.done.sessionOnly.fail": "\u5BF9\u8BDD\u56DE\u9000\u5931\u8D25\uFF1A{error}\u3002\u9879\u76EE\u6587\u4EF6\u672A\u505A\u4EFB\u4F55\u6539\u52A8\u3002",
374
+ "recall.chat.noChild": "\u672A\u8FD4\u56DE\u65B0\u4F1A\u8BDD",
375
+ // ---- 用户消息气泡的动作按钮 ----
376
+ "action.copy": "\u590D\u5236",
377
+ "action.copied": "\u5DF2\u590D\u5236",
378
+ "action.recall": "\u64A4\u56DE",
379
+ "action.recall.title": "\u6574\u6BB5\u56DE\u9000\uFF1A\u6587\u4EF6\u4E0E\u5BF9\u8BDD\u4E00\u5E76\u56DE\u5230\u8BE5\u6D88\u606F\u4E4B\u524D",
380
+ // ---- toast(降级提示 / 快照反馈)----
381
+ "toast.tag": "\u64A4\u56DE\u63D2\u4EF6",
382
+ "toast.skipped": "\u5FEB\u7167\u5DF2\u8DF3\u8FC7\u672A\u7EB3\u5165\u7684\u8DEF\u5F84\uFF1A{names}\uFF08\u64A4\u56DE\u4E0D\u4F1A\u6062\u590D\u6216\u5220\u9664\u8FD9\u4E9B\u8DEF\u5F84\uFF09",
383
+ "toast.skippedMore": " \u7B49 {n} \u9879",
384
+ "toast.snapshotFailed": "\u5FEB\u7167\u5931\u8D25\uFF1A{error}",
385
+ "toast.staleQueue": "\u64A4\u56DE\u524D\u7684\u4E00\u6761\u6392\u961F\u6D88\u606F\u672A\u88AB\u81EA\u52A8\u6E05\u7406\uFF0C\u53EF\u70B9\u51FB\u8BE5\u5361\u7247\u53F3\u4E0A\u89D2\u7684\u5220\u9664\u6309\u94AE\u624B\u52A8\u79FB\u9664",
386
+ "toast.fileAttachRefill": "\u88AB\u64A4\u56DE\u6D88\u606F\u91CC\u7684\u6587\u4EF6\u9644\u4EF6\u65E0\u6CD5\u81EA\u52A8\u56DE\u586B\uFF08\u5B98\u65B9\u63A5\u53E3\u53EA\u652F\u6301\u56FE\u7247\u56DE\u8BFB\uFF09\uFF0C\u8BF7\u91CD\u65B0\u9009\u62E9\u6587\u4EF6",
387
+ // ---- init / snapshot-info 下发的一次性说明 ----
388
+ "notice.unsupported": "\u64A4\u56DE\u63D2\u4EF6\u4EC5\u652F\u6301 Windows / Linux / macOS\uFF0C\u5F53\u524D\u5E73\u53F0\u7684\u5FEB\u7167\u4E0D\u53EF\u7528\u3002",
389
+ "notice.gitMissing": "\u672A\u68C0\u6D4B\u5230 git CLI\uFF0C\u64A4\u56DE\u529F\u80FD\u4E0D\u53EF\u7528\uFF08\u5FEB\u7167\u5F15\u64CE\u4F9D\u8D56 git\uFF09\u3002\u5B89\u88C5 git \u5E76\u91CD\u542F DSH \u540E\u5373\u53EF\u4F7F\u7528\u3002",
390
+ "notice.homeFallback": "home \u76EE\u5F55\u4E0D\u53EF\u5199\uFF0C\u5FEB\u7167\u5DF2\u964D\u7EA7\u5B58\u50A8\u5230\u9879\u76EE\u5185 .dsh-recall-snapshots \u76EE\u5F55\u3002",
391
+ "notice.buildRoot": "\u5F53\u524D\u5DE5\u4F5C\u533A\u4F4D\u4E8E\u6784\u5EFA\u4EA7\u7269\u76EE\u5F55\uFF08\u8DEF\u5F84\u6BB5 {seg}\uFF09\uFF0C\u5DF2\u8DF3\u8FC7\u9879\u76EE\u5FEB\u7167\uFF1B\u5982\u9700\u5728\u6B64\u76EE\u5F55\u4F7F\u7528\u64A4\u56DE\uFF0C\u8BF7\u5728\u63D2\u4EF6\u8BBE\u7F6E\u91CC\u4ECE\u300C\u57FA\u7840\u6392\u9664\u8868\u300D\u79FB\u9664\u8BE5\u9879\u3002",
392
+ // ---- 错误码文案(host 静态 message 的本地化副本;动态细节码不在表内)----
393
+ "err.STALE": "\u9884\u89C8\u540E\u9879\u76EE\u6587\u4EF6\u53D1\u751F\u4E86\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u9884\u89C8\u786E\u8BA4",
394
+ "err.AGENT_BUSY": "Agent \u6B63\u5728\u8FD0\u884C\u4E2D\uFF0C\u8BF7\u5148\u505C\u6B62\u540E\u518D\u64A4\u56DE",
395
+ "err.NO_SNAPSHOT": "\u8BE5\u6D88\u606F\u6CA1\u6709\u53EF\u7528\u7684\u9879\u76EE\u5FEB\u7167",
396
+ "err.NO_STORE": "\u5FEB\u7167\u5B58\u50A8\u4E0D\u53EF\u7528",
397
+ "err.FORMAT_BLOCKED": "\u78C1\u76D8\u683C\u5F0F\u4E0D\u53D7\u652F\u6301\uFF0C\u5199\u5165\u5DF2\u6682\u505C\uFF08\u8BE6\u89C1\u6700\u8FD1\u9519\u8BEF\uFF09",
398
+ "err.UNKNOWN_PATH": "\u672A\u77E5\u7684\u6392\u9664\u6587\u4EF6\u8DEF\u5F84",
399
+ "err.EMPTY_PATCH": "\u6CA1\u6709\u53EF\u5199\u5165\u7684\u914D\u7F6E\u5B57\u6BB5",
400
+ "err.SETTINGS_UNAVAILABLE": "\u8BBE\u7F6E\u670D\u52A1\u4E0D\u53EF\u7528\uFF1A\u8BF7\u5728 profile \u7684 cordis.patch.yml \u6309 id: recall \u8986\u76D6\u914D\u7F6E",
401
+ "err.BODY_TOO_LARGE": "\u8BF7\u6C42\u4F53\u8D85\u8FC7\u5927\u5C0F\u4E0A\u9650",
402
+ "err.NO_ROOT": "\u65E0\u6CD5\u89E3\u6790\u5DE5\u4F5C\u533A",
403
+ "err.NO_SESSION": "\u7F3A\u5C11\u4F1A\u8BDD ID",
404
+ "err.UNKNOWN_OP": "\u672A\u77E5\u7684\u7BA1\u7406\u64CD\u4F5C",
405
+ "err.UNKNOWN_ENDPOINT": "\u672A\u77E5\u7684 API \u7AEF\u70B9",
406
+ "err.INDEX_CORRUPT": "\u5FEB\u7167\u7D22\u5F15\u635F\u574F",
407
+ // ---- 面板内的兜底文案(res 缺失时的最后一级回落)----
408
+ "fallback.preview": "\u65E0\u6CD5\u83B7\u53D6\u5FEB\u7167",
409
+ "fallback.rollback": "\u56DE\u9000\u5931\u8D25",
410
+ // ---- 设置页:快照管理的错误区(按 host kind 键控渲染)----
411
+ "errorKind.git": "\u672A\u68C0\u6D4B\u5230 git CLI \u6216\u7248\u672C\u8FC7\u65E7\uFF1A\u8BF7\u5B89\u88C5\u6216\u5347\u7EA7 git\uFF0C\u5B8C\u6210\u540E\u81EA\u52A8\u6062\u590D",
412
+ "errorKind.space": "\u78C1\u76D8\u7A7A\u95F4\u5DF2\u6EE1\uFF0C\u5FEB\u7167\u5199\u5165\u5931\u8D25\uFF1A\u6E05\u7406\u78C1\u76D8\u7A7A\u95F4\u540E\u81EA\u52A8\u6062\u590D",
413
+ "errorKind.permission": "\u5FEB\u7167\u76EE\u5F55\u65E0\u5199\u5165\u6743\u9650\uFF1A\u8BF7\u68C0\u67E5\u76EE\u5F55\u6743\u9650\u540E\u91CD\u8BD5",
414
+ "errorKind.lock": "\u7591\u4F3C\u591A\u4E2A DSH \u5B9E\u4F8B\u5E76\u53D1\u4F7F\u7528\u540C\u4E00\u5FEB\u7167\u5E93\uFF1A\u8BF7\u786E\u8BA4\u53EA\u542F\u52A8\u4E86\u4E00\u4E2A\uFF1B\u786E\u8BA4\u540E\u4ECD\u5931\u8D25\u65F6\uFF0C\u6309\u300C\u8BBE\u7F6E \xB7 \u63D2\u4EF6\u914D\u7F6E \xB7 \u6700\u8FD1\u9519\u8BEF\u300D\u4E2D\u7684\u8DEF\u5F84\u5220\u9664\u9501\u6587\u4EF6",
415
+ "errorKind.mkdir": "\u5FEB\u7167\u5B58\u50A8\u76EE\u5F55\u88AB\u540C\u540D\u6587\u4EF6\u5360\u7528\uFF1A\u5904\u7406\u540E\u81EA\u52A8\u6062\u590D",
416
+ // ---- 设置页:快照管理卡片 ----
417
+ "manage.busy": "\u6267\u884C\u4E2D\u2026",
418
+ "manage.deletedCount": "\u5DF2\u5220\u9664 {deleted} \u6761\u5FEB\u7167",
419
+ "manage.done.delete": "\u5DF2\u5220\u9664",
420
+ "manage.done.deleteAll": "\u5DF2\u6E05\u7A7A\u5168\u90E8\u5FEB\u7167",
421
+ "manage.done.gc": "gc \u5B8C\u6210",
422
+ "manage.confirm.all": "\u786E\u8BA4\u5220\u9664\u6240\u6709\u5DE5\u4F5C\u533A\u7684\u5168\u90E8\u5FEB\u7167\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002",
423
+ "manage.countLoading": "\u5171 \u2026 \u6761\u5FEB\u7167",
424
+ "manage.countLoaded": "\u5171 {total} \u6761\u5FEB\u7167",
425
+ "manage.countShown": "\uFF08\u5F53\u524D\u663E\u793A\u6700\u65B0 {shown} \u6761\uFF09",
426
+ "manage.countEnd": "\u3002",
427
+ "manage.usageSuffix": "\uFF0C\u5168\u90E8\u5DE5\u4F5C\u533A\u5FEB\u7167\u5B58\u50A8\u5360\u7528 {size}\u3002",
428
+ "manage.health.gitTitle": "\u5FEB\u7167\u5F15\u64CE\u4F9D\u8D56 git",
429
+ "manage.health.gitOk": "git \u53EF\u7528",
430
+ "manage.health.gitBad": "git \u4E0D\u53EF\u7528\uFF08\u5FEB\u7167\u5F15\u64CE\u4F9D\u8D56 git\uFF09",
431
+ "manage.health.stores": " \xB7 \u5FEB\u7167\u5B58\u50A8\uFF1Ahome {n} \u4E2A\u5DE5\u4F5C\u533A",
432
+ "manage.health.storesFallback": "\uFF0C\u964D\u7EA7 {n} \u4E2A",
433
+ "manage.search.placeholder": "\u641C\u7D22\u5DE5\u4F5C\u533A / \u4F1A\u8BDD\u6807\u9898 / \u6D88\u606F\u5185\u5BB9 / ID",
434
+ "manage.search.aria": "\u641C\u7D22\u5FEB\u7167",
435
+ "manage.empty": "\u5728\u4EFB\u610F\u5DE5\u4F5C\u533A\u53D1\u9001\u4E00\u6761\u6D88\u606F\u540E\uFF0C\u8FD9\u91CC\u4F1A\u51FA\u73B0\u5FEB\u7167\u3002",
436
+ "manage.emptyFiltered": "\u65E0\u5339\u914D\u5FEB\u7167",
437
+ "manage.errors.title": "\u6700\u8FD1\u9519\u8BEF ({n})",
438
+ "manage.errors.collapse": "\u6536\u8D77",
439
+ "manage.errors.expand": "\u5C55\u5F00\u5168\u90E8 ({n})",
440
+ "manage.errors.clear": "\u6E05\u7A7A",
441
+ "manage.errors.dup": "\uFF08\xD7{n}\uFF09",
442
+ "manage.loadMore": "\u52A0\u8F7D\u66F4\u591A",
443
+ "manage.refresh": "\u5237\u65B0",
444
+ "manage.gc": "\u7ACB\u5373 gc",
445
+ "manage.gc.title": "\u7ACB\u5373\u5BF9\u5168\u90E8\u5DE5\u4F5C\u533A\u6267\u884C\u4E00\u6B21 git gc\uFF08\u538B\u7F29\u5BF9\u8C61\u5E93\u91CA\u653E\u7A7A\u95F4\uFF09",
446
+ "manage.deleteAll": "\u5168\u90E8\u5220\u9664",
447
+ "manage.deleteAll.title": "\u5220\u9664\u5168\u90E8\u5DE5\u4F5C\u533A\u7684\u6240\u6709\u5FEB\u7167\uFF1B\u4F1A\u76F4\u63A5\u6838\u5BF9\u5E76\u5220\u9664 git tag\uFF08\u5373\u4F7F\u5217\u8868\u4E3A\u7A7A\u4E5F\u53EF\u6E05\u7406\u6B8B\u7559\uFF09",
448
+ // ---- 设置页:插件配置表单 ----
449
+ "config.group.snapshot": "\u5FEB\u7167\u884C\u4E3A",
450
+ "config.group.auto": "\u81EA\u52A8\u6CBB\u7406",
451
+ "config.group.ui": "\u754C\u9762",
452
+ "config.snapshotEnabled.label": "\u542F\u7528\u5FEB\u7167",
453
+ "config.snapshotEnabled.hint": "\u5173\u95ED\u540E\u4E0D\u518D\u65B0\u5EFA\u5FEB\u7167\uFF1B\u5DF2\u6709\u5FEB\u7167\u4ECD\u53EF\u64A4\u56DE",
454
+ "config.refillDraft.label": "\u56DE\u586B\u8F93\u5165\u6846",
455
+ "config.refillDraft.hint": "\u64A4\u56DE\u540E\u628A\u6D88\u606F\u6587\u672C\u56DE\u586B\u8F93\u5165\u6846\uFF0C\u4FBF\u4E8E\u6539\u5B8C\u91CD\u53D1",
456
+ "config.archiveOriginal.label": "\u5F52\u6863\u539F\u4F1A\u8BDD",
457
+ "config.archiveOriginal.hint": "\u539F\u4F1A\u8BDD\u5F52\u6863\u9690\u85CF\uFF0C\u53EF\u4ECE\u5F52\u6863\u627E\u56DE\uFF1B\u5173\u95ED\u5219\u7559\u5728\u5217\u8868\u4FBF\u4E8E\u5BF9\u7167",
458
+ "config.gcSnaps.label": "gc \u89E6\u53D1\u6761\u6570",
459
+ "config.gcSnaps.hint": "\u6BCF\u79EF\u7D2F\u591A\u5C11\u6761\u5FEB\u7167\u89E6\u53D1\u4E00\u6B21 gc",
460
+ "config.gcSnaps.suffix": "\u6761",
461
+ "config.gcHours.label": "gc \u89E6\u53D1\u5C0F\u65F6",
462
+ "config.gcHours.hint": "\u8DDD\u4E0A\u6B21 gc \u8D85\u8FC7\u591A\u5C11\u5C0F\u65F6\u89E6\u53D1\uFF0C\u4E0E\u6761\u6570\u5148\u5230\u5148\u89E6\u53D1",
463
+ "config.gcHours.suffix": "\u5C0F\u65F6",
464
+ "config.maxFileBytes.label": "\u6587\u4EF6\u5927\u5C0F\u4E0A\u9650",
465
+ "config.maxFileBytes.hint": "\u8D85\u8FC7\u8BE5\u5927\u5C0F\u7684\u6587\u4EF6\u4E0D\u8FDB\u5FEB\u7167\u3001\u4E0D\u88AB\u56DE\u9000\u89E6\u78B0",
466
+ "config.maxFileBytes.suffix": "MB",
467
+ "config.maxSnapshotsPerWorkspace.label": "\u5FEB\u7167\u603B\u91CF\u4E0A\u9650",
468
+ "config.maxSnapshotsPerWorkspace.hint": "\u6BCF\u5DE5\u4F5C\u533A\u4FDD\u7559\u7684\u6700\u5927\u5FEB\u7167\u6570\uFF0C\u8D85\u9650\u5220\u9664\u6700\u65E7\u7684\uFF1B0 \u8868\u793A\u4E0D\u9650\u5236",
469
+ "config.maxSnapshotsPerWorkspace.suffix": "\u6761",
470
+ "config.retentionDays.label": "\u5FEB\u7167\u4FDD\u7559\u5929\u6570",
471
+ "config.retentionDays.hint": "\u8D85\u671F\u5FEB\u7167\u81EA\u52A8\u5220\u9664\u6700\u65E7\u7684\uFF1B0 \u8868\u793A\u4E0D\u542F\u7528",
472
+ "config.retentionDays.suffix": "\u5929",
473
+ "config.locale.label": "\u754C\u9762\u8BED\u8A00",
474
+ "config.locale.hint": "auto \u8DDF\u968F\u7CFB\u7EDF\u8BED\u8A00\uFF1B\u4FDD\u5B58\u540E\u751F\u6548\uFF08\u5BBF\u4E3B\u4FA7\u300C\u63D2\u4EF6\u914D\u7F6E\u300D\u81EA\u5E26\u7684\u5B57\u6BB5\u8BF4\u660E\u4ECD\u4E3A\u4E2D\u6587\uFF09",
475
+ "config.locale.auto": "\u8DDF\u968F\u7CFB\u7EDF (auto)",
476
+ "config.tag.modified": "\u5DF2\u4FEE\u6539",
477
+ "config.tag.overridden": "\u5DF2\u8986\u76D6",
478
+ "config.tag.locked": "\u73AF\u5883\u53D8\u91CF\u9501\u5B9A",
479
+ "config.tag.readonly": "\u53EA\u8BFB\u8BBE\u7F6E\u6E90",
480
+ "config.reset": "\u6062\u590D\u9ED8\u8BA4",
481
+ "config.reset.title": "\u628A\u6240\u6709\u5B57\u6BB5\u6062\u590D\u5230\u63D2\u4EF6\u51FA\u5382\u9ED8\u8BA4\u503C",
482
+ "config.advanced.title": "\u9AD8\u7EA7\uFF1A\u57FA\u7840\u6392\u9664\u8868",
483
+ "config.baseExcludes.label": "\u57FA\u7840\u6392\u9664\u8868",
484
+ "config.baseExcludes.hint": "\u5404\u5DE5\u4F5C\u533A\u5171\u4EAB\u7684\u5185\u7F6E\u89C4\u5219\uFF1Bgitignore \u8BED\u6CD5\uFF0C\u6BCF\u884C\u4E00\u6761\uFF1B\u4F18\u5148\u7EA7\u4F4E\u4E8E\u300C\u6392\u9664\u914D\u7F6E\u300D\u7684 exclude.txt",
485
+ "config.msg.loading": "\u6B63\u5728\u8BFB\u53D6\u914D\u7F6E\u2026",
486
+ "config.msg.loadFailed": "\u65E0\u6CD5\u8BFB\u53D6\u914D\u7F6E",
487
+ "config.msg.noChange": "\u6CA1\u6709\u4FEE\u6539",
488
+ "config.msg.saving": "\u4FDD\u5B58\u4E2D\u2026",
489
+ "config.msg.saved": "\u5DF2\u4FDD\u5B58\u5E76\u5373\u65F6\u751F\u6548",
490
+ "config.msg.saveFailed": "\u4FDD\u5B58\u5931\u8D25",
491
+ "config.msg.resetting": "\u6062\u590D\u9ED8\u8BA4\u4E2D\u2026",
492
+ "config.msg.resetDone": "\u5DF2\u6062\u590D\u9ED8\u8BA4\u503C",
493
+ "config.msg.resetFailed": "\u6062\u590D\u9ED8\u8BA4\u5931\u8D25",
494
+ "config.err.gcSnaps": "\u5FEB\u7167\u6761\u6570\u9608\u503C\u5FC5\u987B\u662F >= 1 \u7684\u6574\u6570",
495
+ "config.err.gcHours": "gc \u5C0F\u65F6\u9608\u503C\u5FC5\u987B\u662F >= 1 \u7684\u6574\u6570",
496
+ "config.err.maxFileBytes": "\u6587\u4EF6\u5927\u5C0F\u4E0A\u9650\u81F3\u5C11 0.01 MB",
497
+ "config.err.maxSnapshots": "\u5FEB\u7167\u603B\u91CF\u4E0A\u9650\u5FC5\u987B\u662F >= 0 \u7684\u6574\u6570\uFF080 \u8868\u793A\u4E0D\u9650\u5236\uFF09",
498
+ "config.err.retentionDays": "\u4FDD\u7559\u5929\u6570\u5FC5\u987B\u662F >= 0 \u7684\u6574\u6570\uFF080 \u8868\u793A\u4E0D\u542F\u7528\uFF09",
499
+ "config.err.locale": "\u754C\u9762\u8BED\u8A00\u5FC5\u987B\u662F auto / zh / en \u4E4B\u4E00",
500
+ // ---- 设置页:排除配置卡片 ----
501
+ "exclude.note.home": "\u6B64\u914D\u7F6E\u5168\u5C40\u5171\u4EAB\uFF0C\u5BF9\u6240\u6709\u5DE5\u4F5C\u533A\u7684\u5FEB\u7167\u751F\u6548\u3002",
502
+ "exclude.note.fallback": "home \u76EE\u5F55\u4E0D\u53EF\u5199\u65F6\u6B64\u5DE5\u4F5C\u533A\u964D\u7EA7\u5B58\u50A8\uFF0C\u6392\u9664\u914D\u7F6E\u72EC\u7ACB\u751F\u6548\u3002",
503
+ "exclude.path": "\u5B58\u50A8\u4F4D\u7F6E\uFF1A{path}",
504
+ "exclude.syntax": "gitignore \u8BED\u6CD5\uFF0C\u4E00\u884C\u4E00\u6761\uFF0C\u652F\u6301 # \u6CE8\u91CA\uFF1B\u547D\u4E2D\u9879\u4E0D\u8FDB\u5FEB\u7167\u3001\u4E0D\u88AB\u56DE\u9000\u89E6\u78B0\u3002",
505
+ "exclude.area.aria": "\u5FEB\u7167\u6392\u9664\u6A21\u5F0F\u5217\u8868\uFF08gitignore \u8BED\u6CD5\uFF0C\u4E00\u884C\u4E00\u6761\uFF09",
506
+ "exclude.quick.placeholder": "\u8F93\u5165\u8DEF\u5F84\u6216\u6A21\u5F0F\uFF0C\u56DE\u8F66\u5FEB\u901F\u6DFB\u52A0",
507
+ "exclude.quick.aria": "\u5FEB\u901F\u6DFB\u52A0\u6392\u9664\u6A21\u5F0F",
508
+ "exclude.add": "\u6DFB\u52A0",
509
+ "exclude.chip.title": "\u70B9\u51FB\u8FFD\u52A0 {pattern}",
510
+ "exclude.msg.saving": "\u4FDD\u5B58\u4E2D\u2026",
511
+ "exclude.msg.saved": "\u5DF2\u4FDD\u5B58\uFF0C\u4E0B\u4E00\u6B21\u5FEB\u7167 / \u9884\u89C8 / \u56DE\u9000\u65F6\u751F\u6548",
512
+ "exclude.msg.saveFailed": "\u4FDD\u5B58\u5931\u8D25",
513
+ "exclude.loadFailed": "\u65E0\u6CD5\u8BFB\u53D6\u6392\u9664\u914D\u7F6E",
514
+ "exclude.unsupported": "\u5F53\u524D\u5E73\u53F0\u4E0D\u652F\u6301\u5FEB\u7167\u529F\u80FD\uFF0C\u6392\u9664\u914D\u7F6E\u4E0D\u53EF\u7528\u3002",
515
+ "exclude.loading": "\u6B63\u5728\u52A0\u8F7D\u6392\u9664\u914D\u7F6E\u2026",
516
+ "exclude.empty": "\u5C1A\u672A\u521B\u5EFA\u4EFB\u4F55\u5FEB\u7167\u5B58\u50A8\uFF1A\u5728\u4EFB\u610F\u5DE5\u4F5C\u533A\u53D1\u9001\u4E00\u6761\u6D88\u606F\u540E\uFF0C\u8FD9\u91CC\u4F1A\u51FA\u73B0\u53EF\u7F16\u8F91\u7684\u6392\u9664\u914D\u7F6E\u3002",
517
+ // ---- 设置页:分区折叠头 ----
518
+ "section.exclude": "\u6392\u9664\u914D\u7F6E\uFF08exclude.txt\uFF09",
519
+ "section.manage": "\u5FEB\u7167\u7BA1\u7406"
520
+ };
521
+
522
+ // src/client/locales/en.ts
523
+ var en = {
524
+ // ---- 通用 ----
525
+ "common.close": "Close",
526
+ "common.cancel": "Cancel",
527
+ "common.confirm": "Confirm",
528
+ "common.save": "Save",
529
+ "common.discard": "Discard changes",
530
+ "common.retry": "Retry",
531
+ "common.errorPrefix": "Error: ",
532
+ "common.unknownReason": "unknown reason",
533
+ "common.opFailed": "Operation failed",
534
+ "common.listSep": ", ",
535
+ "common.paren": " ({text})",
536
+ // ---- 变更类型 ----
537
+ "kind.modified": "modified",
538
+ "kind.restored": "restored",
539
+ "kind.added": "deleted",
540
+ // ---- 用户消息内的文件卡片 ----
541
+ "file.unnamed": "unnamed file",
542
+ // ---- 快照管理树的节点文案 ----
543
+ "tree.unknownWorkspace": "Unknown workspace",
544
+ "tree.deletedSession": "(deleted session)",
545
+ "tree.expand": "Expand: {label}",
546
+ "tree.collapse": "Collapse: {label}",
547
+ "tree.snapCount": "{n} snapshots",
548
+ "tree.wsCount": "{n} sessions / {m} snapshots",
549
+ "tree.family.title": "Version family: {chain}",
550
+ "tree.switch": "Switch",
551
+ "tree.switch.title": "Switch to this version session",
552
+ "tree.delete.snapshot.title": "Delete this snapshot (tag and index entry)",
553
+ "tree.delete.snapshot.confirm": "Delete this snapshot? This cannot be undone.",
554
+ "tree.delete.session.title": "Delete all snapshots of this session",
555
+ "tree.delete.session.confirm": "Delete all snapshots of this session? This cannot be undone.",
556
+ "tree.delete.workspace.title": "Delete all snapshots of this workspace",
557
+ "tree.delete.workspace.confirm": "Delete all snapshots of this workspace? This cannot be undone.",
558
+ // ---- 撤回确认面板 ----
559
+ "recall.loading": "Computing changes\u2026",
560
+ "recall.error.title": "Cannot roll back",
561
+ "recall.confirm.title": "Roll back",
562
+ "recall.truncated.files": "\u2026showing the first {shown} of {total} files to change",
563
+ "recall.truncated.chat": "\u2026showing the first {shown} of {total} differences",
564
+ "recall.confirm.sessionOnly": "Project files stay as they are \u2014 nothing is reverted or deleted; only the chat rewinds to before this message.",
565
+ "recall.confirm.rollbackAt": "Restore the project to its state at {time}, when this message was sent.",
566
+ "recall.confirm.rollbackNoTime": "Restore the project to its state when this message was sent.",
567
+ // 句首空格是有意的:本句与 recall.confirm.rollbackAt 拼接成一段(前句以句点
568
+ // 收尾),英文标点族需要空格(与 common.paren / tellChat 同类;中文版不需要)
569
+ "recall.confirm.files": " {total} files will change{summary}. This overwrites current file contents; a safety snapshot of the current state is saved automatically before the rollback (not listed below).",
570
+ "recall.confirm.chatBoth": "The chat also rewinds to before this message: this message and everything after it leaves the current view, and the original session is archived (recoverable from Archive).",
571
+ "recall.confirm.chatFirst": "This is the first user message in this session, so the chat cannot rewind; only project files will be restored.",
572
+ "recall.confirm.refNote": "The differences below are for reference only; the selected mode will not touch any file.",
573
+ "recall.scope.aria": "Recall scope",
574
+ "recall.scope.both": "Files and chat",
575
+ "recall.scope.sessionOnly": "Chat only",
576
+ "recall.confirm.submit.both": "Roll back",
577
+ "recall.confirm.submit.sessionOnly": "Rewind chat",
578
+ "recall.executing.both": "Rolling back\u2026",
579
+ "recall.executing.sessionOnly": "Rewinding chat\u2026",
580
+ "recall.done.title": "Rollback complete",
581
+ "recall.done.both.ok": "Project files and the chat have been rewound to before this message. A new session is open and the original one is archived (recoverable from Archive).",
582
+ "recall.done.both.partial": "The project has been restored to its state when this message was sent.",
583
+ "recall.done.both.failChat": " Chat rewind failed: {error}",
584
+ "recall.done.sessionOnly.ok": "The chat has been rewound to before this message; project files stay as they are. A new session is open and the original one is archived (recoverable from Archive).",
585
+ "recall.done.sessionOnly.fail": "Chat rewind failed: {error}. No project files were touched.",
586
+ "recall.chat.noChild": "no new session returned",
587
+ // ---- 用户消息气泡的动作按钮 ----
588
+ "action.copy": "Copy",
589
+ "action.copied": "Copied",
590
+ "action.recall": "Recall",
591
+ "action.recall.title": "Roll back: files and chat return to before this message",
592
+ // ---- toast ----
593
+ "toast.tag": "Recall plugin",
594
+ "toast.skipped": "Snapshot skipped unlisted paths: {names} (rollback will not restore or delete them)",
595
+ "toast.skippedMore": " and {n} more",
596
+ "toast.snapshotFailed": "Snapshot failed: {error}",
597
+ "toast.staleQueue": "A queued message from before the recall could not be cleaned up automatically; remove it with the delete button on that card",
598
+ "toast.fileAttachRefill": "File attachments from the recalled message cannot be refilled automatically (the official API only reads images back); please re-select the files",
599
+ // ---- init / snapshot-info 说明 ----
600
+ "notice.unsupported": "The recall plugin supports Windows / Linux / macOS only; snapshots are unavailable on this platform.",
601
+ "notice.gitMissing": "git CLI not found, so recall is unavailable (the snapshot engine depends on git). Install git and restart DSH to enable it.",
602
+ "notice.homeFallback": "The home directory is not writable; snapshots fall back to .dsh-recall-snapshots inside the project.",
603
+ "notice.buildRoot": 'This workspace is a build-artifact directory (path segment {seg}), so project snapshots are skipped; to use recall here, remove that entry from "Base excludes" in the plugin settings.',
604
+ // ---- 错误码文案 ----
605
+ "err.STALE": "Project files changed after the preview; preview again to confirm",
606
+ "err.AGENT_BUSY": "The agent is running; stop it before recalling",
607
+ "err.NO_SNAPSHOT": "No project snapshot is available for this message",
608
+ "err.NO_STORE": "Snapshot storage is unavailable",
609
+ "err.FORMAT_BLOCKED": "Unsupported disk format; writes are paused (see Recent errors)",
610
+ "err.UNKNOWN_PATH": "Unknown exclude file path",
611
+ "err.EMPTY_PATCH": "No config field to write",
612
+ "err.SETTINGS_UNAVAILABLE": "Settings service unavailable: override the config by id: recall in the profile cordis.patch.yml",
613
+ "err.BODY_TOO_LARGE": "Request body exceeds the size limit",
614
+ "err.NO_ROOT": "Cannot resolve the workspace",
615
+ "err.NO_SESSION": "Missing session ID",
616
+ "err.UNKNOWN_OP": "Unknown manage operation",
617
+ "err.UNKNOWN_ENDPOINT": "Unknown API endpoint",
618
+ "err.INDEX_CORRUPT": "Snapshot index is corrupted",
619
+ // ---- 面板兜底 ----
620
+ "fallback.preview": "Failed to load the snapshot",
621
+ "fallback.rollback": "Rollback failed",
622
+ // ---- 设置页:最近错误(按 host kind 键控)----
623
+ "errorKind.git": "git CLI not found or too old: install or upgrade git; snapshots resume automatically afterwards",
624
+ "errorKind.space": "Disk is full, snapshot writes failed: free up disk space to resume automatically",
625
+ "errorKind.permission": "No write permission on the snapshot directory: check directory permissions and retry",
626
+ "errorKind.lock": "Possibly multiple DSH instances sharing one snapshot store: make sure only one is running; if it still fails, delete the lock file at the path shown in Settings \xB7 Plugin config \xB7 Recent errors",
627
+ "errorKind.mkdir": "The snapshot directory is occupied by a file of the same name: resolve it to resume automatically",
628
+ // ---- 设置页:快照管理卡片 ----
629
+ "manage.busy": "Working\u2026",
630
+ "manage.deletedCount": "Deleted {deleted} snapshots",
631
+ "manage.done.delete": "Deleted",
632
+ "manage.done.deleteAll": "All snapshots cleared",
633
+ "manage.done.gc": "gc finished",
634
+ "manage.confirm.all": "Delete all snapshots in every workspace? This cannot be undone.",
635
+ "manage.countLoading": "Loading snapshot count\u2026",
636
+ "manage.countLoaded": "{total} snapshots in total",
637
+ "manage.countShown": " (showing the newest {shown})",
638
+ "manage.countEnd": ".",
639
+ "manage.usageSuffix": ", using {size} across all workspaces.",
640
+ "manage.health.gitTitle": "The snapshot engine depends on git",
641
+ "manage.health.gitOk": "git available",
642
+ "manage.health.gitBad": "git unavailable (the snapshot engine depends on git)",
643
+ "manage.health.stores": " \xB7 Snapshot storage: {n} workspaces in home",
644
+ "manage.health.storesFallback": ", {n} in fallback",
645
+ "manage.search.placeholder": "Search workspace / session title / message / ID",
646
+ "manage.search.aria": "Search snapshots",
647
+ "manage.empty": "Send a message in any workspace and its snapshot will appear here.",
648
+ "manage.emptyFiltered": "No matching snapshots",
649
+ "manage.errors.title": "Recent errors ({n})",
650
+ "manage.errors.collapse": "Collapse",
651
+ "manage.errors.expand": "Show all ({n})",
652
+ "manage.errors.clear": "Clear",
653
+ "manage.errors.dup": " (\xD7{n})",
654
+ "manage.loadMore": "Load more",
655
+ "manage.refresh": "Refresh",
656
+ "manage.gc": "Run gc",
657
+ "manage.gc.title": "Run git gc once for every workspace (compacts the object store to free space)",
658
+ "manage.deleteAll": "Delete all",
659
+ "manage.deleteAll.title": "Delete all snapshots in every workspace; git tags are checked and removed directly (leftovers can be cleaned even when the list is empty)",
660
+ // ---- 设置页:插件配置表单 ----
661
+ "config.group.snapshot": "Snapshot behavior",
662
+ "config.group.auto": "Automatic maintenance",
663
+ "config.group.ui": "Interface",
664
+ "config.snapshotEnabled.label": "Enable snapshots",
665
+ "config.snapshotEnabled.hint": "When off, no new snapshots are created; existing ones can still be recalled",
666
+ "config.refillDraft.label": "Refill input box",
667
+ "config.refillDraft.hint": "Put the message text back into the input box after a recall, so you can edit and resend",
668
+ "config.archiveOriginal.label": "Archive original session",
669
+ "config.archiveOriginal.hint": "Archive and hide the original session (recoverable from Archive); when off it stays in the list for comparison",
670
+ "config.gcSnaps.label": "gc trigger count",
671
+ "config.gcSnaps.hint": "Run gc after this many snapshots accumulate",
672
+ "config.gcSnaps.suffix": "snapshots",
673
+ "config.gcHours.label": "gc trigger hours",
674
+ "config.gcHours.hint": "Run gc when this many hours passed since the last one; whichever comes first",
675
+ "config.gcHours.suffix": "hours",
676
+ "config.maxFileBytes.label": "File size limit",
677
+ "config.maxFileBytes.hint": "Files larger than this are not snapshotted and never touched by rollback",
678
+ "config.maxFileBytes.suffix": "MB",
679
+ "config.maxSnapshotsPerWorkspace.label": "Snapshot count limit",
680
+ "config.maxSnapshotsPerWorkspace.hint": "Maximum snapshots kept per workspace; the oldest are deleted beyond it; 0 means unlimited",
681
+ "config.maxSnapshotsPerWorkspace.suffix": "snapshots",
682
+ "config.retentionDays.label": "Snapshot retention days",
683
+ "config.retentionDays.hint": "Delete snapshots older than this; the oldest first; 0 disables it",
684
+ "config.retentionDays.suffix": "days",
685
+ "config.locale.label": "Language",
686
+ "config.locale.hint": "auto follows the system language; takes effect after saving (field descriptions rendered by the host-side plugin config form stay in Chinese)",
687
+ "config.locale.auto": "Follow system (auto)",
688
+ "config.tag.modified": "Modified",
689
+ "config.tag.overridden": "Overridden",
690
+ "config.tag.locked": "Locked by env var",
691
+ "config.tag.readonly": "Read-only settings source",
692
+ "config.reset": "Restore defaults",
693
+ "config.reset.title": "Restore every field to the plugin factory defaults",
694
+ "config.advanced.title": "Advanced: base excludes",
695
+ "config.baseExcludes.label": "Base excludes",
696
+ "config.baseExcludes.hint": 'Built-in rules shared by all workspaces; gitignore syntax, one per line; lower priority than exclude.txt in "Exclude config"',
697
+ "config.msg.loading": "Loading configuration\u2026",
698
+ "config.msg.loadFailed": "Failed to load configuration",
699
+ "config.msg.noChange": "No changes",
700
+ "config.msg.saving": "Saving\u2026",
701
+ "config.msg.saved": "Saved and applied immediately",
702
+ "config.msg.saveFailed": "Failed to save",
703
+ "config.msg.resetting": "Restoring defaults\u2026",
704
+ "config.msg.resetDone": "Defaults restored",
705
+ "config.msg.resetFailed": "Failed to restore defaults",
706
+ "config.err.gcSnaps": "The snapshot count threshold must be an integer >= 1",
707
+ "config.err.gcHours": "The gc hour threshold must be an integer >= 1",
708
+ "config.err.maxFileBytes": "The file size limit must be at least 0.01 MB",
709
+ "config.err.maxSnapshots": "The snapshot count limit must be an integer >= 0 (0 means unlimited)",
710
+ "config.err.retentionDays": "Retention days must be an integer >= 0 (0 disables it)",
711
+ "config.err.locale": "Language must be one of auto / zh / en",
712
+ // ---- 设置页:排除配置卡片 ----
713
+ "exclude.note.home": "This configuration is shared globally and applies to snapshots in every workspace.",
714
+ "exclude.note.fallback": "This workspace stores snapshots in the fallback location (home is not writable), so its exclude config is separate.",
715
+ "exclude.path": "Location: {path}",
716
+ "exclude.syntax": "gitignore syntax, one per line, # comments supported; matched paths are not snapshotted and never touched by rollback.",
717
+ "exclude.area.aria": "Snapshot exclude patterns (gitignore syntax, one per line)",
718
+ "exclude.quick.placeholder": "Type a path or pattern, press Enter to add",
719
+ "exclude.quick.aria": "Quickly add an exclude pattern",
720
+ "exclude.add": "Add",
721
+ "exclude.chip.title": "Click to append {pattern}",
722
+ "exclude.msg.saving": "Saving\u2026",
723
+ "exclude.msg.saved": "Saved; takes effect on the next snapshot / preview / rollback",
724
+ "exclude.msg.saveFailed": "Failed to save",
725
+ "exclude.loadFailed": "Failed to load the exclude configuration",
726
+ "exclude.unsupported": "This platform does not support snapshots, so exclude configuration is unavailable.",
727
+ "exclude.loading": "Loading exclude configuration\u2026",
728
+ "exclude.empty": "No snapshot store exists yet: send a message in any workspace and its editable exclude configuration will appear here.",
729
+ // ---- 设置页:分区折叠头 ----
730
+ "section.exclude": "Exclude config (exclude.txt)",
731
+ "section.manage": "Snapshot manager"
732
+ };
733
+
734
+ // src/client/locales/index.ts
735
+ var DICTS = { zh, en };
736
+ function resolveLocale(pref) {
737
+ if (pref === "zh" || pref === "en") return pref;
738
+ try {
739
+ const nav = typeof navigator === "undefined" ? null : navigator;
740
+ const lang = nav && typeof nav.language === "string" ? nav.language : "";
741
+ if (!lang) return "zh";
742
+ return lang.toLowerCase().indexOf("zh") === 0 ? "zh" : "en";
743
+ } catch (error) {
744
+ return "zh";
745
+ }
746
+ }
747
+ function interpolate(template, params) {
748
+ if (!params) return template;
749
+ return template.replace(/\{(\w+)\}/g, (raw, name) => {
750
+ const value = params[name];
751
+ return value === void 0 ? raw : String(value);
752
+ });
753
+ }
754
+ function translate(locale, key, params) {
755
+ const dict = DICTS[locale];
756
+ const hit = dict[key] !== void 0 ? dict[key] : zh[key];
757
+ return interpolate(hit !== void 0 ? hit : key, params);
758
+ }
759
+ var zhTranslate = (key, params) => translate("zh", key, params);
760
+ function hasTranslation(key) {
761
+ return zh[key] !== void 0;
762
+ }
763
+
306
764
  // src/client/util.ts
307
765
  function clockText(ms) {
308
766
  try {
@@ -329,15 +787,19 @@
329
787
  if (!Number.isFinite(n) || n <= 0) return "";
330
788
  return String(Math.round(n / 1048576 * 100) / 100);
331
789
  }
332
- function buildTree(list) {
790
+ function buildTree(list, t = zhTranslate) {
333
791
  const workspaces = /* @__PURE__ */ new Map();
334
792
  for (const it of list || []) {
335
793
  const rootKey = it.root || "unknown-root";
336
- if (!workspaces.has(rootKey)) workspaces.set(rootKey, { root: it.root || null, name: it.workspace || "\u672A\u77E5\u5DE5\u4F5C\u533A", sessions: /* @__PURE__ */ new Map() });
337
- const ws = workspaces.get(rootKey);
794
+ let ws = workspaces.get(rootKey);
795
+ if (!ws) {
796
+ ws = { root: it.root || null, name: it.workspace || t("tree.unknownWorkspace"), sessions: /* @__PURE__ */ new Map() };
797
+ workspaces.set(rootKey, ws);
798
+ }
338
799
  const sidKey = it.sessionId || "unknown-session";
339
800
  if (!ws.sessions.has(sidKey)) ws.sessions.set(sidKey, { root: ws.root, sessionId: it.sessionId || null, title: it.sessionTitle || null, items: [] });
340
- ws.sessions.get(sidKey).items.push(it);
801
+ const session = ws.sessions.get(sidKey);
802
+ if (session) session.items.push(it);
341
803
  }
342
804
  const wsList = Array.from(workspaces.values()).map((ws) => ({ root: ws.root, name: ws.name, sessions: Array.from(ws.sessions.values()) }));
343
805
  wsList.sort((a, b) => (a.name || "").localeCompare(b.name || ""));
@@ -368,6 +830,13 @@
368
830
  }, [state.message, state.error, state.busy]);
369
831
  }
370
832
  function buildUtil() {
833
+ let locale = resolveLocale("auto");
834
+ function t(key, params) {
835
+ return translate(locale, key, params);
836
+ }
837
+ function setLocalePref(pref) {
838
+ locale = resolveLocale(pref);
839
+ }
371
840
  function api(name, args) {
372
841
  return fetch(recallApiUrl(name), {
373
842
  method: "POST",
@@ -375,17 +844,11 @@
375
844
  body: JSON.stringify(args || {})
376
845
  }).then((r) => r.json());
377
846
  }
378
- const CODE_TEXT = {
379
- STALE: "\u9884\u89C8\u540E\u9879\u76EE\u6587\u4EF6\u53D1\u751F\u4E86\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u9884\u89C8\u786E\u8BA4",
380
- AGENT_BUSY: "Agent \u6B63\u5728\u8FD0\u884C\u4E2D\uFF0C\u8BF7\u5148\u505C\u6B62\u540E\u518D\u64A4\u56DE",
381
- NO_SNAPSHOT: "\u8BE5\u6D88\u606F\u6CA1\u6709\u53EF\u7528\u7684\u9879\u76EE\u5FEB\u7167",
382
- NO_STORE: "\u5FEB\u7167\u5B58\u50A8\u4E0D\u53EF\u7528",
383
- ROLLBACK_FAILED: "\u56DE\u9000\u5931\u8D25"
384
- };
385
847
  function messageFor(res, fallback) {
386
848
  if (!res) return fallback;
387
849
  const code = res.code;
388
- if (code && CODE_TEXT[String(code)]) return CODE_TEXT[String(code)];
850
+ const key = code ? "err." + String(code) : "";
851
+ if (key && hasTranslation(key)) return t(key);
389
852
  const m = res.message;
390
853
  const e = res.error;
391
854
  return String(m || e || fallback);
@@ -406,7 +869,7 @@
406
869
  el.className = "dsh-recall-toast";
407
870
  const tag = document.createElement("span");
408
871
  tag.className = "dsh-recall-toast-tag";
409
- tag.textContent = "\u64A4\u56DE\u63D2\u4EF6";
872
+ tag.textContent = t("toast.tag");
410
873
  const body = document.createElement("span");
411
874
  body.textContent = text;
412
875
  el.appendChild(tag);
@@ -444,18 +907,21 @@
444
907
  const cfg = res.config;
445
908
  if (typeof cfg.refillDraft === "boolean") pluginConfig.refillDraft = cfg.refillDraft;
446
909
  if (typeof cfg.archiveOriginal === "boolean") pluginConfig.archiveOriginal = cfg.archiveOriginal;
910
+ setLocalePref(cfg.locale);
447
911
  }
448
912
  const notice = res && res.notice;
449
913
  if (notice && notice.unsupported) {
450
- showNotice("unsupported", "\u64A4\u56DE\u63D2\u4EF6\u4EC5\u652F\u6301 Windows / Linux / macOS\uFF0C\u5F53\u524D\u5E73\u53F0\u7684\u5FEB\u7167\u4E0D\u53EF\u7528\u3002");
914
+ showNotice("unsupported", t("notice.unsupported"));
451
915
  }
452
916
  if (notice && notice.gitMissing) {
453
- showNotice("git", "\u672A\u68C0\u6D4B\u5230 git CLI\uFF0C\u64A4\u56DE\u529F\u80FD\u4E0D\u53EF\u7528\uFF08\u5FEB\u7167\u5F15\u64CE\u4F9D\u8D56 git\uFF09\u3002\u5B89\u88C5 git \u5E76\u91CD\u542F DSH \u540E\u5373\u53EF\u4F7F\u7528\u3002");
917
+ showNotice("git", t("notice.gitMissing"));
454
918
  }
455
919
  if (notice && notice.homeFallback) {
456
- showNotice("home", "home \u76EE\u5F55\u4E0D\u53EF\u5199\uFF0C\u5FEB\u7167\u5DF2\u964D\u7EA7\u5B58\u50A8\u5230\u9879\u76EE\u5185 .dsh-recall-snapshots \u76EE\u5F55\u3002");
920
+ showNotice("home", t("notice.homeFallback"));
457
921
  }
458
- if (notice && notice.buildRootNotice) {
922
+ if (notice && typeof notice.buildRootArtifactSeg === "string" && notice.buildRootArtifactSeg) {
923
+ showNotice("buildRoot", t("notice.buildRoot", { seg: notice.buildRootArtifactSeg }));
924
+ } else if (notice && notice.buildRootNotice) {
459
925
  showNotice("buildRoot", String(notice.buildRootNotice).slice(0, 140));
460
926
  }
461
927
  }).catch(() => {
@@ -490,8 +956,45 @@
490
956
  }
491
957
  return Promise.resolve(false);
492
958
  }
493
- return { api, messageFor, showNotice, showThrottledToast, ensureInit, clockText, writeClipboard, sizeText, bytesToMb, buildTree, pluginConfig };
959
+ return { api, messageFor, showNotice, showThrottledToast, ensureInit, clockText, writeClipboard, sizeText, bytesToMb, buildTree, t, setLocalePref, pluginConfig };
960
+ }
961
+
962
+ // src/client/log.ts
963
+ function debugFlag() {
964
+ try {
965
+ return String(localStorage.getItem("dsh-recall.debug") || "");
966
+ } catch (error) {
967
+ return "";
968
+ }
969
+ }
970
+ function debugEnabled(ns) {
971
+ const raw = debugFlag();
972
+ if (!raw) return false;
973
+ for (const part of raw.split(",")) {
974
+ const t = part.trim();
975
+ if (t === "*" || t === ns) return true;
976
+ }
977
+ return false;
978
+ }
979
+ function createLogger(ns) {
980
+ const prefix = "[dsh-recall:" + ns + "]";
981
+ return {
982
+ error: (...args) => {
983
+ console.error(prefix, ...args);
984
+ },
985
+ warn: (...args) => {
986
+ console.warn(prefix, ...args);
987
+ },
988
+ info: (...args) => {
989
+ if (debugEnabled(ns)) console.info(prefix, ...args);
990
+ },
991
+ debug: (...args) => {
992
+ if (debugEnabled(ns)) console.debug(prefix, ...args);
993
+ }
994
+ };
494
995
  }
996
+ var appLog = createLogger("app");
997
+ var recallNodeLog = createLogger("recall-node");
495
998
 
496
999
  // src/client/recall-node.ts
497
1000
  function attachmentRefsFromBlocks(blocks) {
@@ -526,26 +1029,29 @@
526
1029
  if (n < 1073741824) return (n / 1048576).toFixed(1) + "MB";
527
1030
  return (n / 1073741824).toFixed(1) + "GB";
528
1031
  }
529
- function fileCardInfo(block) {
1032
+ function fileCardInfo(block, t = zhTranslate) {
530
1033
  if (!block || block.type !== "file") return null;
531
1034
  const ref = block.attachment;
532
- const name = ref && typeof ref === "object" && typeof ref.name === "string" && ref.name !== "" ? ref.name : "\u672A\u547D\u540D\u6587\u4EF6";
1035
+ const name = ref && typeof ref === "object" && typeof ref.name === "string" && ref.name !== "" ? ref.name : t("file.unnamed");
533
1036
  return { name, ext: fileExtOf(name), size: fileCardSizeText(ref && typeof ref === "object" ? ref.bytes : void 0) };
534
1037
  }
535
1038
  var KIND_INFO = {
536
- modified: { label: "\u4FEE\u6539", cls: "modified" },
537
- restored: { label: "\u6062\u590D", cls: "restored" },
538
- added: { label: "\u5220\u9664", cls: "added" }
1039
+ modified: { labelKey: "kind.modified", cls: "modified" },
1040
+ restored: { labelKey: "kind.restored", cls: "restored" },
1041
+ added: { labelKey: "kind.added", cls: "added" }
539
1042
  };
540
- function summaryText(counts) {
1043
+ function isChangeKind(kind) {
1044
+ return typeof kind === "string" && Object.prototype.hasOwnProperty.call(KIND_INFO, kind);
1045
+ }
1046
+ function summaryText(counts, t = zhTranslate) {
541
1047
  const parts = [];
542
1048
  for (const kind of Object.keys(KIND_INFO)) {
543
- if (counts[kind] > 0) parts.push(KIND_INFO[kind].label + " " + counts[kind]);
1049
+ if (counts[kind] > 0) parts.push(t(KIND_INFO[kind].labelKey) + " " + counts[kind]);
544
1050
  }
545
1051
  return parts.join(" \xB7 ");
546
1052
  }
547
1053
  function buildRecallNode(React, util, ctx, sessionsSvc, workspacesSvc, uiWorkspaceSvc) {
548
- const { api, ensureInit, showThrottledToast, writeClipboard, clockText: clockText2, pluginConfig, messageFor } = util;
1054
+ const { api, ensureInit, showThrottledToast, writeClipboard, clockText: clockText2, pluginConfig, messageFor, t } = util;
549
1055
  function CopyIcon() {
550
1056
  return React.createElement(
551
1057
  "svg",
@@ -578,19 +1084,19 @@
578
1084
  return React.createElement(
579
1085
  "div",
580
1086
  { className: "dsh-recall-panel" },
581
- React.createElement("div", { className: "dsh-recall-panel-title" }, "\u6B63\u5728\u8BA1\u7B97\u53D8\u66F4\u2026")
1087
+ React.createElement("div", { className: "dsh-recall-panel-title" }, t("recall.loading"))
582
1088
  );
583
1089
  }
584
1090
  if (recall.stage === "error") {
585
1091
  return React.createElement(
586
1092
  "div",
587
1093
  { className: "dsh-recall-panel" },
588
- React.createElement("div", { className: "dsh-recall-panel-title" }, "\u65E0\u6CD5\u56DE\u9000"),
1094
+ React.createElement("div", { className: "dsh-recall-panel-title" }, t("recall.error.title")),
589
1095
  React.createElement("div", { className: "dsh-recall-panel-note" }, recall.message || ""),
590
1096
  React.createElement(
591
1097
  "div",
592
1098
  { className: "dsh-recall-panel-actions" },
593
- React.createElement("button", { type: "button", className: "dsh-recall-btn", onClick: closePanel }, "\u5173\u95ED")
1099
+ React.createElement("button", { type: "button", className: "dsh-recall-btn", onClick: closePanel }, t("common.close"))
594
1100
  )
595
1101
  );
596
1102
  }
@@ -599,14 +1105,14 @@
599
1105
  const total = typeof recall.total === "number" ? recall.total : changes.length;
600
1106
  const counts = { modified: 0, restored: 0, added: 0 };
601
1107
  for (const c of changes) {
602
- if (c && counts[c.kind] !== void 0) counts[c.kind]++;
1108
+ if (c && isChangeKind(c.kind)) counts[c.kind]++;
603
1109
  }
604
1110
  const rows = changes.map((c, i) => {
605
1111
  const info = KIND_INFO[c.kind];
606
1112
  return React.createElement(
607
1113
  "div",
608
1114
  { className: "dsh-recall-file", key: i },
609
- React.createElement("span", { className: "dsh-recall-badge dsh-recall-badge-" + (info ? info.cls : "") }, info ? info.label : c.kind || ""),
1115
+ React.createElement("span", { className: "dsh-recall-badge dsh-recall-badge-" + (info ? info.cls : "") }, info ? t(info.labelKey) : c.kind || ""),
610
1116
  React.createElement("span", { className: "dsh-recall-rel" }, c.rel || "")
611
1117
  );
612
1118
  });
@@ -616,30 +1122,36 @@
616
1122
  rows.push(React.createElement(
617
1123
  "div",
618
1124
  { className: "dsh-recall-panel-note", key: "truncated" },
619
- sessionOnly ? "\u2026\u4EC5\u663E\u793A\u524D " + changes.length + " \u6761\uFF0C\u5171 " + total + " \u5904\u5DEE\u5F02" : "\u2026\u4EC5\u663E\u793A\u524D " + changes.length + " \u6761\uFF0C\u5171 " + total + " \u4E2A\u6587\u4EF6\u5C06\u53D8\u66F4"
1125
+ t(sessionOnly ? "recall.truncated.chat" : "recall.truncated.files", { shown: changes.length, total })
620
1126
  ));
621
1127
  }
1128
+ const summary = summaryText(counts, t);
1129
+ const rollbackLead = recall.time ? t("recall.confirm.rollbackAt", { time: clockText2(recall.time) }) : t("recall.confirm.rollbackNoTime");
1130
+ const filesLead = t("recall.confirm.files", {
1131
+ total,
1132
+ summary: summary ? t("common.paren", { text: summary }) : ""
1133
+ });
622
1134
  return React.createElement(
623
1135
  "div",
624
1136
  { className: "dsh-recall-panel" },
625
- React.createElement("div", { className: "dsh-recall-panel-title" }, "\u6574\u6BB5\u56DE\u9000"),
1137
+ React.createElement("div", { className: "dsh-recall-panel-title" }, t("recall.confirm.title")),
626
1138
  sessionOnly ? (
627
1139
  // session-only:零文件改动即无不可逆操作缺口——安全快照预告随之隐藏
628
1140
  //(不打安全快照),主说明换成模式语义
629
1141
  React.createElement(
630
1142
  "div",
631
1143
  { className: "dsh-recall-panel-note" },
632
- "\u9879\u76EE\u6587\u4EF6\u4FDD\u6301\u5F53\u524D\u72B6\u6001\uFF0C\u4E0D\u4F1A\u88AB\u56DE\u9000\u6216\u5220\u9664\uFF1B\u5BF9\u8BDD\u56DE\u9000\u5230\u8BE5\u6D88\u606F\u4E4B\u524D\u3002"
1144
+ t("recall.confirm.sessionOnly")
633
1145
  )
634
1146
  ) : React.createElement(
635
1147
  "div",
636
1148
  { className: "dsh-recall-panel-note" },
637
- "\u5C06\u9879\u76EE\u6062\u590D\u5230" + (recall.time ? " " + clockText2(recall.time) + " " : " ") + "\u53D1\u9001\u8BE5\u6D88\u606F\u65F6\u7684\u72B6\u6001\u3002\u5171 " + total + " \u4E2A\u6587\u4EF6\u5C06\u53D8\u66F4" + (summaryText(counts) ? "\uFF08" + summaryText(counts) + "\uFF09" : "") + "\u3002\u6B64\u64CD\u4F5C\u4F1A\u8986\u76D6\u5F53\u524D\u6587\u4EF6\u5185\u5BB9\uFF1B\u56DE\u9000\u524D\u4F1A\u81EA\u52A8\u4FDD\u5B58\u4E00\u4EFD\u5F53\u524D\u72B6\u6001\u7684\u5B89\u5168\u5FEB\u7167\uFF08\u4E0D\u542B\u5728\u4E0B\u65B9\u6E05\u5355\u5185\uFF09\u3002"
1149
+ rollbackLead + filesLead
638
1150
  ),
639
1151
  sessionOnly ? null : React.createElement(
640
1152
  "div",
641
1153
  { className: "dsh-recall-panel-note" },
642
- canRevertChat ? "\u5BF9\u8BDD\u5C06\u4E00\u5E76\u56DE\u9000\u5230\u8BE5\u6D88\u606F\u4E4B\u524D\uFF1A\u8BE5\u6D88\u606F\u53CA\u4E4B\u540E\u7684\u5168\u90E8\u5BF9\u8BDD\u4F1A\u4ECE\u5F53\u524D\u89C6\u56FE\u79FB\u9664\uFF0C\u539F\u4F1A\u8BDD\u5F52\u6863\u4FDD\u5B58\uFF08\u53EF\u4ECE\u5F52\u6863\u627E\u56DE\uFF09\u3002" : "\u8BE5\u6D88\u606F\u662F\u672C\u4F1A\u8BDD\u4E2D\u7B2C\u4E00\u6761\u7528\u6237\u6D88\u606F\uFF0C\u65E0\u6CD5\u56DE\u9000\u5BF9\u8BDD\uFF1B\u786E\u8BA4\u540E\u4EC5\u56DE\u9000\u9879\u76EE\u6587\u4EF6\u3002"
1154
+ t(canRevertChat ? "recall.confirm.chatBoth" : "recall.confirm.chatFirst")
643
1155
  ),
644
1156
  sessionOnly && changes.length > 0 ? (
645
1157
  // 文件清单保留展示但降级为参考语义:清单照常算(Host preview 链路
@@ -647,7 +1159,7 @@
647
1159
  React.createElement(
648
1160
  "div",
649
1161
  { className: "dsh-recall-panel-note", key: "ref-note" },
650
- "\u4EE5\u4E0B\u5DEE\u5F02\u4EC5\u4F5C\u53C2\u8003\uFF0C\u6240\u9009\u6A21\u5F0F\u4E0D\u4F1A\u6539\u52A8\u6587\u4EF6\u3002"
1162
+ t("recall.confirm.refNote")
651
1163
  )
652
1164
  ) : null,
653
1165
  changes.length > 0 ? React.createElement("div", { className: "dsh-recall-list" }, ...rows) : null,
@@ -656,26 +1168,26 @@
656
1168
  // 默认 both 与现状一致,面板每次打开都复位到默认起点
657
1169
  React.createElement(
658
1170
  "div",
659
- { className: "dsh-recall-scope", role: "radiogroup", "aria-label": "\u64A4\u56DE\u8303\u56F4" },
1171
+ { className: "dsh-recall-scope", role: "radiogroup", "aria-label": t("recall.scope.aria") },
660
1172
  React.createElement(
661
1173
  "label",
662
1174
  { className: "dsh-recall-scope-item" },
663
1175
  React.createElement("input", { type: "radio", name: "dsh-recall-scope", checked: scope === "both", onChange: () => onScopeChange("both") }),
664
- React.createElement("span", { className: "dsh-recall-scope-label" }, "\u56DE\u9000\u6587\u4EF6\u4E0E\u5BF9\u8BDD")
1176
+ React.createElement("span", { className: "dsh-recall-scope-label" }, t("recall.scope.both"))
665
1177
  ),
666
1178
  React.createElement(
667
1179
  "label",
668
1180
  { className: "dsh-recall-scope-item" },
669
1181
  React.createElement("input", { type: "radio", name: "dsh-recall-scope", checked: scope === "session-only", onChange: () => onScopeChange("session-only") }),
670
- React.createElement("span", { className: "dsh-recall-scope-label" }, "\u4EC5\u64A4\u56DE\u5BF9\u8BDD")
1182
+ React.createElement("span", { className: "dsh-recall-scope-label" }, t("recall.scope.sessionOnly"))
671
1183
  )
672
1184
  )
673
1185
  ) : null,
674
1186
  React.createElement(
675
1187
  "div",
676
1188
  { className: "dsh-recall-panel-actions" },
677
- React.createElement("button", { type: "button", className: "dsh-recall-btn", onClick: closePanel }, "\u53D6\u6D88"),
678
- React.createElement("button", { type: "button", className: "dsh-recall-btn dsh-recall-btn-danger", onClick: executeRecall }, sessionOnly ? "\u786E\u8BA4\u64A4\u56DE\u5BF9\u8BDD" : "\u786E\u8BA4\u56DE\u9000")
1189
+ React.createElement("button", { type: "button", className: "dsh-recall-btn", onClick: closePanel }, t("common.cancel")),
1190
+ React.createElement("button", { type: "button", className: "dsh-recall-btn dsh-recall-btn-danger", onClick: executeRecall }, t(sessionOnly ? "recall.confirm.submit.sessionOnly" : "recall.confirm.submit.both"))
679
1191
  )
680
1192
  );
681
1193
  }
@@ -683,26 +1195,26 @@
683
1195
  return React.createElement(
684
1196
  "div",
685
1197
  { className: "dsh-recall-panel" },
686
- React.createElement("div", { className: "dsh-recall-panel-title" }, scope === "session-only" ? "\u6B63\u5728\u64A4\u56DE\u5BF9\u8BDD\u2026" : "\u6B63\u5728\u56DE\u9000\u2026")
1198
+ React.createElement("div", { className: "dsh-recall-panel-title" }, t(scope === "session-only" ? "recall.executing.sessionOnly" : "recall.executing.both"))
687
1199
  );
688
1200
  }
689
1201
  if (recall.stage === "done") {
690
1202
  return React.createElement(
691
1203
  "div",
692
1204
  { className: "dsh-recall-panel" },
693
- React.createElement("div", { className: "dsh-recall-panel-title" }, "\u56DE\u9000\u5B8C\u6210"),
1205
+ React.createElement("div", { className: "dsh-recall-panel-title" }, t("recall.done.title")),
694
1206
  React.createElement(
695
1207
  "div",
696
1208
  { className: "dsh-recall-panel-note" },
697
1209
  scope === "session-only" ? (
698
1210
  // session-only 文案矩阵:文件侧零改动是确定事实,失败时也只描述对话侧
699
- recall.chatReverted ? "\u5BF9\u8BDD\u5DF2\u56DE\u9000\u5230\u8BE5\u6D88\u606F\u4E4B\u524D\uFF0C\u9879\u76EE\u6587\u4EF6\u4FDD\u6301\u5F53\u524D\u72B6\u6001\u3002\u65B0\u4F1A\u8BDD\u5DF2\u6253\u5F00\uFF0C\u539F\u4F1A\u8BDD\u5DF2\u5F52\u6863\uFF08\u53EF\u4ECE\u5F52\u6863\u627E\u56DE\uFF09\u3002" : "\u5BF9\u8BDD\u56DE\u9000\u5931\u8D25\uFF1A" + (recall.chatError || "\u672A\u77E5\u539F\u56E0") + "\u3002\u9879\u76EE\u6587\u4EF6\u672A\u505A\u4EFB\u4F55\u6539\u52A8\u3002"
700
- ) : recall.chatReverted ? "\u9879\u76EE\u6587\u4EF6\u4E0E\u5BF9\u8BDD\u5DF2\u56DE\u9000\u5230\u8BE5\u6D88\u606F\u4E4B\u524D\u3002\u65B0\u4F1A\u8BDD\u5DF2\u6253\u5F00\uFF0C\u539F\u4F1A\u8BDD\u5DF2\u5F52\u6863\uFF08\u53EF\u4ECE\u5F52\u6863\u627E\u56DE\uFF09\u3002" : "\u9879\u76EE\u5DF2\u6062\u590D\u5230\u53D1\u9001\u8BE5\u6D88\u606F\u65F6\u7684\u72B6\u6001\u3002" + (recall.chatError ? " \u5BF9\u8BDD\u56DE\u9000\u5931\u8D25\uFF1A" + recall.chatError : "")
1211
+ recall.chatReverted ? t("recall.done.sessionOnly.ok") : t("recall.done.sessionOnly.fail", { error: recall.chatError || t("common.unknownReason") })
1212
+ ) : recall.chatReverted ? t("recall.done.both.ok") : t("recall.done.both.partial") + (recall.chatError ? t("recall.done.both.failChat", { error: recall.chatError }) : "")
701
1213
  ),
702
1214
  React.createElement(
703
1215
  "div",
704
1216
  { className: "dsh-recall-panel-actions" },
705
- React.createElement("button", { type: "button", className: "dsh-recall-btn", onClick: closePanel }, "\u5173\u95ED")
1217
+ React.createElement("button", { type: "button", className: "dsh-recall-btn", onClick: closePanel }, t("common.close"))
706
1218
  )
707
1219
  );
708
1220
  }
@@ -809,8 +1321,8 @@
809
1321
  } catch (error) {
810
1322
  }
811
1323
  if (Date.now() >= deadline) {
812
- console.warn("[dsh-recall-plugin] \u6B8B\u7559\u6392\u961F\u6D88\u606F\u672A\u80FD\u81EA\u52A8\u6E05\u7406\uFF08\u4F1A\u8BDD\u9762\u672A\u5C31\u7EEA\uFF09\uFF1A", itemIds);
813
- showThrottledToast("\u64A4\u56DE\u524D\u7684\u4E00\u6761\u6392\u961F\u6D88\u606F\u672A\u88AB\u81EA\u52A8\u6E05\u7406\uFF0C\u53EF\u70B9\u51FB\u8BE5\u5361\u7247\u53F3\u4E0A\u89D2\u7684\u5220\u9664\u6309\u94AE\u624B\u52A8\u79FB\u9664");
1324
+ recallNodeLog.warn("\u6B8B\u7559\u6392\u961F\u6D88\u606F\u672A\u80FD\u81EA\u52A8\u6E05\u7406\uFF08\u4F1A\u8BDD\u9762\u672A\u5C31\u7EEA\uFF09\uFF1A", itemIds);
1325
+ showThrottledToast(t("toast.staleQueue"));
814
1326
  return;
815
1327
  }
816
1328
  setTimeout(attempt, 250);
@@ -849,18 +1361,21 @@
849
1361
  if (!alive) return;
850
1362
  if (res && res.has) {
851
1363
  if (recent && Array.isArray(res.skipped) && res.skipped.length) {
852
- const names = res.skipped.slice(0, 5).join("\u3001") + (res.skipped.length > 5 ? " \u7B49 " + res.skipped.length + " \u9879" : "");
853
- showThrottledToast("\u5FEB\u7167\u5DF2\u8DF3\u8FC7\u672A\u7EB3\u5165\u7684\u8DEF\u5F84\uFF1A" + names + "\uFF08\u64A4\u56DE\u4E0D\u4F1A\u6062\u590D\u6216\u5220\u9664\u8FD9\u4E9B\u8DEF\u5F84\uFF09");
1364
+ const names = res.skipped.slice(0, 5).join(t("common.listSep")) + (res.skipped.length > 5 ? t("toast.skippedMore", { n: res.skipped.length }) : "");
1365
+ showThrottledToast(t("toast.skipped", { names }));
854
1366
  }
855
1367
  setHasSnapshot(true);
856
1368
  return;
857
1369
  }
858
1370
  if (res && res.failed) {
859
- if (recent) showThrottledToast("\u5FEB\u7167\u5931\u8D25\uFF1A" + String(res.error || "\u672A\u77E5\u539F\u56E0").slice(0, 140));
1371
+ if (recent) showThrottledToast(t("toast.snapshotFailed", { error: String(res.error || t("common.unknownReason")).slice(0, 140) }));
860
1372
  return;
861
1373
  }
862
- if (res && res.notice) {
863
- if (recent) showThrottledToast(String(res.notice).slice(0, 140));
1374
+ if (res && (res.artifactSeg || res.notice)) {
1375
+ if (recent) {
1376
+ const text2 = res.artifactSeg ? t("notice.buildRoot", { seg: res.artifactSeg }) : String(res.notice).slice(0, 140);
1377
+ showThrottledToast(text2);
1378
+ }
864
1379
  return;
865
1380
  }
866
1381
  if (recent && attempts < MAX_ATTEMPTS) timer = setTimeout(schedule, RETRY_MS);
@@ -897,7 +1412,7 @@
897
1412
  setRecall({ stage: "loading" });
898
1413
  api("preview", { messageId, sessionId }).then((res) => {
899
1414
  if (!res || !res.ok) {
900
- setRecall({ stage: "error", message: messageFor(res, "\u65E0\u6CD5\u83B7\u53D6\u5FEB\u7167") });
1415
+ setRecall({ stage: "error", message: messageFor(res, t("fallback.preview")) });
901
1416
  return;
902
1417
  }
903
1418
  setRecall({
@@ -926,7 +1441,7 @@
926
1441
  setRecall({ stage: "loading" });
927
1442
  api("preview", { messageId, sessionId }).then((res2) => {
928
1443
  if (!res2 || !res2.ok) {
929
- setRecall({ stage: "error", message: messageFor(res2, "\u65E0\u6CD5\u83B7\u53D6\u5FEB\u7167") });
1444
+ setRecall({ stage: "error", message: messageFor(res2, t("fallback.preview")) });
930
1445
  return;
931
1446
  }
932
1447
  setRecall({
@@ -943,7 +1458,7 @@
943
1458
  });
944
1459
  return;
945
1460
  }
946
- setRecall({ stage: "error", message: messageFor(res, "\u56DE\u9000\u5931\u8D25") });
1461
+ setRecall({ stage: "error", message: messageFor(res, t("fallback.rollback")) });
947
1462
  return;
948
1463
  }
949
1464
  const cutSeq = typeof res.cutSeq === "number" ? res.cutSeq : previewCut;
@@ -963,11 +1478,11 @@
963
1478
  });
964
1479
  if (pluginConfig.archiveOriginal && workspacesSvc && typeof workspacesSvc.archiveSession === "function") {
965
1480
  workspacesSvc.archiveSession(sessionId, { stopActivity: true }).catch((error) => {
966
- console.warn("[dsh-recall-plugin] archive original session failed:", error);
1481
+ recallNodeLog.warn("archive original session failed:", error);
967
1482
  });
968
1483
  }
969
1484
  } else {
970
- chatError = "\u672A\u8FD4\u56DE\u65B0\u4F1A\u8BDD";
1485
+ chatError = t("recall.chat.noChild");
971
1486
  }
972
1487
  } catch (error) {
973
1488
  chatError = String(error);
@@ -975,7 +1490,7 @@
975
1490
  }
976
1491
  if (pluginConfig.refillDraft) {
977
1492
  fillDraft(fillTarget, text, attachmentFiles);
978
- if (fileBlocks.length > 0) showThrottledToast("\u88AB\u64A4\u56DE\u6D88\u606F\u91CC\u7684\u6587\u4EF6\u9644\u4EF6\u65E0\u6CD5\u81EA\u52A8\u56DE\u586B\uFF08\u5B98\u65B9\u63A5\u53E3\u53EA\u652F\u6301\u56FE\u7247\u56DE\u8BFB\uFF09\uFF0C\u8BF7\u91CD\u65B0\u9009\u62E9\u6587\u4EF6");
1493
+ if (fileBlocks.length > 0) showThrottledToast(t("toast.fileAttachRefill"));
979
1494
  }
980
1495
  setHasSnapshot(false);
981
1496
  setRecall({ stage: "done", count: typeof res.count === "number" ? res.count : changes.length, chatReverted, chatError });
@@ -997,7 +1512,7 @@
997
1512
  ));
998
1513
  }
999
1514
  for (let i = 0; i < fileBlocks.length; i++) {
1000
- const info = fileCardInfo(fileBlocks[i]);
1515
+ const info = fileCardInfo(fileBlocks[i], t);
1001
1516
  if (!info) continue;
1002
1517
  bubbleChildren.push(React.createElement(
1003
1518
  "div",
@@ -1021,8 +1536,8 @@
1021
1536
  key: "copy",
1022
1537
  type: "button",
1023
1538
  className: "dsh-recall-action",
1024
- "aria-label": copied ? "\u5DF2\u590D\u5236" : "\u590D\u5236",
1025
- title: copied ? "\u5DF2\u590D\u5236" : "\u590D\u5236",
1539
+ "aria-label": copied ? t("action.copied") : t("action.copy"),
1540
+ title: copied ? t("action.copied") : t("action.copy"),
1026
1541
  onClick: onCopy
1027
1542
  }, copied ? React.createElement(CheckIcon, {}) : React.createElement(CopyIcon, {})));
1028
1543
  if (hasSnapshot) {
@@ -1030,8 +1545,8 @@
1030
1545
  key: "recall",
1031
1546
  type: "button",
1032
1547
  className: "dsh-recall-action",
1033
- "aria-label": "\u64A4\u56DE",
1034
- title: "\u6574\u6BB5\u56DE\u9000\uFF1A\u6587\u4EF6\u4E0E\u5BF9\u8BDD\u4E00\u5E76\u56DE\u5230\u8BE5\u6D88\u606F\u4E4B\u524D",
1548
+ "aria-label": t("action.recall"),
1549
+ title: t("action.recall.title"),
1035
1550
  onClick: openPreview
1036
1551
  }, React.createElement(UndoIcon, {})));
1037
1552
  }
@@ -1048,8 +1563,8 @@
1048
1563
 
1049
1564
  // src/client/config-card.ts
1050
1565
  function buildConfigForm(React, util, SectionToggle) {
1051
- const { api, bytesToMb: bytesToMb2 } = util;
1052
- function ConfigForm() {
1566
+ const { api, bytesToMb: bytesToMb2, t, setLocalePref } = util;
1567
+ function ConfigForm(props) {
1053
1568
  const [baseline, setBaseline] = React.useState(null);
1054
1569
  const [draft, setDraft] = React.useState(null);
1055
1570
  const [envLocks, setEnvLocks] = React.useState({});
@@ -1071,15 +1586,18 @@
1071
1586
  refillDraft: v.refillDraft !== false,
1072
1587
  snapshotEnabled: v.snapshotEnabled !== false,
1073
1588
  archiveOriginal: v.archiveOriginal !== false,
1074
- retentionDays: String(v.retentionDays == null ? "" : v.retentionDays)
1589
+ retentionDays: String(v.retentionDays == null ? "" : v.retentionDays),
1590
+ locale: typeof v.locale === "string" && v.locale ? v.locale : "auto"
1075
1591
  };
1592
+ setLocalePref(next.locale);
1593
+ if (props.onLocaleApplied) props.onLocaleApplied();
1076
1594
  setDraft(next);
1077
1595
  setBaseline(next);
1078
1596
  setEnvLocks(res.envLocks || {});
1079
1597
  setOverridden(res.overridden || {});
1080
1598
  setWritable(res.writable !== false);
1081
1599
  } else {
1082
- setState({ busy: false, message: res && (res.message || res.error) || "\u65E0\u6CD5\u8BFB\u53D6\u914D\u7F6E", error: true });
1600
+ setState({ busy: false, message: res && (res.message || res.error) || t("config.msg.loadFailed"), error: true });
1083
1601
  }
1084
1602
  }).catch((e) => setState({ busy: false, message: String(e), error: true }));
1085
1603
  }
@@ -1092,18 +1610,19 @@
1092
1610
  function save() {
1093
1611
  if (state.busy || !draft || !baseline) return;
1094
1612
  const patch = {};
1095
- for (const key of ["gcSnaps", "gcHours", "maxFileBytes", "maxSnapshotsPerWorkspace", "baseExcludes", "refillDraft", "snapshotEnabled", "archiveOriginal", "retentionDays"]) {
1096
- if (draft[key] !== baseline[key]) patch[key] = draft[key];
1613
+ for (const key of ["gcSnaps", "gcHours", "maxFileBytes", "maxSnapshotsPerWorkspace", "baseExcludes", "refillDraft", "snapshotEnabled", "archiveOriginal", "retentionDays", "locale"]) {
1614
+ const value = draft[key];
1615
+ if (value !== void 0 && value !== baseline[key]) patch[key] = value;
1097
1616
  }
1098
1617
  if (!Object.keys(patch).length) {
1099
- setState({ busy: false, message: "\u6CA1\u6709\u4FEE\u6539", error: false });
1618
+ setState({ busy: false, message: t("config.msg.noChange"), error: false });
1100
1619
  return;
1101
1620
  }
1102
1621
  const clean = {};
1103
1622
  if (patch.gcSnaps !== void 0) {
1104
1623
  const n = parseInt(String(patch.gcSnaps), 10);
1105
1624
  if (!Number.isFinite(n) || n < 1) {
1106
- setState({ busy: false, message: "\u5FEB\u7167\u6761\u6570\u9608\u503C\u5FC5\u987B\u662F >= 1 \u7684\u6574\u6570", error: true });
1625
+ setState({ busy: false, message: t("config.err.gcSnaps"), error: true });
1107
1626
  return;
1108
1627
  }
1109
1628
  clean.gcSnaps = n;
@@ -1111,7 +1630,7 @@
1111
1630
  if (patch.gcHours !== void 0) {
1112
1631
  const n = parseInt(String(patch.gcHours), 10);
1113
1632
  if (!Number.isFinite(n) || n < 1) {
1114
- setState({ busy: false, message: "gc \u5C0F\u65F6\u9608\u503C\u5FC5\u987B\u662F >= 1 \u7684\u6574\u6570", error: true });
1633
+ setState({ busy: false, message: t("config.err.gcHours"), error: true });
1115
1634
  return;
1116
1635
  }
1117
1636
  clean.gcHours = n;
@@ -1119,7 +1638,7 @@
1119
1638
  if (patch.maxFileBytes !== void 0) {
1120
1639
  const mb = Number(patch.maxFileBytes);
1121
1640
  if (!Number.isFinite(mb) || mb < 0.01) {
1122
- setState({ busy: false, message: "\u6587\u4EF6\u5927\u5C0F\u4E0A\u9650\u81F3\u5C11 0.01 MB", error: true });
1641
+ setState({ busy: false, message: t("config.err.maxFileBytes"), error: true });
1123
1642
  return;
1124
1643
  }
1125
1644
  clean.maxFileBytes = Math.round(mb * 1048576);
@@ -1127,7 +1646,7 @@
1127
1646
  if (patch.maxSnapshotsPerWorkspace !== void 0) {
1128
1647
  const n = parseInt(String(patch.maxSnapshotsPerWorkspace), 10);
1129
1648
  if (!Number.isFinite(n) || n < 0) {
1130
- setState({ busy: false, message: "\u5FEB\u7167\u603B\u91CF\u4E0A\u9650\u5FC5\u987B\u662F >= 0 \u7684\u6574\u6570\uFF080 \u8868\u793A\u4E0D\u9650\u5236\uFF09", error: true });
1649
+ setState({ busy: false, message: t("config.err.maxSnapshots"), error: true });
1131
1650
  return;
1132
1651
  }
1133
1652
  clean.maxSnapshotsPerWorkspace = n;
@@ -1138,21 +1657,30 @@
1138
1657
  if (patch.retentionDays !== void 0) {
1139
1658
  const n = parseInt(String(patch.retentionDays), 10);
1140
1659
  if (!Number.isFinite(n) || n < 0) {
1141
- setState({ busy: false, message: "\u4FDD\u7559\u5929\u6570\u5FC5\u987B\u662F >= 0 \u7684\u6574\u6570\uFF080 \u8868\u793A\u4E0D\u542F\u7528\uFF09", error: true });
1660
+ setState({ busy: false, message: t("config.err.retentionDays"), error: true });
1142
1661
  return;
1143
1662
  }
1144
1663
  clean.retentionDays = n;
1145
1664
  }
1665
+ if (patch.locale !== void 0) {
1666
+ const v = String(patch.locale);
1667
+ if (v !== "auto" && v !== "zh" && v !== "en") {
1668
+ setState({ busy: false, message: t("config.err.locale"), error: true });
1669
+ return;
1670
+ }
1671
+ clean.locale = v;
1672
+ }
1146
1673
  if (patch.baseExcludes !== void 0) {
1147
1674
  clean.baseExcludes = String(patch.baseExcludes).split("\n").map((l) => l.trim()).filter(Boolean);
1148
1675
  }
1149
- setState({ busy: true, message: "\u4FDD\u5B58\u4E2D\u2026", error: false });
1676
+ setState({ busy: true, message: t("config.msg.saving"), error: false });
1150
1677
  api("config-set", { patch: clean }).then((res) => {
1151
1678
  if (res && res.ok) {
1152
- setState({ busy: false, message: "\u5DF2\u4FDD\u5B58\u5E76\u5373\u65F6\u751F\u6548", error: false });
1679
+ if (clean.locale !== void 0) setLocalePref(clean.locale);
1680
+ setState({ busy: false, message: t("config.msg.saved"), error: false });
1153
1681
  load();
1154
1682
  } else {
1155
- setState({ busy: false, message: res && (res.message || res.error) || "\u4FDD\u5B58\u5931\u8D25", error: true });
1683
+ setState({ busy: false, message: res && (res.message || res.error) || t("config.msg.saveFailed"), error: true });
1156
1684
  }
1157
1685
  }).catch((e) => setState({ busy: false, message: String(e), error: true }));
1158
1686
  }
@@ -1184,10 +1712,10 @@
1184
1712
  step: opts && opts.step,
1185
1713
  onChange: (e) => edit(key, e.target.value)
1186
1714
  }),
1187
- opts && opts.suffix ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, opts.suffix) : null,
1188
- changed && !locked ? React.createElement("span", { className: "dsh-recall-cfg-tag dsh-recall-cfg-tag-modified" }, "\u5DF2\u4FEE\u6539") : null,
1189
- overridden && overridden[key] !== void 0 ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u8986\u76D6") : null,
1190
- locked ? React.createElement("span", { className: "dsh-recall-cfg-tag dsh-recall-cfg-tag-locked" }, "\u73AF\u5883\u53D8\u91CF\u9501\u5B9A") : null
1715
+ opts && opts.suffixKey ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, t(opts.suffixKey)) : null,
1716
+ changed && !locked ? React.createElement("span", { className: "dsh-recall-cfg-tag dsh-recall-cfg-tag-modified" }, t("config.tag.modified")) : null,
1717
+ overridden && overridden[key] !== void 0 ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, t("config.tag.overridden")) : null,
1718
+ locked ? React.createElement("span", { className: "dsh-recall-cfg-tag dsh-recall-cfg-tag-locked" }, t("config.tag.locked")) : null
1191
1719
  ),
1192
1720
  React.createElement("div", { className: "dsh-recall-cfg-hint" }, hint)
1193
1721
  );
@@ -1216,26 +1744,55 @@
1216
1744
  disabled: !writable,
1217
1745
  onClick: () => edit(key, !on)
1218
1746
  }, React.createElement("span", { className: "dsh-recall-cfg-switch-thumb" })),
1219
- changed ? React.createElement("span", { className: "dsh-recall-cfg-tag dsh-recall-cfg-tag-modified" }, "\u5DF2\u4FEE\u6539") : null,
1220
- overridden && overridden[key] !== void 0 ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u8986\u76D6") : null
1747
+ changed ? React.createElement("span", { className: "dsh-recall-cfg-tag dsh-recall-cfg-tag-modified" }, t("config.tag.modified")) : null,
1748
+ overridden && overridden[key] !== void 0 ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, t("config.tag.overridden")) : null
1221
1749
  ),
1222
1750
  React.createElement("div", { className: "dsh-recall-cfg-hint" }, hint)
1223
1751
  );
1224
1752
  }
1753
+ function localeRow() {
1754
+ const changed = Boolean(draft && baseline && draft.locale !== baseline.locale);
1755
+ return React.createElement(
1756
+ "div",
1757
+ { className: "dsh-recall-cfg-row", key: "locale" },
1758
+ React.createElement("span", { className: "dsh-recall-cfg-label", id: "dsh-recall-cfg-label-locale" }, t("config.locale.label")),
1759
+ React.createElement(
1760
+ "div",
1761
+ { className: "dsh-recall-cfg-line" },
1762
+ React.createElement(
1763
+ "select",
1764
+ {
1765
+ id: "dsh-recall-cfg-locale",
1766
+ "aria-labelledby": "dsh-recall-cfg-label-locale",
1767
+ className: "dsh-recall-cfg-select",
1768
+ value: draft ? draft.locale : "auto",
1769
+ disabled: !writable,
1770
+ onChange: (e) => edit("locale", e.target.value)
1771
+ },
1772
+ React.createElement("option", { value: "auto" }, t("config.locale.auto")),
1773
+ React.createElement("option", { value: "zh" }, "\u4E2D\u6587"),
1774
+ React.createElement("option", { value: "en" }, "English")
1775
+ ),
1776
+ changed ? React.createElement("span", { className: "dsh-recall-cfg-tag dsh-recall-cfg-tag-modified" }, t("config.tag.modified")) : null,
1777
+ overridden && overridden.locale !== void 0 ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, t("config.tag.overridden")) : null
1778
+ ),
1779
+ React.createElement("div", { className: "dsh-recall-cfg-hint" }, t("config.locale.hint"))
1780
+ );
1781
+ }
1225
1782
  function resetDefaults() {
1226
1783
  if (state.busy || !writable) return;
1227
- setState({ busy: true, message: "\u6062\u590D\u9ED8\u8BA4\u4E2D\u2026", error: false });
1784
+ setState({ busy: true, message: t("config.msg.resetting"), error: false });
1228
1785
  api("config-reset", {}).then((res) => {
1229
1786
  if (res && res.ok) {
1230
1787
  load();
1231
- setState({ busy: false, message: "\u5DF2\u6062\u590D\u9ED8\u8BA4\u503C", error: false });
1788
+ setState({ busy: false, message: t("config.msg.resetDone"), error: false });
1232
1789
  } else {
1233
- setState({ busy: false, message: res && (res.message || res.error) || "\u6062\u590D\u9ED8\u8BA4\u5931\u8D25", error: true });
1790
+ setState({ busy: false, message: res && (res.message || res.error) || t("config.msg.resetFailed"), error: true });
1234
1791
  }
1235
1792
  }).catch((e) => setState({ busy: false, message: String(e), error: true }));
1236
1793
  }
1237
1794
  if (!draft || !baseline) {
1238
- return React.createElement("div", { className: "dsh-recall-ex-note" }, state.message || "\u6B63\u5728\u8BFB\u53D6\u914D\u7F6E\u2026");
1795
+ return React.createElement("div", { className: "dsh-recall-ex-note" }, state.message || t("config.msg.loading"));
1239
1796
  }
1240
1797
  return React.createElement(
1241
1798
  "div",
@@ -1247,52 +1804,56 @@
1247
1804
  React.createElement(
1248
1805
  "div",
1249
1806
  { className: "dsh-recall-cfg-grid" },
1250
- // V5 表单分组:9 字段平铺 → 「快照行为 / 自动治理」两组语义分组小标题,
1251
- // 降低认知负担;「高级:基础排除表」沿用 SectionToggle 折叠,不重复加标题。
1252
- React.createElement("div", { className: "dsh-recall-cfg-group" }, "\u5FEB\u7167\u884C\u4E3A"),
1253
- boolRow("snapshotEnabled", "\u542F\u7528\u5FEB\u7167", "\u5173\u95ED\u540E\u4E0D\u518D\u65B0\u5EFA\u5FEB\u7167\uFF1B\u5DF2\u6709\u5FEB\u7167\u4ECD\u53EF\u64A4\u56DE"),
1254
- boolRow("refillDraft", "\u56DE\u586B\u8F93\u5165\u6846", "\u64A4\u56DE\u540E\u628A\u6D88\u606F\u6587\u672C\u56DE\u586B\u8F93\u5165\u6846\uFF0C\u4FBF\u4E8E\u6539\u5B8C\u91CD\u53D1"),
1255
- boolRow("archiveOriginal", "\u5F52\u6863\u539F\u4F1A\u8BDD", "\u539F\u4F1A\u8BDD\u5F52\u6863\u9690\u85CF\uFF0C\u53EF\u4ECE\u5F52\u6863\u627E\u56DE\uFF1B\u5173\u95ED\u5219\u7559\u5728\u5217\u8868\u4FBF\u4E8E\u5BF9\u7167"),
1256
- React.createElement("div", { className: "dsh-recall-cfg-group" }, "\u81EA\u52A8\u6CBB\u7406"),
1257
- // 单位后缀统一挂输入框右侧(与状态标签同基线),不再只藏在说明文字里
1258
- numRow("gcSnaps", "gc \u89E6\u53D1\u6761\u6570", "\u6BCF\u79EF\u7D2F\u591A\u5C11\u6761\u5FEB\u7167\u89E6\u53D1\u4E00\u6B21 gc", { suffix: "\u6761", min: 1, step: 1 }),
1259
- numRow("gcHours", "gc \u89E6\u53D1\u5C0F\u65F6", "\u8DDD\u4E0A\u6B21 gc \u8D85\u8FC7\u591A\u5C11\u5C0F\u65F6\u89E6\u53D1\uFF0C\u4E0E\u6761\u6570\u5148\u5230\u5148\u89E6\u53D1", { suffix: "\u5C0F\u65F6", min: 1, step: 1 }),
1260
- numRow("maxFileBytes", "\u6587\u4EF6\u5927\u5C0F\u4E0A\u9650", "\u8D85\u8FC7\u8BE5\u5927\u5C0F\u7684\u6587\u4EF6\u4E0D\u8FDB\u5FEB\u7167\u3001\u4E0D\u88AB\u56DE\u9000\u89E6\u78B0", { suffix: "MB", min: 0.01, step: 0.5 }),
1261
- numRow("maxSnapshotsPerWorkspace", "\u5FEB\u7167\u603B\u91CF\u4E0A\u9650", "\u6BCF\u5DE5\u4F5C\u533A\u4FDD\u7559\u7684\u6700\u5927\u5FEB\u7167\u6570\uFF0C\u8D85\u9650\u5220\u9664\u6700\u65E7\u7684\uFF1B0 \u8868\u793A\u4E0D\u9650\u5236", { suffix: "\u6761", min: 0, step: 1 }),
1262
- numRow("retentionDays", "\u5FEB\u7167\u4FDD\u7559\u5929\u6570", "\u8D85\u671F\u5FEB\u7167\u81EA\u52A8\u5220\u9664\u6700\u65E7\u7684\uFF1B0 \u8868\u793A\u4E0D\u542F\u7528", { suffix: "\u5929", min: 0, step: 1 }),
1807
+ // V5 表单分组:9 字段平铺 → 「快照行为 / 自动治理 / 界面」三组语义分组
1808
+ // 小标题,降低认知负担;「高级:基础排除表」沿用 SectionToggle 折叠,
1809
+ // 不重复加标题。
1810
+ React.createElement("div", { className: "dsh-recall-cfg-group" }, t("config.group.snapshot")),
1811
+ boolRow("snapshotEnabled", t("config.snapshotEnabled.label"), t("config.snapshotEnabled.hint")),
1812
+ boolRow("refillDraft", t("config.refillDraft.label"), t("config.refillDraft.hint")),
1813
+ boolRow("archiveOriginal", t("config.archiveOriginal.label"), t("config.archiveOriginal.hint")),
1814
+ React.createElement("div", { className: "dsh-recall-cfg-group" }, t("config.group.auto")),
1815
+ // 单位后缀统一挂输入框右侧(与状态标签同基线),不再只藏在说明文字里;
1816
+ // 传词表键而非字面量——单位词也是语言事实(条/小时 vs snapshots/hours)
1817
+ numRow("gcSnaps", t("config.gcSnaps.label"), t("config.gcSnaps.hint"), { suffixKey: "config.gcSnaps.suffix", min: 1, step: 1 }),
1818
+ numRow("gcHours", t("config.gcHours.label"), t("config.gcHours.hint"), { suffixKey: "config.gcHours.suffix", min: 1, step: 1 }),
1819
+ numRow("maxFileBytes", t("config.maxFileBytes.label"), t("config.maxFileBytes.hint"), { suffixKey: "config.maxFileBytes.suffix", min: 0.01, step: 0.5 }),
1820
+ numRow("maxSnapshotsPerWorkspace", t("config.maxSnapshotsPerWorkspace.label"), t("config.maxSnapshotsPerWorkspace.hint"), { suffixKey: "config.maxSnapshotsPerWorkspace.suffix", min: 0, step: 1 }),
1821
+ numRow("retentionDays", t("config.retentionDays.label"), t("config.retentionDays.hint"), { suffixKey: "config.retentionDays.suffix", min: 0, step: 1 }),
1822
+ React.createElement("div", { className: "dsh-recall-cfg-group" }, t("config.group.ui")),
1823
+ localeRow(),
1263
1824
  // 操作区在「基础排除表」折叠头之前:按钮服务整个表单(含折叠区之外的字段),
1264
1825
  // 排在折叠头之后会被误读为折叠区内容、折叠时像漏收起(用户实测反馈);
1265
1826
  // 因此也不把按钮藏进折叠分支——否则折叠基础排除表后将无法保存。
1266
1827
  React.createElement(
1267
1828
  "div",
1268
1829
  { className: "dsh-recall-panel-actions" },
1269
- state.message ? React.createElement("span", { role: "status", "aria-live": "polite", className: "dsh-recall-ex-status" + (state.error ? " dsh-recall-ex-status-error" : " dsh-recall-ex-status-success") }, (state.error ? "\u9519\u8BEF\uFF1A" : "") + state.message) : null,
1270
- React.createElement("button", { type: "button", className: "dsh-recall-btn", disabled: state.busy || !writable, onClick: () => setDraft(baseline ? Object.assign({}, baseline) : null) }, "\u653E\u5F03\u4FEE\u6539"),
1830
+ state.message ? React.createElement("span", { role: "status", "aria-live": "polite", className: "dsh-recall-ex-status" + (state.error ? " dsh-recall-ex-status-error" : " dsh-recall-ex-status-success") }, (state.error ? t("common.errorPrefix") : "") + state.message) : null,
1831
+ React.createElement("button", { type: "button", className: "dsh-recall-btn", disabled: state.busy || !writable, onClick: () => setDraft(baseline ? Object.assign({}, baseline) : null) }, t("common.discard")),
1271
1832
  React.createElement("button", {
1272
1833
  type: "button",
1273
1834
  className: "dsh-recall-btn",
1274
1835
  disabled: state.busy || !writable,
1275
- title: "\u628A\u6240\u6709\u5B57\u6BB5\u6062\u590D\u5230\u63D2\u4EF6\u51FA\u5382\u9ED8\u8BA4\u503C",
1836
+ title: t("config.reset.title"),
1276
1837
  onClick: resetDefaults
1277
- }, "\u6062\u590D\u9ED8\u8BA4"),
1838
+ }, t("config.reset")),
1278
1839
  // 「保存」是表单唯一主动作,升主色实心按钮;放弃修改/恢复默认维持次级
1279
1840
  // 灰底(同排按钮只有一个视觉焦点,配色令牌经官方主题产物核验,见 css.ts)
1280
- React.createElement("button", { type: "button", className: "dsh-recall-btn dsh-recall-btn-primary", disabled: state.busy || !writable, onClick: save }, "\u4FDD\u5B58"),
1281
- !writable ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u53EA\u8BFB\u8BBE\u7F6E\u6E90") : null
1841
+ React.createElement("button", { type: "button", className: "dsh-recall-btn dsh-recall-btn-primary", disabled: state.busy || !writable, onClick: save }, t("common.save")),
1842
+ !writable ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, t("config.tag.readonly")) : null
1282
1843
  ),
1283
1844
  // divider:本条是折叠分区列表的首项,上方分界线把「字段表单」与「折叠
1284
1845
  // 分区」分开(否则保存按钮行紧贴折叠头,读起来像同一组字段)
1285
- React.createElement(SectionToggle, { title: "\u9AD8\u7EA7\uFF1A\u57FA\u7840\u6392\u9664\u8868", open: showAdvanced, onToggle: () => setShowAdvanced((v) => !v), divider: true }),
1846
+ React.createElement(SectionToggle, { title: t("config.advanced.title"), open: showAdvanced, onToggle: () => setShowAdvanced((v) => !v), divider: true }),
1286
1847
  showAdvanced ? React.createElement(
1287
1848
  "div",
1288
1849
  { className: "dsh-recall-cfg-row", key: "baseExcludes" },
1289
1850
  // V4:标签上提(与 numRow 同法);cfg-line 只剩 tags,textarea/hint 通栏
1290
- React.createElement("span", { className: "dsh-recall-cfg-label", id: "dsh-recall-cfg-label-baseExcludes" }, "\u57FA\u7840\u6392\u9664\u8868"),
1851
+ React.createElement("span", { className: "dsh-recall-cfg-label", id: "dsh-recall-cfg-label-baseExcludes" }, t("config.baseExcludes.label")),
1291
1852
  React.createElement(
1292
1853
  "div",
1293
1854
  { className: "dsh-recall-cfg-line" },
1294
- draft.baseExcludes !== baseline.baseExcludes ? React.createElement("span", { className: "dsh-recall-cfg-tag dsh-recall-cfg-tag-modified" }, "\u5DF2\u4FEE\u6539") : null,
1295
- overridden && overridden.baseExcludes !== void 0 ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u8986\u76D6") : null
1855
+ draft.baseExcludes !== baseline.baseExcludes ? React.createElement("span", { className: "dsh-recall-cfg-tag dsh-recall-cfg-tag-modified" }, t("config.tag.modified")) : null,
1856
+ overridden && overridden.baseExcludes !== void 0 ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, t("config.tag.overridden")) : null
1296
1857
  ),
1297
1858
  // textarea/hint 加 cfg-span 通栏:折叠区在共享 grid 内展开时,内容若只占
1298
1859
  // 第二列,左侧标签列会成为竖直死区(实测 textarea 被挤窄);通栏后与
@@ -1307,7 +1868,7 @@
1307
1868
  disabled: !writable,
1308
1869
  onChange: (e) => edit("baseExcludes", e.target.value)
1309
1870
  }),
1310
- React.createElement("div", { className: "dsh-recall-cfg-hint dsh-recall-cfg-span" }, "\u5404\u5DE5\u4F5C\u533A\u5171\u4EAB\u7684\u5185\u7F6E\u89C4\u5219\uFF1Bgitignore \u8BED\u6CD5\uFF0C\u6BCF\u884C\u4E00\u6761\uFF1B\u4F18\u5148\u7EA7\u4F4E\u4E8E\u300C\u6392\u9664\u914D\u7F6E\u300D\u7684 exclude.txt")
1871
+ React.createElement("div", { className: "dsh-recall-cfg-hint dsh-recall-cfg-span" }, t("config.baseExcludes.hint"))
1311
1872
  ) : null
1312
1873
  )
1313
1874
  );
@@ -1318,7 +1879,7 @@
1318
1879
  // src/client/exclude-card.ts
1319
1880
  var EXCLUDE_SUGGESTIONS = ["dist/", "build/", "out/", "coverage/", "*.log", ".env"];
1320
1881
  function buildExcludeCards(React, util) {
1321
- const { api } = util;
1882
+ const { api, t } = util;
1322
1883
  function ExcludeCard(props) {
1323
1884
  const file = props.file;
1324
1885
  const [draft, setDraft] = React.useState(file.content || "");
@@ -1331,20 +1892,20 @@
1331
1892
  setDraft((d) => (d && !d.endsWith("\n") ? d + "\n" : d) + pattern + "\n");
1332
1893
  }
1333
1894
  function addQuick() {
1334
- const t = quick.trim();
1335
- if (!t) return;
1336
- appendPattern(t);
1895
+ const pattern = quick.trim();
1896
+ if (!pattern) return;
1897
+ appendPattern(pattern);
1337
1898
  setQuick("");
1338
1899
  }
1339
1900
  function save() {
1340
1901
  if (state.busy || !dirty) return;
1341
- setState({ busy: true, message: "\u4FDD\u5B58\u4E2D\u2026", error: false });
1902
+ setState({ busy: true, message: t("exclude.msg.saving"), error: false });
1342
1903
  api("exclude-set", { path: file.path, content: draft }).then((res) => {
1343
1904
  if (res && res.ok) {
1344
1905
  setBaseline(draft);
1345
- setState({ busy: false, message: "\u5DF2\u4FDD\u5B58\uFF0C\u4E0B\u4E00\u6B21\u5FEB\u7167 / \u9884\u89C8 / \u56DE\u9000\u65F6\u751F\u6548", error: false });
1906
+ setState({ busy: false, message: t("exclude.msg.saved"), error: false });
1346
1907
  } else {
1347
- setState({ busy: false, message: res && (res.message || res.error) || "\u4FDD\u5B58\u5931\u8D25", error: true });
1908
+ setState({ busy: false, message: res && (res.message || res.error) || t("exclude.msg.saveFailed"), error: true });
1348
1909
  }
1349
1910
  }).catch((error) => {
1350
1911
  setState({ busy: false, message: String(error), error: true });
@@ -1367,13 +1928,13 @@
1367
1928
  React.createElement(
1368
1929
  "div",
1369
1930
  { className: "dsh-recall-ex-note" },
1370
- file.home ? "\u6B64\u914D\u7F6E\u5168\u5C40\u5171\u4EAB\uFF0C\u5BF9\u6240\u6709\u5DE5\u4F5C\u533A\u7684\u5FEB\u7167\u751F\u6548\u3002" : "home \u76EE\u5F55\u4E0D\u53EF\u5199\u65F6\u6B64\u5DE5\u4F5C\u533A\u964D\u7EA7\u5B58\u50A8\uFF0C\u6392\u9664\u914D\u7F6E\u72EC\u7ACB\u751F\u6548\u3002"
1931
+ file.home ? t("exclude.note.home") : t("exclude.note.fallback")
1371
1932
  ),
1372
- React.createElement("div", { className: "dsh-recall-ex-path" }, "\u5B58\u50A8\u4F4D\u7F6E\uFF1A" + file.path),
1373
- React.createElement("div", { className: "dsh-recall-ex-note" }, "gitignore \u8BED\u6CD5\uFF0C\u4E00\u884C\u4E00\u6761\uFF0C\u652F\u6301 # \u6CE8\u91CA\uFF1B\u547D\u4E2D\u9879\u4E0D\u8FDB\u5FEB\u7167\u3001\u4E0D\u88AB\u56DE\u9000\u89E6\u78B0\u3002"),
1933
+ React.createElement("div", { className: "dsh-recall-ex-path" }, t("exclude.path", { path: file.path })),
1934
+ React.createElement("div", { className: "dsh-recall-ex-note" }, t("exclude.syntax")),
1374
1935
  React.createElement("textarea", {
1375
1936
  className: "dsh-recall-ex-area",
1376
- "aria-label": "\u5FEB\u7167\u6392\u9664\u6A21\u5F0F\u5217\u8868\uFF08gitignore \u8BED\u6CD5\uFF0C\u4E00\u884C\u4E00\u6761\uFF09",
1937
+ "aria-label": t("exclude.area.aria"),
1377
1938
  value: draft,
1378
1939
  spellCheck: false,
1379
1940
  onChange: (e) => setDraft(e.target.value)
@@ -1384,8 +1945,8 @@
1384
1945
  React.createElement("input", {
1385
1946
  className: "dsh-recall-ex-input",
1386
1947
  value: quick,
1387
- placeholder: "\u8F93\u5165\u8DEF\u5F84\u6216\u6A21\u5F0F\uFF0C\u56DE\u8F66\u5FEB\u901F\u6DFB\u52A0",
1388
- "aria-label": "\u5FEB\u901F\u6DFB\u52A0\u6392\u9664\u6A21\u5F0F",
1948
+ placeholder: t("exclude.quick.placeholder"),
1949
+ "aria-label": t("exclude.quick.aria"),
1389
1950
  onChange: (e) => setQuick(e.target.value),
1390
1951
  onKeyDown: (e) => {
1391
1952
  if (e.key === "Enter") {
@@ -1394,23 +1955,23 @@
1394
1955
  }
1395
1956
  }
1396
1957
  }),
1397
- React.createElement("button", { type: "button", className: "dsh-recall-btn", onClick: addQuick }, "\u6DFB\u52A0"),
1958
+ React.createElement("button", { type: "button", className: "dsh-recall-btn", onClick: addQuick }, t("exclude.add")),
1398
1959
  ...suggestions.map((s) => React.createElement("button", {
1399
1960
  key: "chip-" + s,
1400
1961
  type: "button",
1401
1962
  className: "dsh-recall-ex-chip",
1402
- title: "\u70B9\u51FB\u8FFD\u52A0 " + s,
1963
+ title: t("exclude.chip.title", { pattern: s }),
1403
1964
  onClick: () => appendPattern(s)
1404
1965
  }, s))
1405
1966
  ),
1406
1967
  React.createElement(
1407
1968
  "div",
1408
1969
  { className: "dsh-recall-panel-actions" },
1409
- state.message ? React.createElement("span", { role: "status", "aria-live": "polite", className: "dsh-recall-ex-status" + (state.error ? " dsh-recall-ex-status-error" : " dsh-recall-ex-status-success") }, (state.error ? "\u9519\u8BEF\uFF1A" : "") + state.message) : null,
1410
- React.createElement("button", { type: "button", className: "dsh-recall-btn", disabled: !dirty || state.busy, onClick: discard }, "\u653E\u5F03\u4FEE\u6539"),
1970
+ state.message ? React.createElement("span", { role: "status", "aria-live": "polite", className: "dsh-recall-ex-status" + (state.error ? " dsh-recall-ex-status-error" : " dsh-recall-ex-status-success") }, (state.error ? t("common.errorPrefix") : "") + state.message) : null,
1971
+ React.createElement("button", { type: "button", className: "dsh-recall-btn", disabled: !dirty || state.busy, onClick: discard }, t("common.discard")),
1411
1972
  // 保存升主色实心(与配置表单同一约定:每卡唯一主动作,对齐官方插件卡
1412
1973
  // footer 的 discard 幽灵 + save 实心组合)
1413
- React.createElement("button", { type: "button", className: "dsh-recall-btn dsh-recall-btn-primary", disabled: !dirty || state.busy, onClick: save }, "\u4FDD\u5B58")
1974
+ React.createElement("button", { type: "button", className: "dsh-recall-btn dsh-recall-btn-primary", disabled: !dirty || state.busy, onClick: save }, t("common.save"))
1414
1975
  )
1415
1976
  );
1416
1977
  }
@@ -1425,10 +1986,10 @@
1425
1986
  return;
1426
1987
  }
1427
1988
  if (res && res.unsupported) {
1428
- setError("\u5F53\u524D\u5E73\u53F0\u4E0D\u652F\u6301\u5FEB\u7167\u529F\u80FD\uFF0C\u6392\u9664\u914D\u7F6E\u4E0D\u53EF\u7528\u3002");
1989
+ setError(t("exclude.unsupported"));
1429
1990
  return;
1430
1991
  }
1431
- setError(res && (res.message || res.error) || "\u65E0\u6CD5\u8BFB\u53D6\u6392\u9664\u914D\u7F6E");
1992
+ setError(res && (res.message || res.error) || t("exclude.loadFailed"));
1432
1993
  }).catch((e) => setError(String(e)));
1433
1994
  }
1434
1995
  React.useEffect(() => {
@@ -1442,15 +2003,15 @@
1442
2003
  React.createElement(
1443
2004
  "div",
1444
2005
  { className: "dsh-recall-panel-actions" },
1445
- React.createElement("button", { type: "button", className: "dsh-recall-btn", onClick: load }, "\u91CD\u8BD5")
2006
+ React.createElement("button", { type: "button", className: "dsh-recall-btn", onClick: load }, t("common.retry"))
1446
2007
  )
1447
2008
  );
1448
2009
  }
1449
2010
  if (files === null) {
1450
- return React.createElement("div", { className: "dsh-recall-ex-note" }, "\u6B63\u5728\u52A0\u8F7D\u6392\u9664\u914D\u7F6E\u2026");
2011
+ return React.createElement("div", { className: "dsh-recall-ex-note" }, t("exclude.loading"));
1451
2012
  }
1452
2013
  if (!files.length) {
1453
- return React.createElement("div", { className: "dsh-recall-ex-note" }, "\u5C1A\u672A\u521B\u5EFA\u4EFB\u4F55\u5FEB\u7167\u5B58\u50A8\uFF1A\u5728\u4EFB\u610F\u5DE5\u4F5C\u533A\u53D1\u9001\u4E00\u6761\u6D88\u606F\u540E\uFF0C\u8FD9\u91CC\u4F1A\u51FA\u73B0\u53EF\u7F16\u8F91\u7684\u6392\u9664\u914D\u7F6E\u3002");
2014
+ return React.createElement("div", { className: "dsh-recall-ex-note" }, t("exclude.empty"));
1454
2015
  }
1455
2016
  return React.createElement(
1456
2017
  "div",
@@ -1502,7 +2063,7 @@
1502
2063
  return result;
1503
2064
  }
1504
2065
  function buildSnapshotManager(React, util, sessionsSvc, workspacesSvc, uiWorkspaceSvc) {
1505
- const { api, clockText: clockText2, sizeText: sizeText2, buildTree: buildTree2 } = util;
2066
+ const { api, clockText: clockText2, sizeText: sizeText2, buildTree: buildTree2, t } = util;
1506
2067
  function DeleteButton(props) {
1507
2068
  return React.createElement("button", {
1508
2069
  type: "button",
@@ -1637,16 +2198,16 @@
1637
2198
  api("status", { op: "clear" }).catch(() => {
1638
2199
  });
1639
2200
  }
1640
- function run(op, extra, doneText) {
2201
+ function run(op, extra, doneKey) {
1641
2202
  if (state.busy) return;
1642
- setState({ busy: true, message: "\u6267\u884C\u4E2D\u2026", error: false });
2203
+ setState({ busy: true, message: t("manage.busy"), error: false });
1643
2204
  api("manage", Object.assign({ op }, extra || {})).then((res) => {
1644
2205
  if (res && res.ok) {
1645
2206
  const deleted = res.deleted;
1646
- setState({ busy: false, message: typeof deleted === "number" ? "\u5DF2\u5220\u9664 " + deleted + " \u6761\u5FEB\u7167" : doneText, error: false });
2207
+ setState({ busy: false, message: typeof deleted === "number" ? t("manage.deletedCount", { deleted }) : t(doneKey), error: false });
1647
2208
  refresh();
1648
2209
  } else {
1649
- setState({ busy: false, message: res && (res.message || res.error) || "\u64CD\u4F5C\u5931\u8D25", error: true });
2210
+ setState({ busy: false, message: res && (res.message || res.error) || t("common.opFailed"), error: true });
1650
2211
  }
1651
2212
  }).catch((e) => setState({ busy: false, message: String(e), error: true }));
1652
2213
  }
@@ -1657,17 +2218,17 @@
1657
2218
  "div",
1658
2219
  { className: "dsh-recall-tree-confirm" },
1659
2220
  props.text,
1660
- React.createElement("button", { type: "button", className: "dsh-recall-ex-chip dsh-recall-ex-chip-danger", onClick: props.onConfirm }, "\u786E\u8BA4"),
1661
- React.createElement("button", { type: "button", className: "dsh-recall-ex-chip", onClick: props.onCancel }, "\u53D6\u6D88")
2221
+ React.createElement("button", { type: "button", className: "dsh-recall-ex-chip dsh-recall-ex-chip-danger", onClick: props.onConfirm }, t("common.confirm")),
2222
+ React.createElement("button", { type: "button", className: "dsh-recall-ex-chip", onClick: props.onCancel }, t("common.cancel"))
1662
2223
  );
1663
2224
  }
1664
2225
  function renderDeleteAllConfirm() {
1665
2226
  if (!confirming || confirming.kind !== "all") return null;
1666
2227
  return React.createElement(ConfirmRow, {
1667
- text: "\u786E\u8BA4\u5220\u9664\u6240\u6709\u5DE5\u4F5C\u533A\u7684\u5168\u90E8\u5FEB\u7167\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002",
2228
+ text: t("manage.confirm.all"),
1668
2229
  onConfirm: () => {
1669
2230
  setConfirming(null);
1670
- run("deleteAll", {}, "\u5DF2\u6E05\u7A7A\u5168\u90E8\u5FEB\u7167");
2231
+ run("deleteAll", {}, "manage.done.deleteAll");
1671
2232
  },
1672
2233
  onCancel: () => setConfirming(null)
1673
2234
  });
@@ -1684,7 +2245,7 @@
1684
2245
  const filteredItems = q ? (items || []).filter(
1685
2246
  (it) => (it.workspace || "").toLowerCase().indexOf(q) >= 0 || (it.sessionTitle || "").toLowerCase().indexOf(q) >= 0 || (it.messageText || "").toLowerCase().indexOf(q) >= 0 || String(it.id || "").toLowerCase().indexOf(q) >= 0
1686
2247
  ) : items;
1687
- const tree = buildTree2(filteredItems);
2248
+ const tree = buildTree2(filteredItems, t);
1688
2249
  const allSessionIds = Array.from(new Set((items || []).map((it) => it.sessionId).filter(Boolean)));
1689
2250
  const versionMap = groupByLineage(allSessionIds, lineage);
1690
2251
  let listById = null;
@@ -1703,17 +2264,17 @@
1703
2264
  } catch (e) {
1704
2265
  archivedIds = null;
1705
2266
  }
1706
- function confirmDelete(kind, key, extra, text) {
1707
- setConfirming({ kind, key, extra, text });
2267
+ function confirmDelete(kind, key, extra, textKey) {
2268
+ setConfirming({ kind, key, extra, text: textKey });
1708
2269
  }
1709
- function renderConfirm(kind, key, extra, text) {
2270
+ function renderConfirm(kind, key, extra, textKey) {
1710
2271
  if (!confirming || confirming.kind !== kind || confirming.key !== key) return null;
1711
2272
  return React.createElement(ConfirmRow, {
1712
- text,
2273
+ text: t(textKey),
1713
2274
  onConfirm: () => {
1714
2275
  const c = confirming;
1715
2276
  setConfirming(null);
1716
- run("delete", c.extra, "\u5DF2\u5220\u9664");
2277
+ run("delete", c.extra, "manage.done.delete");
1717
2278
  },
1718
2279
  onCancel: () => setConfirming(null)
1719
2280
  });
@@ -1737,18 +2298,18 @@
1737
2298
  // 删除紧贴本条消息摘要(而非行尾):按行扫读时动作落在「看的那一行」,
1738
2299
  // 不必横跳到右侧同列按钮再回读行号
1739
2300
  React.createElement(DeleteButton, {
1740
- title: "\u5220\u9664\u8BE5\u5FEB\u7167\uFF08tag \u4E0E\u7D22\u5F15\u6761\u76EE\uFF09",
1741
- onClick: () => confirmDelete("snapshot", key, { messageId: it.id, root: it.root || null }, "\u786E\u8BA4\u5220\u9664\u8BE5\u5FEB\u7167\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002")
2301
+ title: t("tree.delete.snapshot.title"),
2302
+ onClick: () => confirmDelete("snapshot", key, { messageId: it.id, root: it.root || null }, "tree.delete.snapshot.confirm")
1742
2303
  })
1743
2304
  )
1744
2305
  ),
1745
- renderConfirm("snapshot", key, { messageId: it.id, root: it.root || null }, "\u786E\u8BA4\u5220\u9664\u8BE5\u5FEB\u7167\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002")
2306
+ renderConfirm("snapshot", key, { messageId: it.id, root: it.root || null }, "tree.delete.snapshot.confirm")
1746
2307
  );
1747
2308
  }
1748
2309
  function renderSession(s) {
1749
2310
  const key = "session-" + (s.root || "") + "-" + s.sessionId;
1750
2311
  const open = expanded.has(key);
1751
- const label = s.title || (titlesPending && s.sessionId ? "\u2026" : "\uFF08\u5DF2\u5220\u9664\u4F1A\u8BDD\uFF09");
2312
+ const label = s.title || (titlesPending && s.sessionId ? "\u2026" : t("tree.deletedSession"));
1752
2313
  const version = s.sessionId ? versionMap.get(String(s.sessionId)) : null;
1753
2314
  const switchable = Boolean(s.sessionId && listById && listById[s.sessionId] && !(archivedIds && archivedIds.has(String(s.sessionId))));
1754
2315
  return React.createElement(
@@ -1766,7 +2327,7 @@
1766
2327
  type: "button",
1767
2328
  className: "dsh-recall-tree-toggle",
1768
2329
  "aria-expanded": open,
1769
- "aria-label": (open ? "\u6536\u8D77" : "\u5C55\u5F00") + "\uFF1A" + label,
2330
+ "aria-label": t(open ? "tree.collapse" : "tree.expand", { label }),
1770
2331
  onClick: (e) => {
1771
2332
  e.stopPropagation();
1772
2333
  toggle(key);
@@ -1776,11 +2337,11 @@
1776
2337
  "span",
1777
2338
  { className: "dsh-recall-tree-label", title: s.sessionId || "" },
1778
2339
  React.createElement("span", { className: "dsh-recall-tree-title" }, label),
1779
- version ? React.createElement("span", { className: "dsh-recall-tree-meta", title: "\u7248\u672C\u5BB6\u65CF\uFF1A" + version.family.join(" \u2192 ") }, "v" + version.index + "/" + version.family.length) : null,
1780
- React.createElement("span", { className: "dsh-recall-tree-meta" }, s.items.length + " \u6761"),
2340
+ version ? React.createElement("span", { className: "dsh-recall-tree-meta", title: t("tree.family.title", { chain: version.family.join(" \u2192 ") }) }, "v" + version.index + "/" + version.family.length) : null,
2341
+ React.createElement("span", { className: "dsh-recall-tree-meta" }, t("tree.snapCount", { n: s.items.length })),
1781
2342
  s.sessionId ? React.createElement(DeleteButton, {
1782
- title: "\u5220\u9664\u8BE5\u4F1A\u8BDD\u5168\u90E8\u5FEB\u7167",
1783
- onClick: () => confirmDelete("session", key, { scope: "session", sessionId: s.sessionId, root: s.root || null }, "\u786E\u8BA4\u5220\u9664\u8BE5\u4F1A\u8BDD\u5168\u90E8\u5FEB\u7167\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002")
2343
+ title: t("tree.delete.session.title"),
2344
+ onClick: () => confirmDelete("session", key, { scope: "session", sessionId: s.sessionId, root: s.root || null }, "tree.delete.session.confirm")
1784
2345
  }) : null
1785
2346
  ),
1786
2347
  // 「切换」留在行尾单占右侧:它是导航动作(离开当前视图),与行内
@@ -1788,7 +2349,7 @@
1788
2349
  switchable ? React.createElement("button", {
1789
2350
  type: "button",
1790
2351
  className: "dsh-recall-ex-chip",
1791
- title: "\u5207\u6362\u5230\u8BE5\u7248\u672C\u4F1A\u8BDD",
2352
+ title: t("tree.switch.title"),
1792
2353
  onClick: (ev) => {
1793
2354
  ev.stopPropagation();
1794
2355
  try {
@@ -1797,10 +2358,10 @@
1797
2358
  } catch (e) {
1798
2359
  }
1799
2360
  }
1800
- }, "\u5207\u6362") : null
2361
+ }, t("tree.switch")) : null
1801
2362
  ),
1802
2363
  open ? React.createElement("div", { className: "dsh-recall-tree-children" }, ...s.items.map(renderLeaf)) : null,
1803
- s.sessionId ? renderConfirm("session", key, { scope: "session", sessionId: s.sessionId, root: s.root || null }, "\u786E\u8BA4\u5220\u9664\u8BE5\u4F1A\u8BDD\u5168\u90E8\u5FEB\u7167\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002") : null
2364
+ s.sessionId ? renderConfirm("session", key, { scope: "session", sessionId: s.sessionId, root: s.root || null }, "tree.delete.session.confirm") : null
1804
2365
  );
1805
2366
  }
1806
2367
  function renderWorkspace(ws) {
@@ -1820,7 +2381,7 @@
1820
2381
  type: "button",
1821
2382
  className: "dsh-recall-tree-toggle",
1822
2383
  "aria-expanded": open,
1823
- "aria-label": (open ? "\u6536\u8D77" : "\u5C55\u5F00") + "\uFF1A" + ws.name,
2384
+ "aria-label": t(open ? "tree.collapse" : "tree.expand", { label: ws.name }),
1824
2385
  onClick: (e) => {
1825
2386
  e.stopPropagation();
1826
2387
  toggle(key);
@@ -1830,20 +2391,26 @@
1830
2391
  "span",
1831
2392
  { className: "dsh-recall-tree-label", title: ws.root || "" },
1832
2393
  React.createElement("span", { className: "dsh-recall-tree-name" }, ws.name),
1833
- React.createElement("span", { className: "dsh-recall-tree-meta" }, sessionCount + " \u4F1A\u8BDD / " + snapCount + " \u5FEB\u7167"),
2394
+ React.createElement("span", { className: "dsh-recall-tree-meta" }, t("tree.wsCount", { n: sessionCount, m: snapCount })),
1834
2395
  ws.root ? React.createElement(DeleteButton, {
1835
- title: "\u5220\u9664\u8BE5\u5DE5\u4F5C\u533A\u5168\u90E8\u5FEB\u7167",
1836
- onClick: () => confirmDelete("workspace", key, { scope: "workspace", root: ws.root }, "\u786E\u8BA4\u5220\u9664\u8BE5\u5DE5\u4F5C\u533A\u5168\u90E8\u5FEB\u7167\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002")
2396
+ title: t("tree.delete.workspace.title"),
2397
+ onClick: () => confirmDelete("workspace", key, { scope: "workspace", root: ws.root }, "tree.delete.workspace.confirm")
1837
2398
  }) : null
1838
2399
  )
1839
2400
  ),
1840
2401
  open ? React.createElement("div", { className: "dsh-recall-tree-children" }, ...ws.sessions.map(renderSession)) : null,
1841
- ws.root ? renderConfirm("workspace", key, { scope: "workspace", root: ws.root }, "\u786E\u8BA4\u5220\u9664\u8BE5\u5DE5\u4F5C\u533A\u5168\u90E8\u5FEB\u7167\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002") : null
2402
+ ws.root ? renderConfirm("workspace", key, { scope: "workspace", root: ws.root }, "tree.delete.workspace.confirm") : null
1842
2403
  );
1843
2404
  }
1844
2405
  const treeNodes = tree.map(renderWorkspace);
1845
2406
  const loaded = items ? items.length : null;
1846
- const countText = loaded === null ? "\u5171 \u2026 \u6761\u5FEB\u7167" : "\u5171 " + total + " \u6761\u5FEB\u7167" + (limit < total ? "\uFF08\u5F53\u524D\u663E\u793A\u6700\u65B0 " + loaded + " \u6761\uFF09" : "");
2407
+ const countText = loaded === null ? t("manage.countLoading") : t("manage.countLoaded", { total }) + (limit < total ? t("manage.countShown", { shown: loaded }) : "");
2408
+ function errorLine(e, key) {
2409
+ const kindKey = e.kind ? "errorKind." + e.kind : "";
2410
+ const localized = kindKey !== "" && hasTranslation(kindKey);
2411
+ const base = localized ? t(kindKey) + (typeof e.count === "number" && e.count > 1 ? t("manage.errors.dup", { n: e.count }) : "") : String(e.hint || e.message || "");
2412
+ return React.createElement("div", { className: "dsh-recall-ex-note", key, title: e.message || "" }, clockText2(e.time) + " " + base);
2413
+ }
1847
2414
  function loadMore() {
1848
2415
  const next = Math.min(Math.max(total, limit), 2e3);
1849
2416
  if (next <= limit) return;
@@ -1859,7 +2426,7 @@
1859
2426
  React.createElement(
1860
2427
  "div",
1861
2428
  { className: "dsh-recall-ex-note" },
1862
- usage === null ? countText + "\u3002" : countText + "\uFF0C\u5168\u90E8\u5DE5\u4F5C\u533A\u5FEB\u7167\u5B58\u50A8\u5360\u7528 " + sizeText2(usage) + "\u3002"
2429
+ usage === null ? countText + t("manage.countEnd") : countText + t("manage.usageSuffix", { size: sizeText2(usage) })
1863
2430
  ),
1864
2431
  // V6 健康行徽章化:git 状态用彩色 pill(成功/失败,官方状态行配对),
1865
2432
  // 从普通 note 提升为卡片顶部横幅(渲染在搜索框与树之前);存储计数
@@ -1869,9 +2436,9 @@
1869
2436
  { className: "dsh-recall-ex-note", key: "health" },
1870
2437
  React.createElement("span", {
1871
2438
  className: "dsh-recall-health-pill " + (health.gitAvailable ? "dsh-recall-health-pill-ok" : "dsh-recall-health-pill-bad"),
1872
- title: "\u5FEB\u7167\u5F15\u64CE\u4F9D\u8D56 git"
1873
- }, health.gitAvailable ? "git \u53EF\u7528" : "git \u4E0D\u53EF\u7528\uFF08\u5FEB\u7167\u5F15\u64CE\u4F9D\u8D56 git\uFF09"),
1874
- " \xB7 \u5FEB\u7167\u5B58\u50A8\uFF1Ahome " + health.homeStores + " \u4E2A\u5DE5\u4F5C\u533A" + (health.fallbackStores ? "\uFF0C\u964D\u7EA7 " + health.fallbackStores + " \u4E2A" : "")
2439
+ title: t("manage.health.gitTitle")
2440
+ }, t(health.gitAvailable ? "manage.health.gitOk" : "manage.health.gitBad")),
2441
+ t("manage.health.stores", { n: health.homeStores }) + (health.fallbackStores ? t("manage.health.storesFallback", { n: health.fallbackStores }) : "")
1875
2442
  ) : null,
1876
2443
  // 搜索行:图标绝对定位在框内左侧(pointer-events:none 不挡点击),输入框
1877
2444
  // 靠 padding-left 让出图标位;高度由 CSS 的 .dsh-recall-search 覆写加高 20%
@@ -1898,8 +2465,8 @@
1898
2465
  ),
1899
2466
  React.createElement("input", {
1900
2467
  className: "dsh-recall-ex-input",
1901
- placeholder: "\u641C\u7D22\u5DE5\u4F5C\u533A / \u4F1A\u8BDD\u6807\u9898 / \u6D88\u606F\u5185\u5BB9 / ID",
1902
- "aria-label": "\u641C\u7D22\u5FEB\u7167",
2468
+ placeholder: t("manage.search.placeholder"),
2469
+ "aria-label": t("manage.search.aria"),
1903
2470
  value: query,
1904
2471
  spellCheck: false,
1905
2472
  onChange: (e) => setQuery(e.target.value)
@@ -1913,41 +2480,41 @@
1913
2480
  ...[1, 2, 3, 4, 5].map((n) => React.createElement("div", { key: "sk-" + n, className: "dsh-recall-tree-skeleton-row" }))
1914
2481
  ) : null,
1915
2482
  treeNodes.length > 0 ? React.createElement("div", { className: "dsh-recall-tree" }, ...treeNodes) : null,
1916
- items && items.length === 0 && !q ? React.createElement("div", { className: "dsh-recall-empty", key: "empty" }, "\u5728\u4EFB\u610F\u5DE5\u4F5C\u533A\u53D1\u9001\u4E00\u6761\u6D88\u606F\u540E\uFF0C\u8FD9\u91CC\u4F1A\u51FA\u73B0\u5FEB\u7167\u3002") : null,
1917
- q && filteredItems && filteredItems.length === 0 ? React.createElement("div", { className: "dsh-recall-empty", key: "no-match" }, "\u65E0\u5339\u914D\u5FEB\u7167") : null,
2483
+ items && items.length === 0 && !q ? React.createElement("div", { className: "dsh-recall-empty", key: "empty" }, t("manage.empty")) : null,
2484
+ q && filteredItems && filteredItems.length === 0 ? React.createElement("div", { className: "dsh-recall-empty", key: "no-match" }, t("manage.emptyFiltered")) : null,
1918
2485
  renderDeleteAllConfirm(),
1919
2486
  // V6:错误区从卡片最底上移到操作区上方(fail-loud 可见性);标题
1920
2487
  // error 色带条数徽章,时间戳格式维持原样
1921
2488
  errors && errors.length > 0 ? React.createElement(
1922
2489
  "div",
1923
2490
  { key: "errors" },
1924
- React.createElement("div", { className: "dsh-recall-errors-title" }, "\u6700\u8FD1\u9519\u8BEF (" + errors.length + ")"),
1925
- (showAllErrors ? errors : errors.slice(0, 5)).map((e, i) => React.createElement("div", { key: i, className: "dsh-recall-ex-note" }, clockText2(e.time) + " " + e.message)),
2491
+ React.createElement("div", { className: "dsh-recall-errors-title" }, t("manage.errors.title", { n: errors.length })),
2492
+ (showAllErrors ? errors : errors.slice(0, 5)).map(errorLine),
1926
2493
  React.createElement(
1927
2494
  "div",
1928
2495
  { className: "dsh-recall-panel-actions" },
1929
- errors.length > 5 ? React.createElement("button", { type: "button", className: "dsh-recall-ex-chip", onClick: () => setShowAllErrors((v) => !v) }, showAllErrors ? "\u6536\u8D77" : "\u5C55\u5F00\u5168\u90E8 (" + errors.length + ")") : null,
1930
- React.createElement("button", { type: "button", className: "dsh-recall-ex-chip", onClick: clearErrors }, "\u6E05\u7A7A")
2496
+ errors.length > 5 ? React.createElement("button", { type: "button", className: "dsh-recall-ex-chip", onClick: () => setShowAllErrors((v) => !v) }, showAllErrors ? t("manage.errors.collapse") : t("manage.errors.expand", { n: errors.length })) : null,
2497
+ React.createElement("button", { type: "button", className: "dsh-recall-ex-chip", onClick: clearErrors }, t("manage.errors.clear"))
1931
2498
  )
1932
2499
  ) : null,
1933
2500
  React.createElement(
1934
2501
  "div",
1935
2502
  { className: "dsh-recall-panel-actions" },
1936
- state.message ? React.createElement("span", { role: "status", "aria-live": "polite", className: "dsh-recall-ex-status" + (state.error ? " dsh-recall-ex-status-error" : " dsh-recall-ex-status-success") }, (state.error ? "\u9519\u8BEF\uFF1A" : "") + state.message) : null,
2503
+ state.message ? React.createElement("span", { role: "status", "aria-live": "polite", className: "dsh-recall-ex-status" + (state.error ? " dsh-recall-ex-status-error" : " dsh-recall-ex-status-success") }, (state.error ? t("common.errorPrefix") : "") + state.message) : null,
1937
2504
  limit < total ? React.createElement("button", {
1938
2505
  type: "button",
1939
2506
  className: "dsh-recall-btn",
1940
2507
  disabled: state.busy,
1941
2508
  onClick: loadMore
1942
- }, "\u52A0\u8F7D\u66F4\u591A") : null,
1943
- React.createElement("button", { type: "button", className: "dsh-recall-btn", disabled: state.busy, onClick: refresh }, "\u5237\u65B0"),
2509
+ }, t("manage.loadMore")) : null,
2510
+ React.createElement("button", { type: "button", className: "dsh-recall-btn", disabled: state.busy, onClick: () => refresh() }, t("manage.refresh")),
1944
2511
  React.createElement("button", {
1945
2512
  type: "button",
1946
2513
  className: "dsh-recall-btn",
1947
2514
  disabled: state.busy,
1948
- title: "\u7ACB\u5373\u5BF9\u5168\u90E8\u5DE5\u4F5C\u533A\u6267\u884C\u4E00\u6B21 git gc\uFF08\u538B\u7F29\u5BF9\u8C61\u5E93\u91CA\u653E\u7A7A\u95F4\uFF09",
1949
- onClick: () => run("gc", {}, "gc \u5B8C\u6210")
1950
- }, "\u7ACB\u5373 gc"),
2515
+ title: t("manage.gc.title"),
2516
+ onClick: () => run("gc", {}, "manage.done.gc")
2517
+ }, t("manage.gc")),
1951
2518
  // V5:全部删除固定为操作区最后一个按钮(排在立即 gc 之后)——即使
1952
2519
  // 「加载更多」出现/消失也不漂移;danger 与普通按钮间在 panel-actions
1953
2520
  // 统一 gap:8px 之上再加 btn-gap 的 8px 物理间隔,危险按钮与常规按钮
@@ -1956,9 +2523,9 @@
1956
2523
  type: "button",
1957
2524
  className: "dsh-recall-btn dsh-recall-btn-danger dsh-recall-btn-gap",
1958
2525
  disabled: state.busy,
1959
- title: "\u5220\u9664\u5168\u90E8\u5DE5\u4F5C\u533A\u7684\u6240\u6709\u5FEB\u7167\uFF1B\u4F1A\u76F4\u63A5\u6838\u5BF9\u5E76\u5220\u9664 git tag\uFF08\u5373\u4F7F\u5217\u8868\u4E3A\u7A7A\u4E5F\u53EF\u6E05\u7406\u6B8B\u7559\uFF09",
2526
+ title: t("manage.deleteAll.title"),
1960
2527
  onClick: () => setConfirming({ kind: "all" })
1961
- }, "\u5168\u90E8\u5220\u9664")
2528
+ }, t("manage.deleteAll"))
1962
2529
  )
1963
2530
  );
1964
2531
  }
@@ -2000,8 +2567,11 @@
2000
2567
  const { ConfigForm } = buildConfigForm(React, util, SectionToggle);
2001
2568
  const { ExcludeFilesSection } = buildExcludeCards(React, util);
2002
2569
  const { ManageCard } = buildSnapshotManager(React, util, sessionsSvc, workspacesSvc, uiWorkspaceSvc);
2570
+ const { t } = util;
2003
2571
  function RecallSettingsCard() {
2004
2572
  const [sections, setSections] = React.useState({ exclude: false, manage: false });
2573
+ const [, setLocaleTick] = React.useState(0);
2574
+ const notifyLocale = React.useCallback(() => setLocaleTick((n) => n + 1), []);
2005
2575
  function toggle(key) {
2006
2576
  setSections((prev) => Object.assign({}, prev, { [key]: !prev[key] }));
2007
2577
  }
@@ -2011,14 +2581,14 @@
2011
2581
  React.createElement(
2012
2582
  "div",
2013
2583
  { className: "dsh-recall-settings-body" },
2014
- React.createElement(ConfigForm),
2015
- React.createElement(SectionToggle, { title: "\u6392\u9664\u914D\u7F6E\uFF08exclude.txt\uFF09", open: sections.exclude, onToggle: () => toggle("exclude") }),
2584
+ React.createElement(ConfigForm, { onLocaleApplied: notifyLocale }),
2585
+ React.createElement(SectionToggle, { title: t("section.exclude"), open: sections.exclude, onToggle: () => toggle("exclude") }),
2016
2586
  // section-body 只做纵向间距与展开入场动画,不带描边/底色:外层卡片框
2017
2587
  // 去掉后内容区再套一层框等于把「去包裹」加回来(用户实测反馈)。「高级:
2018
2588
  // 基础排除表」不包——它在 cfg-grid 内本就走通栏,且 label 列对齐依赖
2019
2589
  // grid item 身份,包容器会破坏跨行对齐。
2020
2590
  sections.exclude ? React.createElement("div", { className: "dsh-recall-section-body" }, React.createElement(ExcludeFilesSection)) : null,
2021
- React.createElement(SectionToggle, { title: "\u5FEB\u7167\u7BA1\u7406", open: sections.manage, onToggle: () => toggle("manage") }),
2591
+ React.createElement(SectionToggle, { title: t("section.manage"), open: sections.manage, onToggle: () => toggle("manage") }),
2022
2592
  sections.manage ? React.createElement("div", { className: "dsh-recall-section-body" }, React.createElement(ManageCard)) : null
2023
2593
  )
2024
2594
  );
@@ -2066,7 +2636,7 @@
2066
2636
  );
2067
2637
  });
2068
2638
  } catch (error) {
2069
- console.error("[dsh-recall-plugin] slot register failed (" + slotKey + "):", error);
2639
+ appLog.error("slot register failed (" + slotKey + "):", error);
2070
2640
  }
2071
2641
  }
2072
2642
  try {
@@ -2075,7 +2645,7 @@
2075
2645
  RecallSettingsCard
2076
2646
  ));
2077
2647
  } catch (error) {
2078
- console.error("[dsh-recall-plugin] settings card register failed:", error);
2648
+ appLog.error("settings card register failed:", error);
2079
2649
  }
2080
2650
  try {
2081
2651
  slots.inject("plugins.bundle.config", () => slots.register(
@@ -2083,8 +2653,9 @@
2083
2653
  RecallSettingsCard
2084
2654
  ));
2085
2655
  } catch (error) {
2086
- console.error("[dsh-recall-plugin] plugin manager config register failed:", error);
2656
+ appLog.error("plugin manager config register failed:", error);
2087
2657
  }
2658
+ appLog.info("client applied");
2088
2659
  };
2089
2660
  }
2090
2661