dsh-maoying 0.1.12 → 0.1.13

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
@@ -55,19 +55,15 @@ window.__ModuleLoader__.load({
55
55
  var API_TIMEOUT_MS = 60000;
56
56
 
57
57
  var STATUS_FLOW = ["pending", "writing", "review", "accepted"];
58
- var STATUS_LABEL = { pending: "待写", writing: "写作中", review: "审校中", accepted: "已定稿" };
59
58
  var FS_ORDER = ["planted", "progressing", "resolved", "abandoned"];
60
59
  var FS_FLOW = { planted: "progressing", progressing: "resolved" };
61
- var FS_LABEL = { planted: "埋设", progressing: "推进中", resolved: "已回收", abandoned: "已弃用" };
62
- var PRI_LABEL = { high: "高", normal: "中", low: "低" };
63
60
  var PHASES = ["idea", "setting", "outline", "writing", "review", "consistency", "export"];
64
- var PHASE_LABEL = { idea: "立意", setting: "设定", outline: "大纲", writing: "写作", review: "审校", consistency: "一致性", export: "导出" };
65
- var SCI_LABEL = { known: "已确证", extrapolation: "推演", hypothesis: "假说" };
66
- var CLUE_KIND_LABEL = { clue: "线索", red_herring: "红鲱鱼", inference: "推理", question: "疑问" };
61
+ // snake→camel 键表:clue.* 词典键用小驼峰,数据里的 kind 是 snake_case。
62
+ var CLUE_KIND_KEYS = { clue: "clue", red_herring: "redHerring", inference: "inference", question: "question" };
67
63
  var TABS = [
68
- ["overview", "概览"], ["chapters", "章节"], ["characters", "角色"],
69
- ["foreshadows", "伏笔"], ["relations", "关系图"], ["timeline", "时间线"],
70
- ["world", "设定"], ["index", "索引板"], ["settings", "小说设置"],
64
+ ["overview", "tabs.overview"], ["chapters", "tabs.chapters"], ["characters", "tabs.characters"],
65
+ ["foreshadows", "tabs.foreshadows"], ["relations", "tabs.relations"], ["timeline", "tabs.timeline"],
66
+ ["world", "tabs.world"], ["index", "tabs.index"], ["settings", "tabs.settings"],
71
67
  ];
72
68
 
73
69
  // 角色徽章四色(role 是自由文本):按关键词归 tone,命中即返。
@@ -85,16 +81,16 @@ window.__ModuleLoader__.load({
85
81
  }
86
82
  function statusBadge(status) {
87
83
  var s = STATUS_FLOW.indexOf(status) !== -1 ? status : "pending";
88
- return h("span", { className: "myt-badge", "data-status": s }, STATUS_LABEL[s] + (status && s !== status ? "(" + status + ")" : ""));
84
+ return h("span", { className: "myt-badge", "data-status": s }, tt("status." + s) + (status && s !== status ? tf("status.rawSuffix", { raw: status }) : ""));
89
85
  }
90
86
  function phasePill(phase) {
91
87
  var p = PHASES.indexOf(phase) !== -1 ? phase : null;
92
- if (!p) return h("span", { className: "myt-phase", "data-phase": "idea", title: phase || "" }, phase ? String(phase) : "未设阶段");
93
- return h("span", { className: "myt-phase", "data-phase": p }, PHASE_LABEL[p]);
88
+ if (!p) return h("span", { className: "myt-phase", "data-phase": "idea", title: phase || "" }, phase ? String(phase) : tt("phase.unset"));
89
+ return h("span", { className: "myt-phase", "data-phase": p }, tt("phase." + p));
94
90
  }
95
91
  function priDot(pri) {
96
92
  var p = pri === "high" || pri === "low" ? pri : "normal";
97
- return h("span", { className: "myt-priDot", "data-pri": p, title: "优先级:" + (PRI_LABEL[p] || pri || "中"), "aria-label": "优先级" + (PRI_LABEL[p] || "") });
93
+ return h("span", { className: "myt-priDot", "data-pri": p, title: tf("common.priorityTitle", { pri: tt("pri." + p) }), "aria-label": tf("common.priorityAria", { pri: tt("pri." + p) }) });
98
94
  }
99
95
 
100
96
  // ── 格式化小工具 ─────────────────────────────────────────────────────
@@ -114,7 +110,7 @@ window.__ModuleLoader__.load({
114
110
  // 章节「卷-章」定位:volume 缺省只显示第 N 章。
115
111
  function chWhere(ch) {
116
112
  if (!ch) return "";
117
- var num = typeof ch.num === "number" ? "第 " + ch.num + " 章" : "";
113
+ var num = typeof ch.num === "number" ? tf("common.chapterNum", { n: ch.num }) : "";
118
114
  return ch.volume ? String(ch.volume) + " · " + num : num;
119
115
  }
120
116
  function chFullTitle(ch) {
@@ -178,11 +174,11 @@ window.__ModuleLoader__.load({
178
174
  var d = null;
179
175
  try { d = raw ? JSON.parse(raw) : null; } catch (err) { d = null; }
180
176
  if (!r.ok) throw new Error((d && d.error) || ("HTTP " + r.status));
181
- if (!d) throw new Error("响应不是 JSON(HTTP " + r.status + ")");
177
+ if (!d) throw new Error(tf("api.notJson", { code: r.status }));
182
178
  return d;
183
179
  });
184
180
  }).catch(function (err) {
185
- if (ctrl && err && err.name === "AbortError") throw new Error("请求超时(" + API_TIMEOUT_MS / 1000 + " 秒)");
181
+ if (ctrl && err && err.name === "AbortError") throw new Error(tf("api.timeout", { s: API_TIMEOUT_MS / 1000 }));
186
182
  throw err;
187
183
  }).finally(function () {
188
184
  if (timer !== null) clearTimeout(timer);
@@ -196,7 +192,7 @@ window.__ModuleLoader__.load({
196
192
  qs = usp.toString();
197
193
  }
198
194
  return maoyingApi(path + (qs ? (path.indexOf("?") === -1 ? "?" : "&") + qs : "")).catch(function (err) {
199
- toast.err("毛颖创作台:" + err.message);
195
+ toast.err(tf("toast.prefix", { msg: err.message }));
200
196
  throw err;
201
197
  });
202
198
  }
@@ -205,17 +201,17 @@ window.__ModuleLoader__.load({
205
201
  toast.ok(apiPostOkText(path, d));
206
202
  return d;
207
203
  }, function (err) {
208
- toast.err("毛颖创作台:" + err.message);
204
+ toast.err(tf("toast.prefix", { msg: err.message }));
209
205
  throw err;
210
206
  });
211
207
  }
212
208
  // 写操作成功提示按路由分流(server 返回 chapters 计数时带上)。
213
209
  function apiPostOkText(path, d) {
214
- if (path === "rebuild-index") return "索引已重建(" + fmtNum(d && d.chapters) + " 章)";
215
- if (path === "chapter-status") return "章节状态已更新";
216
- if (path === "foreshadow-status") return "伏笔状态已更新";
217
- if (path === "phase") return "阶段已推进到「" + (PHASE_LABEL[d && d.phase] || (d && d.phase) || "—") + "」";
218
- return "操作成功";
210
+ if (path === "rebuild-index") return tf("toast.indexRebuilt", { n: fmtNum(d && d.chapters) });
211
+ if (path === "chapter-status") return tt("toast.chapterStatus");
212
+ if (path === "foreshadow-status") return tt("toast.foreshadowStatus");
213
+ if (path === "phase") return tf("toast.phaseAdvanced", { phase: d && d.phase && PHASES.indexOf(d.phase) !== -1 ? tt("phase." + d.phase) : (d && d.phase) || "—" });
214
+ return tt("toast.success");
219
215
  }
220
216
 
221
217
  // ── Toast 控制器(模块级单例;ok 4s / warn 6s 自动消失,err 手动关)────
@@ -267,7 +263,7 @@ window.__ModuleLoader__.load({
267
263
  },
268
264
  h("div", { className: "myt-toastText" }, it.text),
269
265
  h("button", {
270
- type: "button", className: "myt-toastClose", "aria-label": "关闭提示",
266
+ type: "button", className: "myt-toastClose", "aria-label": tt("common.closeTip"),
271
267
  onClick: function () { toast.dismiss(it.id); },
272
268
  }, h("span", { className: "myt-closeGlyph", "aria-hidden": "true" }, "×")));
273
269
  }));
@@ -446,14 +442,14 @@ window.__ModuleLoader__.load({
446
442
 
447
443
  // ── 引导卡(无 novel 工作区 / 未初始化):maoying init 提示 + 复制 ──────
448
444
  function GuideCard(props) {
449
- var title = props.title || "该工作区还没有小说项目";
450
- var desc = props.desc || "在下面给出的命令里初始化目录骨架(或把已有 novel 目录放进工作区根),然后刷新本面板。";
445
+ var title = props.title || tt("guide.defaultTitle");
446
+ var desc = props.desc || tt("guide.defaultDesc");
451
447
  var _b = useState(false), busy = _b[0], setBusy = _b[1];
452
448
  function doCopy() {
453
449
  setBusy(true);
454
450
  copyText("maoying init").then(function (ok) {
455
- if (ok) toast.ok("已复制 maoying init");
456
- else toast.warn("复制失败,请手动选择命令文本");
451
+ if (ok) toast.ok(tt("guide.copied"));
452
+ else toast.warn(tt("guide.copyFailed"));
457
453
  }).finally(function () { setBusy(false); });
458
454
  }
459
455
  return h("div", { className: "myt-card myt-guide" },
@@ -461,7 +457,7 @@ window.__ModuleLoader__.load({
461
457
  h("p", { className: "myt-guideText" }, desc),
462
458
  h("div", { className: "myt-guideCode" },
463
459
  h("code", { className: "myt-guideCodeText" }, "maoying init"),
464
- h("button", { type: "button", className: "myt-btn myt-copyBtn", disabled: busy, onClick: doCopy }, busy ? "复制中…" : "复制命令")));
460
+ h("button", { type: "button", className: "myt-btn myt-copyBtn", disabled: busy, onClick: doCopy }, busy ? tt("guide.copying") : tt("guide.copyCmd"))));
465
461
  }
466
462
 
467
463
  // ── 骨架 / 错误 / 空态小件 ──────────────────────────────────────────────
@@ -474,9 +470,9 @@ window.__ModuleLoader__.load({
474
470
  return h("div", { className: "myt-strip myt-stripError" },
475
471
  h("span", { className: "myt-stripIcon" }, ic("IconWarningOutlineRegular", 14)),
476
472
  h("span", { className: "myt-stripText" }, props.text),
477
- props.onRetry ? h("button", { type: "button", className: "myt-btn", style: { marginLeft: "auto" }, onClick: props.onRetry }, "重试") : null);
473
+ props.onRetry ? h("button", { type: "button", className: "myt-btn", style: { marginLeft: "auto" }, onClick: props.onRetry }, tt("common.retry")) : null);
478
474
  }
479
- function Empty(props) { return h("div", { className: "myt-empty" }, props.text || "暂无数据"); }
475
+ function Empty(props) { return h("div", { className: "myt-empty" }, props.text || tt("common.empty")); }
480
476
 
481
477
  // tab 内容错误边界:任何 tab / 阅读页渲染抛错都降级为卡内错误态(错误摘要
482
478
  // + 重试按钮),不再把整个面板炸成白屏。错误边界只有 class 组件能做
@@ -501,12 +497,12 @@ window.__ModuleLoader__.load({
501
497
  render() {
502
498
  var err = this.state.err;
503
499
  if (!err) return this.props.children;
504
- var msg = String((err && err.message) || err || "未知错误");
500
+ var msg = String((err && err.message) || err || tt("common.unknownErr"));
505
501
  return h("div", { className: "myt-card" },
506
- h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, "该分区渲染出错")),
507
- h("div", { className: "myt-muted", style: { overflowWrap: "anywhere" } }, "错误摘要:" + truncate(msg, 300)),
502
+ h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, tt("errBoundary.title"))),
503
+ h("div", { className: "myt-muted", style: { overflowWrap: "anywhere" } }, tf("errBoundary.summary", { msg: truncate(msg, 300) })),
508
504
  h("div", { className: "myt-heroMeta" },
509
- h("button", { type: "button", className: "myt-btn", onClick: this.retry }, "重试")));
505
+ h("button", { type: "button", className: "myt-btn", onClick: this.retry }, tt("common.retry"))));
510
506
  }
511
507
  }
512
508
 
@@ -526,7 +522,18 @@ window.__ModuleLoader__.load({
526
522
  var _rd = useState(null), readerFile = _rd[0], setReaderFile = _rd[1];
527
523
  // 系统设置:字号档位持久化 + 面板整体缩放(portal 节点读同一 CSS 变量)。
528
524
  var _sm = useState(false), showSettings = _sm[0], setShowSettings = _sm[1];
529
- var _fs = useState(function () { return lsGet(FONT_SCALE_KEY) || "1"; }), fontScale = _fs[0], setFontScale = _fs[1];
525
+ var _fs = useState(function () {
526
+ var stored = lsGet(FONT_SCALE_KEY) || "1";
527
+ return FONT_SCALES.some(function (o) { return o[0] === stored; }) ? stored : "1"; // 旧档位(1.8/2)回落 100%
528
+ }), fontScale = _fs[0], setFontScale = _fs[1];
529
+ var _lg = useState(function () {
530
+ var stored = lsGet(LANG_KEY) || "zh";
531
+ return UI_LANGS.some(function (o) { return o[0] === stored; }) ? stored : "zh";
532
+ }), lang = _lg[0], setLang = _lg[1];
533
+ useEffect(function () {
534
+ UI_LANG = lang;
535
+ lsSet(LANG_KEY, lang);
536
+ }, [lang]);
530
537
  useEffect(function () {
531
538
  if (typeof document !== "undefined" && document.documentElement) {
532
539
  document.documentElement.style.setProperty("--myt-zoom", fontScale);
@@ -618,19 +625,19 @@ window.__ModuleLoader__.load({
618
625
  } else if (!effectiveWs) {
619
626
  // 全无 novel 工作区:引导卡(maoying init + 复制)。
620
627
  body = h("div", { className: "myt-stack" },
621
- wsErr ? ErrorStrip({ text: "工作区列表拉取失败:" + wsErr, onRetry: loadWorkspaces }) : null,
628
+ wsErr ? ErrorStrip({ text: tf("shell.wsListFailed", { msg: wsErr }), onRetry: loadWorkspaces }) : null,
622
629
  h(GuideCard, {
623
- title: "还没有可用的小说工作区",
624
- desc: "任一工作区根目录下出现 novel/project.yml 即自动入选;先用命令初始化目录骨架:",
630
+ title: tt("guide.noWsTitle"),
631
+ desc: tt("guide.noWsDesc"),
625
632
  }));
626
633
  } else if (loading && !snap) {
627
634
  body = h(SkelPage);
628
635
  } else if (stateErr) {
629
- body = h(ErrorStrip, { text: "快照拉取失败:" + stateErr, onRetry: reload });
636
+ body = h(ErrorStrip, { text: tf("shell.stateFailed", { msg: stateErr }), onRetry: reload });
630
637
  } else if (snap && snap.found === false) {
631
638
  body = h(GuideCard, {
632
- title: "该工作区尚未初始化小说项目",
633
- desc: "工作区根目录下没有 novel/project.yml。在该工作区根目录执行:",
639
+ title: tt("guide.notInitTitle"),
640
+ desc: tt("guide.notInitDesc"),
634
641
  });
635
642
  } else if (snap) {
636
643
  var tabProps = { snap: snap, reload: reload, wsPath: effectiveWs };
@@ -650,37 +657,37 @@ window.__ModuleLoader__.load({
650
657
  h("header", { className: "myt-header" },
651
658
  h("div", null,
652
659
  h("h1", { className: "myt-title" }, "毛颖创作台",
653
- ver && ver.current ? h("span", { className: "myt-verTag", title: "当前版本" }, ver.current) : null,
660
+ ver && ver.current ? h("span", { className: "myt-verTag", title: tt("header.curVerTitle") }, ver.current) : null,
654
661
  ver && ver.updateAvailable
655
- ? h("a", { className: "myt-verBadge", href: ver.releaseUrl || "#", target: "_blank", rel: "noreferrer", title: "当前 " + ver.current + ",点击打开新版发布页" },
656
- "有新版 " + ver.latest + " ↗")
662
+ ? h("a", { className: "myt-verBadge", href: ver.releaseUrl || "#", target: "_blank", rel: "noreferrer", title: tf("header.upgradeTip", { cur: ver.current }) },
663
+ tf("header.newVersion", { ver: ver.latest }))
657
664
  : null),
658
- h("p", { className: "myt-subtitle" }, "网文项目可视化与状态流转 · 只读正文,写状态")),
665
+ h("p", { className: "myt-subtitle" }, tt("header.subtitle"))),
659
666
  h("div", { className: "myt-headActions" },
660
667
  workspaces && novelWorkspaces.length
661
- ? [h("span", { className: "myt-wsLabel", key: "l" }, "工作区"),
662
- h("select", { className: "myt-wsSelect", key: "s", value: effectiveWs, onChange: chooseWs, "aria-label": "选择工作区" },
668
+ ? [h("span", { className: "myt-wsLabel", key: "l" }, tt("header.workspace")),
669
+ h("select", { className: "myt-wsSelect", key: "s", value: effectiveWs, onChange: chooseWs, "aria-label": tt("header.wsAria") },
663
670
  workspaces.map(function (w, i) {
664
671
  return h("option", { key: w.path || i, value: w.path || "", disabled: !w.hasNovel },
665
- (w.hasNovel ? "" : "(无 novel)") + (w.name || w.path || "未命名"));
672
+ (w.hasNovel ? "" : tt("header.noNovel")) + (w.name || w.path || tt("header.untitled")));
666
673
  }))]
667
674
  : null,
668
- h("button", { type: "button", className: "myt-btn myt-btnIcon", title: "系统设置", "aria-label": "系统设置", onClick: function () { setShowSettings(true); } }, ic("IconSettingsOutlineRegular", 14)),
669
- h("button", { type: "button", className: "myt-btn myt-btnIcon", title: "重新拉取", "aria-label": "重新拉取", disabled: loading, onClick: reload }, ic("IconRefreshOutlineRegular", 14)),
670
- h("button", { type: "button", className: "myt-closeBtn", "aria-label": "关闭创作台", title: "关闭", onClick: onClose }, h("span", { className: "myt-closeGlyph", "aria-hidden": "true" }, "×")))),
671
- readerFile ? null : h("nav", { className: "myt-tabs", "aria-label": "创作台分区" },
675
+ h("button", { type: "button", className: "myt-btn myt-btnIcon", title: tt("header.settings"), "aria-label": tt("header.settings"), onClick: function () { setShowSettings(true); } }, ic("IconSettingsOutlineRegular", 14)),
676
+ h("button", { type: "button", className: "myt-btn myt-btnIcon", title: tt("header.refresh"), "aria-label": tt("header.refresh"), disabled: loading, onClick: reload }, ic("IconRefreshOutlineRegular", 14)),
677
+ h("button", { type: "button", className: "myt-closeBtn", "aria-label": tt("header.closeAria"), title: tt("header.closeTitle"), onClick: onClose }, h("span", { className: "myt-closeGlyph", "aria-hidden": "true" }, "×")))),
678
+ readerFile ? null : h("nav", { className: "myt-tabs", "aria-label": tt("tabs.aria") },
672
679
  TABS.map(function (t) {
673
680
  return h("button", {
674
681
  key: t[0], type: "button", className: "myt-tab", "data-active": tab === t[0] ? "true" : undefined,
675
682
  onClick: function () { gotoTab(t[0]); },
676
- }, t[1]);
683
+ }, tt(t[1]));
677
684
  })),
678
685
  h("div", { className: "myt-body" },
679
686
  h("div", { className: "myt-page" },
680
687
  snap && Array.isArray(snap.warnings) && snap.warnings.length ? WarningsStrip(snap.warnings) : null,
681
688
  // 错误边界包住 tab 内容:key 随 tab / 阅读页变化重挂,切分区自动清错误态。
682
689
  h(TabErrorBoundary, { key: readerFile ? "reader:" + readerFile : tab, onRetry: reload }, body))),
683
- showSettings ? h(SettingsModal, { scale: fontScale, onPick: setFontScale, onClose: function () { setShowSettings(false); } }) : null,
690
+ showSettings ? h(SettingsModal, { scale: fontScale, onPick: setFontScale, lang: lang, onPickLang: setLang, onClose: function () { setShowSettings(false); } }) : null,
684
691
  h(ToastHost));
685
692
  }
686
693
 
@@ -689,7 +696,7 @@ window.__ModuleLoader__.load({
689
696
  return h("div", { className: "myt-strip myt-stripWarn" },
690
697
  h("span", { className: "myt-stripIcon" }, ic("IconWarningOutlineRegular", 14)),
691
698
  h("span", { className: "myt-stripText" },
692
- h("div", null, h("strong", null, "部分文件解析异常(已跳过):")),
699
+ h("div", null, h("strong", null, tt("warnings.title"))),
693
700
  warnings.map(function (w, i) { return h("div", { key: i }, "· " + String(w)); })));
694
701
  }
695
702
 
@@ -713,44 +720,44 @@ window.__ModuleLoader__.load({
713
720
  return h("div", { className: "myt-stack" },
714
721
  h("div", { className: "myt-card myt-hero" },
715
722
  h("div", { className: "myt-heroMeta" },
716
- project.author ? h("span", { className: "myt-muted" }, "作者:" + project.author) : null,
723
+ project.author ? h("span", { className: "myt-muted" }, tf("overview.author", { name: project.author })) : null,
717
724
  project.genre ? h("span", { className: "myt-badge" }, project.genre) : null,
718
725
  project.length ? h("span", { className: "myt-badge" }, project.length) : null,
719
726
  project.language ? h("span", { className: "myt-badge" }, project.language) : null),
720
727
  h("h2", { className: "myt-heroTitle" }, orDash(project.title)),
721
728
  h("div", { className: "myt-heroMeta" },
722
729
  phasePill(project.phase),
723
- nextPhase ? h("button", { type: "button", className: "myt-btn", disabled: phaseBusy, onClick: advancePhase, title: "推进到「" + PHASE_LABEL[nextPhase] + "」" }, phaseBusy ? "推进中…" : "推进阶段") : h("span", { className: "myt-muted" }, project.phase ? "已是最终阶段" : null)),
730
+ nextPhase ? h("button", { type: "button", className: "myt-btn", disabled: phaseBusy, onClick: advancePhase, title: tf("phase.advanceTitle", { next: tt("phase." + nextPhase) }) }, phaseBusy ? tt("phase.advancing") : tt("phase.advance")) : h("span", { className: "myt-muted" }, project.phase ? tt("phase.final") : null)),
724
731
  project.description ? h("p", { className: "myt-heroDesc" }, project.description) : null),
725
732
  h("div", { className: "myt-statRow" },
726
- h("div", { className: "myt-card myt-stat" }, h("span", { className: "myt-statNum" }, fmtNum(totals.words)), h("span", { className: "myt-statLabel" }, "总字数(CJK)")),
727
- h("div", { className: "myt-card myt-stat" }, h("span", { className: "myt-statNum" }, fmtNum(totals.chapters)), h("span", { className: "myt-statLabel" }, "章节总数")),
733
+ h("div", { className: "myt-card myt-stat" }, h("span", { className: "myt-statNum" }, fmtNum(totals.words)), h("span", { className: "myt-statLabel" }, tt("overview.totalWords"))),
734
+ h("div", { className: "myt-card myt-stat" }, h("span", { className: "myt-statNum" }, fmtNum(totals.chapters)), h("span", { className: "myt-statLabel" }, tt("overview.totalChapters"))),
728
735
  h("div", { className: "myt-card myt-stat" },
729
736
  h("span", { className: "myt-statNum" }, fmtNum((snap.characters || []).length)),
730
- h("span", { className: "myt-statLabel" }, "角色数")),
737
+ h("span", { className: "myt-statLabel" }, tt("overview.characters"))),
731
738
  h("div", { className: "myt-card myt-stat" },
732
739
  h("span", { className: "myt-statNum" }, fmtNum((snap.foreshadows || []).filter(function (f) { return f.status === "planted" || f.status === "progressing"; }).length)),
733
- h("span", { className: "myt-statLabel" }, "未回收伏笔"))),
740
+ h("span", { className: "myt-statLabel" }, tt("overview.openForeshadows")))),
734
741
  h("div", { className: "myt-card" },
735
- h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, "章节状态分布")),
742
+ h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, tt("overview.statusDist"))),
736
743
  h("div", { className: "myt-distBar", "aria-hidden": "true" }, STATUS_FLOW.map(function (s) {
737
744
  var n = byStatus[s] || 0;
738
745
  return n > 0 ? h("div", { key: s, className: "myt-distSeg", "data-status": s, style: { flexGrow: n } }) : null;
739
746
  })),
740
747
  h("div", { className: "myt-heroMeta" }, STATUS_FLOW.map(function (s) {
741
- return h("span", { key: s, className: "myt-muted" }, STATUS_LABEL[s] + " " + (byStatus[s] || 0));
748
+ return h("span", { key: s, className: "myt-muted" }, tt("status." + s) + " " + (byStatus[s] || 0));
742
749
  }))),
743
750
  h("div", { className: "myt-card" },
744
- h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, "最近章节")),
751
+ h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, tt("overview.recent"))),
745
752
  recent.length
746
753
  ? h("div", { className: "myt-recentList" }, recent.map(function (ch) {
747
754
  return h("button", { key: ch.file, type: "button", className: "myt-recentRow", onClick: function () { gotoTab("chapters", ch.file); } },
748
755
  statusBadge(ch.status),
749
756
  h("span", { className: "myt-chTitle" }, orDash(ch.title)),
750
757
  h("span", { className: "myt-chPath" }, chWhere(ch)),
751
- h("span", { className: "myt-wordsCell" }, fmtNum(ch.words) + " 字"));
758
+ h("span", { className: "myt-wordsCell" }, tf("common.charCount", { n: fmtNum(ch.words) })));
752
759
  }))
753
- : h(Empty, { text: "chapters/ 目录还没有章节文件" })));
760
+ : h(Empty, { text: tt("chapters.empty") })));
754
761
  }
755
762
 
756
763
  // ── 轻量 Markdown 渲染(零依赖):先整体 HTML 转义再施加 md 变换,杜绝
@@ -857,28 +864,204 @@ window.__ModuleLoader__.load({
857
864
 
858
865
  // ── 系统设置:字号五档(面板整体 zoom,抽屉/弹层 portal 节点同缩放)──────
859
866
  var FONT_SCALE_KEY = "maoying.fontScale";
860
- var FONT_SCALES = [["1", "默认(100%)"], ["1.2", "大(120%)"], ["1.5", "更大(150%)"], ["1.8", "超大(180%)"], ["2", "巨大(200%)"]];
867
+ var FONT_SCALES = [["1", "100%"], ["1.1", "110%"], ["1.2", "120%"], ["1.3", "130%"], ["1.4", "140%"], ["1.5", "150%"]];
868
+ // ── 界面语言(系统设置可选中文/English):UI_LANG 为模块级当前语言,
869
+ // PageShell 在 lang state 变化时改写它并整树重渲。tt/tf 是唯一取词口:
870
+ // zh 词典的值必须与历史硬编码文案逐字一致(中文界面零回归),en 词典
871
+ // 不留中文;词典缺键回落 key 本身(便于渐进迁移,一眼看出漏网串)。 ──
872
+ var LANG_KEY = "maoying.lang";
873
+ var UI_LANGS = [["zh", "中文"], ["en", "English"]];
874
+ var UI_LANG = "zh";
875
+ var I18N = {
876
+ zh: {
877
+ "shell.title": "毛颖创作台", "header.subtitle": "网文项目可视化与状态流转 · 只读正文,写状态",
878
+ "header.curVerTitle": "当前版本", "header.upgradeTip": "当前 {cur},点击打开新版发布页", "header.newVersion": "有新版 {ver} ↗",
879
+ "header.workspace": "工作区", "header.wsAria": "选择工作区", "header.noNovel": "(无 novel)", "header.untitled": "未命名",
880
+ "header.settings": "系统设置", "header.refresh": "重新拉取", "header.closeAria": "关闭创作台", "header.closeTitle": "关闭",
881
+ "tabs.aria": "创作台分区",
882
+ "tabs.overview": "概览", "tabs.chapters": "章节", "tabs.characters": "角色", "tabs.foreshadows": "伏笔", "tabs.relations": "关系图",
883
+ "tabs.timeline": "时间线", "tabs.world": "设定", "tabs.index": "索引板", "tabs.settings": "小说设置",
884
+ "shell.wsListFailed": "工作区列表拉取失败:{msg}", "shell.stateFailed": "快照拉取失败:{msg}",
885
+ "guide.noWsTitle": "还没有可用的小说工作区", "guide.noWsDesc": "任一工作区根目录下出现 novel/project.yml 即自动入选;先用命令初始化目录骨架:",
886
+ "guide.notInitTitle": "该工作区尚未初始化小说项目", "guide.notInitDesc": "工作区根目录下没有 novel/project.yml。在该工作区根目录执行:",
887
+ "guide.defaultTitle": "该工作区还没有小说项目", "guide.defaultDesc": "在下面给出的命令里初始化目录骨架(或把已有 novel 目录放进工作区根),然后刷新本面板。",
888
+ "guide.copied": "已复制 maoying init", "guide.copyFailed": "复制失败,请手动选择命令文本", "guide.copying": "复制中…", "guide.copyCmd": "复制命令",
889
+ "common.retry": "重试", "common.empty": "暂无数据", "common.uncategorized": "未分类", "common.charCount": "{n} 字",
890
+ "common.closeTip": "关闭提示", "common.unknownErr": "未知错误", "common.priorityTitle": "优先级:{pri}", "common.priorityAria": "优先级{pri}",
891
+ "common.chapterNum": "第 {n} 章", "pri.high": "高", "pri.normal": "中", "pri.low": "低",
892
+ "toast.prefix": "毛颖创作台:{msg}", "toast.success": "操作成功", "toast.indexRebuilt": "索引已重建({n} 章)",
893
+ "toast.chapterStatus": "章节状态已更新", "toast.foreshadowStatus": "伏笔状态已更新", "toast.phaseAdvanced": "阶段已推进到「{phase}」",
894
+ "api.notJson": "响应不是 JSON(HTTP {code})", "api.timeout": "请求超时({s} 秒)",
895
+ "status.pending": "待写", "status.writing": "写作中", "status.review": "审校中", "status.accepted": "已定稿", "status.rawSuffix": "({raw})",
896
+ "phase.idea": "立意", "phase.setting": "设定", "phase.outline": "大纲", "phase.writing": "写作", "phase.review": "审校",
897
+ "phase.consistency": "一致性", "phase.export": "导出", "phase.unset": "未设阶段",
898
+ "phase.advanceTitle": "推进到「{next}」", "phase.advance": "推进阶段", "phase.advancing": "推进中…", "phase.final": "已是最终阶段",
899
+ "foreshadow.planted": "埋设", "foreshadow.progressing": "推进中", "foreshadow.resolved": "已回收", "foreshadow.abandoned": "已弃用",
900
+ "foreshadow.advance": "推进", "foreshadow.resolve": "回收", "foreshadow.abandon": "弃用", "foreshadow.advancing": "推进中…",
901
+ "foreshadow.plantedUnmarked": "埋设章未标", "foreshadow.resolvedAt": "回收于 {ch}", "foreshadow.priorityLabel": "优先级 {pri}",
902
+ "foreshadow.backfill": "回填", "foreshadow.backfillTip": "从作者正文回填", "foreshadow.missingStatus": "缺 status",
903
+ "foreshadow.noIdTip": "该条目缺 id,无法写状态",
904
+ "foreshadow.noStatusTip": "该条目缺 status 行,服务端会拒绝流转(409);请先在 foreshadows.yml 补上 status",
905
+ "foreshadow.empty": "foreshadows.yml 还没有伏笔条目", "foreshadow.colEmpty": "空",
906
+ "clue.clue": "线索", "clue.redHerring": "红鲱鱼", "clue.inference": "推理", "clue.question": "疑问",
907
+ "sci.known": "已确证", "sci.extrapolation": "推演", "sci.hypothesis": "假说", "world.unlabeled": "未标级",
908
+ "warnings.title": "部分文件解析异常(已跳过):",
909
+ "index.title": "索引板(clue-board / memories)", "index.rebuild": "重建索引", "index.rebuilding": "重建中…",
910
+ "index.rebuildTip": "按 CLI index 同一逻辑重建 index.yml", "index.clueBoard": "线索板", "index.clueEmpty": "clue-board.yml 还没有条目",
911
+ "index.memBoard": "记忆板", "index.memEmpty": "memories.yml 还没有条目", "index.promise": "承诺线",
912
+ "timeline.empty": "timeline.yml 还没有时间线条目", "timeline.importance": "重要度 {n}", "timeline.linkedChapter": "关联章节:{ch}",
913
+ "world.title": "世界观条目", "world.empty": "world/ 目录还没有设定条目", "world.science": "科学设定",
914
+ "world.scienceEmpty": "science/ 目录还没有科学设定", "world.research": "资料检索", "world.ref": "参考 {n}:{r}",
915
+ "relations.title": "角色关系图", "relations.counts": "{n} 节点 · {m} 关系",
916
+ "relations.help": "拖节点固定位置 · 滚轮缩放 · 双击复位 · 悬停边看关系描述",
917
+ "relations.unlinkedTitle": "未入图角色(无关系边)", "relations.noData": "没有角色或关系数据,无法绘图",
918
+ "relations.edgeTipPrefix": "{type}:", "relations.noDesc": "(无描述)",
919
+ "char.empty": "characters/ 目录还没有角色卡",
920
+ "char.field.age": "年龄", "char.field.gender": "性别", "char.field.appearance": "外貌", "char.field.personality": "性格",
921
+ "char.field.background": "背景", "char.field.motivation": "动机", "char.field.arc": "成长弧",
922
+ "char.years": "{n} 岁", "char.linePersonality": "性格 ", "char.lineMotivation": "动机 ",
923
+ "char.modalAria": "角色详情", "char.closeAria": "关闭角色卡", "char.markers": "标记", "char.file": "文件",
924
+ "overview.author": "作者:{name}", "overview.totalWords": "总字数(CJK)", "overview.totalChapters": "章节总数",
925
+ "overview.characters": "角色数", "overview.openForeshadows": "未回收伏笔", "overview.statusDist": "章节状态分布", "overview.recent": "最近章节",
926
+ "chapters.empty": "chapters/ 目录还没有章节文件", "chapters.writeQueue": "按单写作", "chapters.rowTip": "点击打开该章",
927
+ "chapters.outlineWords": "章纲 {n} 字", "chapters.all": "全部", "chapters.thVolume": "卷 · 章", "chapters.thTitle": "标题",
928
+ "chapters.thStatus": "状态", "chapters.thWords": "字数", "chapters.thSummary": "梗概", "chapters.rowReadTip": "点击阅读正文",
929
+ "chapters.emptyFiltered": "该状态下没有章节",
930
+ "reader.back": "返回", "reader.backTip": "返回章节列表", "reader.copyPath": "复制路径", "reader.copyPathTip": "复制章节文件路径",
931
+ "reader.copied": "已复制章节文件路径", "reader.copyFailed": "复制失败,请手动选择路径文本", "reader.loadFailed": "正文读取失败:{msg}",
932
+ "reader.noOutline": "暂无章纲", "reader.emptyBody": "(空章节)", "reader.finalState": "已定稿(终态)", "reader.statusFlow": "状态流转:",
933
+ "reader.flowing": "流转中…", "reader.advanceTo": "推进到「{next}」",
934
+ "outline.title": "章纲对照", "outline.empty": "(章纲为空)", "outline.collapseTip": "收起章纲", "outline.expandTip": "展开章纲",
935
+ "novel.metaTitle": "书稿元信息", "novel.metaNote": "对应 novel/project.yml · 行尾注释会原样保留",
936
+ "novel.fTitle": "书名", "novel.titlePh": "如:长夜余火", "novel.fAuthor": "作者", "novel.authorPh": "笔名",
937
+ "novel.fGenre": "题材", "novel.genrePh": "如:东方玄幻 / 都市异能 / 历史架空", "novel.fLength": "预期篇幅",
938
+ "novel.lengthPh": "如:约100万字 / 300章", "novel.fLanguage": "正文语言", "novel.fPhase": "工作流阶段", "novel.fMode": "共创模式",
939
+ "novel.fDesc": "一句话简介", "novel.descPh": "写封面文案时可直接引用", "novel.titleRequired": "书名不能为空",
940
+ "mode.aiDraft": "AI 代笔(AI 写正文)", "mode.coWrite": "逐章共创(按章指派谁写)", "mode.authorDraft": "作者亲笔(AI 只备料审稿)",
941
+ "novel.saving": "保存中…", "novel.save": "保存设置",
942
+ "settings.title": "系统设置", "settings.closeAria": "关闭设置", "settings.language": "语言",
943
+ "settings.languageHint": "界面文字随语言选项切换,立即生效并记忆。", "settings.fontScale": "字号",
944
+ "settings.fontScaleHint": "按当前字号的百分比缩放整个创作台,立即生效并记忆。",
945
+ "errBoundary.title": "该分区渲染出错", "errBoundary.summary": "错误摘要:{msg}",
946
+ "dim.cognitiveFrame": "认知框架", "dim.emotionalAnchor": "情感锚点", "dim.worldTexture": "世界质感",
947
+ "dim.suspense": "悬念", "dim.interpersonalTension": "人际张力",
948
+ },
949
+ en: {
950
+ "shell.title": "Maoying Studio", "header.subtitle": "Web-novel project dashboard & status flow · read-only text, writable status",
951
+ "header.curVerTitle": "Current version", "header.upgradeTip": "Current {cur} — click to open the release page", "header.newVersion": "New version {ver} ↗",
952
+ "header.workspace": "Workspace", "header.wsAria": "Select workspace", "header.noNovel": "(no novel)", "header.untitled": "Untitled",
953
+ "header.settings": "Settings", "header.refresh": "Refresh", "header.closeAria": "Close Maoying Studio", "header.closeTitle": "Close",
954
+ "tabs.aria": "Studio sections",
955
+ "tabs.overview": "Overview", "tabs.chapters": "Chapters", "tabs.characters": "Characters", "tabs.foreshadows": "Foreshadowing", "tabs.relations": "Relations",
956
+ "tabs.timeline": "Timeline", "tabs.world": "Worldbuilding", "tabs.index": "Index Board", "tabs.settings": "Novel Settings",
957
+ "shell.wsListFailed": "Failed to load workspace list: {msg}", "shell.stateFailed": "Failed to load snapshot: {msg}",
958
+ "guide.noWsTitle": "No novel workspace available yet", "guide.noWsDesc": "A workspace is picked up automatically once novel/project.yml exists at its root; initialize the directory scaffold with this command first:",
959
+ "guide.notInitTitle": "No novel project in this workspace yet", "guide.notInitDesc": "novel/project.yml is missing at the workspace root. Run this at the workspace root:",
960
+ "guide.defaultTitle": "No novel project in this workspace yet", "guide.defaultDesc": "Initialize the directory scaffold with the command below (or drop an existing novel/ directory into the workspace root), then refresh this panel.",
961
+ "guide.copied": "Copied maoying init", "guide.copyFailed": "Copy failed — select the command text manually", "guide.copying": "Copying…", "guide.copyCmd": "Copy command",
962
+ "common.retry": "Retry", "common.empty": "No data", "common.uncategorized": "Uncategorized", "common.charCount": "{n} chars",
963
+ "common.closeTip": "Dismiss notification", "common.unknownErr": "Unknown error", "common.priorityTitle": "Priority: {pri}", "common.priorityAria": "Priority {pri}",
964
+ "common.chapterNum": "Ch. {n}", "pri.high": "High", "pri.normal": "Normal", "pri.low": "Low",
965
+ "toast.prefix": "Maoying Studio: {msg}", "toast.success": "Success", "toast.indexRebuilt": "Index rebuilt ({n} chapters)",
966
+ "toast.chapterStatus": "Chapter status updated", "toast.foreshadowStatus": "Foreshadowing status updated", "toast.phaseAdvanced": "Phase advanced to {phase}",
967
+ "api.notJson": "Response is not JSON (HTTP {code})", "api.timeout": "Request timed out ({s} s)",
968
+ "status.pending": "Pending", "status.writing": "Writing", "status.review": "Review", "status.accepted": "Accepted", "status.rawSuffix": "({raw})",
969
+ "phase.idea": "Idea", "phase.setting": "Setting", "phase.outline": "Outline", "phase.writing": "Writing", "phase.review": "Review",
970
+ "phase.consistency": "Consistency", "phase.export": "Export", "phase.unset": "No phase",
971
+ "phase.advanceTitle": "Advance to {next}", "phase.advance": "Advance phase", "phase.advancing": "Advancing…", "phase.final": "Final phase",
972
+ "foreshadow.planted": "Planted", "foreshadow.progressing": "In Progress", "foreshadow.resolved": "Resolved", "foreshadow.abandoned": "Abandoned",
973
+ "foreshadow.advance": "Advance", "foreshadow.resolve": "Resolve", "foreshadow.abandon": "Abandon", "foreshadow.advancing": "Advancing…",
974
+ "foreshadow.plantedUnmarked": "Planting chapter unmarked", "foreshadow.resolvedAt": "Resolved in {ch}", "foreshadow.priorityLabel": "Priority {pri}",
975
+ "foreshadow.backfill": "Backfill", "foreshadow.backfillTip": "Backfilled from the author's manuscript", "foreshadow.missingStatus": "missing status",
976
+ "foreshadow.noIdTip": "This entry has no id — its status cannot be written",
977
+ "foreshadow.noStatusTip": "This entry has no status line, the server will reject the transition (409); add a status in foreshadows.yml first",
978
+ "foreshadow.empty": "No entries in foreshadows.yml yet", "foreshadow.colEmpty": "Empty",
979
+ "clue.clue": "Clue", "clue.redHerring": "Red herring", "clue.inference": "Inference", "clue.question": "Question",
980
+ "sci.known": "Confirmed", "sci.extrapolation": "Extrapolation", "sci.hypothesis": "Hypothesis", "world.unlabeled": "Unrated",
981
+ "warnings.title": "Some files failed to parse (skipped):",
982
+ "index.title": "Index Board (clue-board / memories)", "index.rebuild": "Rebuild index", "index.rebuilding": "Rebuilding…",
983
+ "index.rebuildTip": "Rebuild index.yml with the same logic as the CLI index command", "index.clueBoard": "Clue board", "index.clueEmpty": "No entries in clue-board.yml yet",
984
+ "index.memBoard": "Memory board", "index.memEmpty": "No entries in memories.yml yet", "index.promise": "Promise thread",
985
+ "timeline.empty": "No entries in timeline.yml yet", "timeline.importance": "Importance {n}", "timeline.linkedChapter": "Related chapter: {ch}",
986
+ "world.title": "Worldbuilding entries", "world.empty": "No worldbuilding entries in world/ yet", "world.science": "Science settings",
987
+ "world.scienceEmpty": "No science entries in science/ yet", "world.research": "Research", "world.ref": "Ref {n}: {r}",
988
+ "relations.title": "Character relations", "relations.counts": "{n} nodes · {m} relations",
989
+ "relations.help": "Drag a node to pin it · scroll to zoom · double-click to reset · hover an edge for its description",
990
+ "relations.unlinkedTitle": "Unlinked characters (no relation edges)", "relations.noData": "No character or relation data to draw",
991
+ "relations.edgeTipPrefix": "{type}: ", "relations.noDesc": "(no description)",
992
+ "char.empty": "No character cards in characters/ yet",
993
+ "char.field.age": "Age", "char.field.gender": "Gender", "char.field.appearance": "Appearance", "char.field.personality": "Personality",
994
+ "char.field.background": "Background", "char.field.motivation": "Motivation", "char.field.arc": "Character arc",
995
+ "char.years": "Age {n}", "char.linePersonality": "Personality ", "char.lineMotivation": "Motivation ",
996
+ "char.modalAria": "Character details", "char.closeAria": "Close character card", "char.markers": "Tags", "char.file": "File",
997
+ "overview.author": "Author: {name}", "overview.totalWords": "Total characters (CJK)", "overview.totalChapters": "Chapters",
998
+ "overview.characters": "Characters", "overview.openForeshadows": "Unresolved foreshadowing", "overview.statusDist": "Chapter status breakdown", "overview.recent": "Recent chapters",
999
+ "chapters.empty": "No chapter files in chapters/ yet", "chapters.writeQueue": "Writing Queue", "chapters.rowTip": "Click to open this chapter",
1000
+ "chapters.outlineWords": "Outline {n} chars", "chapters.all": "All", "chapters.thVolume": "Vol · Ch", "chapters.thTitle": "Title",
1001
+ "chapters.thStatus": "Status", "chapters.thWords": "Words", "chapters.thSummary": "Summary", "chapters.rowReadTip": "Click to read",
1002
+ "chapters.emptyFiltered": "No chapters with this status",
1003
+ "reader.back": "Back", "reader.backTip": "Back to chapter list", "reader.copyPath": "Copy path", "reader.copyPathTip": "Copy chapter file path",
1004
+ "reader.copied": "Copied chapter file path", "reader.copyFailed": "Copy failed — select the path text manually", "reader.loadFailed": "Failed to load chapter text: {msg}",
1005
+ "reader.noOutline": "No chapter outline", "reader.emptyBody": "(empty chapter)", "reader.finalState": "Accepted (final)", "reader.statusFlow": "Status:",
1006
+ "reader.flowing": "Transitioning…", "reader.advanceTo": "Advance to {next}",
1007
+ "outline.title": "Outline", "outline.empty": "(outline is empty)", "outline.collapseTip": "Collapse outline", "outline.expandTip": "Expand outline",
1008
+ "novel.metaTitle": "Manuscript metadata", "novel.metaNote": "Maps to novel/project.yml · trailing comments are preserved",
1009
+ "novel.fTitle": "Title", "novel.titlePh": "e.g. The Long Night", "novel.fAuthor": "Author", "novel.authorPh": "Pen name",
1010
+ "novel.fGenre": "Genre", "novel.genrePh": "e.g. Eastern fantasy / Urban powers / Alternate history", "novel.fLength": "Planned length",
1011
+ "novel.lengthPh": "e.g. ~1M characters / 300 chapters", "novel.fLanguage": "Text language", "novel.fPhase": "Workflow phase", "novel.fMode": "Co-writing mode",
1012
+ "novel.fDesc": "One-line synopsis", "novel.descPh": "Can be quoted directly in cover copy", "novel.titleRequired": "Title is required",
1013
+ "mode.aiDraft": "AI ghostwriting (AI drafts the prose)", "mode.coWrite": "Chapter-by-chapter co-writing (assign a writer per chapter)", "mode.authorDraft": "Author writes (AI only preps and reviews)",
1014
+ "novel.saving": "Saving…", "novel.save": "Save",
1015
+ "settings.title": "Settings", "settings.closeAria": "Close settings", "settings.language": "Language",
1016
+ "settings.languageHint": "UI text follows the language option; it applies immediately and is remembered.", "settings.fontScale": "Font size",
1017
+ "settings.fontScaleHint": "Scales the whole studio as a percentage of the current font size; applies immediately and is remembered.",
1018
+ "errBoundary.title": "This section failed to render", "errBoundary.summary": "Error summary: {msg}",
1019
+ "dim.cognitiveFrame": "Cognitive Frame", "dim.emotionalAnchor": "Emotional Anchor", "dim.worldTexture": "World Texture",
1020
+ "dim.suspense": "Suspense", "dim.interpersonalTension": "Interpersonal Tension",
1021
+ },
1022
+ };
1023
+ function tt(key) {
1024
+ var d = I18N[UI_LANG] || I18N.zh;
1025
+ var v = d[key];
1026
+ if (v === undefined) v = I18N.zh[key];
1027
+ return v === undefined ? key : v;
1028
+ }
1029
+ function tf(key, vars) {
1030
+ var s = tt(key);
1031
+ if (vars) for (var k in vars) s = s.split("{" + k + "}").join(String(vars[k]));
1032
+ return s;
1033
+ }
861
1034
  function SettingsModal(props) {
862
1035
  var scale = props.scale;
863
1036
  var onPick = props.onPick;
1037
+ var lang = props.lang;
1038
+ var onPickLang = props.onPickLang;
864
1039
  var onClose = props.onClose;
865
1040
  return h(MaskPortal, {
866
1041
  className: "myt-settingsMask",
867
1042
  onMouseDown: function (e) { if (e.target === e.currentTarget) onClose(); },
868
1043
  },
869
- h("div", { className: "myt-settings", role: "dialog", "aria-modal": "true", "aria-label": "系统设置" },
1044
+ h("div", { className: "myt-settings", role: "dialog", "aria-modal": "true", "aria-label": tt("settings.title") },
870
1045
  h("div", { className: "myt-settingsHead" },
871
- h("h3", { className: "myt-settingsTitle" }, "系统设置"),
872
- h("button", { type: "button", className: "myt-closeBtn", "aria-label": "关闭设置", onClick: onClose }, h("span", { className: "myt-closeGlyph", "aria-hidden": "true" }, "×"))),
1046
+ h("h3", { className: "myt-settingsTitle" }, tt("settings.title")),
1047
+ h("button", { type: "button", className: "myt-closeBtn", "aria-label": tt("settings.closeAria"), onClick: onClose }, h("span", { className: "myt-closeGlyph", "aria-hidden": "true" }, "×"))),
873
1048
  h("div", { className: "myt-settingsBody" },
874
- h("div", { className: "myt-settingsLabel" }, "字号"),
1049
+ h("div", { className: "myt-settingsLabel" }, tt("settings.language")),
1050
+ h("div", { className: "myt-setRow" }, UI_LANGS.map(function (o) {
1051
+ return h("button", {
1052
+ key: o[0], type: "button", className: "myt-setOpt", "data-active": lang === o[0] ? "true" : undefined,
1053
+ onClick: function () { onPickLang(o[0]); },
1054
+ }, o[1]);
1055
+ })),
1056
+ h("div", { className: "myt-settingsHint" }, tt("settings.languageHint")),
1057
+ h("div", { className: "myt-settingsLabel" }, tt("settings.fontScale")),
875
1058
  h("div", { className: "myt-setRow" }, FONT_SCALES.map(function (o) {
876
1059
  return h("button", {
877
1060
  key: o[0], type: "button", className: "myt-setOpt", "data-active": scale === o[0] ? "true" : undefined,
878
1061
  onClick: function () { onPick(o[0]); },
879
1062
  }, o[1]);
880
1063
  })),
881
- h("div", { className: "myt-settingsHint" }, "按当前字号的百分比缩放整个创作台,立即生效并记忆。"))));
1064
+ h("div", { className: "myt-settingsHint" }, tt("settings.fontScaleHint")))));
882
1065
  }
883
1066
 
884
1067
  // ── 章节阅读页(整页视图,替代右侧抽屉):返回按钮 + 章纲对照 + 正文 +
@@ -922,32 +1105,32 @@ window.__ModuleLoader__.load({
922
1105
  }
923
1106
  function copyChapterPath() {
924
1107
  copyText(chapterAbsPath(snap && snap.novelDir, file)).then(function (ok) {
925
- if (ok) toast.ok("已复制章节文件路径");
926
- else toast.warn("复制失败,请手动选择路径文本");
1108
+ if (ok) toast.ok(tt("reader.copied"));
1109
+ else toast.warn(tt("reader.copyFailed"));
927
1110
  });
928
1111
  }
929
1112
  return h("div", { className: "myt-reader" },
930
1113
  h("div", { className: "myt-readerBar" },
931
- h("button", { type: "button", className: "myt-btn", onClick: onBack, title: "返回章节列表" },
932
- ic("IconChevronRightOutlineRegular", 13), "返回"),
1114
+ h("button", { type: "button", className: "myt-btn", onClick: onBack, title: tt("reader.backTip") },
1115
+ ic("IconChevronRightOutlineRegular", 13), tt("reader.back")),
933
1116
  h("h2", { className: "myt-readerTitle" }, ch ? chFullTitle(ch) : file),
934
1117
  ch ? statusBadge(ch.status) : null,
935
1118
  h("div", { className: "myt-readerBarActions" },
936
1119
  h("button", {
937
1120
  type: "button", className: "myt-btn myt-copyBtn",
938
- title: "复制章节文件路径", "aria-label": "复制章节文件路径",
1121
+ title: tt("reader.copyPathTip"), "aria-label": tt("reader.copyPathTip"),
939
1122
  onClick: copyChapterPath,
940
- }, ic("IconCopyOutlineRegular", 13), "复制路径"))),
1123
+ }, ic("IconCopyOutlineRegular", 13), tt("reader.copyPath")))),
941
1124
  h("div", { className: "myt-readerBody" },
942
1125
  !detail || detail.loading ? h("div", { className: "myt-stack" }, h("div", { className: "myt-skel myt-skelRow" }), h("div", { className: "myt-skel myt-skelRow", style: { width: "80%" } }), h("div", { className: "myt-skel myt-skelRow", style: { width: "64%" } }))
943
- : detail.err ? h(ErrorStrip, { text: "正文读取失败:" + detail.err, onRetry: function () { loadDetail(file); } })
1126
+ : detail.err ? h(ErrorStrip, { text: tf("reader.loadFailed", { msg: detail.err }), onRetry: function () { loadDetail(file); } })
944
1127
  : [
945
1128
  (function () {
946
1129
  var ol = detail.data && detail.data.outline;
947
1130
  return ol ? h(OutlinePanel, { key: "outline", outline: ol })
948
- : h("div", { key: "outline-none", className: "myt-outlineEmpty" }, "暂无章纲");
1131
+ : h("div", { key: "outline-none", className: "myt-outlineEmpty" }, tt("reader.noOutline"));
949
1132
  })(),
950
- h("pre", { key: "body", className: "myt-chapterBody" }, (detail.data && detail.data.body) || "(空章节)"),
1133
+ h("pre", { key: "body", className: "myt-chapterBody" }, (detail.data && detail.data.body) || tt("reader.emptyBody")),
951
1134
  ],
952
1135
  ch ? h("div", { className: "myt-dimGrid" }, FIVE_DIMS.map(function (dim) {
953
1136
  return h("div", { key: dim[0], className: "myt-dim" },
@@ -958,10 +1141,10 @@ window.__ModuleLoader__.load({
958
1141
  (function () {
959
1142
  var idx = STATUS_FLOW.indexOf(ch.status);
960
1143
  var next = idx !== -1 && idx < STATUS_FLOW.length - 1 ? STATUS_FLOW[idx + 1] : null;
961
- if (!next) return h("span", { className: "myt-muted" }, "已定稿(终态)");
962
- return [h("span", { className: "myt-muted", key: "t" }, "状态流转:"),
1144
+ if (!next) return h("span", { className: "myt-muted" }, tt("reader.finalState"));
1145
+ return [h("span", { className: "myt-muted", key: "t" }, tt("reader.statusFlow")),
963
1146
  h("button", { key: "b", type: "button", className: "myt-btn myt-btnPrimary", disabled: flowBusy, onClick: advance },
964
- flowBusy ? "流转中…" : "推进到「" + STATUS_LABEL[next] + "」")];
1147
+ flowBusy ? tt("reader.flowing") : tf("reader.advanceTo", { next: tt("status." + next) }))];
965
1148
  })()) : null);
966
1149
  }
967
1150
 
@@ -987,7 +1170,7 @@ window.__ModuleLoader__.load({
987
1170
  function set(k, v) { setForm(Object.assign({}, form, (function () { var o = {}; o[k] = v; return o; })())); }
988
1171
  function save() {
989
1172
  if (busy) return;
990
- if (!String(form.title || "").trim()) { setSaveErr("书名不能为空"); return; }
1173
+ if (!String(form.title || "").trim()) { setSaveErr(tt("novel.titleRequired")); return; }
991
1174
  setBusy(true); setSaveErr("");
992
1175
  // 成败提示统一走 apiPost 的 toast 通道(这里不再另弹,避免双 toast);
993
1176
  // 卡内 saveErr 只承担表单旁的错误态。
@@ -1017,27 +1200,27 @@ window.__ModuleLoader__.load({
1017
1200
  return h("div", { className: "myt-stack" },
1018
1201
  h("div", { className: "myt-card" },
1019
1202
  h("div", { className: "myt-cardHead" },
1020
- h("h3", { className: "myt-cardTitle" }, "书稿元信息"),
1021
- h("span", { className: "myt-muted" }, "对应 novel/project.yml · 行尾注释会原样保留")),
1203
+ h("h3", { className: "myt-cardTitle" }, tt("novel.metaTitle")),
1204
+ h("span", { className: "myt-muted" }, tt("novel.metaNote"))),
1022
1205
  h("div", { className: "myt-setGrid" },
1023
- field("书名", "title", "如:长夜余火"),
1024
- field("作者", "author", "笔名"),
1025
- field("题材", "genre", "如:东方玄幻 / 都市异能 / 历史架空"),
1026
- field("预期篇幅", "length", "如:约100万字 / 300章"),
1027
- picker("正文语言", "language", [["zh", "中文"], ["en", "English"], ["ja", "日本語"]]),
1028
- picker("工作流阶段", "phase", PHASES.map(function (p) { return [p, PHASE_LABEL[p] || p]; })),
1029
- picker("共创模式", "mode", [["ai-draft", "AI 代笔(AI 写正文)"], ["co-write", "逐章共创(按章指派谁写)"], ["author-draft", "作者亲笔(AI 只备料审稿)"]]),
1030
- field("一句话简介", "description", "写封面文案时可直接引用", true))),
1206
+ field(tt("novel.fTitle"), "title", tt("novel.titlePh")),
1207
+ field(tt("novel.fAuthor"), "author", tt("novel.authorPh")),
1208
+ field(tt("novel.fGenre"), "genre", tt("novel.genrePh")),
1209
+ field(tt("novel.fLength"), "length", tt("novel.lengthPh")),
1210
+ picker(tt("novel.fLanguage"), "language", [["zh", "中文"], ["en", "English"], ["ja", "日本語"]]),
1211
+ picker(tt("novel.fPhase"), "phase", PHASES.map(function (p) { return [p, tt("phase." + p)]; })),
1212
+ picker(tt("novel.fMode"), "mode", [["ai-draft", tt("mode.aiDraft")], ["co-write", tt("mode.coWrite")], ["author-draft", tt("mode.authorDraft")]]),
1213
+ field(tt("novel.fDesc"), "description", tt("novel.descPh"), true))),
1031
1214
  saveErr ? h(ErrorStrip, { text: saveErr }) : null,
1032
1215
  h("div", { className: "myt-setActions" },
1033
1216
  h("button", { type: "button", className: "myt-btn myt-btnPrimary", disabled: busy, onClick: save },
1034
- busy ? "保存中…" : "保存设置")));
1217
+ busy ? tt("novel.saving") : tt("novel.save"))));
1035
1218
  }
1036
1219
 
1037
1220
  // ── 章节 tab:状态筛选 pills + 表格 + 行抽屉(只读正文 + 五维 + 流转)────
1038
1221
  var FIVE_DIMS = [
1039
- ["cognitive_frame", "认知框架"], ["emotional_anchor", "情感锚点"], ["world_texture", "世界质感"],
1040
- ["suspense", "悬念"], ["interpersonal_tension", "人际张力"],
1222
+ ["cognitive_frame", tt("dim.cognitiveFrame")], ["emotional_anchor", tt("dim.emotionalAnchor")], ["world_texture", tt("dim.worldTexture")],
1223
+ ["suspense", tt("dim.suspense")], ["interpersonal_tension", tt("dim.interpersonalTension")],
1041
1224
  ];
1042
1225
  // 章纲对照栏(五期):outline 非空时渲染可折叠原文(默认展开,只读,样式
1043
1226
  // 与正文区分);共享常量避免重渲染时折叠态意外重置——组件随抽屉挂载,
@@ -1045,16 +1228,16 @@ window.__ModuleLoader__.load({
1045
1228
  function OutlinePanel(props) {
1046
1229
  var outline = props.outline;
1047
1230
  var _op = useState(true), open = _op[0], setOpen = _op[1];
1048
- var body = outline && typeof outline.body === "string" && outline.body.trim() !== "" ? outline.body : "(章纲为空)";
1231
+ var body = outline && typeof outline.body === "string" && outline.body.trim() !== "" ? outline.body : tt("outline.empty");
1049
1232
  return h("div", { className: "myt-outlinePanel" },
1050
1233
  h("button", {
1051
1234
  type: "button", className: "myt-outlineHead",
1052
1235
  "aria-expanded": open ? "true" : "false",
1053
- title: open ? "收起章纲" : "展开章纲",
1236
+ title: open ? tt("outline.collapseTip") : tt("outline.expandTip"),
1054
1237
  onClick: function () { setOpen(!open); },
1055
1238
  },
1056
- h("span", { className: "myt-outlineTitle" }, "章纲对照"),
1057
- typeof (outline && outline.words) === "number" ? h("span", { className: "myt-outlineWords" }, fmtNum(outline.words) + " 字") : null,
1239
+ h("span", { className: "myt-outlineTitle" }, tt("outline.title")),
1240
+ typeof (outline && outline.words) === "number" ? h("span", { className: "myt-outlineWords" }, tf("common.charCount", { n: fmtNum(outline.words) })) : null,
1058
1241
  h("span", { className: "myt-outlineChevron", "aria-hidden": "true" },
1059
1242
  ic(open ? "IconChevronDownOutlineRegular" : "IconChevronRightOutlineRegular", 14))),
1060
1243
  open ? h(MdView, { text: body, className: "myt-outlineBody" }) : null);
@@ -1093,14 +1276,14 @@ window.__ModuleLoader__.load({
1093
1276
  return h("div", { className: "myt-stack" },
1094
1277
  writeOrder.length ? h("div", { className: "myt-card myt-writeOrder" },
1095
1278
  h("div", { className: "myt-cardHead" },
1096
- h("h3", { className: "myt-cardTitle" }, "按单写作"),
1279
+ h("h3", { className: "myt-cardTitle" }, tt("chapters.writeQueue")),
1097
1280
  h("span", { className: "myt-boardCount" }, writeOrder.length)),
1098
1281
  h("div", { className: "myt-writeOrderList" }, writeOrder.map(function (item) {
1099
1282
  var ch = item.ch;
1100
1283
  var ol = item.ol;
1101
1284
  return h("button", {
1102
1285
  key: ch.file, type: "button", className: "myt-writeOrderRow",
1103
- title: "点击打开该章",
1286
+ title: tt("chapters.rowTip"),
1104
1287
  onClick: function () { onOpen(ch.file); },
1105
1288
  },
1106
1289
  h("span", { className: "myt-chPath" }, chWhere(ch) || ch.file),
@@ -1108,11 +1291,11 @@ window.__ModuleLoader__.load({
1108
1291
  statusBadge(ch.status),
1109
1292
  ol && typeof ol.excerpt === "string" && ol.excerpt !== ""
1110
1293
  ? h("span", { className: "myt-writeOrderExcerpt", title: ol.excerpt }, truncate(ol.excerpt, 60)) : null,
1111
- ol && ol.words ? h("span", { className: "myt-wordsCell" }, "章纲 " + fmtNum(ol.words) + " 字") : null);
1294
+ ol && ol.words ? h("span", { className: "myt-wordsCell" }, tf("chapters.outlineWords", { n: fmtNum(ol.words) })) : null);
1112
1295
  }))) : null,
1113
1296
  h("div", { className: "myt-filterRow" },
1114
- [{ k: "all", label: "全部", n: chapters.length }].concat(STATUS_FLOW.map(function (s) {
1115
- return { k: s, label: STATUS_LABEL[s], n: counts[s] || 0 };
1297
+ [{ k: "all", label: tt("chapters.all"), n: chapters.length }].concat(STATUS_FLOW.map(function (s) {
1298
+ return { k: s, label: tt("status." + s), n: counts[s] || 0 };
1116
1299
  })).map(function (f) {
1117
1300
  return h("button", {
1118
1301
  key: f.k, type: "button", className: "myt-filterPill",
@@ -1123,22 +1306,22 @@ window.__ModuleLoader__.load({
1123
1306
  rows.length ? h("div", { className: "myt-tableWrap" },
1124
1307
  h("table", { className: "myt-table" },
1125
1308
  h("thead", null, h("tr", null,
1126
- h("th", null, "卷 · 章"), h("th", null, "标题"), h("th", null, "状态"), h("th", null, "字数"), h("th", null, "梗概"))),
1309
+ h("th", null, tt("chapters.thVolume")), h("th", null, tt("chapters.thTitle")), h("th", null, tt("chapters.thStatus")), h("th", null, tt("chapters.thWords")), h("th", null, tt("chapters.thSummary")))),
1127
1310
  h("tbody", null, rows.map(function (ch) {
1128
- return h("tr", { key: ch.file, onClick: function () { onOpen(ch.file); }, title: "点击阅读正文" },
1311
+ return h("tr", { key: ch.file, onClick: function () { onOpen(ch.file); }, title: tt("chapters.rowReadTip") },
1129
1312
  h("td", null, h("span", { className: "myt-chPath" }, chWhere(ch) || "—")),
1130
1313
  h("td", null, h("span", { className: "myt-chTitle" }, orDash(ch.title))),
1131
1314
  h("td", null, statusBadge(ch.status)),
1132
1315
  h("td", null, h("span", { className: "myt-wordsCell" }, fmtNum(ch.words))),
1133
1316
  h("td", null, h("div", { className: "myt-summaryCell myt-muted" }, orDash(ch.summary))));
1134
1317
  }))))
1135
- : (chapters.length ? h(Empty, { text: "该状态下没有章节" }) : h(Empty, { text: "chapters/ 目录还没有章节文件" })));
1318
+ : (chapters.length ? h(Empty, { text: tt("chapters.emptyFiltered") }) : h(Empty, { text: tt("chapters.empty") })));
1136
1319
  }
1137
1320
 
1138
1321
  // ── 角色 tab:卡墙 + 全卡弹层 ───────────────────────────────────────────
1139
1322
  var CHAR_FULL_ROWS = [
1140
- ["age", "年龄"], ["gender", "性别"], ["appearance", "外貌"], ["personality", "性格"],
1141
- ["background", "背景"], ["motivation", "动机"], ["arc", "成长弧"],
1323
+ ["age", tt("char.field.age")], ["gender", tt("char.field.gender")], ["appearance", tt("char.field.appearance")], ["personality", tt("char.field.personality")],
1324
+ ["background", tt("char.field.background")], ["motivation", tt("char.field.motivation")], ["arc", tt("char.field.arc")],
1142
1325
  ];
1143
1326
  function CharactersTab(props) {
1144
1327
  var characters = props.snap.characters || [];
@@ -1152,20 +1335,20 @@ window.__ModuleLoader__.load({
1152
1335
  h("span", { className: "myt-charName" }, orDash(c.name)),
1153
1336
  roleBadge(c.role)),
1154
1337
  h("div", { className: "myt-charMeta" },
1155
- [c.age !== null && c.age !== undefined ? c.age + " 岁" : null, c.gender || null].filter(Boolean).join(" · ") || "—"),
1156
- c.personality ? h("div", { className: "myt-charLine" }, h("span", { className: "myt-charLabel" }, "性格 "), truncate(c.personality, 60)) : null,
1157
- c.motivation ? h("div", { className: "myt-charLine" }, h("span", { className: "myt-charLabel" }, "动机 "), truncate(c.motivation, 60)) : null);
1158
- })) : h(Empty, { text: "characters/ 目录还没有角色卡" }),
1338
+ [c.age !== null && c.age !== undefined ? tf("char.years", { n: c.age }) : null, c.gender || null].filter(Boolean).join(" · ") || "—"),
1339
+ c.personality ? h("div", { className: "myt-charLine" }, h("span", { className: "myt-charLabel" }, tt("char.linePersonality")), truncate(c.personality, 60)) : null,
1340
+ c.motivation ? h("div", { className: "myt-charLine" }, h("span", { className: "myt-charLabel" }, tt("char.lineMotivation")), truncate(c.motivation, 60)) : null);
1341
+ })) : h(Empty, { text: tt("char.empty") }),
1159
1342
  modal ? h(MaskPortal, {
1160
1343
  className: "myt-modalMask",
1161
1344
  onMouseDown: function (e) { if (e.target === e.currentTarget) setModalFile(null); },
1162
1345
  },
1163
- h("div", { className: "myt-modalCard", role: "dialog", "aria-modal": "true", "aria-label": "角色详情" },
1346
+ h("div", { className: "myt-modalCard", role: "dialog", "aria-modal": "true", "aria-label": tt("char.modalAria") },
1164
1347
  h("div", { className: "myt-modalHead" },
1165
1348
  h("h2", { className: "myt-modalTitle" }, orDash(modal.name)),
1166
1349
  h("div", { className: "myt-heroMeta" },
1167
1350
  roleBadge(modal.role),
1168
- h("button", { type: "button", className: "myt-closeBtn", "aria-label": "关闭角色卡", onClick: function () { setModalFile(null); } }, h("span", { className: "myt-closeGlyph", "aria-hidden": "true" }, "×")))),
1351
+ h("button", { type: "button", className: "myt-closeBtn", "aria-label": tt("char.closeAria"), onClick: function () { setModalFile(null); } }, h("span", { className: "myt-closeGlyph", "aria-hidden": "true" }, "×")))),
1169
1352
  h("div", { className: "myt-modalBody" },
1170
1353
  CHAR_FULL_ROWS.map(function (row) {
1171
1354
  return h("div", { key: row[0], className: "myt-kvRow" },
@@ -1173,13 +1356,13 @@ window.__ModuleLoader__.load({
1173
1356
  h("span", { className: "myt-kvVal" }, orDash(modal[row[0]])));
1174
1357
  }),
1175
1358
  h("div", { className: "myt-kvRow" },
1176
- h("span", { className: "myt-kvKey" }, "标记"),
1359
+ h("span", { className: "myt-kvKey" }, tt("char.markers")),
1177
1360
  h("span", { className: "myt-kvVal myt-tagRow" },
1178
1361
  (modal.markers || []).length
1179
1362
  ? modal.markers.map(function (m, i) { return h("span", { key: i, className: "myt-markerTag" }, m); })
1180
1363
  : "—")),
1181
1364
  h("div", { className: "myt-kvRow" },
1182
- h("span", { className: "myt-kvKey" }, "文件"),
1365
+ h("span", { className: "myt-kvKey" }, tt("char.file")),
1183
1366
  h("span", { className: "myt-kvVal" }, modal.file || "—"))))) : null);
1184
1367
  }
1185
1368
 
@@ -1201,7 +1384,7 @@ window.__ModuleLoader__.load({
1201
1384
  var items = list.filter(function (f) { return (f.status || "planted") === col; });
1202
1385
  return h("div", { key: col, className: "myt-boardCol" },
1203
1386
  h("div", { className: "myt-boardHead" },
1204
- h("span", { className: "myt-boardTitle" }, FS_LABEL[col]),
1387
+ h("span", { className: "myt-boardTitle" }, tt("foreshadow." + col)),
1205
1388
  h("span", { className: "myt-boardCount" }, items.length)),
1206
1389
  items.map(function (fs) {
1207
1390
  var next = FS_FLOW[col] || null;
@@ -1211,26 +1394,26 @@ window.__ModuleLoader__.load({
1211
1394
  var noStatus = !fs.status;
1212
1395
  var noWrite = !fs.id || noStatus;
1213
1396
  var noWriteTip = !fs.id
1214
- ? "该条目缺 id,无法写状态"
1215
- : "该条目缺 status 行,服务端会拒绝流转(409);请先在 foreshadows.yml 补上 status";
1216
- var chRange = [fs.planted_chapter || "埋设章未标", fs.expected_resolve].filter(Boolean).join(" → ");
1397
+ ? tt("foreshadow.noIdTip")
1398
+ : tt("foreshadow.noStatusTip");
1399
+ var chRange = [fs.planted_chapter || tt("foreshadow.plantedUnmarked"), fs.expected_resolve].filter(Boolean).join(" → ");
1217
1400
  return h("div", { key: fs.id || fs.title, className: "myt-fsCard" },
1218
1401
  h("div", { className: "myt-fsTitle" },
1219
1402
  priDot(fs.priority), h("span", null, orDash(fs.title) || orDash(fs.id)),
1220
1403
  // 回填徽章(五期账本回填):区别于写前规划条目,悬浮见来源。
1221
- fs.source === "backfill" ? h("span", { className: "myt-backfillBadge", title: "从作者正文回填" }, "回填") : null,
1222
- noStatus ? h("span", { className: "myt-fsNoStatus", title: noWriteTip }, "缺 status") : null),
1404
+ fs.source === "backfill" ? h("span", { className: "myt-backfillBadge", title: tt("foreshadow.backfillTip") }, tt("foreshadow.backfill")) : null,
1405
+ noStatus ? h("span", { className: "myt-fsNoStatus", title: noWriteTip }, tt("foreshadow.missingStatus")) : null),
1223
1406
  h("div", { className: "myt-fsMeta" },
1224
- h("span", null, col === "resolved" && fs.resolved_chapter ? "回收于 " + fs.resolved_chapter : chRange),
1225
- fs.priority && fs.priority !== "normal" ? h("span", null, "优先级 " + (PRI_LABEL[fs.priority] || fs.priority)) : null),
1407
+ h("span", null, col === "resolved" && fs.resolved_chapter ? tf("foreshadow.resolvedAt", { ch: fs.resolved_chapter }) : chRange),
1408
+ fs.priority && fs.priority !== "normal" ? h("span", null, tf("foreshadow.priorityLabel", { pri: ["high", "low", "normal"].indexOf(fs.priority) !== -1 ? tt("pri." + fs.priority) : fs.priority })) : null),
1226
1409
  fs.notes ? h("div", { className: "myt-fsNotes", title: fs.notes }, fs.notes) : null,
1227
1410
  h("div", { className: "myt-fsActions" },
1228
- next ? h("button", { type: "button", className: "myt-btn", disabled: busy || noWrite, title: noWriteTip, onClick: function () { setStatus(fs, next); } }, busy ? "推进中…" : (col === "planted" ? "推进" : "回收")) : null,
1411
+ next ? h("button", { type: "button", className: "myt-btn", disabled: busy || noWrite, title: noWriteTip, onClick: function () { setStatus(fs, next); } }, busy ? tt("foreshadow.advancing") : (col === "planted" ? tt("foreshadow.advance") : tt("foreshadow.resolve"))) : null,
1229
1412
  col !== "resolved" && col !== "abandoned"
1230
- ? h("button", { type: "button", className: "myt-btn myt-btnDanger", disabled: busy || noWrite, title: noWriteTip, onClick: function () { setStatus(fs, "abandoned"); } }, "弃用") : null));
1413
+ ? h("button", { type: "button", className: "myt-btn myt-btnDanger", disabled: busy || noWrite, title: noWriteTip, onClick: function () { setStatus(fs, "abandoned"); } }, tt("foreshadow.abandon")) : null));
1231
1414
  }),
1232
- items.length ? null : h("div", { className: "myt-muted" }, "空"));
1233
- })) : h(Empty, { text: "foreshadows.yml 还没有伏笔条目" }));
1415
+ items.length ? null : h("div", { className: "myt-muted" }, tt("foreshadow.colEmpty")));
1416
+ })) : h(Empty, { text: tt("foreshadow.empty") }));
1234
1417
  }
1235
1418
 
1236
1419
  // ── 时间线 tab:垂直轴 + importance 点位(1-5 → 大小/深浅)──────────────
@@ -1239,17 +1422,17 @@ window.__ModuleLoader__.load({
1239
1422
  if (items.length) {
1240
1423
  items.sort(function (a, b) { return String(a.year || "").localeCompare(String(b.year || ""), "zh-CN", { numeric: true }); });
1241
1424
  }
1242
- if (!items.length) return h(Empty, { text: "timeline.yml 还没有时间线条目" });
1425
+ if (!items.length) return h(Empty, { text: tt("timeline.empty") });
1243
1426
  return h("div", { className: "myt-card" },
1244
1427
  h("div", { className: "myt-timeline" }, items.map(function (t, i) {
1245
1428
  var imp = Math.min(5, Math.max(1, typeof t.importance === "number" ? t.importance : 3));
1246
1429
  return h("div", { key: i, className: "myt-tlRow" },
1247
1430
  h("div", { className: "myt-tlYear" }, orDash(t.year)),
1248
- h("div", { className: "myt-tlAxis" }, h("span", { className: "myt-tlDot", "data-imp": String(imp), title: "重要度 " + imp })),
1431
+ h("div", { className: "myt-tlAxis" }, h("span", { className: "myt-tlDot", "data-imp": String(imp), title: tf("timeline.importance", { n: imp }) })),
1249
1432
  h("div", { className: "myt-tlCard" },
1250
1433
  h("div", { className: "myt-tlTitle" }, orDash(t.title)),
1251
1434
  t.description ? h("div", { className: "myt-tlDesc" }, t.description) : null,
1252
- t.chapter ? h("div", { className: "myt-tlChapter" }, "关联章节:" + t.chapter) : null));
1435
+ t.chapter ? h("div", { className: "myt-tlChapter" }, tf("timeline.linkedChapter", { ch: t.chapter })) : null));
1253
1436
  })));
1254
1437
  }
1255
1438
 
@@ -1264,14 +1447,14 @@ window.__ModuleLoader__.load({
1264
1447
  // 真值 → TypeError → 整面板白屏;Object.create(null) 杜绝。
1265
1448
  var groups = Object.create(null);
1266
1449
  world.forEach(function (w) {
1267
- var key = w.category || "未分类";
1450
+ var key = w.category || tt("common.uncategorized");
1268
1451
  if (!groups[key]) groups[key] = [];
1269
1452
  groups[key].push(w);
1270
1453
  });
1271
1454
  var groupKeys = Object.keys(groups).sort(function (a, b) { return a.localeCompare(b, "zh-CN"); });
1272
1455
  return h("div", { className: "myt-stack" },
1273
1456
  h("div", { className: "myt-card" },
1274
- h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, "世界观条目")),
1457
+ h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, tt("world.title"))),
1275
1458
  groupKeys.length ? h("div", { className: "myt-stack" }, groupKeys.map(function (key) {
1276
1459
  return h("div", { key: key, className: "myt-worldGroup" },
1277
1460
  h("div", { className: "myt-listHead" }, key, h("span", { className: "myt-boardCount" }, groups[key].length)),
@@ -1282,23 +1465,23 @@ window.__ModuleLoader__.load({
1282
1465
  (w.tags || []).length ? h("span", { className: "myt-tagRow" }, w.tags.map(function (t, j) { return h("span", { key: j, className: "myt-tag" }, t); })) : null),
1283
1466
  w.description ? h(MdView, { text: w.description, className: "myt-worldDesc" }) : null);
1284
1467
  }));
1285
- })) : h(Empty, { text: "world/ 目录还没有设定条目" })),
1468
+ })) : h(Empty, { text: tt("world.empty") })),
1286
1469
  h("div", { className: "myt-card" },
1287
- h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, "科学设定"),
1470
+ h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, tt("world.science")),
1288
1471
  h("span", { className: "myt-boardCount" }, science.length)),
1289
1472
  science.length ? h("div", { className: "myt-stack" }, science.map(function (s, i) {
1290
- var label = SCI_LABEL[s.label] ? s.label : null;
1473
+ var label = ["known", "extrapolation", "hypothesis"].indexOf(s.label) !== -1 ? s.label : null;
1291
1474
  return h("div", { key: s.file || i, className: "myt-worldItem" },
1292
1475
  h("div", { className: "myt-worldHead" },
1293
1476
  h("span", { className: "myt-worldName" }, orDash(s.name)),
1294
1477
  h("span", { className: "myt-badge myt-sciTag", "data-label": label || "hypothesis", title: s.label && !label ? s.label : undefined },
1295
- label ? SCI_LABEL[label] : (s.label || "未标级"))),
1478
+ label ? tt("sci." + label) : (s.label || tt("world.unlabeled")))),
1296
1479
  s.description ? h(MdView, { text: s.description, className: "myt-worldDesc" }) : null,
1297
1480
  (s.references || []).length ? h("div", { className: "myt-refList" },
1298
- s.references.map(function (r, j) { return h("span", { key: j, className: "myt-refItem" }, "参考 " + (j + 1) + ":" + r); })) : null);
1299
- })) : h(Empty, { text: "science/ 目录还没有科学设定" })),
1481
+ s.references.map(function (r, j) { return h("span", { key: j, className: "myt-refItem" }, tf("world.ref", { n: j + 1, r: r })); })) : null);
1482
+ })) : h(Empty, { text: tt("world.scienceEmpty") })),
1300
1483
  research.length ? h("div", { className: "myt-card" },
1301
- h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, "资料检索"),
1484
+ h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, tt("world.research")),
1302
1485
  h("span", { className: "myt-boardCount" }, research.length)),
1303
1486
  h("div", { className: "myt-tagRow" }, research.map(function (r, i) {
1304
1487
  return h("span", { key: r.file || i, className: "myt-tag", title: r.file || "" }, orDash(r.topic) || r.file);
@@ -1322,27 +1505,28 @@ window.__ModuleLoader__.load({
1322
1505
  // 无原型容器:kind/category 是数据文件自由文本,见 WorldTab 处说明。
1323
1506
  var clueGroups = Object.create(null);
1324
1507
  clues.forEach(function (c) {
1325
- var key = c.kind || "未分类";
1508
+ var key = c.kind || tt("common.uncategorized");
1326
1509
  if (!clueGroups[key]) clueGroups[key] = [];
1327
1510
  clueGroups[key].push(c);
1328
1511
  });
1329
1512
  var memGroups = Object.create(null);
1330
1513
  memories.forEach(function (m) {
1331
- var key = m.category || "未分类";
1514
+ var key = m.category || tt("common.uncategorized");
1332
1515
  if (!memGroups[key]) memGroups[key] = [];
1333
1516
  memGroups[key].push(m);
1334
1517
  });
1335
1518
  function groupKey(a, b) { return a.localeCompare(b, "zh-CN"); }
1336
1519
  return h("div", { className: "myt-stack" },
1337
1520
  h("div", { className: "myt-cardHead" },
1338
- h("h3", { className: "myt-cardTitle" }, "索引板(clue-board / memories)"),
1339
- h("button", { type: "button", className: "myt-btn", disabled: rebuildBusy, onClick: rebuild, title: "按 CLI index 同一逻辑重建 index.yml" },
1340
- rebuildBusy ? "重建中…" : "重建索引")),
1521
+ h("h3", { className: "myt-cardTitle" }, tt("index.title")),
1522
+ h("button", { type: "button", className: "myt-btn", disabled: rebuildBusy, onClick: rebuild, title: tt("index.rebuildTip") },
1523
+ rebuildBusy ? tt("index.rebuilding") : tt("index.rebuild"))),
1341
1524
  h("div", { className: "myt-indexCols" },
1342
1525
  h("div", { className: "myt-card" },
1343
- h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, "线索板"), h("span", { className: "myt-boardCount" }, clues.length)),
1526
+ h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, tt("index.clueBoard")), h("span", { className: "myt-boardCount" }, clues.length)),
1344
1527
  clues.length ? Object.keys(clueGroups).sort(groupKey).map(function (kind) {
1345
- var label = CLUE_KIND_LABEL[kind] || kind;
1528
+ var ck = CLUE_KIND_KEYS[kind];
1529
+ var label = ck ? tt("clue." + ck) : kind;
1346
1530
  return h("div", { key: kind, className: "myt-worldGroup" },
1347
1531
  h("div", { className: "myt-listHead" }, label, h("span", { className: "myt-boardCount" }, clueGroups[kind].length)),
1348
1532
  clueGroups[kind].map(function (c, i) {
@@ -1350,21 +1534,21 @@ window.__ModuleLoader__.load({
1350
1534
  h("span", { className: "myt-clueText" }, orDash(c.content)),
1351
1535
  c.related_chapter ? h("span", { className: "myt-clueChapter" }, c.related_chapter) : null);
1352
1536
  }));
1353
- }) : h(Empty, { text: "clue-board.yml 还没有条目" })),
1537
+ }) : h(Empty, { text: tt("index.clueEmpty") })),
1354
1538
  h("div", { className: "myt-card" },
1355
- h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, "记忆板"), h("span", { className: "myt-boardCount" }, memories.length)),
1539
+ h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, tt("index.memBoard")), h("span", { className: "myt-boardCount" }, memories.length)),
1356
1540
  memories.length ? Object.keys(memGroups).sort(groupKey).map(function (cat) {
1357
1541
  var isPromise = /promise|承诺/i.test(cat);
1358
1542
  return h("div", { key: cat, className: "myt-worldGroup" },
1359
1543
  h("div", { className: "myt-listHead" },
1360
- cat, isPromise ? h("span", { className: "myt-markerTag" }, "承诺线") : null,
1544
+ cat, isPromise ? h("span", { className: "myt-markerTag" }, tt("index.promise")) : null,
1361
1545
  h("span", { className: "myt-boardCount" }, memGroups[cat].length)),
1362
1546
  memGroups[cat].map(function (m, i) {
1363
1547
  return h("div", { key: i, className: "myt-memItem", "data-promise": isPromise ? "true" : undefined },
1364
1548
  h("span", { className: "myt-clueText" }, orDash(m.content)),
1365
1549
  m.source_chapter ? h("span", { className: "myt-memChapter" }, m.source_chapter) : null);
1366
1550
  }));
1367
- }) : h(Empty, { text: "memories.yml 还没有条目" }))));
1551
+ }) : h(Empty, { text: tt("index.memEmpty") }))));
1368
1552
  }
1369
1553
 
1370
1554
  // ── 关系图 tab:canvas 力导向(斥力 + 弹簧 + 中心引力,rAF 循环)─────────
@@ -1573,7 +1757,7 @@ window.__ModuleLoader__.load({
1573
1757
  tipRafRef.current = 0;
1574
1758
  var e2 = hitEdge(p.x, p.y);
1575
1759
  if (e2) {
1576
- setTip({ x: p.x, y: p.y, text: (e2.type ? e2.type + ":" : "") + (e2.desc || "(无描述)") });
1760
+ setTip({ x: p.x, y: p.y, text: (e2.type ? e2.type + ":" : "") + (e2.desc || tt("relations.noDesc")) });
1577
1761
  } else {
1578
1762
  setTip(function (prev) { return prev ? null : prev; });
1579
1763
  }
@@ -1606,18 +1790,18 @@ window.__ModuleLoader__.load({
1606
1790
  canvas.removeEventListener("dblclick", onDbl);
1607
1791
  };
1608
1792
  }, [snap]);
1609
- if (!graph.nodes.length) return h(Empty, { text: "没有角色或关系数据,无法绘图" });
1793
+ if (!graph.nodes.length) return h(Empty, { text: tt("relations.noData") });
1610
1794
  return h("div", { className: "myt-stack" },
1611
1795
  h("div", { className: "myt-card" },
1612
1796
  h("div", { className: "myt-cardHead" },
1613
- h("h3", { className: "myt-cardTitle" }, "角色关系图"),
1614
- h("span", { className: "myt-muted" }, graph.nodes.length + " 节点 · " + graph.edges.length + " 关系")),
1797
+ h("h3", { className: "myt-cardTitle" }, tt("relations.title")),
1798
+ h("span", { className: "myt-muted" }, tf("relations.counts", { n: graph.nodes.length, m: graph.edges.length }))),
1615
1799
  h("div", { className: "myt-graphWrap", ref: wrapRef },
1616
1800
  h("canvas", { className: "myt-graphCanvas", ref: canvasRef, "data-dragging": "false" }),
1617
1801
  tip ? h("div", { className: "myt-graphTip", style: { left: Math.min(tip.x + 12, 320) + "px", top: Math.max(tip.y - 34, 4) + "px" } }, tip.text) : null),
1618
- h("div", { className: "myt-graphFoot" }, "拖节点固定位置 · 滚轮缩放 · 双击复位 · 悬停边看关系描述")),
1802
+ h("div", { className: "myt-graphFoot" }, tt("relations.help"))),
1619
1803
  graph.unlinked.length ? h("div", { className: "myt-card" },
1620
- h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, "未入图角色(无关系边)")),
1804
+ h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, tt("relations.unlinkedTitle"))),
1621
1805
  h("div", { className: "myt-unlinked" }, graph.unlinked.map(function (n, i) {
1622
1806
  return h("span", { key: n || i, className: "myt-unlinkedTag" }, n);
1623
1807
  }))) : null);