sage-mem 1.8.2 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -2509,7 +2509,7 @@ const StarmapRuntime = (function (require, _react) {
2509
2509
  const isOpen = !!props.expanded;
2510
2510
  return h("div", { className: "smem-card", key: item.file }, [
2511
2511
  h("div", { className: "smem-card-head", key: "head" }, [
2512
- h("span", { className: "smem-badge", key: "badge", title: "类型 " + String(item.type) }, meta[0] + " " + meta[1]),
2512
+ h("span", { className: "smem-badge", key: "badge", title: t("card.typeTitle", { type: String(item.type) }) }, meta[0] + " " + typeLabel(TYPE_META[item.type] ? item.type : "reference")),
2513
2513
  h("span", { className: "smem-card-title", key: "title" }, item.file),
2514
2514
  h("span", { className: "smem-muted", key: "size" }, fmtSize(item.size)),
2515
2515
  ]),
@@ -2524,37 +2524,36 @@ const StarmapRuntime = (function (require, _react) {
2524
2524
  "data-clamp": isOpen ? "0" : "1",
2525
2525
  key: "body",
2526
2526
  onClick: function () { props.onToggle(item.file); },
2527
- title: isOpen ? "点击收起" : "点击展开全文",
2527
+ title: isOpen ? t("card.collapse") : t("card.expand"),
2528
2528
  }, String(item.description))
2529
2529
  : null,
2530
2530
  h("div", { className: "smem-card-foot", key: "foot" },
2531
2531
  props.archiving
2532
2532
  ? h("div", { className: "smem-ar-confirm", key: "ar" }, [
2533
- h("div", { key: "what" }, "把「" + item.file + "」收进档案馆:它不再参与检索与星图,文件移进 archive/,随时可以恢复。"),
2533
+ h("div", { key: "what" }, t("card.archiveWhat", { file: item.file })),
2534
2534
  // 「可留空」必须跟输入框贴在一起、并且独立成行 —— 它是这一块最容易
2535
2535
  // 被误读成「必须填才能按」的地方。
2536
- h("label", { className: "smem-label", key: "lb" }, "归档理由(可留空)"),
2536
+ h("label", { className: "smem-label", key: "lb" }, t("card.archiveReason")),
2537
2537
  h("input", {
2538
2538
  className: "smem-input", key: "in",
2539
2539
  value: props.archiveReason || "",
2540
- placeholder: "例如:结论已被 project_beta 取代",
2540
+ placeholder: t("card.archiveReasonPlaceholder"),
2541
2541
  onChange: function (e) { props.onArchiveReason(e.target.value); },
2542
2542
  }),
2543
- h("div", { className: "smem-ar-hint", key: "hint" },
2544
- "写下当时的判断,以后翻到它时就知道这条还值不值得捞回来。"),
2543
+ h("div", { className: "smem-ar-hint", key: "hint" }, t("card.archiveHint")),
2545
2544
  h("div", { className: "smem-row", key: "btns" }, [
2546
- h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "ok", disabled: props.busy, onClick: function () { props.onConfirmArchive(item.file); } }, "确认归档"),
2547
- h("button", { type: "button", className: "smem-btn", key: "no", disabled: props.busy, onClick: props.onCancelArchive }, "取消"),
2545
+ h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "ok", disabled: props.busy, onClick: function () { props.onConfirmArchive(item.file); } }, t("card.confirmArchive")),
2546
+ h("button", { type: "button", className: "smem-btn", key: "no", disabled: props.busy, onClick: props.onCancelArchive }, t("card.cancel")),
2548
2547
  ]),
2549
2548
  ])
2550
2549
  : h("div", { className: "smem-actions", key: "acts" },
2551
2550
  props.deleting
2552
2551
  ? [
2553
- h("button", { type: "button", className: "smem-btn smem-btn-danger", key: "yes", onClick: function () { props.onConfirmDelete(item.file); } }, "确认删除"),
2554
- h("button", { type: "button", className: "smem-btn", key: "no", onClick: function () { props.onCancelDelete(); } }, "取消"),
2552
+ h("button", { type: "button", className: "smem-btn smem-btn-danger", key: "yes", onClick: function () { props.onConfirmDelete(item.file); } }, t("card.confirmDelete")),
2553
+ h("button", { type: "button", className: "smem-btn", key: "no", onClick: function () { props.onCancelDelete(); } }, t("card.cancel")),
2555
2554
  ]
2556
2555
  : [
2557
- h("button", { type: "button", className: "smem-btn", key: "edit", onClick: function () { props.onEdit(item.file); } }, "编辑"),
2556
+ h("button", { type: "button", className: "smem-btn", key: "edit", onClick: function () { props.onEdit(item.file); } }, t("card.edit")),
2558
2557
  // 语义分组:左边只放「编辑」(安全、可逆),右边放「锁定 / 归档 / 删除」(对这条记忆的退役/保护决定)。
2559
2558
  h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
2560
2559
  /**
@@ -2570,21 +2569,21 @@ const StarmapRuntime = (function (require, _react) {
2570
2569
  className: "smem-btn" + (item.locked ? " smem-btn-locked" : ""),
2571
2570
  key: "lock",
2572
2571
  title: item.locked
2573
- ? "已锁定:自动归档不会选它,手动归档也会被拒绝。点一下解锁"
2574
- : "锁定这条记忆:自动与手动归档都不再动它(重要的人设 / 偏好建议锁上)",
2572
+ ? t("card.lockedTitle")
2573
+ : t("card.unlockedTitle"),
2575
2574
  disabled: props.busy,
2576
2575
  onClick: function () { props.onToggleLock(item.file, !item.locked); },
2577
- }, item.locked ? "🔒 已锁定" : "🔓 锁定"),
2576
+ }, item.locked ? t("card.locked") : t("card.lock")),
2578
2577
  // 归档排在「锁定」和「删除」之间:比编辑轻、比删除温和,且随时可逆。
2579
2578
  h("button", {
2580
2579
  type: "button", className: "smem-btn", key: "arch",
2581
2580
  title: item.locked
2582
- ? "这条已锁定,归档会被拒绝(先解锁)"
2583
- : "收进档案馆:不再参与检索与星图,随时可以恢复",
2581
+ ? t("card.archiveLockedTitle")
2582
+ : t("card.archiveTitle"),
2584
2583
  disabled: props.busy || item.locked === true,
2585
2584
  onClick: function () { props.onAskArchive(item.file); },
2586
- }, "归档"),
2587
- h("button", { type: "button", className: "smem-btn smem-btn-danger-ghost", key: "del", onClick: function () { props.onAskDelete(item.file); } }, "删除"),
2585
+ }, t("card.archive")),
2586
+ h("button", { type: "button", className: "smem-btn smem-btn-danger-ghost", key: "del", onClick: function () { props.onAskDelete(item.file); } }, t("card.delete")),
2588
2587
  ])),
2589
2588
  ]);
2590
2589
  }
@@ -2612,9 +2611,9 @@ const StarmapRuntime = (function (require, _react) {
2612
2611
  const descOpen = sDescOpen[0], setDescOpen = sDescOpen[1];
2613
2612
  return h("div", { className: "smem-card smem-card--archived", key: item.file }, [
2614
2613
  h("div", { className: "smem-card-head", key: "head" }, [
2615
- h("span", { className: "smem-badge", key: "badge", title: "类型 " + String(item.type) }, meta[0] + " " + meta[1]),
2614
+ h("span", { className: "smem-badge", key: "badge", title: t("card.typeTitle", { type: String(item.type) }) }, meta[0] + " " + typeLabel(TYPE_META[item.type] ? item.type : "reference")),
2616
2615
  h("span", { className: "smem-card-title", key: "title" }, item.file),
2617
- h("span", { className: "smem-ar-meta", key: "at", title: "归档时间" }, "已归档 " + (item.archivedAt || "(时间未知)")),
2616
+ h("span", { className: "smem-ar-meta", key: "at", title: t("card.archivedAtTitle") }, t("card.archivedAt", { at: item.archivedAt || t("card.unknownTime") })),
2618
2617
  h("span", { className: "smem-muted", key: "size" }, fmtSize(item.size)),
2619
2618
  ]),
2620
2619
  (item.tags && item.tags.length)
@@ -2628,21 +2627,20 @@ const StarmapRuntime = (function (require, _react) {
2628
2627
  ? h("div", {
2629
2628
  className: "smem-narrative", key: "desc",
2630
2629
  "data-clamp": descOpen ? "0" : "1",
2631
- title: descOpen ? "点击收起" : "点击展开全文",
2630
+ title: descOpen ? t("card.collapse") : t("card.expand"),
2632
2631
  onClick: function () { setDescOpen(!descOpen); },
2633
2632
  }, String(item.description))
2634
2633
  : null,
2635
2634
  h("div", {
2636
2635
  className: "smem-ar-reason smem-clamp-2" + (descOpen ? " smem-clamp-2--open" : ""),
2637
- key: "why", title: "归档理由:" + (item.archivedReason || "(没写)"),
2636
+ key: "why", title: t("card.reason", { reason: item.archivedReason || t("card.noReason") }),
2638
2637
  onClick: function () { setDescOpen(!descOpen); },
2639
- }, "归档理由:" + (item.archivedReason || "(没写)")),
2638
+ }, t("card.reason", { reason: item.archivedReason || t("card.noReason") })),
2640
2639
  props.editing
2641
2640
  ? h("div", { className: "smem-ar-edit", key: "edit" }, [
2642
- h("div", { className: "smem-ar-hint", key: "note" },
2643
- "原样文本编辑:下面就是这份文件的全文(含 frontmatter 与归档留痕),保存时原样写回,不会被重新生成。"),
2641
+ h("div", { className: "smem-ar-hint", key: "note" }, t("card.rawEditNote")),
2644
2642
  props.loading
2645
- ? h("div", { className: "smem-status", key: "loading" }, "读取中…")
2643
+ ? h("div", { className: "smem-status", key: "loading" }, t("card.reading"))
2646
2644
  : h("textarea", {
2647
2645
  className: "smem-textarea smem-ar-editor", key: "ta",
2648
2646
  value: props.text || "",
@@ -2650,20 +2648,17 @@ const StarmapRuntime = (function (require, _react) {
2650
2648
  onChange: function (e) { props.onText(e.target.value); },
2651
2649
  }),
2652
2650
  h("div", { className: "smem-row", key: "btns" }, [
2653
- h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "save", disabled: props.busy || props.loading, onClick: function () { props.onSave(item.file); } }, props.saving ? "保存中…" : "保存"),
2654
- h("button", { type: "button", className: "smem-btn", key: "cancel", disabled: props.busy, onClick: props.onCancel }, "取消"),
2651
+ h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "save", disabled: props.busy || props.loading, onClick: function () { props.onSave(item.file); } }, props.saving ? t("card.saving") : t("card.save")),
2652
+ h("button", { type: "button", className: "smem-btn", key: "cancel", disabled: props.busy, onClick: props.onCancel }, t("card.cancel")),
2655
2653
  ]),
2656
2654
  ])
2657
2655
  : props.deleting
2658
2656
  ? h("div", { className: "smem-ar-confirm", key: "del" }, [
2659
- h("div", { key: "what" },
2660
- "彻底删除「" + item.file + "」:它会从 archive/ 里被真的删掉,不能恢复。" +
2661
- "要保留它就先点「恢复」,把它放回活动记忆。"),
2662
- h("div", { className: "smem-ar-hint", key: "hint" },
2663
- "这一步之后没有任何界面能把内容找回来 —— 只剩你自己在磁盘上留过备份。"),
2657
+ h("div", { key: "what" }, t("card.purgeWhat", { file: item.file })),
2658
+ h("div", { className: "smem-ar-hint", key: "hint" }, t("card.purgeHint")),
2664
2659
  h("div", { className: "smem-row", key: "btns" }, [
2665
- h("button", { type: "button", className: "smem-btn smem-btn-danger", key: "yes", disabled: props.busy, onClick: function () { props.onConfirmDelete(item.file); } }, "确认彻底删除"),
2666
- h("button", { type: "button", className: "smem-btn", key: "no", disabled: props.busy, onClick: props.onCancelDelete }, "取消"),
2660
+ h("button", { type: "button", className: "smem-btn smem-btn-danger", key: "yes", disabled: props.busy, onClick: function () { props.onConfirmDelete(item.file); } }, t("card.confirmPurge")),
2661
+ h("button", { type: "button", className: "smem-btn", key: "no", disabled: props.busy, onClick: props.onCancelDelete }, t("card.cancel")),
2667
2662
  ]),
2668
2663
  ])
2669
2664
  : h("div", { className: "smem-card-foot", key: "foot" },
@@ -2672,21 +2667,21 @@ const StarmapRuntime = (function (require, _react) {
2672
2667
  type: "button", className: "smem-btn", key: "edit",
2673
2668
  disabled: props.busy,
2674
2669
  onClick: function () { props.onEdit(item.file); },
2675
- }, "编辑"),
2670
+ }, t("card.edit")),
2676
2671
  h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
2677
2672
  h("button", {
2678
2673
  type: "button", className: "smem-btn smem-btn-primary", key: "restore",
2679
2674
  disabled: props.busy,
2680
2675
  onClick: function () { props.onRestore(item.file); },
2681
- }, props.restoring ? "恢复中…" : "恢复"),
2676
+ }, props.restoring ? t("card.restoring") : t("card.restore")),
2682
2677
  // 放在最后、且用 danger-ghost:它是这张卡上唯一不可逆的动作,
2683
2678
  // 不该跟「恢复」长得一样。
2684
2679
  h("button", {
2685
2680
  type: "button", className: "smem-btn smem-btn-danger-ghost", key: "purge",
2686
2681
  disabled: props.busy,
2687
- title: "从 archive/ 里彻底删掉,不可恢复(要留就先恢复)",
2682
+ title: t("card.purgeTitle"),
2688
2683
  onClick: function () { props.onAskDelete(item.file); },
2689
- }, "彻底删除"),
2684
+ }, t("card.purge")),
2690
2685
  ])),
2691
2686
  ]);
2692
2687
  }
@@ -2703,9 +2698,6 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
2703
2698
  };
2704
2699
  const autoArchiveOf = (v) => (v === "off" || v === "auto" ? v : "report");
2705
2700
 
2706
- /** 头部带上的「我在哪」:五个 tab 的中文名(与 .smem-tabs 上的字面一致)。 */
2707
- const TAB_LABEL = { files: "文件列表", star: "记忆星图", autodream: "自动做梦", archive: "自动归档", audit: "体检" };
2708
-
2709
2701
  /**
2710
2702
  * 工具面板头部的「收起」按钮:抽屉里开的工具要能一键收回去 ——
2711
2703
  * 面板不收,列表就还在下面被顶着。
@@ -2851,14 +2843,14 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
2851
2843
  */
2852
2844
  function StarLine() {
2853
2845
  return h("div", { className: "smem-star" }, [
2854
- h("span", { key: "ask" }, "⭐ 顺手留颗 Star,作者能高兴一整天"),
2846
+ h("span", { key: "ask" }, t("star.ask")),
2855
2847
  h("a", {
2856
2848
  key: "cta",
2857
2849
  href: "https://github.com/gezi-wen/sage-mem",
2858
2850
  target: "_blank",
2859
2851
  rel: "noreferrer",
2860
2852
  className: "smem-star-link",
2861
- }, "行,给你一颗 Star"),
2853
+ }, t("star.cta")),
2862
2854
  ]);
2863
2855
  }
2864
2856
 
@@ -3064,6 +3056,440 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
3064
3056
  return { provider: "", model: "" };
3065
3057
  }
3066
3058
 
3059
+ // ── i18n pilot: settings.memory ─────────────────────────────────────
3060
+ // The `zh` values below are copied verbatim from the strings this shard
3061
+ // replaces; they must render byte-identically to the pre-i18n client.
3062
+ // The `en` draft is a complete key-for-key translation. Placeholders use
3063
+ // the host locale service's own syntax (`{name}`), so a bound `t` and the
3064
+ // local fallback below substitute the same way.
3065
+ const NS = "settings.memory";
3066
+
3067
+ const ZH = {
3068
+ "section.label": "记忆管理",
3069
+ "page.back": "返回会话",
3070
+ "sidebar.label": "记忆管理",
3071
+
3072
+ "tab.files": "文件列表",
3073
+ "tab.star": "记忆星图",
3074
+ "tab.autodream": "自动做梦",
3075
+ "tab.archive": "自动归档",
3076
+ "tab.audit": "体检",
3077
+
3078
+ "type.user": "用户",
3079
+ "type.feedback": "反馈",
3080
+ "type.project": "项目",
3081
+ "type.reference": "参考",
3082
+ "type.special": "特殊",
3083
+
3084
+ "header.dirReadable": "memory 目录可读",
3085
+ "header.dirUnreadable": "memory 目录不可读",
3086
+ "header.dirTitle": "{state} · {tab}",
3087
+ "header.counts": "活动中 {active} · 已归档 {archived}",
3088
+ "header.countsFiltered": "筛出 {shown} / {total} · 活动中 {active} · 已归档 {archived}",
3089
+ "header.auditSwitch": "切到体检看是哪几条",
3090
+ "header.auditClean": "体检没有发现问题",
3091
+ "header.auditRun": "跑一次体检(只读)",
3092
+ "header.auditProblems": "体检 {count} 个问题",
3093
+ "header.auditPass": "体检 通过",
3094
+ "header.auditNotRun": "体检 · 未跑",
3095
+ "header.restartTitle": "有改动要重启 DSH 才生效",
3096
+ "header.restartPending": "待重启生效",
3097
+ "header.restartPendingCount": "{count} 项待重启生效",
3098
+ "header.openFolderTitle": "在系统文件管理器里打开记忆目录",
3099
+ "header.openFolder": "打开记忆文件夹",
3100
+ "header.refresh": "刷新",
3101
+ "action.add": "+ 添加记忆",
3102
+
3103
+ "form.addTitle": "添加记忆",
3104
+ "form.editTitle": "编辑 {file}",
3105
+ "form.type": "类型:",
3106
+ "form.fileName": "文件名(.md,留空自动 {type}_note.md)",
3107
+ "form.fileNamePlaceholder": "例如 project_example.md",
3108
+ "form.desc": "描述(写进 frontmatter,检索与列表都靠它)",
3109
+ "form.descPlaceholder": "一句话说清这条记忆是什么",
3110
+ "form.tags": "自定义标签(可选,逗号分隔;会写进 frontmatter 的 tags,列表页可按它筛)",
3111
+ "form.tagsPlaceholder": "例如 部署, 笔记, dsh",
3112
+ "form.body": "正文(可选,frontmatter 自动生成)",
3113
+ "form.bodyPlaceholder": "要长期记住的细节",
3114
+
3115
+ "update.found": "发现新版本 v{latest}(当前 v{current})",
3116
+ "update.notes": "看更新内容",
3117
+ "update.dismissTitle": "这次不看了(下次进面板还会提示)",
3118
+
3119
+ "toolbar.search": "搜索文件名 / 描述 / 类型 / 标签…",
3120
+ "tools.button": "工具 ▾",
3121
+ "tools.title": "归档候选 / 保留名编辑",
3122
+ "tools.titleDebug": "归档候选 / 保留名编辑 / 注入调试",
3123
+ "tools.candidates": "归档候选",
3124
+ "tools.candidatesCount": "{count} 条",
3125
+ "tools.candidatesEmpty": "未取",
3126
+ "tools.raw": "保留名编辑",
3127
+ "tools.rawCount": "{count} 个",
3128
+ "tools.debug": "注入调试",
3129
+ "tools.debugCount": "{count} 次",
3130
+
3131
+ "filter.sumStatus": "状态:{value}",
3132
+ "filter.sumType": "类型:{value}",
3133
+ "filter.sumTag": "标签:#{value}",
3134
+ "filter.sumQuery": "搜索:{value}",
3135
+ "filter.clearAll": "清除全部",
3136
+ "filter.statusLabel": "状态",
3137
+ "filter.all": "全部",
3138
+ "filter.allTitle": "活动与已归档一起看(归档条走灰色)",
3139
+ "filter.active": "活动中",
3140
+ "filter.activeTitle": "正在参与检索与星图的记忆",
3141
+ "filter.archived": "已归档",
3142
+ "filter.archivedTitle": "已收进档案馆:不参与检索与星图,可编辑 / 恢复",
3143
+ "filter.typeLabel": "类型",
3144
+ "filter.tags": "标签",
3145
+ "filter.collapseTags": "收起标签",
3146
+ "filter.moreTagsTitle": "还有一些标签没显示",
3147
+ "filter.moreTags": "更多标签 ({count})",
3148
+ "filter.clear": "清除筛选",
3149
+ "filter.remove": "撤掉这项筛选",
3150
+
3151
+ "files.emptyTitle": "还没有记忆文件。对话中值得记住的事会写到这里,也可以手动写入。",
3152
+ "files.skeletonTitle": "写入一份 MEMORY.md 索引骨架(已有索引不会覆盖)",
3153
+ "files.skeleton": "建立索引骨架",
3154
+ "files.archiveEmpty": "档案馆是空的 —— 在活动记忆那张卡上点「归档」,它就会收到这里。",
3155
+ "files.backToFiles": "回文件列表",
3156
+ "files.activeEmpty": "活动记忆是空的 —— 状态筛到「已归档」还能捞出 {count} 条。",
3157
+ "files.goArchived": "切到已归档",
3158
+ "files.noMatch": "没有符合搜索或筛选条件的文件。",
3159
+ "files.archiveIntro": "档案馆里的记忆不参与检索与星图,原文仍在记忆目录的 archive/ 下 —— 恢复后它就回到活动列表;「编辑」是原样文本编辑,改完存回同一份文件;「彻底删除」会真的从 archive/ 里删掉它(不可恢复,删除前还要再确认一次)。",
3160
+
3161
+ "card.typeTitle": "类型 {type}",
3162
+ "card.collapse": "点击收起",
3163
+ "card.expand": "点击展开全文",
3164
+ "card.archiveWhat": "把「{file}」收进档案馆:它不再参与检索与星图,文件移进 archive/,随时可以恢复。",
3165
+ "card.archiveReason": "归档理由(可留空)",
3166
+ "card.archiveReasonPlaceholder": "例如:结论已被 project_beta 取代",
3167
+ "card.archiveHint": "写下当时的判断,以后翻到它时就知道这条还值不值得捞回来。",
3168
+ "card.confirmArchive": "确认归档",
3169
+ "card.cancel": "取消",
3170
+ "card.confirmDelete": "确认删除",
3171
+ "card.edit": "编辑",
3172
+ "card.lockedTitle": "已锁定:自动归档不会选它,手动归档也会被拒绝。点一下解锁",
3173
+ "card.unlockedTitle": "锁定这条记忆:自动与手动归档都不再动它(重要的人设 / 偏好建议锁上)",
3174
+ "card.locked": "🔒 已锁定",
3175
+ "card.lock": "🔓 锁定",
3176
+ "card.archiveLockedTitle": "这条已锁定,归档会被拒绝(先解锁)",
3177
+ "card.archiveTitle": "收进档案馆:不再参与检索与星图,随时可以恢复",
3178
+ "card.archive": "归档",
3179
+ "card.delete": "删除",
3180
+ "card.archivedAtTitle": "归档时间",
3181
+ "card.archivedAt": "已归档 {at}",
3182
+ "card.unknownTime": "(时间未知)",
3183
+ "card.reason": "归档理由:{reason}",
3184
+ "card.noReason": "(没写)",
3185
+ "card.rawEditNote": "原样文本编辑:下面就是这份文件的全文(含 frontmatter 与归档留痕),保存时原样写回,不会被重新生成。",
3186
+ "card.reading": "读取中…",
3187
+ "card.saving": "保存中…",
3188
+ "card.save": "保存",
3189
+ "card.purgeWhat": "彻底删除「{file}」:它会从 archive/ 里被真的删掉,不能恢复。要保留它就先点「恢复」,把它放回活动记忆。",
3190
+ "card.purgeHint": "这一步之后没有任何界面能把内容找回来 —— 只剩你自己在磁盘上留过备份。",
3191
+ "card.confirmPurge": "确认彻底删除",
3192
+ "card.restoring": "恢复中…",
3193
+ "card.restore": "恢复",
3194
+ "card.purgeTitle": "从 archive/ 里彻底删掉,不可恢复(要留就先恢复)",
3195
+ "card.purge": "彻底删除",
3196
+
3197
+ "star.ask": "⭐ 顺手留颗 Star,作者能高兴一整天",
3198
+ "star.cta": "行,给你一颗 Star",
3199
+
3200
+ "pluginCard.saveFailed": "保存失败(未知原因)",
3201
+ "pluginCard.noSetSettings": "宿主侧还没有 setSettings 接口(插件版本较旧)",
3202
+ "pluginCard.title": "侧栏入口",
3203
+ "pluginCard.on": "已开启",
3204
+ "pluginCard.off": "已关闭",
3205
+ "pluginCard.label": "在侧栏显示「记忆管理」入口",
3206
+ "pluginCard.note": "默认开启;改动立即生效,不需要重启。关闭后「设置 → 记忆管理」入口仍然保留。",
3207
+
3208
+ "error.remote": "remote 调用失败",
3209
+ "error.listFiles": "listFiles RPC 失败: {message}",
3210
+ "error.noRaw": "宿主侧还没有 readRaw / writeRaw 接口(插件版本较旧)",
3211
+ "error.noOpenDir": "宿主侧还没有 openMemoryDir 接口(插件版本较旧)",
3212
+ "error.noArchive": "宿主侧还没有 archive 接口(插件版本较旧)",
3213
+ "error.noLock": "宿主侧还没有 setLocked 接口(插件版本较旧)",
3214
+ "error.noRestore": "宿主侧还没有 restore 接口(插件版本较旧)",
3215
+ "error.noDeleteArchived": "宿主侧还没有 deleteArchived 接口(插件版本较旧)",
3216
+ "error.noReadArchived": "宿主侧还没有 readArchived 接口(插件版本较旧)",
3217
+ "error.noWriteArchived": "宿主侧还没有 writeArchived 接口(插件版本较旧)",
3218
+ "error.noAudit": "宿主侧还没有 audit 接口(插件版本较旧)",
3219
+ "error.noArchiveCandidates": "宿主侧还没有 archiveCandidates 接口(插件版本较旧)",
3220
+ "error.noGetSettings": "宿主侧还没有 getSettings 接口(插件版本较旧)",
3221
+ "error.noReadRaw": "宿主侧还没有 readRaw 接口(插件版本较旧)",
3222
+ "error.noWriteRaw": "宿主侧还没有 writeRaw 接口(插件版本较旧)",
3223
+ "error.readFailed": "读不到 {file}:{message}",
3224
+ "error.descEmpty": "描述不能为空",
3225
+ "error.writeRejected": "写入被拒绝",
3226
+ "error.deleteRejected": "删除被拒绝",
3227
+ "error.archiveRejected": "归档被拒绝",
3228
+ "error.lockRejected": "锁定操作被拒绝",
3229
+ "error.restoreRejected": "恢复被拒绝",
3230
+ "error.saveRejected": "保存被拒绝",
3231
+ "error.skeletonFailed": "建立索引骨架失败",
3232
+ "error.openDirFailed": "打开记忆目录失败",
3233
+ "notice.indexExists": "MEMORY.md 已经存在,没有改动它。",
3234
+ "notice.indexCreated": "已建立 MEMORY.md 索引骨架。",
3235
+ "notice.updated": "已更新 {file}",
3236
+ "notice.saved": "已保存 {file}",
3237
+ "notice.rawNew": "已新建 {file}",
3238
+ "notice.deleted": "已删除 {file}",
3239
+ "notice.archived": "已归档 {file} —— 它现在在「已归档」视图里,随时可以恢复",
3240
+ "notice.batchPartial": "已归档 {done} 条,{failed} 条没成功:{files}",
3241
+ "notice.batchDone": "已归档 {done} 条 —— 它们现在在档案馆里,随时可以恢复",
3242
+ "notice.locked": "已锁定 {file} —— 自动归档不会再选它,手动归档也会被拒绝",
3243
+ "notice.unlocked": "已解锁 {file}",
3244
+ "notice.restored": "已恢复 {file} —— 它回到了活动记忆里",
3245
+ "notice.restoredIndexed": "已恢复 {file} —— 它回到了活动记忆里,并补进了索引",
3246
+ "notice.purged": "已彻底删除 {file} —— 归档区里也没有它了,找不回来",
3247
+ };
3248
+
3249
+ const EN = {
3250
+ "section.label": "Memory management",
3251
+ "page.back": "Back to conversation",
3252
+ "sidebar.label": "Memory management",
3253
+
3254
+ "tab.files": "Files",
3255
+ "tab.star": "Star map",
3256
+ "tab.autodream": "Autodream",
3257
+ "tab.archive": "Auto-archive",
3258
+ "tab.audit": "Audit",
3259
+
3260
+ "type.user": "User",
3261
+ "type.feedback": "Feedback",
3262
+ "type.project": "Project",
3263
+ "type.reference": "Reference",
3264
+ "type.special": "Special",
3265
+
3266
+ "header.dirReadable": "memory directory readable",
3267
+ "header.dirUnreadable": "memory directory unreadable",
3268
+ "header.dirTitle": "{state} · {tab}",
3269
+ "header.counts": "Active {active} · Archived {archived}",
3270
+ "header.countsFiltered": "Showing {shown} / {total} · Active {active} · Archived {archived}",
3271
+ "header.auditSwitch": "Switch to Audit to see which entries",
3272
+ "header.auditClean": "Audit found no problems",
3273
+ "header.auditRun": "Run an audit (read-only)",
3274
+ "header.auditProblems": "Audit: {count} problems",
3275
+ "header.auditPass": "Audit passed",
3276
+ "header.auditNotRun": "Audit · not run",
3277
+ "header.restartTitle": "Changes take effect after restarting DSH",
3278
+ "header.restartPending": "restart to apply",
3279
+ "header.restartPendingCount": "{count} items pending restart",
3280
+ "header.openFolderTitle": "Open the memory directory in the system file manager",
3281
+ "header.openFolder": "Open memory folder",
3282
+ "header.refresh": "Refresh",
3283
+ "action.add": "+ Add memory",
3284
+
3285
+ "form.addTitle": "Add memory",
3286
+ "form.editTitle": "Edit {file}",
3287
+ "form.type": "Type:",
3288
+ "form.fileName": "File name (.md; leave blank to use {type}_note.md automatically)",
3289
+ "form.fileNamePlaceholder": "e.g. project_example.md",
3290
+ "form.desc": "Description (written to frontmatter; retrieval and the list both rely on it)",
3291
+ "form.descPlaceholder": "One sentence saying what this memory is",
3292
+ "form.tags": "Custom tags (optional, comma-separated; written to frontmatter tags, filterable in the list)",
3293
+ "form.tagsPlaceholder": "e.g. deploy, notes, dsh",
3294
+ "form.body": "Body (optional; frontmatter is generated automatically)",
3295
+ "form.bodyPlaceholder": "Details worth remembering long-term",
3296
+
3297
+ "update.found": "New version v{latest} available (current v{current})",
3298
+ "update.notes": "See what's new",
3299
+ "update.dismissTitle": "Skip this time (it will show again next time)",
3300
+
3301
+ "toolbar.search": "Search file name / description / type / tag…",
3302
+ "tools.button": "Tools ▾",
3303
+ "tools.title": "Archive candidates / Reserved-name editor",
3304
+ "tools.titleDebug": "Archive candidates / Reserved-name editor / Injection debug",
3305
+ "tools.candidates": "Archive candidates",
3306
+ "tools.candidatesCount": "{count}",
3307
+ "tools.candidatesEmpty": "not loaded",
3308
+ "tools.raw": "Reserved-name editor",
3309
+ "tools.rawCount": "{count}",
3310
+ "tools.debug": "Injection debug",
3311
+ "tools.debugCount": "{count}",
3312
+
3313
+ "filter.sumStatus": "Status: {value}",
3314
+ "filter.sumType": "Type: {value}",
3315
+ "filter.sumTag": "Tag: #{value}",
3316
+ "filter.sumQuery": "Search: {value}",
3317
+ "filter.clearAll": "Clear all",
3318
+ "filter.statusLabel": "Status",
3319
+ "filter.all": "All",
3320
+ "filter.allTitle": "See active and archived together (archived rows are grey)",
3321
+ "filter.active": "Active",
3322
+ "filter.activeTitle": "Memories currently participating in retrieval and the star map",
3323
+ "filter.archived": "Archived",
3324
+ "filter.archivedTitle": "Moved to the archive: out of retrieval and the star map; can be edited / restored",
3325
+ "filter.typeLabel": "Type",
3326
+ "filter.tags": "Tags",
3327
+ "filter.collapseTags": "Collapse tags",
3328
+ "filter.moreTagsTitle": "Some tags are hidden",
3329
+ "filter.moreTags": "More tags ({count})",
3330
+ "filter.clear": "Clear filters",
3331
+ "filter.remove": "Remove this filter",
3332
+
3333
+ "files.emptyTitle": "No memory files yet. Things worth remembering from a conversation are written here, and you can add them by hand.",
3334
+ "files.skeletonTitle": "Write a MEMORY.md index skeleton (an existing index is never overwritten)",
3335
+ "files.skeleton": "Create index skeleton",
3336
+ "files.archiveEmpty": "The archive is empty — press Archive on an active memory card and it will land here.",
3337
+ "files.backToFiles": "Back to files",
3338
+ "files.activeEmpty": "There are no active memories — switching the status filter to Archived still finds {count}.",
3339
+ "files.goArchived": "Switch to archived",
3340
+ "files.noMatch": "No files match the search or filters.",
3341
+ "files.archiveIntro": "Archived memories stay out of retrieval and the star map; the original text remains in archive/ under the memory directory — restoring returns it to the active list. Edit opens the raw text and saves back to the same file; Delete permanently really removes it from archive/ (irreversible, and asks once more before deleting).",
3342
+
3343
+ "card.typeTitle": "Type {type}",
3344
+ "card.collapse": "Click to collapse",
3345
+ "card.expand": "Click to expand",
3346
+ "card.archiveWhat": "Move “{file}” into the archive: it stops participating in retrieval and the star map, moves under archive/, and can be restored at any time.",
3347
+ "card.archiveReason": "Archive reason (optional)",
3348
+ "card.archiveReasonPlaceholder": "e.g. superseded by project_beta",
3349
+ "card.archiveHint": "Write down the judgment you are making now, so a future reader knows whether this is still worth recovering.",
3350
+ "card.confirmArchive": "Confirm archive",
3351
+ "card.cancel": "Cancel",
3352
+ "card.confirmDelete": "Confirm delete",
3353
+ "card.edit": "Edit",
3354
+ "card.lockedTitle": "Locked: auto-archive never selects it and a manual archive is refused. Click to unlock",
3355
+ "card.unlockedTitle": "Lock this memory: auto and manual archiving both leave it alone (lock important personas / preferences)",
3356
+ "card.locked": "🔒 Locked",
3357
+ "card.lock": "🔓 Lock",
3358
+ "card.archiveLockedTitle": "This memory is locked, so archiving will be refused (unlock first)",
3359
+ "card.archiveTitle": "Move into the archive: out of retrieval and the star map, restorable at any time",
3360
+ "card.archive": "Archive",
3361
+ "card.delete": "Delete",
3362
+ "card.archivedAtTitle": "Archived at",
3363
+ "card.archivedAt": "Archived {at}",
3364
+ "card.unknownTime": "(time unknown)",
3365
+ "card.reason": "Archive reason: {reason}",
3366
+ "card.noReason": "(not written)",
3367
+ "card.rawEditNote": "Raw text editing: what follows is the whole file (including frontmatter and archive traces). Saving writes it back unchanged, without regenerating anything.",
3368
+ "card.reading": "Reading…",
3369
+ "card.saving": "Saving…",
3370
+ "card.save": "Save",
3371
+ "card.purgeWhat": "Permanently delete “{file}”: it is really removed from archive/ and cannot be recovered. To keep it, press Restore first and put it back among the active memories.",
3372
+ "card.purgeHint": "After this step no screen can bring the content back — only a backup you kept on disk can.",
3373
+ "card.confirmPurge": "Confirm permanent delete",
3374
+ "card.restoring": "Restoring…",
3375
+ "card.restore": "Restore",
3376
+ "card.purgeTitle": "Remove permanently from archive/, irreversible (restore first to keep it)",
3377
+ "card.purge": "Delete permanently",
3378
+
3379
+ "star.ask": "⭐ Leave a star — it makes the author's day",
3380
+ "star.cta": "Sure, here's a star",
3381
+
3382
+ "pluginCard.saveFailed": "Save failed (unknown reason)",
3383
+ "pluginCard.noSetSettings": "The host has no setSettings API yet (older plugin version)",
3384
+ "pluginCard.title": "Sidebar entry",
3385
+ "pluginCard.on": "On",
3386
+ "pluginCard.off": "Off",
3387
+ "pluginCard.label": "Show the “Memory management” entry in the sidebar",
3388
+ "pluginCard.note": "On by default; changes take effect immediately, no restart needed. Turning it off keeps the Settings → Memory management entry.",
3389
+
3390
+ "error.remote": "remote call failed",
3391
+ "error.listFiles": "listFiles RPC failed: {message}",
3392
+ "error.noRaw": "The host has no readRaw / writeRaw API yet (older plugin version)",
3393
+ "error.noOpenDir": "The host has no openMemoryDir API yet (older plugin version)",
3394
+ "error.noArchive": "The host has no archive API yet (older plugin version)",
3395
+ "error.noLock": "The host has no setLocked API yet (older plugin version)",
3396
+ "error.noRestore": "The host has no restore API yet (older plugin version)",
3397
+ "error.noDeleteArchived": "The host has no deleteArchived API yet (older plugin version)",
3398
+ "error.noReadArchived": "The host has no readArchived API yet (older plugin version)",
3399
+ "error.noWriteArchived": "The host has no writeArchived API yet (older plugin version)",
3400
+ "error.noAudit": "The host has no audit API yet (older plugin version)",
3401
+ "error.noArchiveCandidates": "The host has no archiveCandidates API yet (older plugin version)",
3402
+ "error.noGetSettings": "The host has no getSettings API yet (older plugin version)",
3403
+ "error.noReadRaw": "The host has no readRaw API yet (older plugin version)",
3404
+ "error.noWriteRaw": "The host has no writeRaw API yet (older plugin version)",
3405
+ "error.readFailed": "Cannot read {file}: {message}",
3406
+ "error.descEmpty": "Description cannot be empty",
3407
+ "error.writeRejected": "Write rejected",
3408
+ "error.deleteRejected": "Delete rejected",
3409
+ "error.archiveRejected": "Archive rejected",
3410
+ "error.lockRejected": "Lock operation rejected",
3411
+ "error.restoreRejected": "Restore rejected",
3412
+ "error.saveRejected": "Save rejected",
3413
+ "error.skeletonFailed": "Failed to create the index skeleton",
3414
+ "error.openDirFailed": "Failed to open the memory directory",
3415
+ "notice.indexExists": "MEMORY.md already exists; it was left unchanged.",
3416
+ "notice.indexCreated": "MEMORY.md index skeleton created.",
3417
+ "notice.updated": "Updated {file}",
3418
+ "notice.saved": "Saved {file}",
3419
+ "notice.rawNew": "Created {file}",
3420
+ "notice.deleted": "Deleted {file}",
3421
+ "notice.archived": "Archived {file} — it is now under the Archived view and can be restored at any time",
3422
+ "notice.batchPartial": "Archived {done}, {failed} failed: {files}",
3423
+ "notice.batchDone": "Archived {done} — they are now in the archive and can be restored at any time",
3424
+ "notice.locked": "Locked {file} — auto-archive will not select it and a manual archive will be refused",
3425
+ "notice.unlocked": "Unlocked {file}",
3426
+ "notice.restored": "Restored {file} — it is back among the active memories",
3427
+ "notice.restoredIndexed": "Restored {file} — it is back among the active memories and was added to the index",
3428
+ "notice.purged": "Permanently deleted {file} — it is gone from the archive as well and cannot be recovered",
3429
+ };
3430
+
3431
+ /**
3432
+ * Local placeholder fill, used only when the host has no locale service.
3433
+ * Mirrors the locale runtime: `{name}` is replaced, unknown names stay.
3434
+ */
3435
+ function i18nFill(template, params) {
3436
+ if (!params) return template;
3437
+ return String(template).replace(/\{(\w+)\}/g, function (match, name) {
3438
+ return Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : match;
3439
+ });
3440
+ }
3441
+
3442
+ /** The zh dictionary is the fallback: a host without locale renders today's UI. */
3443
+ function i18nZh(key, params) {
3444
+ return i18nFill(Object.prototype.hasOwnProperty.call(ZH, key) ? ZH[key] : key, params);
3445
+ }
3446
+
3447
+ // Bound in `apply` when (and only when) the host provides the locale service.
3448
+ let t = i18nZh;
3449
+
3450
+ /** Known type ids get translated; unknown frontmatter values stay visible. */
3451
+ function typeLabel(id) {
3452
+ const key = "type." + String(id);
3453
+ return Object.prototype.hasOwnProperty.call(ZH, key) ? t(key) : String(id);
3454
+ }
3455
+
3456
+ /** Same for the tab names used in the directory tooltip. */
3457
+ function tabLabel(id) {
3458
+ const key = "tab." + String(id);
3459
+ return Object.prototype.hasOwnProperty.call(ZH, key) ? t(key) : "";
3460
+ }
3461
+
3462
+ /**
3463
+ * Register the dictionaries and bind `t`, optionally.
3464
+ *
3465
+ * `locale` is NOT a hard dependency (it is absent in older hosts, the
3466
+ * render harness and the unit tests): without it everything above keeps
3467
+ * rendering from `ZH`. Registration goes through `ctx.effect`, so the
3468
+ * dictionary is withdrawn when this plugin is disposed.
3469
+ */
3470
+ function setupI18n(ctx) {
3471
+ let locale = null;
3472
+ try {
3473
+ if (ctx && typeof ctx.get === "function") locale = ctx.get("locale");
3474
+ if (!locale && ctx && ctx.locale) locale = ctx.locale;
3475
+ } catch (e) {
3476
+ locale = null;
3477
+ }
3478
+ if (!locale || typeof locale.bind !== "function") return false;
3479
+ const install = function () {
3480
+ let dispose = null;
3481
+ if (typeof locale.register === "function") {
3482
+ dispose = locale.register(NS, { zh: ZH, en: EN });
3483
+ } else {
3484
+ return null;
3485
+ }
3486
+ t = locale.bind(NS);
3487
+ return dispose;
3488
+ };
3489
+ if (typeof ctx.effect === "function") ctx.effect(install, "sage-mem: i18n " + NS);
3490
+ else install();
3491
+ return true;
3492
+ }
3067
3493
  /**
3068
3494
  * 「自动做梦」面板 —— 让记忆自己整理一遍(引擎在 宿主侧 lib/autodream.js)。
3069
3495
  *
@@ -3964,24 +4390,24 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
3964
4390
  }));
3965
4391
  } else if (totalCount === 0) {
3966
4392
  filesView = h("div", { className: "smem-empty", key: "empty" }, [
3967
- h("div", { key: "t" }, "还没有记忆文件。对话中值得记住的事会写到这里,也可以手动写入。"),
3968
- h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "add", onClick: openAdd }, "+ 添加记忆"),
3969
- h("button", { type: "button", className: "smem-btn", key: "idx", title: "写入一份 MEMORY.md 索引骨架(已有索引不会覆盖)", onClick: createIndexSkeleton }, "建立索引骨架"),
4393
+ h("div", { key: "t" }, t("files.emptyTitle")),
4394
+ h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "add", onClick: openAdd }, t("action.add")),
4395
+ h("button", { type: "button", className: "smem-btn", key: "idx", title: t("files.skeletonTitle"), onClick: createIndexSkeleton }, t("files.skeleton")),
3970
4396
  ]);
3971
4397
  } else if (statusFilter === "archived" && archiveItems.length === 0) {
3972
4398
  filesView = h("div", { className: "smem-empty", key: "empty" }, [
3973
- h("div", { key: "t" }, "档案馆是空的 —— 在活动记忆那张卡上点「归档」,它就会收到这里。"),
3974
- h("button", { type: "button", className: "smem-btn", key: "back", onClick: function () { setStatusFilter("active"); } }, "回文件列表"),
4399
+ h("div", { key: "t" }, t("files.archiveEmpty")),
4400
+ h("button", { type: "button", className: "smem-btn", key: "back", onClick: function () { setStatusFilter("active"); } }, t("files.backToFiles")),
3975
4401
  ]);
3976
4402
  } else if (statusFilter === "active" && items.length === 0) {
3977
4403
  filesView = h("div", { className: "smem-empty", key: "empty" }, [
3978
- h("div", { key: "t" }, "活动记忆是空的 —— 状态筛到「已归档」还能捞出 " + archiveItems.length + " 条。"),
3979
- h("button", { type: "button", className: "smem-btn", key: "go", onClick: function () { setStatusFilter("archived"); } }, "切到已归档"),
4404
+ h("div", { key: "t" }, t("files.activeEmpty", { count: archiveItems.length })),
4405
+ h("button", { type: "button", className: "smem-btn", key: "go", onClick: function () { setStatusFilter("archived"); } }, t("files.goArchived")),
3980
4406
  ]);
3981
4407
  } else if (filtered.length === 0) {
3982
4408
  filesView = h("div", { className: "smem-empty", key: "empty" }, [
3983
- h("div", { key: "t" }, "没有符合搜索或筛选条件的文件。"),
3984
- h("button", { type: "button", className: "smem-btn", key: "clear", onClick: function () { setStatusFilter("all"); setTypeFilter([]); setTagFilter([]); setQuery(""); } }, "清除筛选"),
4409
+ h("div", { key: "t" }, t("files.noMatch")),
4410
+ h("button", { type: "button", className: "smem-btn", key: "clear", onClick: function () { setStatusFilter("all"); setTypeFilter([]); setTagFilter([]); setQuery(""); } }, t("filter.clear")),
3985
4411
  ]);
3986
4412
  } else {
3987
4413
  const list = h("div", { className: "smem-list", key: "list" }, filtered.map(function (it) {
@@ -4033,9 +4459,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4033
4459
  }));
4034
4460
  filesView = statusFilter === "archived"
4035
4461
  ? [
4036
- h("div", { className: "smem-ar-banner", key: "banner" },
4037
- "档案馆里的记忆不参与检索与星图,原文仍在记忆目录的 archive/ 下 —— 恢复后它就回到活动列表;" +
4038
- "「编辑」是原样文本编辑,改完存回同一份文件;「彻底删除」会真的从 archive/ 里删掉它(不可恢复,删除前还要再确认一次)。"),
4462
+ h("div", { className: "smem-ar-banner", key: "banner" }, t("files.archiveIntro")),
4039
4463
  list,
4040
4464
  ]
4041
4465
  : list;
@@ -4054,56 +4478,56 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4054
4478
  hasFilter
4055
4479
  ? h("div", { className: "smem-filter-summary", key: "sum" }, [
4056
4480
  statusFilter !== "all"
4057
- ? sumItem("状态:" + (statusFilter === "active" ? "活动中" : "已归档"), function () { setStatusFilter("all"); })
4481
+ ? sumItem(t("filter.sumStatus", { value: statusFilter === "active" ? t("filter.active") : t("filter.archived") }), function () { setStatusFilter("all"); })
4058
4482
  : null,
4059
- typeFilter.map(function (t) {
4060
- return sumItem("类型:" + (TYPE_LABEL[t] || t), function () { toggleIn(setTypeFilter, t); });
4483
+ typeFilter.map(function (typeId) {
4484
+ return sumItem(t("filter.sumType", { value: typeLabel(typeId) }), function () { toggleIn(setTypeFilter, typeId); });
4061
4485
  }),
4062
4486
  tagFilter.map(function (g) {
4063
- return sumItem("标签:#" + g, function () { toggleIn(setTagFilter, g); });
4487
+ return sumItem(t("filter.sumTag", { value: g }), function () { toggleIn(setTagFilter, g); });
4064
4488
  }),
4065
- query.trim() ? sumItem("搜索:" + query.trim(), function () { setQuery(""); }) : null,
4489
+ query.trim() ? sumItem(t("filter.sumQuery", { value: query.trim() }), function () { setQuery(""); }) : null,
4066
4490
  h("button", {
4067
4491
  type: "button", className: "smem-sum-clear", key: "all",
4068
4492
  onClick: function () { setStatusFilter("all"); setTypeFilter([]); setTagFilter([]); setQuery(""); },
4069
- }, "清除全部"),
4493
+ }, t("filter.clearAll")),
4070
4494
  ])
4071
4495
  : null,
4072
- h("span", { className: "smem-filters-label", key: "lst" }, "状态"),
4496
+ h("span", { className: "smem-filters-label", key: "lst" }, t("filter.statusLabel")),
4073
4497
  h("button", {
4074
4498
  type: "button", key: "st-all", className: "smem-chip",
4075
4499
  "data-on": statusFilter === "all" ? "1" : "0",
4076
- title: "活动与已归档一起看(归档条走灰色)",
4500
+ title: t("filter.allTitle"),
4077
4501
  onClick: function () { setStatusFilter("all"); },
4078
- }, ["全部", h("span", { className: "smem-chip-n", key: "n" }, String(totalCount))]),
4502
+ }, [t("filter.all"), h("span", { className: "smem-chip-n", key: "n" }, String(totalCount))]),
4079
4503
  h("button", {
4080
4504
  type: "button", key: "st-active", className: "smem-chip",
4081
4505
  "data-on": statusFilter === "active" ? "1" : "0",
4082
- title: "正在参与检索与星图的记忆",
4506
+ title: t("filter.activeTitle"),
4083
4507
  onClick: function () { setStatusFilter("active"); },
4084
- }, ["活动中", h("span", { className: "smem-chip-n", key: "n" }, String(items.length))]),
4508
+ }, [t("filter.active"), h("span", { className: "smem-chip-n", key: "n" }, String(items.length))]),
4085
4509
  h("button", {
4086
4510
  type: "button", key: "st-arch", className: "smem-chip",
4087
4511
  "data-on": statusFilter === "archived" ? "1" : "0",
4088
- title: "已收进档案馆:不参与检索与星图,可编辑 / 恢复",
4512
+ title: t("filter.archivedTitle"),
4089
4513
  onClick: function () { setStatusFilter("archived"); },
4090
- }, ["已归档", h("span", { className: "smem-chip-n", key: "n" }, String(archiveItems.length))]),
4514
+ }, [t("filter.archived"), h("span", { className: "smem-chip-n", key: "n" }, String(archiveItems.length))]),
4091
4515
  h("span", { className: "smem-sep", key: "sep0" }),
4092
- h("span", { className: "smem-filters-label", key: "lt" }, "类型"),
4516
+ h("span", { className: "smem-filters-label", key: "lt" }, t("filter.typeLabel")),
4093
4517
  h("button", {
4094
4518
  type: "button", key: "all", className: "smem-chip",
4095
4519
  "data-on": typeFilter.length ? "0" : "1",
4096
4520
  onClick: function () { setTypeFilter([]); },
4097
- }, ["全部", h("span", { className: "smem-chip-n", key: "n" }, String(statusItems.length))]),
4098
- typeList.map(function (t) {
4521
+ }, [t("filter.all"), h("span", { className: "smem-chip-n", key: "n" }, String(statusItems.length))]),
4522
+ typeList.map(function (typeId) {
4099
4523
  return h("button", {
4100
- type: "button", key: t, className: "smem-chip",
4101
- "data-on": typeFilter.indexOf(t) >= 0 ? "1" : "0",
4102
- onClick: function () { toggleIn(setTypeFilter, t); },
4103
- }, [TYPE_LABEL[t] || t, h("span", { className: "smem-chip-n", key: "n" }, String(typeCounts[t]))]);
4524
+ type: "button", key: typeId, className: "smem-chip",
4525
+ "data-on": typeFilter.indexOf(typeId) >= 0 ? "1" : "0",
4526
+ onClick: function () { toggleIn(setTypeFilter, typeId); },
4527
+ }, [typeLabel(typeId), h("span", { className: "smem-chip-n", key: "n" }, String(typeCounts[typeId]))]);
4104
4528
  }),
4105
4529
  tagList.length ? h("span", { className: "smem-sep", key: "sep" }) : null,
4106
- tagList.length ? h("span", { className: "smem-filters-label", key: "lgt" }, "标签") : null,
4530
+ tagList.length ? h("span", { className: "smem-filters-label", key: "lgt" }, t("filter.tags")) : null,
4107
4531
  tagShown.map(function (g) {
4108
4532
  return h("button", {
4109
4533
  type: "button", key: "tag-" + g, className: "smem-chip",
@@ -4114,15 +4538,15 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4114
4538
  tagList.length > TAG_VISIBLE_MAX
4115
4539
  ? h("button", {
4116
4540
  type: "button", key: "moretag", className: "smem-btn",
4117
- title: tagOpen ? "收起标签" : "还有一些标签没显示",
4541
+ title: tagOpen ? t("filter.collapseTags") : t("filter.moreTagsTitle"),
4118
4542
  onClick: function () { setTagOpen(!tagOpen); },
4119
- }, tagOpen ? "收起标签" : ("更多标签 (" + String(tagHiddenCount) + ")") )
4543
+ }, tagOpen ? t("filter.collapseTags") : t("filter.moreTags", { count: String(tagHiddenCount) }) )
4120
4544
  : null,
4121
4545
  hasFilter
4122
4546
  ? h("button", {
4123
4547
  type: "button", key: "clear", className: "smem-btn",
4124
4548
  onClick: function () { setStatusFilter("all"); setTypeFilter([]); setTagFilter([]); setQuery(""); },
4125
- }, "清除筛选")
4549
+ }, t("filter.clear"))
4126
4550
  : null,
4127
4551
 
4128
4552
  ]);
@@ -4270,8 +4694,8 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4270
4694
  remote.listFiles()
4271
4695
  .then(function (res) {
4272
4696
  if (res && typeof res === "object" && res.ok === false) {
4273
- const msg = res.error && res.error.message ? res.error.message : "remote 调用失败";
4274
- throw new Error("listFiles RPC 失败: " + msg);
4697
+ const msg = res.error && res.error.message ? res.error.message : t("error.remote");
4698
+ throw new Error(t("error.listFiles", { message: msg }));
4275
4699
  }
4276
4700
  const list = unwrap(res);
4277
4701
  setItems(Array.isArray(list) ? list : []);
@@ -4384,20 +4808,20 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4384
4808
  setError(null);
4385
4809
  setNotice("");
4386
4810
  if (!remote || typeof remote.readRaw !== "function" || typeof remote.writeRaw !== "function") {
4387
- setError("宿主侧还没有 readRaw / writeRaw 接口(插件版本较旧)");
4811
+ setError(t("error.noRaw"));
4388
4812
  return;
4389
4813
  }
4390
4814
  setBusy(true);
4391
4815
  // 先读再写:**已有索引绝不覆盖**。readRaw 对不存在的文件会 reject,正好当探针。
4392
4816
  remote.readRaw("MEMORY.md").then(function () {
4393
4817
  setBusy(false);
4394
- setNotice("MEMORY.md 已经存在,没有改动它。");
4818
+ setNotice(t("notice.indexExists"));
4395
4819
  }, function () {
4396
4820
  return remote.writeRaw("MEMORY.md", INDEX_SKELETON).then(function (res) {
4397
4821
  const out = unwrap(res);
4398
4822
  setBusy(false);
4399
- if (out && out.ok === false) { setError(out.error || "建立索引骨架失败"); return; }
4400
- setNotice("已建立 MEMORY.md 索引骨架。");
4823
+ if (out && out.ok === false) { setError(out.error || t("error.skeletonFailed")); return; }
4824
+ setNotice(t("notice.indexCreated"));
4401
4825
  load();
4402
4826
  });
4403
4827
  }).catch(function (e) {
@@ -4409,13 +4833,13 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4409
4833
  function openMemoryFolder() {
4410
4834
  setError(null);
4411
4835
  if (!remote || typeof remote.openMemoryDir !== "function") {
4412
- setError("宿主侧还没有 openMemoryDir 接口(插件版本较旧)");
4836
+ setError(t("error.noOpenDir"));
4413
4837
  return;
4414
4838
  }
4415
4839
  remote.openMemoryDir()
4416
4840
  .then(function (res) {
4417
4841
  const result = unwrap(res);
4418
- if (result && result.ok === false) setError(result.error || "打开记忆目录失败");
4842
+ if (result && result.ok === false) setError(result.error || t("error.openDirFailed"));
4419
4843
  })
4420
4844
  .catch(function (e) { setError(String((e && e.message) || e)); });
4421
4845
  }
@@ -4462,7 +4886,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4462
4886
  }
4463
4887
 
4464
4888
  function submit() {
4465
- if (!formDesc.trim()) { setError("描述不能为空"); return; }
4889
+ if (!formDesc.trim()) { setError(t("error.descEmpty")); return; }
4466
4890
  setBusy(true); setError(null); setNotice("");
4467
4891
  const type = formType;
4468
4892
  const name = (formName.trim() || (type + "_note.md"));
@@ -4487,9 +4911,9 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4487
4911
  remote.writeFile(safeName, content)
4488
4912
  .then(function (r) {
4489
4913
  // host 侧对保留名 / 非字符串内容 / 超限一律返回 { ok: false, error },不再抛异常。
4490
- if (r && r.ok === false) { setError(r.error || "写入被拒绝"); return; }
4914
+ if (r && r.ok === false) { setError(r.error || t("error.writeRejected")); return; }
4491
4915
  closeForm();
4492
- setNotice(editingFile ? "已更新 " + safeName : "已保存 " + safeName);
4916
+ setNotice(t(editingFile ? "notice.updated" : "notice.saved", { file: safeName }));
4493
4917
  load();
4494
4918
  })
4495
4919
  .catch(function (e) { setError(String((e && e.message) || e)); })
@@ -4500,9 +4924,9 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4500
4924
  setBusy(true); setError(null); setNotice("");
4501
4925
  remote.deleteFile(file)
4502
4926
  .then(function (r) {
4503
- if (r && r.ok === false) { setError(r.error || "删除被拒绝"); return; }
4927
+ if (r && r.ok === false) { setError(r.error || t("error.deleteRejected")); return; }
4504
4928
  setDeleting(null);
4505
- setNotice("已删除 " + file);
4929
+ setNotice(t("notice.deleted", { file: file }));
4506
4930
  load();
4507
4931
  })
4508
4932
  .catch(function (e) { setError(String((e && e.message) || e)); })
@@ -4517,16 +4941,16 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4517
4941
  */
4518
4942
  function confirmArchive(file) {
4519
4943
  if (typeof remote.archive !== "function") {
4520
- setError("宿主侧还没有 archive 接口(插件版本较旧)");
4944
+ setError(t("error.noArchive"));
4521
4945
  return;
4522
4946
  }
4523
4947
  setBusy(true); setError(null); setNotice("");
4524
4948
  remote.archive(file, archiveReason.trim())
4525
4949
  .then(function (r) {
4526
- if (r && r.ok === false) { setError(r.error || "归档被拒绝"); return; }
4950
+ if (r && r.ok === false) { setError(r.error || t("error.archiveRejected")); return; }
4527
4951
  setArchiving(null);
4528
4952
  setArchiveReason("");
4529
- setNotice("已归档 " + file + " —— 它现在在「已归档」视图里,随时可以恢复");
4953
+ setNotice(t("notice.archived", { file: file }));
4530
4954
  load();
4531
4955
  })
4532
4956
  .catch(function (e) { setError(String((e && e.message) || e)); })
@@ -4541,17 +4965,17 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4541
4965
  */
4542
4966
  function toggleLock(file, locked) {
4543
4967
  if (typeof remote.setLocked !== "function") {
4544
- setError("宿主侧还没有 setLocked 接口(插件版本较旧)");
4968
+ setError(t("error.noLock"));
4545
4969
  return;
4546
4970
  }
4547
4971
  setBusy(true); setError(null); setNotice("");
4548
4972
  remote.setLocked(file, locked)
4549
4973
  .then(function (r) {
4550
4974
  const d = unwrap(r);
4551
- if (d && d.ok === false) { setError(d.error || "锁定操作被拒绝"); return; }
4975
+ if (d && d.ok === false) { setError(d.error || t("error.lockRejected")); return; }
4552
4976
  setNotice(locked
4553
- ? ("已锁定 " + file + " —— 自动归档不会再选它,手动归档也会被拒绝")
4554
- : ("已解锁 " + file));
4977
+ ? t("notice.locked", { file: file })
4978
+ : t("notice.unlocked", { file: file }));
4555
4979
  load();
4556
4980
  })
4557
4981
  .catch(function (e) { setError(String((e && e.message) || e)); })
@@ -4560,7 +4984,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4560
4984
 
4561
4985
  /** 从档案馆捞回来:文件移回记忆目录,归档留痕被抹掉。 */
4562
4986
  function restoreFile(file) { if (typeof remote.restore !== "function") {
4563
- setError("宿主侧还没有 restore 接口(插件版本较旧)");
4987
+ setError(t("error.noRestore"));
4564
4988
  return;
4565
4989
  }
4566
4990
  setBusy(true); setError(null); setNotice("");
@@ -4568,9 +4992,9 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4568
4992
  setArchDelete(null);
4569
4993
  remote.restore(file)
4570
4994
  .then(function (r) {
4571
- if (r && r.ok === false) { setError(r.error || "恢复被拒绝"); return; }
4995
+ if (r && r.ok === false) { setError(r.error || t("error.restoreRejected")); return; }
4572
4996
  // 恢复会顺带把这条补进索引:说清楚,否则用户不知道「漏索引」是怎么好的
4573
- setNotice("已恢复 " + file + " —— 它回到了活动记忆里" + (r && r.indexed ? ",并补进了索引" : ""));
4997
+ setNotice(t(r && r.indexed ? "notice.restoredIndexed" : "notice.restored", { file: file }));
4574
4998
  load();
4575
4999
  })
4576
5000
  .catch(function (e) { setError(String((e && e.message) || e)); })
@@ -4585,7 +5009,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4585
5009
  */
4586
5010
  function deleteArchivedFile(file) {
4587
5011
  if (typeof remote.deleteArchived !== "function") {
4588
- setError("宿主侧还没有 deleteArchived 接口(插件版本较旧)");
5012
+ setError(t("error.noDeleteArchived"));
4589
5013
  return;
4590
5014
  }
4591
5015
  const item = archiveItems.filter(function (x) { return x.file === file; })[0];
@@ -4593,9 +5017,9 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4593
5017
  setBusy(true); setError(null); setNotice("");
4594
5018
  remote.deleteArchived(file, stamp)
4595
5019
  .then(function (r) {
4596
- if (r && r.ok === false) { setError(r.error || "删除被拒绝"); return; }
5020
+ if (r && r.ok === false) { setError(r.error || t("error.deleteRejected")); return; }
4597
5021
  setArchDelete(null);
4598
- setNotice("已彻底删除 " + file + " —— 归档区里也没有它了,找不回来");
5022
+ setNotice(t("notice.purged", { file: file }));
4599
5023
  load();
4600
5024
  })
4601
5025
  .catch(function (e) { setError(String((e && e.message) || e)); })
@@ -4612,7 +5036,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4612
5036
  */
4613
5037
  function openArchEdit(file) {
4614
5038
  if (typeof remote.readArchived !== "function") {
4615
- setError("宿主侧还没有 readArchived 接口(插件版本较旧)");
5039
+ setError(t("error.noReadArchived"));
4616
5040
  return;
4617
5041
  }
4618
5042
  setError(null); setNotice("");
@@ -4635,16 +5059,16 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4635
5059
 
4636
5060
  function saveArchEdit(file) {
4637
5061
  if (typeof remote.writeArchived !== "function") {
4638
- setError("宿主侧还没有 writeArchived 接口(插件版本较旧)");
5062
+ setError(t("error.noWriteArchived"));
4639
5063
  return;
4640
5064
  }
4641
5065
  setArchSaving(true); setError(null); setNotice("");
4642
5066
  // 原样交回:不 trim、不补 frontmatter、不做任何「智能」处理。
4643
5067
  remote.writeArchived(file, archText)
4644
5068
  .then(function (r) {
4645
- if (r && r.ok === false) { setError(r.error || "保存被拒绝"); return; }
5069
+ if (r && r.ok === false) { setError(r.error || t("error.saveRejected")); return; }
4646
5070
  setArchEdit(null);
4647
- setNotice("已保存 " + file);
5071
+ setNotice(t("notice.saved", { file: file }));
4648
5072
  load();
4649
5073
  })
4650
5074
  .catch(function (e) { setError(String((e && e.message) || e)); })
@@ -4656,7 +5080,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4656
5080
  // ── 工具面板的三个数据通道 ──────────────────────────────────────
4657
5081
 
4658
5082
  function runAudit() {
4659
- if (typeof remote.audit !== "function") { setError("宿主侧还没有 audit 接口(插件版本较旧)"); return; }
5083
+ if (typeof remote.audit !== "function") { setError(t("error.noAudit")); return; }
4660
5084
  setAuditBusy(true); setError(null);
4661
5085
  remote.audit()
4662
5086
  .then(function (r) { setAudit(unwrap(r) || null); })
@@ -4665,7 +5089,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4665
5089
  }
4666
5090
 
4667
5091
  function loadCandidates() {
4668
- if (typeof remote.archiveCandidates !== "function") { setError("宿主侧还没有 archiveCandidates 接口(插件版本较旧)"); return; }
5092
+ if (typeof remote.archiveCandidates !== "function") { setError(t("error.noArchiveCandidates")); return; }
4669
5093
  setCandBusy(true); setError(null);
4670
5094
  remote.archiveCandidates()
4671
5095
  .then(function (r) {
@@ -4682,7 +5106,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4682
5106
  }
4683
5107
 
4684
5108
  function loadSettings() {
4685
- if (typeof remote.getSettings !== "function") { setError("宿主侧还没有 getSettings 接口(插件版本较旧)"); return; }
5109
+ if (typeof remote.getSettings !== "function") { setError(t("error.noGetSettings")); return; }
4686
5110
  remote.getSettings()
4687
5111
  .then(function (r) { const d = unwrap(r) || {}; setSettings(d); })
4688
5112
  .catch(function (e) { setError(String((e && e.message) || e)); });
@@ -4750,7 +5174,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4750
5174
  */
4751
5175
  function archiveCandidates(list) {
4752
5176
  if (!list.length) return;
4753
- if (typeof remote.archive !== "function") { setError("宿主侧还没有 archive 接口(插件版本较旧)"); return; }
5177
+ if (typeof remote.archive !== "function") { setError(t("error.noArchive")); return; }
4754
5178
  setBusy(true); setError(null); setNotice("");
4755
5179
  const done = [];
4756
5180
  const failed = [];
@@ -4760,8 +5184,8 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4760
5184
  setCandConfirm(null);
4761
5185
  setPicked({});
4762
5186
  setNotice(failed.length
4763
- ? ("已归档 " + done.length + " 条," + failed.length + " 条没成功:" + failed.join("、"))
4764
- : ("已归档 " + done.length + " 条 —— 它们现在在档案馆里,随时可以恢复"));
5187
+ ? t("notice.batchPartial", { done: done.length, failed: failed.length, files: failed.join("、") })
5188
+ : t("notice.batchDone", { done: done.length }));
4765
5189
  load();
4766
5190
  loadCandidates();
4767
5191
  return;
@@ -4778,7 +5202,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4778
5202
  }
4779
5203
 
4780
5204
  function openRaw(name) {
4781
- if (typeof remote.readRaw !== "function") { setError("宿主侧还没有 readRaw 接口(插件版本较旧)"); return; }
5205
+ if (typeof remote.readRaw !== "function") { setError(t("error.noReadRaw")); return; }
4782
5206
  setError(null); setNotice("");
4783
5207
  setRawName(name);
4784
5208
  setRawText("");
@@ -4801,20 +5225,20 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4801
5225
  setRawLoading(false);
4802
5226
  return;
4803
5227
  }
4804
- setError("读不到 " + name + ":" + msg);
5228
+ setError(t("error.readFailed", { file: name, message: msg }));
4805
5229
  setRawLoading(false);
4806
5230
  setRawName(null);
4807
5231
  });
4808
5232
  }
4809
5233
 
4810
5234
  function saveRaw(name) {
4811
- if (typeof remote.writeRaw !== "function") { setError("宿主侧还没有 writeRaw 接口(插件版本较旧)"); return; }
5235
+ if (typeof remote.writeRaw !== "function") { setError(t("error.noWriteRaw")); return; }
4812
5236
  setRawBusy(true); setError(null); setNotice("");
4813
5237
  // 原样交回:不 trim、不补 frontmatter、不做任何「智能」处理。
4814
5238
  remote.writeRaw(name, rawText)
4815
5239
  .then(function (r) {
4816
- if (r && r.ok === false) { setError(r.error || "保存被拒绝"); return; }
4817
- setNotice((rawIsNew ? "已新建 " : "已保存 ") + name);
5240
+ if (r && r.ok === false) { setError(r.error || t("error.saveRejected")); return; }
5241
+ setNotice(t(rawIsNew ? "notice.rawNew" : "notice.saved", { file: name }));
4818
5242
  setRawIsNew(false);
4819
5243
  })
4820
5244
  .catch(function (e) { setError(String((e && e.message) || e)); })
@@ -4883,12 +5307,13 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4883
5307
  function sumItem(label, onRemove) {
4884
5308
  return h("span", { className: "smem-sum-item", key: "sum-" + label }, [
4885
5309
  label,
4886
- h("button", { type: "button", className: "smem-sum-x", key: "x", title: "撤掉这项筛选", onClick: onRemove }, "×"),
5310
+ h("button", { type: "button", className: "smem-sum-x", key: "x", title: t("filter.remove"), onClick: onRemove }, "×"),
4887
5311
  ]);
4888
5312
  }
4889
5313
  // 两个计数永远一起露脸:用户不必先筛状态才知道档案馆里有没有东西。
4890
- const headerCount = (hasFilter ? "筛出 " + String(filtered.length) + " / " + String(statusItems.length) + " · " : "") +
4891
- "活动中 " + String(items.length) + " · 已归档 " + String(archiveItems.length);
5314
+ const headerCount = hasFilter
5315
+ ? t("header.countsFiltered", { shown: filtered.length, total: statusItems.length, active: items.length, archived: archiveItems.length })
5316
+ : t("header.counts", { active: items.length, archived: archiveItems.length });
4892
5317
  // 头部带的「待重启生效」徽标:有多少项要重启才生效,从 limits / pendingLimits 的差算出来
4893
5318
  // (reserved 名单的变化 getSettings 只给 restartRequired 布尔值,数不出来就不写数)。
4894
5319
  const LIMIT_KEYS = ["maxResults", "maxChars", "maxBaseline", "maxSessionBytes", "staleDays"];
@@ -4919,39 +5344,39 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4919
5344
 
4920
5345
  return h("div", { className: "smem-root" }, [
4921
5346
  h("div", { className: "smem-tabs", key: "tabs", role: "tablist" }, [
4922
- h("button", { type: "button", className: "smem-tab" + (tab === "files" ? " smem-tab--on" : ""), key: "f", role: "tab", "aria-selected": tab === "files" ? "true" : "false", onClick: function () { setTab("files"); } }, "文件列表"),
4923
- h("button", { type: "button", className: "smem-tab" + (tab === "star" ? " smem-tab--on" : ""), key: "s", role: "tab", "aria-selected": tab === "star" ? "true" : "false", onClick: function () { setTab("star"); } }, "记忆星图"),
4924
- h("button", { type: "button", className: "smem-tab" + (tab === "autodream" ? " smem-tab--on" : ""), key: "d", role: "tab", "aria-selected": tab === "autodream" ? "true" : "false", onClick: function () { setTab("autodream"); } }, "自动做梦"),
5347
+ h("button", { type: "button", className: "smem-tab" + (tab === "files" ? " smem-tab--on" : ""), key: "f", role: "tab", "aria-selected": tab === "files" ? "true" : "false", onClick: function () { setTab("files"); } }, t("tab.files")),
5348
+ h("button", { type: "button", className: "smem-tab" + (tab === "star" ? " smem-tab--on" : ""), key: "s", role: "tab", "aria-selected": tab === "star" ? "true" : "false", onClick: function () { setTab("star"); } }, t("tab.star")),
5349
+ h("button", { type: "button", className: "smem-tab" + (tab === "autodream" ? " smem-tab--on" : ""), key: "d", role: "tab", "aria-selected": tab === "autodream" ? "true" : "false", onClick: function () { setTab("autodream"); } }, t("tab.autodream")),
4925
5350
  // 视觉分组:左边「看」(文件列表 / 星图),右边「设置 · 自动化」。
4926
5351
  h("span", { className: "smem-tabs-sep", key: "sep", "aria-hidden": "true" }),
4927
5352
  // 「自动归档」排在自动做梦之后:归档是**确定性规则**(按闲置天数算,不调模型),
4928
5353
  // 和「让模型整理记忆」是两件事。塞在自动做梦的设置里用户找不到
4929
5354
  // (2026-10-11 用户原话:「扔这里连我都找不到」)。
4930
- h("button", { type: "button", className: "smem-tab" + (tab === "archive" ? " smem-tab--on" : ""), key: "ar", role: "tab", "aria-selected": tab === "archive" ? "true" : "false", onClick: function () { setTab("archive"); } }, "自动归档"),
5355
+ h("button", { type: "button", className: "smem-tab" + (tab === "archive" ? " smem-tab--on" : ""), key: "ar", role: "tab", "aria-selected": tab === "archive" ? "true" : "false", onClick: function () { setTab("archive"); } }, t("tab.archive")),
4931
5356
  // 体检再排在「自动归档」之后、独立成一屏:它只报告不修改,和「文件列表」里的
4932
5357
  // 增删改是两件事,塞在工具抽屉里会让人以为它是个动作按钮。
4933
- h("button", { type: "button", className: "smem-tab" + (tab === "audit" ? " smem-tab--on" : ""), key: "a", role: "tab", "aria-selected": tab === "audit" ? "true" : "false", onClick: openAuditTab }, "体检"),
5358
+ h("button", { type: "button", className: "smem-tab" + (tab === "audit" ? " smem-tab--on" : ""), key: "a", role: "tab", "aria-selected": tab === "audit" ? "true" : "false", onClick: openAuditTab }, t("tab.audit")),
4934
5359
  ]),
4935
5360
  // 四个 tab 共用的一条头部带:我在哪 · 目录状态 · 活动/归档计数 · 体检徽标 · 待重启徽标 · 主操作。
4936
5361
  // 层级形状固定:导航=下划线(.smem-tabs)、筛选=chip、动作=按钮,三种不混用。
4937
5362
  h("div", { className: "smem-head", key: "head" }, [
4938
- h("span", { className: "smem-title", key: "t" }, "记忆管理"),
5363
+ h("span", { className: "smem-title", key: "t" }, t("section.label")),
4939
5364
  // 目录状态收进状态点的 title(不可读时下面另有一条 smem-err,错误本来就该显眼)。
4940
- h("span", { className: "smem-dot" + (dirOk ? "" : " smem-dot--bad"), key: "dot", title: (dirOk ? "memory 目录可读" : "memory 目录不可读") + " · " + String(TAB_LABEL[tab] || "") }),
5365
+ h("span", { className: "smem-dot" + (dirOk ? "" : " smem-dot--bad"), key: "dot", title: t("header.dirTitle", { state: dirOk ? t("header.dirReadable") : t("header.dirUnreadable"), tab: tabLabel(tab) }) }),
4941
5366
  h("span", { className: "smem-badge", key: "cnt" }, headerCount),
4942
5367
  // 体检徽标:**抽屉关着也一直看得见**,点它就切到「体检」那一屏。
4943
5368
  h("button", {
4944
5369
  type: "button", key: "aud",
4945
5370
  className: "smem-tool-badge" + (audit && audit.problems ? " smem-tool-badge--bad" : ""),
4946
- title: audit ? (audit.problems ? "切到体检看是哪几条" : "体检没有发现问题") : "跑一次体检(只读)",
5371
+ title: audit ? (audit.problems ? t("header.auditSwitch") : t("header.auditClean")) : t("header.auditRun"),
4947
5372
  onClick: openAuditTab,
4948
- }, audit ? (audit.problems ? ("体检 " + audit.problems + " 个问题") : "体检 通过") : "体检 · 未跑"),
5373
+ }, audit ? (audit.problems ? t("header.auditProblems", { count: audit.problems }) : t("header.auditPass")) : t("header.auditNotRun")),
4949
5374
  restartPending
4950
5375
  ? h("button", {
4951
5376
  type: "button", key: "rs", className: "smem-tool-badge smem-ad-badge--warn",
4952
- title: "有改动要重启 DSH 才生效",
5377
+ title: t("header.restartTitle"),
4953
5378
  onClick: function () { setTab("autodream"); setAdSeg("settings"); },
4954
- }, (pendingDiff ? (pendingDiff + " 项") : "") + "待重启生效")
5379
+ }, t(pendingDiff ? "header.restartPendingCount" : "header.restartPending", { count: pendingDiff }))
4955
5380
  : null,
4956
5381
  // 主操作:只放当前 tab 的那一个(星图/自动做梦的主操作在各自面板里,不重复摆一份)。
4957
5382
  // 弹簧:把主操作顶到右边。**换行时它留在上一行**,主操作于是落到下一行、
@@ -4959,30 +5384,30 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4959
5384
  // 也推到最右,看起来像"跑出格子了")。
4960
5385
  h("span", { className: "smem-head-spacer", key: "sp" }),
4961
5386
  h("span", { className: "smem-head-actions", key: "act" }, tab === "files" ? [
4962
- h("button", { type: "button", className: "smem-btn", key: "open", disabled: busy || !dirOk, title: "在系统文件管理器里打开记忆目录", onClick: openMemoryFolder }, "打开记忆文件夹"),
4963
- h("button", { type: "button", className: "smem-btn", key: "refresh", disabled: busy || loading, onClick: load }, "刷新"),
4964
- h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "add", onClick: openAdd }, "+ 添加记忆"),
5387
+ h("button", { type: "button", className: "smem-btn", key: "open", disabled: busy || !dirOk, title: t("header.openFolderTitle"), onClick: openMemoryFolder }, t("header.openFolder")),
5388
+ h("button", { type: "button", className: "smem-btn", key: "refresh", disabled: busy || loading, onClick: load }, t("header.refresh")),
5389
+ h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "add", onClick: openAdd }, t("action.add")),
4965
5390
  ] : null),
4966
5391
  ]),
4967
5392
  // 更新提示:四个 tab 共用一条,只在真有新版时出现;「×」只是这次不看
4968
5393
  updateInfo
4969
5394
  ? h("div", { className: "smem-update", key: "update" }, [
4970
- h("span", { key: "t" }, "发现新版本 v" + String(updateInfo.latest) + "(当前 v" + String(updateInfo.current) + ")"),
5395
+ h("span", { key: "t" }, t("update.found", { latest: updateInfo.latest, current: updateInfo.current })),
4971
5396
  h("a", {
4972
5397
  key: "l", className: "smem-update-link",
4973
5398
  href: updateInfo.releasesUrl || "https://github.com/gezi-wen/sage-mem/releases",
4974
5399
  target: "_blank", rel: "noreferrer",
4975
- }, "看更新内容"),
5400
+ }, t("update.notes")),
4976
5401
  h("button", {
4977
5402
  type: "button", key: "x", className: "smem-update-x",
4978
- title: "这次不看了(下次进面板还会提示)",
5403
+ title: t("update.dismissTitle"),
4979
5404
  onClick: function () { setUpdateInfo(null); },
4980
5405
  }, "×"),
4981
5406
  ])
4982
5407
  : null,
4983
5408
  // 工具栏只剩「搜索 + 工具 ▾」:刷新与添加已经收进头部带的主操作位。
4984
5409
  tab === "files" ? h("div", { className: "smem-toolbar", key: "bar" }, [
4985
- h("input", { className: "smem-search", key: "q", value: query, placeholder: "搜索文件名 / 描述 / 类型 / 标签…", onChange: function (e) { setQuery(e.target.value); } }),
5410
+ h("input", { className: "smem-search", key: "q", value: query, placeholder: t("toolbar.search"), onChange: function (e) { setQuery(e.target.value); } }),
4986
5411
  // 工具抽屉:关着时这里只有一个按钮,零占位;面板一次只开一个。
4987
5412
  // `data-smem-drawer` 是「点外部关闭」的排除标记:这一整块里的点击
4988
5413
  // 都算「抽屉内部」,包括开抽屉的那一次点击本身(见上面 effect 的长注释)。
@@ -4992,27 +5417,27 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4992
5417
  className: "smem-btn" + (drawer ? " smem-btn-primary" : ""),
4993
5418
  "aria-expanded": drawer ? "true" : "false",
4994
5419
  "aria-haspopup": "menu",
4995
- title: "归档候选 / 保留名编辑" + (dbgOn ? " / 注入调试" : ""),
5420
+ title: t(dbgOn ? "tools.titleDebug" : "tools.title"),
4996
5421
  onClick: function () { setDrawer(!drawer); },
4997
- }, "工具 ▾"),
5422
+ }, t("tools.button")),
4998
5423
  drawer ? h("div", { className: "smem-drawer-pop", key: "pop" }, [
4999
5424
  // 体检独立成 tab 之后,抽屉里只剩这两件「要动手」的事。
5000
5425
  h("button", {
5001
5426
  type: "button", className: "smem-drawer-item", key: "c",
5002
5427
  "data-on": tool === "candidates" ? "1" : "0",
5003
5428
  onClick: function () { toggleTool("candidates"); setDrawer(false); },
5004
- }, ["归档候选", h("span", { className: "smem-status", key: "s" }, cand ? (cand.count + " 条") : "未取")]),
5429
+ }, [t("tools.candidates"), h("span", { className: "smem-status", key: "s" }, cand ? t("tools.candidatesCount", { count: cand.count }) : t("tools.candidatesEmpty"))]),
5005
5430
  h("button", {
5006
5431
  type: "button", className: "smem-drawer-item", key: "r",
5007
5432
  "data-on": tool === "raw" ? "1" : "0",
5008
5433
  onClick: function () { toggleTool("raw"); setDrawer(false); },
5009
- }, ["保留名编辑", h("span", { className: "smem-status", key: "s" }, settings && settings.reserved ? (settings.reserved.length + " 个") : "")]),
5434
+ }, [t("tools.raw"), h("span", { className: "smem-status", key: "s" }, settings && settings.reserved ? t("tools.rawCount", { count: settings.reserved.length }) : "")]),
5010
5435
  // 1.7.0:只在调试开关打开时出现(关着时抽屉仍是两项,界面零变化)
5011
5436
  dbgOn ? h("button", {
5012
5437
  type: "button", className: "smem-drawer-item", key: "d",
5013
5438
  "data-on": tool === "debug" ? "1" : "0",
5014
5439
  onClick: function () { toggleTool("debug"); setDrawer(false); },
5015
- }, ["注入调试", h("span", { className: "smem-status", key: "s" }, dbg && Array.isArray(dbg.records) ? (dbg.records.length + " 次") : "")]) : null,
5440
+ }, [t("tools.debug"), h("span", { className: "smem-status", key: "s" }, dbg && Array.isArray(dbg.records) ? t("tools.debugCount", { count: dbg.records.length }) : "")]) : null,
5016
5441
  ]) : null,
5017
5442
  ]),
5018
5443
  ]) : null,
@@ -5090,25 +5515,25 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
5090
5515
  notice && !error ? h("div", { className: "smem-ok", key: "ok", role: "status" }, "✓ " + notice) : null,
5091
5516
  formOpen
5092
5517
  ? h("div", { className: "smem-form", key: "form" }, [
5093
- h("div", { className: "smem-form-title", key: "ft" }, editingFile ? "编辑 " + editingFile : "添加记忆"),
5518
+ h("div", { className: "smem-form-title", key: "ft" }, editingFile ? t("form.editTitle", { file: editingFile }) : t("form.addTitle")),
5094
5519
  h("div", { className: "smem-row", key: "cat" }, [
5095
- h("span", { className: "smem-label", key: "l" }, "类型:"),
5096
- h("div", { className: "smem-chips", key: "chips" }, TYPE_ORDER.map(function (t) {
5097
- const meta = TYPE_META[t];
5098
- return h(Chip, { key: t, label: meta[0] + " " + meta[1], on: formType === t, onClick: function () { setFormType(t); } });
5520
+ h("span", { className: "smem-label", key: "l" }, t("form.type")),
5521
+ h("div", { className: "smem-chips", key: "chips" }, TYPE_ORDER.map(function (id) {
5522
+ const meta = TYPE_META[id];
5523
+ return h(Chip, { key: id, label: meta[0] + " " + typeLabel(id), on: formType === id, onClick: function () { setFormType(id); } });
5099
5524
  })),
5100
5525
  ]),
5101
- h("label", { className: "smem-label", key: "ln" }, "文件名(.md,留空自动 " + formType + "_note.md)"),
5102
- h("input", { className: "smem-input", key: "tn", value: formName, onChange: function (e) { setFormName(e.target.value); }, placeholder: "例如 project_example.md" }),
5103
- h("label", { className: "smem-label", key: "ld" }, "描述(写进 frontmatter,检索与列表都靠它)"),
5104
- h("input", { className: "smem-input", key: "td", value: formDesc, onChange: function (e) { setFormDesc(e.target.value); }, placeholder: "一句话说清这条记忆是什么" }),
5105
- h("label", { className: "smem-label", key: "lt" }, "自定义标签(可选,逗号分隔;会写进 frontmatter 的 tags,列表页可按它筛)"),
5106
- h("input", { className: "smem-input", key: "tt", value: formTags, onChange: function (e) { setFormTags(e.target.value); }, placeholder: "例如 部署, 笔记, dsh" }),
5107
- h("label", { className: "smem-label", key: "lb" }, "正文(可选,frontmatter 自动生成)"),
5108
- h("textarea", { className: "smem-textarea", key: "tb", rows: 6, value: formBody, onChange: function (e) { setFormBody(e.target.value); }, placeholder: "要长期记住的细节" }),
5526
+ h("label", { className: "smem-label", key: "ln" }, t("form.fileName", { type: formType })),
5527
+ h("input", { className: "smem-input", key: "tn", value: formName, onChange: function (e) { setFormName(e.target.value); }, placeholder: t("form.fileNamePlaceholder") }),
5528
+ h("label", { className: "smem-label", key: "ld" }, t("form.desc")),
5529
+ h("input", { className: "smem-input", key: "td", value: formDesc, onChange: function (e) { setFormDesc(e.target.value); }, placeholder: t("form.descPlaceholder") }),
5530
+ h("label", { className: "smem-label", key: "lt" }, t("form.tags")),
5531
+ h("input", { className: "smem-input", key: "tt", value: formTags, onChange: function (e) { setFormTags(e.target.value); }, placeholder: t("form.tagsPlaceholder") }),
5532
+ h("label", { className: "smem-label", key: "lb" }, t("form.body")),
5533
+ h("textarea", { className: "smem-textarea", key: "tb", rows: 6, value: formBody, onChange: function (e) { setFormBody(e.target.value); }, placeholder: t("form.bodyPlaceholder") }),
5109
5534
  h("div", { className: "smem-row", key: "btns" }, [
5110
- h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "save", disabled: busy, onClick: submit }, busy ? "保存中…" : "保存"),
5111
- h("button", { type: "button", className: "smem-btn", key: "cancel", disabled: busy, onClick: closeForm }, "取消"),
5535
+ h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "save", disabled: busy, onClick: submit }, busy ? t("card.saving") : t("card.save")),
5536
+ h("button", { type: "button", className: "smem-btn", key: "cancel", disabled: busy, onClick: closeForm }, t("card.cancel")),
5112
5537
  ]),
5113
5538
  ])
5114
5539
  : null,
@@ -5177,7 +5602,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
5177
5602
 
5178
5603
  /** 卡片里的错误文案:remote 可能 reject 一个字符串 / 对象 / Error,尽量展开成人话。 */
5179
5604
  function pluginCardErr(e) {
5180
- if (!e) return "保存失败(未知原因)";
5605
+ if (!e) return t("pluginCard.saveFailed");
5181
5606
  if (e instanceof Error) return e.message || String(e);
5182
5607
  try {
5183
5608
  const s = JSON.stringify(e);
@@ -5227,7 +5652,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
5227
5652
  function toggle(next) {
5228
5653
  if (busy) return;
5229
5654
  if (!remote || typeof remote.setSettings !== "function") {
5230
- setErr("宿主侧还没有 setSettings 接口(插件版本较旧)");
5655
+ setErr(t("pluginCard.noSetSettings"));
5231
5656
  return;
5232
5657
  }
5233
5658
  setBusy(true); setErr("");
@@ -5235,7 +5660,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
5235
5660
  setBusy(false);
5236
5661
  // setSettings 是 { ok, settings, restartRequired } 形状,不要过 unwrap。
5237
5662
  if (out && out.ok === false) {
5238
- setErr(out.error || "保存被拒绝");
5663
+ setErr(out.error || t("error.saveRejected"));
5239
5664
  return;
5240
5665
  }
5241
5666
  const settings = out && out.settings ? out.settings : null;
@@ -5253,8 +5678,8 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
5253
5678
 
5254
5679
  return h("div", { className: "smem-plugin-card" }, [
5255
5680
  h("div", { className: "smem-plugin-card-head", key: "h" }, [
5256
- h("span", { className: "smem-plugin-card-title", key: "t" }, "侧栏入口"),
5257
- h("span", { className: "smem-plugin-card-status", key: "s" }, on ? "已开启" : "已关闭"),
5681
+ h("span", { className: "smem-plugin-card-title", key: "t" }, t("pluginCard.title")),
5682
+ h("span", { className: "smem-plugin-card-status", key: "s" }, on ? t("pluginCard.on") : t("pluginCard.off")),
5258
5683
  ]),
5259
5684
  h("label", { className: "smem-check smem-plugin-card-check", key: "row" }, [
5260
5685
  h("input", {
@@ -5263,10 +5688,10 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
5263
5688
  disabled: busy,
5264
5689
  onChange: function (e) { toggle(!!(e.target && e.target.checked)); },
5265
5690
  }),
5266
- h("span", { key: "l" }, "在侧栏显示「记忆管理」入口"),
5691
+ h("span", { key: "l" }, t("pluginCard.label")),
5267
5692
  ]),
5268
5693
  h("div", { className: "smem-plugin-card-note", key: "n" },
5269
- "默认开启;改动立即生效,不需要重启。关闭后「设置 → 记忆管理」入口仍然保留。"),
5694
+ t("pluginCard.note")),
5270
5695
  err ? h("div", { className: "smem-plugin-card-err", key: "e" }, err) : null,
5271
5696
  ]);
5272
5697
  }
@@ -5291,7 +5716,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
5291
5716
  h("div", { className: "smem-page-top", key: "top" }, [
5292
5717
  h("button", {
5293
5718
  type: "button", className: "smem-page-back", key: "back",
5294
- title: "返回会话", onClick: onBack,
5719
+ title: t("page.back"), onClick: onBack,
5295
5720
  }, [
5296
5721
  h("svg", {
5297
5722
  key: "i", width: 14, height: 14, viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true",
@@ -5299,7 +5724,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
5299
5724
  d: "M10 3.5 5.5 8l4.5 4.5", stroke: "currentColor", strokeWidth: "1.6",
5300
5725
  strokeLinecap: "round", strokeLinejoin: "round",
5301
5726
  })),
5302
- h("span", { key: "l" }, "返回会话"),
5727
+ h("span", { key: "l" }, t("page.back")),
5303
5728
  ]),
5304
5729
  ]),
5305
5730
  h(Section, { key: "sec", ...props, ctx: sidebarCtx }),
@@ -5317,7 +5742,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
5317
5742
  name: "sidebar.panellist",
5318
5743
  id: SIDEBAR_ENTRY_ID,
5319
5744
  order: 20,
5320
- label: "记忆管理",
5745
+ label: () => t("sidebar.label"),
5321
5746
  }, SidebarEntryIcon);
5322
5747
  }));
5323
5748
  // 中央主区 keyed 槽:key 与侧栏 id 相同;inject 面把「返回会话」交给页面组件。
@@ -5372,13 +5797,14 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
5372
5797
  tag.textContent = CSS;
5373
5798
  document.head.appendChild(tag);
5374
5799
  }
5800
+ setupI18n(ctx);
5375
5801
  ctx.slots.inject("settings.section", () =>
5376
5802
  ctx.slots.register(
5377
5803
  {
5378
5804
  name: "settings.section",
5379
5805
  id: "sage-mem",
5380
5806
  order: 20,
5381
- label: "记忆管理",
5807
+ label: () => t("section.label"),
5382
5808
  // 声明「记忆星图」子槽:sage-starmap 向它注入星空视图,
5383
5809
  // 记忆管理页用 renderSlot 渲染。声明 children 后 shell 才会
5384
5810
  // 向本 section 组件传入 renderSlot。