dsh-plugin-update 0.5.6 → 0.5.8

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/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  格式为 Keep-a-Changelog 子集:`Added/Fixed/Changed` 必写,`Deprecated/Removed/Security` 有则透传,`Unreleased` 面板忽略,禁止 git-log 直倒。面板缺日志时中性提示,不挡安装。
4
4
 
5
+ ## [0.5.8] - 2026-10-06
6
+
7
+ ### Added
8
+ - 语言底座 v2 按宿主语言单语渲染,入口两条链先生效(#60,地图 #51):新增 `src/lang.ts` 信号层(显式 `locale` > `document.documentElement.lang` > `navigator.languages` > 中文兜底,非中文落英文;单例 `MutationObserver` + 服务订阅,切换即时重绘,`unmount` 停订,无 DOM 落中文);`bilingual.ts` 新增 `copyHTML/copyText(key, lang, values)` 单语纯函数出口(一次一种语言,单 `span[lang]`),旧 `bilingualHTML/Text` 转为按当前语言包装,字典新增 `batch-entry.*` 6 键(13 键全 `draft`);`entry/entry-batch` 新增 `locale` 选项全走字典单语块(含聚合口径与 `已是最新` 小字);`panel/panel-batch` 仅加 `locale` 占位暂不消费(#61-#66 按面迁移);双语门禁改断言单语(缺语言/混语言即红,冻结词元除外);`build.mjs` 加 `lang.ts` 单元;README 补单语与窗口期说明。
9
+
10
+ ## [0.5.7] - 2026-10-06
11
+
12
+ ### Fixed
13
+ - 拆入口通道安装禁令(#58 真因):入口件 HTTP 直挂不再源码级禁 updateInstall(入口件核心本就没有安装代码路径,禁令只连坐入口件打开的面板;真正门禁在宿主侧核心);入口打开面板后安装电话端到端验证通过。
14
+
5
15
  ## [0.5.6] - 2026-10-06
6
16
 
7
17
  ### Added
package/README.md CHANGED
@@ -183,7 +183,9 @@ const entry = mountUpdateEntry(document.getElementById('upd-entry'), {
183
183
  | 什么时候查 | `mount` / `never` | `mount` | 进页面静默查一次(**只调 `.updateStatus`,只读**);`never` 则只在点击时查 |
184
184
  | 点了做什么 | `has-update` / `always` / `manual` / `direct` | `has-update` | 有新版才开面板;检查完总是开;交给 `onActivate` 自己跳;点开即弹窗、不预查 |
185
185
 
186
- 按钮文案随状态自己变:`检查更新` / `有新版 1.1.0` / `正在安装…` / `待重启` / `更新失败,点此查看`。
186
+ 按钮文案随状态自己变(按宿主语言单语渲染:中文界面纯中文、英文界面纯英文,一次只出现一种语言):`检查更新` / `有新版 1.1.0` / `正在安装…` / `待重启` / `更新失败,点此查看`(英文见集中字典同 key)。
187
+
188
+ 语言跟随(单语):入口件默认跟随 `document.documentElement.lang` > `navigator.languages` > 中文兜底;显式覆盖传 `locale: 'zh' | 'en' | { getActive(): string; subscribe?(cb): () => void }`(对象形态可接宿主语言服务,切换即时重绘,`unmount` 后停订;无 DOM 环境不抛,落中文)。
187
189
 
188
190
  无新版时原地那句小字(`已是最新 X.Y.Z`)只在 `has-update` 下出现:不想看它就用 `openOn: 'always'`(检查完总是开弹窗,无新版在弹窗里看“已是最新”)或 `openOn: 'direct'`(点开即弹窗,连预查都省了,面板挂载即自查;徽标形态仍走回调口径)。
189
191
 
@@ -212,10 +214,12 @@ const entry = mountUpdateBatchEntry(document.getElementById('upd-entry'), {
212
214
  ```
213
215
 
214
216
  聚合口径与批量面板总账同一份数法(含「忙失败占位翻回可更新」):`忙 > 失败 > 待重启 > 可更新 > 待查`,
215
- 文案为 `检查更新` / `N 家可更新` / `正在安装…` / `N 家待重启` / `N 家失败,点此查看`。
217
+ 文案按宿主语言单语渲染(中文如左,英文见字典 `batch-entry.*` 同 key),`locale` 选项与单入口同形态,切换即时重绘。
216
218
  三自由度与单入口同取值,缺省不同:`openOn` 缺省 `'always'`(查完总是开批量面板;`has-update` 下有事才开,否则原地给总账一句)。
217
219
  `autoCheck` 缺省 `'mount'`(只调 `.batchStatus`,只读);铁律同单入口:任何路径都不自动安装。
218
220
 
221
+ 窗口期说明(必读):`mountUpdatePanel` 与 `mountUpdateBatchPanel` 已接受 `locale` 选项(与入口件同形态),但本版本暂不消费——传了不生效,面板内容仍按既有中文渲染;语言跟随的按面迁移在后续版本逐面落地,入口件两条链已先生效。
222
+
219
223
  ### 第 2.6 节:多目标批量更新(一个插件管 N 个插件的更新)
220
224
 
221
225
  一个插件替自己**和另外几个插件**管更新时,别把 N 个面板并排——用户在那块界面上只问三件事:
@@ -1,30 +1,40 @@
1
+ import { type AppLang } from './lang.js';
1
2
  /** 字典版本钉(#53 Q3 解耦):key 冻即解阻塞 #54,文案按词标 draft/locked;开发钉此版本,转正只换文案不换 key。 */
2
3
  export declare const BILINGUAL_DICT_VERSION = "2026-10-06-pin53";
3
- /** 首批 key 全集(entry 7 键,命名沿 #52 §1 key 提案,area.group.name)。 */
4
- export type BilingualKey = 'entry.label.idle' | 'entry.label.failed' | 'entry.label.busy' | 'entry.label.restart' | 'entry.label.has-update' | 'entry.action.checking' | 'entry.note.up-to-date';
5
- /** 一条双语:en/zh 模板 + 成熟度。模板内具名槽如 {version},运行时值填入(值永不翻译)。 */
4
+ /** key 全集(entry 7 键 + batch-entry 6 键,命名沿 #52 §1 key 提案,area.group.name;既有 7 键 en/zh/draft 不动)。 */
5
+ export type BilingualKey = 'entry.label.idle' | 'entry.label.failed' | 'entry.label.busy' | 'entry.label.restart' | 'entry.label.has-update' | 'entry.action.checking' | 'entry.note.up-to-date' | 'batch-entry.label.idle' | 'batch-entry.label.update' | 'batch-entry.label.busy' | 'batch-entry.label.restart' | 'batch-entry.label.failed' | 'batch-entry.action.checking';
6
+ /** 一条双语:en/zh 模板 + 成熟度。模板内具名槽如 {version}/{count},运行时值填入(值永不翻译)。 */
6
7
  export interface BilingualEntry {
7
8
  en: string;
8
9
  zh: string;
9
- /** true=待母语+域内双签(#53 Q3),false=已双签转正。首批全 draft(无评审不假锁)。 */
10
+ /** true=待母语+域内双签(#53 Q3),false=已双签转正。首批与新增全 draft(无评审不假锁)。 */
10
11
  draft: boolean;
11
12
  }
12
13
  /**
13
14
  * 集中字典(机器源,类型锁死:Record<BilingualKey, BilingualEntry> 缺键即 tsc 报错)。
14
15
  * 人读源为 #57 规格 + #53 结论英文定调;英文终稿待母语评审,故全标 draft。
15
- * zh 逐字等于 entry.ts 现有散落文案(兼容既有测试与接入方 label() 口径)。
16
+ * zh 逐字等于 entry.ts / entry-batch.ts 既有散落文案(兼容既有测试与接入方 label() 口径)。
16
17
  */
17
18
  export declare const BILINGUAL_STRINGS: Record<BilingualKey, BilingualEntry>;
18
19
  /** 取一条双语原文(类型锁死:未知 key 在 tsc 即拦;运行时未知亦抛,不静默回退)。 */
19
20
  export declare function bilingualEntry(key: BilingualKey): BilingualEntry;
20
21
  /**
21
- * 纯文本双语(一行,英文在前中文在后,空白分隔):用于 title / aria-label 等放不下 HTML 的属性位。
22
- * 返回原文(未转义),调用方按上下文转义后再拼入属性。
22
+ * 单语纯文本(v2 主出口,纯函数):只渲染 lang 那一种语言,不读全局信号。
23
+ * lang 显式入参('zh'/'en'/BCP47,缺省 zh);返回原文(未转义),调用方按上下文转义后再拼入属性。
24
+ */
25
+ export declare function copyText(key: BilingualKey, lang?: AppLang | string | null, values?: Record<string, unknown>): string;
26
+ /**
27
+ * 单语语义块 HTML(v2 主出口,纯函数):一次只出现一种语言,语义块仍带 lang 属性。
28
+ * lang 显式入参(缺省 zh);返回已转义可直接拼入 innerHTML 的片段;调用方不得再对整体转义,不得拆 span 重组。
29
+ */
30
+ export declare function copyHTML(key: BilingualKey, lang?: AppLang | string | null, values?: Record<string, unknown>): string;
31
+ /**
32
+ * 历史名包装(v2 前叫双语,v2 起为单语,保留旧名见文件头说明):
33
+ * 无显式 lang 时按当前语言单语渲染(读全局信号),有覆盖需求请直接用 copyHTML/copyText。
23
34
  */
24
35
  export declare function bilingualText(key: BilingualKey, values?: Record<string, unknown>): string;
25
36
  /**
26
- * 双语语义块 HTML(唯一渲染出口):英文在前中文在后,空白分隔,lang 属性齐全。
27
- * 返回已转义可直接拼入 innerHTML 的片段;调用方不得再对整体转义,不得拆 span 重组。
37
+ * 历史名包装(见上):无显式 lang 时按当前语言单语渲染;返回单语块(只含当前语言一个 span)。
28
38
  */
29
39
  export declare function bilingualHTML(key: BilingualKey, values?: Record<string, unknown>): string;
30
40
  /** 是否 draft(供 #56 门禁读零 draft 放行条件;#54 自身不断言零 draft)。 */
@@ -32,8 +42,8 @@ export declare function isDraft(key: BilingualKey): boolean;
32
42
  /** 全表 draft key 一览(#56 门禁:空数组才放行合入主分支)。 */
33
43
  export declare function draftKeys(): BilingualKey[];
34
44
  /**
35
- * 双语块最小样式(与 UPDATE_ENTRY_CSS 同缝注入):
36
- * 平时行内双语(英文前中文后,空白分隔),窄处竖排不断行(@media 360px 以下 column)。
37
- * 只管排版,不管字重字号(字重间距留给 #55 视觉矩阵验收)。
45
+ * 单语块最小样式(与 UPDATE_ENTRY_CSS 同缝注入):
46
+ * v2 起窄处竖排退场(一次只一种语言,不断行压力消失,#55 横排/竖排结论作废,排版改由 #67 仲裁);
47
+ * 只保留 overflow-wrap,不断行不溢出;不管字重字号。
38
48
  */
39
49
  export declare const BILINGUAL_CSS: string;
package/dist/bilingual.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // AUTO-GENERATED by node build.mjs — DO NOT EDIT. Source: src/bilingual.ts
2
+ import { normalizeLangTag, resolveLang } from "./lang.js";
2
3
  const BILINGUAL_DICT_VERSION = "2026-10-06-pin53";
3
4
  const BILINGUAL_STRINGS = {
4
5
  "entry.label.idle": { en: "Check for updates", zh: "\u68C0\u67E5\u66F4\u65B0", draft: true },
@@ -7,7 +8,13 @@ const BILINGUAL_STRINGS = {
7
8
  "entry.label.restart": { en: "Restart required", zh: "\u5F85\u91CD\u542F", draft: true },
8
9
  "entry.label.has-update": { en: "Update available {version}", zh: "\u6709\u65B0\u7248 {version}", draft: true },
9
10
  "entry.action.checking": { en: "Checking for updates\u2026", zh: "\u6B63\u5728\u67E5\u65B0\u7248\u2026", draft: true },
10
- "entry.note.up-to-date": { en: "Up to date {version}", zh: "\u5DF2\u662F\u6700\u65B0 {version}", draft: true }
11
+ "entry.note.up-to-date": { en: "Up to date {version}", zh: "\u5DF2\u662F\u6700\u65B0 {version}", draft: true },
12
+ "batch-entry.label.idle": { en: "Check for updates", zh: "\u68C0\u67E5\u66F4\u65B0", draft: true },
13
+ "batch-entry.label.update": { en: "{count} updates available", zh: "{count} \u5BB6\u53EF\u66F4\u65B0", draft: true },
14
+ "batch-entry.label.busy": { en: "Installing\u2026", zh: "\u6B63\u5728\u5B89\u88C5\u2026", draft: true },
15
+ "batch-entry.label.restart": { en: "{count} restarts required", zh: "{count} \u5BB6\u5F85\u91CD\u542F", draft: true },
16
+ "batch-entry.label.failed": { en: "{count} failed \u2014 View details", zh: "{count} \u5BB6\u5931\u8D25\uFF0C\u70B9\u6B64\u67E5\u770B", draft: true },
17
+ "batch-entry.action.checking": { en: "Checking for updates\u2026", zh: "\u6B63\u5728\u67E5\u65B0\u7248\u2026", draft: true }
11
18
  };
12
19
  function escapeHtml(value) {
13
20
  return String(value ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
@@ -25,15 +32,22 @@ function bilingualEntry(key) {
25
32
  if (!hit) throw new Error("[dsh-plugin-update] \u672A\u77E5\u6587\u6848 key\uFF1A" + String(key));
26
33
  return hit;
27
34
  }
28
- function bilingualText(key, values) {
35
+ function copyText(key, lang, values) {
36
+ const l = normalizeLangTag(lang ?? "zh");
29
37
  const e = bilingualEntry(key);
30
- return formatTemplate(e.en, values) + " " + formatTemplate(e.zh, values);
38
+ return formatTemplate(l === "en" ? e.en : e.zh, values);
31
39
  }
32
- function bilingualHTML(key, values) {
40
+ function copyHTML(key, lang, values) {
41
+ const l = normalizeLangTag(lang ?? "zh");
33
42
  const e = bilingualEntry(key);
34
- const en = escapeHtml(formatTemplate(e.en, values));
35
- const zh = escapeHtml(formatTemplate(e.zh, values));
36
- return '<span class="dsh-upd-bi"><span lang="en">' + en + '</span> <span lang="zh">' + zh + "</span></span>";
43
+ const text = escapeHtml(formatTemplate(l === "en" ? e.en : e.zh, values));
44
+ return '<span class="dsh-upd-bi"><span lang="' + l + '">' + text + "</span></span>";
45
+ }
46
+ function bilingualText(key, values) {
47
+ return copyText(key, resolveLang(), values);
48
+ }
49
+ function bilingualHTML(key, values) {
50
+ return copyHTML(key, resolveLang(), values);
37
51
  }
38
52
  function isDraft(key) {
39
53
  return bilingualEntry(key).draft;
@@ -42,9 +56,8 @@ function draftKeys() {
42
56
  return Object.keys(BILINGUAL_STRINGS).filter((k) => BILINGUAL_STRINGS[k].draft);
43
57
  }
44
58
  const BILINGUAL_CSS = [
45
- ".dsh-upd-bi{display:inline-flex;align-items:baseline;gap:.5em;max-width:100%}",
46
- '.dsh-upd-bi [lang="en"],.dsh-upd-bi [lang="zh"]{overflow-wrap:anywhere}',
47
- "@media (max-width:360px){.dsh-upd-bi{flex-direction:column;gap:0}}"
59
+ ".dsh-upd-bi{display:inline;overflow-wrap:anywhere}",
60
+ ".dsh-upd-bi [lang]{overflow-wrap:anywhere}"
48
61
  ].join("\n");
49
62
  export {
50
63
  BILINGUAL_CSS,
@@ -53,6 +66,8 @@ export {
53
66
  bilingualEntry,
54
67
  bilingualHTML,
55
68
  bilingualText,
69
+ copyHTML,
70
+ copyText,
56
71
  draftKeys,
57
72
  isDraft
58
73
  };
@@ -1,3 +1,5 @@
1
+ import { type BilingualKey } from './bilingual.js';
2
+ import { type AppLang, type LocaleOption } from './lang.js';
1
3
  import { type BatchRowView } from './panel-batch.js';
2
4
  import { type UpdatePanelTheme } from './panel.js';
3
5
  /** 批量入口件的形态(与单入口同一套取值)。 */
@@ -46,6 +48,8 @@ export interface UpdateBatchEntryOptions {
46
48
  openOn?: BatchEntryOpenOn;
47
49
  /** 覆盖默认按钮文案(不传就用聚合文案)。 */
48
50
  label?: string;
51
+ /** 语言覆盖(#60 v2):'zh' | 'en' | { getActive(): string; subscribe?(cb): () => void },不传即跟随全局信号。 */
52
+ locale?: LocaleOption;
49
53
  /** 可选:键 -> 中文名覆盖(透传给批量面板)。 */
50
54
  titles?: Record<string, string>;
51
55
  /** 轮询间隔(毫秒,透传给批量面板;缺省 1500,下限 250)。 */
@@ -79,12 +83,21 @@ export interface UpdateBatchEntryController {
79
83
  /**
80
84
  * 聚合读数:计数唯一出处 batchLedgerCounts(与面板总账同一份,忙失败占位翻回可更新)。
81
85
  * hasUpdate = 忙/失败/待重启/可更新任一非零(openOn has-update 按它开面板;全 settled/待查/跳过即 idle)。
86
+ * v2 单语:label 与 HTML 出口同语言(lang 显式入参,缺省跟随全局;zh 口径逐字兼容旧中文)。
82
87
  */
83
- export declare function batchEntrySummary(state: UpdateBatchEntryState | null | undefined): BatchEntrySummary;
84
- /** 状态档(唯一出处:batchEntrySummary,不各写一份)。 */
88
+ export declare function batchEntrySummary(state: UpdateBatchEntryState | null | undefined, lang?: AppLang | string | null): BatchEntrySummary;
89
+ /** 状态档(唯一出处:batchEntrySummary,不各写一份;lang 仅影响 label,不影响档位)。 */
85
90
  export declare function batchEntryStateKind(state: UpdateBatchEntryState | null | undefined): BatchEntryStateKind;
86
- /** 批量入口件文案(唯一出处:测试与接入方都读它,不各写一份)。 */
87
- export declare function batchEntryLabelFor(state: UpdateBatchEntryState | null | undefined): string;
91
+ /** 批量入口件文案第二出口(唯一出处;与 HTML 出口同语言,v2 起按 lang 取字典)。 */
92
+ export declare function batchEntryLabelFor(state: UpdateBatchEntryState | null | undefined, lang?: AppLang | string | null): string;
93
+ /** 批量 key(与 entryStateKind 同输入,分支只认稳定码与计数,不读文案)。 */
94
+ export declare function batchEntryBilingualKeyFor(state: UpdateBatchEntryState | null | undefined): BilingualKey;
95
+ /** 批量插值({count} 运行时值永不翻译,冻结词元两语言逐字相同)。 */
96
+ export declare function batchEntryBilingualValuesFor(state: UpdateBatchEntryState | null | undefined): Record<string, string>;
97
+ /** 批量单语块 HTML(按钮正文唯一来源;lang 显式入参,缺省跟随全局)。 */
98
+ export declare function batchEntryBilingualHTMLFor(state: UpdateBatchEntryState | null | undefined, lang?: AppLang | string | null): string;
99
+ /** 批量单语纯文本(title/aria 用;与 HTML 出口同语言)。 */
100
+ export declare function batchEntryBilingualTextFor(state: UpdateBatchEntryState | null | undefined, lang?: AppLang | string | null): string;
88
101
  export declare function mountUpdateBatchEntry(container: {
89
102
  innerHTML: string;
90
103
  addEventListener?: (type: string, listener: (ev: unknown) => void) => void;
@@ -1,6 +1,8 @@
1
1
  // AUTO-GENERATED by node build.mjs — DO NOT EDIT. Source: src/entry-batch.ts
2
2
  import { MIN_PANEL_POLL_MS } from "./config.js";
3
3
  import { UPDATE_ENTRY_CSS } from "./entry.js";
4
+ import { BILINGUAL_CSS, copyHTML, copyText } from "./bilingual.js";
5
+ import { normalizeLangTag, resolveLang, subscribeLang } from "./lang.js";
4
6
  import {
5
7
  batchLedgerCounts,
6
8
  batchLedgerText,
@@ -11,8 +13,6 @@ import {
11
13
  failureCodeOf,
12
14
  normalizePanelTheme
13
15
  } from "./panel.js";
14
- const LABEL_IDLE = "\u68C0\u67E5\u66F4\u65B0";
15
- const LABEL_BUSY = "\u6B63\u5728\u5B89\u88C5\u2026";
16
16
  function isObject(value) {
17
17
  return !!value && typeof value === "object" && !Array.isArray(value);
18
18
  }
@@ -23,7 +23,7 @@ function asRows(value) {
23
23
  function escapeHtml(value) {
24
24
  return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
25
25
  }
26
- function batchEntrySummary(state) {
26
+ function batchEntrySummary(state, lang) {
27
27
  const rows = state?.rows ?? null;
28
28
  const counts = batchLedgerCounts(Array.isArray(rows) ? rows : []);
29
29
  const total = Array.isArray(rows) ? rows.length : 0;
@@ -47,22 +47,23 @@ function batchEntrySummary(state) {
47
47
  }
48
48
  }
49
49
  const hasUpdate = counts.installing > 0 || counts.failed > 0 || counts.restart > 0 || counts.updatable > 0;
50
+ const l = lang !== void 0 && lang !== null ? normalizeLangTag(lang) : resolveLang();
50
51
  let label;
51
52
  switch (kind) {
52
53
  case "busy":
53
- label = LABEL_BUSY;
54
+ label = copyText("batch-entry.label.busy", l);
54
55
  break;
55
56
  case "failed":
56
- label = counts.failed > 0 ? counts.failed + " \u5BB6\u5931\u8D25\uFF0C\u70B9\u6B64\u67E5\u770B" : "\u66F4\u65B0\u5931\u8D25\uFF0C\u70B9\u6B64\u67E5\u770B";
57
+ label = counts.failed > 0 ? copyText("batch-entry.label.failed", l, { count: String(counts.failed) }) : copyText("entry.label.failed", l);
57
58
  break;
58
59
  case "restart":
59
- label = counts.restart + " \u5BB6\u5F85\u91CD\u542F";
60
+ label = copyText("batch-entry.label.restart", l, { count: String(counts.restart) });
60
61
  break;
61
62
  case "update":
62
- label = counts.updatable + " \u5BB6\u53EF\u66F4\u65B0";
63
+ label = copyText("batch-entry.label.update", l, { count: String(counts.updatable) });
63
64
  break;
64
65
  default:
65
- label = LABEL_IDLE;
66
+ label = copyText("batch-entry.label.idle", l);
66
67
  break;
67
68
  }
68
69
  return {
@@ -82,8 +83,44 @@ function batchEntrySummary(state) {
82
83
  function batchEntryStateKind(state) {
83
84
  return batchEntrySummary(state).kind;
84
85
  }
85
- function batchEntryLabelFor(state) {
86
- return batchEntrySummary(state).label;
86
+ function batchEntryLabelFor(state, lang) {
87
+ return batchEntrySummary(state, lang).label;
88
+ }
89
+ function batchEntryBilingualKeyFor(state) {
90
+ const s = batchEntrySummary(state);
91
+ switch (s.kind) {
92
+ case "busy":
93
+ return "batch-entry.label.busy";
94
+ case "failed":
95
+ return s.failed > 0 ? "batch-entry.label.failed" : "entry.label.failed";
96
+ case "restart":
97
+ return "batch-entry.label.restart";
98
+ case "update":
99
+ return "batch-entry.label.update";
100
+ default:
101
+ return "batch-entry.label.idle";
102
+ }
103
+ }
104
+ function batchEntryBilingualValuesFor(state) {
105
+ const s = batchEntrySummary(state);
106
+ switch (s.kind) {
107
+ case "update":
108
+ return { count: String(s.updatable) };
109
+ case "restart":
110
+ return { count: String(s.restart) };
111
+ case "failed":
112
+ return s.failed > 0 ? { count: String(s.failed) } : {};
113
+ default:
114
+ return {};
115
+ }
116
+ }
117
+ function batchEntryBilingualHTMLFor(state, lang) {
118
+ const l = lang !== void 0 && lang !== null ? normalizeLangTag(lang) : resolveLang();
119
+ return copyHTML(batchEntryBilingualKeyFor(state), l, batchEntryBilingualValuesFor(state));
120
+ }
121
+ function batchEntryBilingualTextFor(state, lang) {
122
+ const l = lang !== void 0 && lang !== null ? normalizeLangTag(lang) : resolveLang();
123
+ return copyText(batchEntryBilingualKeyFor(state), l, batchEntryBilingualValuesFor(state));
87
124
  }
88
125
  function mountUpdateBatchEntry(container, options) {
89
126
  if (!container || typeof container.innerHTML !== "string") {
@@ -117,6 +154,20 @@ function mountUpdateBatchEntry(container, options) {
117
154
  throw new Error("[dsh-plugin-update] \u9762\u677F\u8F6E\u8BE2\u95F4\u9694\u975E\u6CD5\uFF1A\u4E0D\u5F97\u5C0F\u4E8E 250 \u6BEB\u79D2\uFF08\u6536\u5230 " + JSON.stringify(options.pollMs) + "\uFF09");
118
155
  }
119
156
  const labelOverride = typeof options.label === "string" && options.label ? options.label : null;
157
+ const localeOpt = options.locale ?? void 0;
158
+ if (localeOpt !== void 0 && localeOpt !== null) {
159
+ const isStr = typeof localeOpt === "string";
160
+ const isObj = typeof localeOpt === "object" && typeof localeOpt.getActive === "function";
161
+ if (!isStr && !isObj) {
162
+ throw new Error("[dsh-plugin-update] invalid locale: expected zh / en / BCP47 or { getActive(), subscribe? }");
163
+ }
164
+ if (isStr && !localeOpt.trim()) {
165
+ throw new Error("[dsh-plugin-update] invalid locale: empty string");
166
+ }
167
+ }
168
+ function currentLang() {
169
+ return resolveLang(localeOpt);
170
+ }
120
171
  const titles = options.titles && typeof options.titles === "object" ? options.titles : {};
121
172
  const onActivate = typeof options.onActivate === "function" ? options.onActivate : null;
122
173
  const call = options.call;
@@ -154,7 +205,7 @@ function mountUpdateBatchEntry(container, options) {
154
205
  return { rows, error };
155
206
  }
156
207
  function summaryOf() {
157
- return batchEntrySummary(stateOf());
208
+ return batchEntrySummary(stateOf(), currentLang());
158
209
  }
159
210
  function currentLabel() {
160
211
  return labelOverride ?? summaryOf().label;
@@ -163,13 +214,15 @@ function mountUpdateBatchEntry(container, options) {
163
214
  return {};
164
215
  }
165
216
  function entryHTML() {
166
- const summary = summaryOf();
167
- const text = activating ? "\u6B63\u5728\u67E5\u65B0\u7248\u2026" : currentLabel();
217
+ const lang = currentLang();
218
+ const summary = batchEntrySummary(stateOf(), lang);
219
+ const labelHTML = labelOverride ? escapeHtml(labelOverride) : activating ? copyHTML("batch-entry.action.checking", lang) : copyHTML(batchEntryBilingualKeyFor(stateOf()), lang, batchEntryBilingualValuesFor(stateOf()));
220
+ const labelText = labelOverride ?? (activating ? copyText("batch-entry.action.checking", lang) : copyText(batchEntryBilingualKeyFor(stateOf()), lang, batchEntryBilingualValuesFor(stateOf())));
168
221
  const busyAttr = activating ? ' disabled aria-busy="true"' : "";
169
222
  const themeAttr = theme === "archive" ? ' data-theme="archive"' : "";
170
- const control = variant === "badge" ? '<button type="button" class="dsh-upd-entry-dot" ' + ENTRY_ATTR + '="activate" title="' + escapeHtml(text) + '" aria-label="' + escapeHtml(text) + '"' + busyAttr + "></button>" : '<button type="button" class="dsh-upd-entry-btn" ' + ENTRY_ATTR + '="activate"' + busyAttr + ">" + escapeHtml(text) + "</button>";
223
+ const control = variant === "badge" ? '<button type="button" class="dsh-upd-entry-dot" ' + ENTRY_ATTR + '="activate" title="' + escapeHtml(labelText) + '" aria-label="' + escapeHtml(labelText) + '"' + busyAttr + "></button>" : '<button type="button" class="dsh-upd-entry-btn" ' + ENTRY_ATTR + '="activate"' + busyAttr + ">" + labelHTML + "</button>";
171
224
  const noteHTML = note ? '<span class="dsh-upd-entry-note" data-dsh-upd-note="1">' + escapeHtml(note) + "</span>" : "";
172
- return "<style>" + UPDATE_ENTRY_CSS + '</style>\n<span class="dsh-upd-entry" data-variant="' + variant + '" data-state="' + summary.kind + '"' + themeAttr + ">" + control + noteHTML + "</span>";
225
+ return "<style>" + UPDATE_ENTRY_CSS + "\n" + BILINGUAL_CSS + '</style>\n<span class="dsh-upd-entry" data-variant="' + variant + '" data-state="' + summary.kind + '"' + themeAttr + ">" + control + noteHTML + "</span>";
173
226
  }
174
227
  function render() {
175
228
  if (!mounted) return;
@@ -219,6 +272,7 @@ function mountUpdateBatchEntry(container, options) {
219
272
  onRestartRequested: options.onRestartRequested,
220
273
  copyText: options.copyText,
221
274
  autoChangelog: options.autoChangelog,
275
+ locale: localeOpt,
222
276
  // 面板点「关闭」即走入口件的完整关闭(收 dialog + 还原按钮 + 重查一次),不再是面板自己停轮询。
223
277
  onCloseRequested: () => close(),
224
278
  call: panelCall
@@ -314,6 +368,10 @@ function mountUpdateBatchEntry(container, options) {
314
368
  function unmount() {
315
369
  if (!mounted) return;
316
370
  mounted = false;
371
+ try {
372
+ unsubLang();
373
+ } catch {
374
+ }
317
375
  const opened = panel;
318
376
  panel = null;
319
377
  panelMode = null;
@@ -332,6 +390,9 @@ function mountUpdateBatchEntry(container, options) {
332
390
  } catch {
333
391
  }
334
392
  }
393
+ const unsubLang = subscribeLang(() => {
394
+ render();
395
+ }, localeOpt);
335
396
  try {
336
397
  ;
337
398
  container.addEventListener?.("click", onClick);
@@ -348,6 +409,10 @@ function mountUpdateBatchEntry(container, options) {
348
409
  return { refresh, open, close, label: currentLabel, summary: summaryOf, setTheme, unmount };
349
410
  }
350
411
  export {
412
+ batchEntryBilingualHTMLFor,
413
+ batchEntryBilingualKeyFor,
414
+ batchEntryBilingualTextFor,
415
+ batchEntryBilingualValuesFor,
351
416
  batchEntryLabelFor,
352
417
  batchEntryStateKind,
353
418
  batchEntrySummary,
package/dist/entry.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { type UpdatePanelContainer, type UpdatePanelTheme } from './panel.js';
2
2
  import type { UpdateSnapshot } from './ports.js';
3
3
  import { type BilingualKey } from './bilingual.js';
4
+ import { type AppLang, type LocaleOption } from './lang.js';
4
5
  /** 入口件的形态。 */
5
6
  export type EntryVariant = 'button' | 'badge' | 'inline';
6
7
  /** 什么时候主动查一次(只读,联网一次)。 */
@@ -35,6 +36,8 @@ export interface UpdateEntryOptions {
35
36
  openOn?: EntryOpenOn;
36
37
  /** 覆盖默认按钮文案(不传就用状态联动文案)。 */
37
38
  label?: string;
39
+ /** 语言覆盖(#60 v2):'zh' | 'en' | { getActive(): string; subscribe?(cb): () => void },不传即跟随 html[lang] > navigator > zh。 */
40
+ locale?: LocaleOption;
38
41
  profileName?: string;
39
42
  pollMs?: number;
40
43
  /**
@@ -68,16 +71,16 @@ export interface UpdateEntryController {
68
71
  export declare function entryStateKind(state: UpdateEntryState | null | undefined): EntryStateKind;
69
72
  /**
70
73
  * 入口件双语 key(#54 底座调用链):与 entryStateKind 同一输入(快照 + 失败码),分支只认稳定码,永不读文案。
71
- * 数据层只传 key + {version} 运行时值,表现层经 bilingualHTML 拼语义块。
74
+ * 数据层只传 key + {version} 运行时值,表现层经 copyHTML 拼单语块。
72
75
  */
73
76
  export declare function entryBilingualKeyFor(state: UpdateEntryState | null | undefined): BilingualKey;
74
- /** 双语插值(运行时值永不翻译:同一 version 在中英两 span 各出现一次)。 */
77
+ /** 双语插值(运行时值永不翻译:同一 version 在单语块里原样透传,冻结词元两语言逐字相同)。 */
75
78
  export declare function entryBilingualValuesFor(state: UpdateEntryState | null | undefined): Record<string, string>;
76
- /** 一路调用链的语义块 HTML(entryHTML 按钮正文唯一来源,无覆盖时必走此函数)。 */
77
- export declare function entryBilingualHTMLFor(state: UpdateEntryState | null | undefined): string;
78
- /** 属性位纯文本双语(title / aria-label 用,放不下 HTML 时的同内容平面版)。 */
79
- export declare function entryBilingualTextFor(state: UpdateEntryState | null | undefined): string;
80
- /** 入口件文案(唯一出处:测试与接入方都读它,不各写一份)。 */
81
- export declare function entryLabelFor(state: UpdateEntryState | null | undefined): string;
79
+ /** 一路调用链的单语块 HTML(entryHTML 按钮正文唯一来源,无覆盖时必走此函数;lang 显式入参,缺省跟随全局)。 */
80
+ export declare function entryBilingualHTMLFor(state: UpdateEntryState | null | undefined, lang?: AppLang | string | null): string;
81
+ /** 属性位纯文本单语(title / aria-label 用,放不下 HTML 时的同内容平面版;与 HTML 出口同语言)。 */
82
+ export declare function entryBilingualTextFor(state: UpdateEntryState | null | undefined, lang?: AppLang | string | null): string;
83
+ /** 入口件文案第二出口(唯一出处:测试与接入方都读它;与 HTML 出口同语言,v2 起按 lang 取字典,不再硬编码中文)。 */
84
+ export declare function entryLabelFor(state: UpdateEntryState | null | undefined, lang?: AppLang | string | null): string;
82
85
  export declare const UPDATE_ENTRY_CSS: string;
83
86
  export declare function mountUpdateEntry(container: UpdatePanelContainer, options: UpdateEntryOptions): UpdateEntryController;
package/dist/entry.js CHANGED
@@ -5,11 +5,8 @@ import {
5
5
  mountUpdatePanel,
6
6
  normalizePanelTheme
7
7
  } from "./panel.js";
8
- import { BILINGUAL_CSS, bilingualHTML, bilingualText } from "./bilingual.js";
9
- const LABEL_IDLE = "\u68C0\u67E5\u66F4\u65B0";
10
- const LABEL_FAILED = "\u66F4\u65B0\u5931\u8D25\uFF0C\u70B9\u6B64\u67E5\u770B";
11
- const LABEL_BUSY = "\u6B63\u5728\u5B89\u88C5\u2026";
12
- const LABEL_RESTART = "\u5F85\u91CD\u542F";
8
+ import { BILINGUAL_CSS, copyHTML, copyText } from "./bilingual.js";
9
+ import { resolveLang, subscribeLang } from "./lang.js";
13
10
  function hasUpdateOf(snapshot) {
14
11
  if (!snapshot) return false;
15
12
  const latest = snapshot.latestVersion;
@@ -53,27 +50,17 @@ function entryBilingualValuesFor(state) {
53
50
  }
54
51
  return {};
55
52
  }
56
- function entryBilingualHTMLFor(state) {
57
- return bilingualHTML(entryBilingualKeyFor(state), entryBilingualValuesFor(state));
53
+ function entryBilingualHTMLFor(state, lang) {
54
+ const l = lang ?? resolveLang();
55
+ return copyHTML(entryBilingualKeyFor(state), l, entryBilingualValuesFor(state));
58
56
  }
59
- function entryBilingualTextFor(state) {
60
- return bilingualText(entryBilingualKeyFor(state), entryBilingualValuesFor(state));
57
+ function entryBilingualTextFor(state, lang) {
58
+ const l = lang ?? resolveLang();
59
+ return copyText(entryBilingualKeyFor(state), l, entryBilingualValuesFor(state));
61
60
  }
62
- function entryLabelFor(state) {
63
- switch (entryStateKind(state)) {
64
- case "busy":
65
- return LABEL_BUSY;
66
- case "restart":
67
- return LABEL_RESTART;
68
- case "failed":
69
- return LABEL_FAILED;
70
- case "update": {
71
- const latest = state?.snapshot?.latestVersion;
72
- return typeof latest === "string" && latest.trim() ? `\u6709\u65B0\u7248 ${latest.trim()}` : LABEL_IDLE;
73
- }
74
- default:
75
- return LABEL_IDLE;
76
- }
61
+ function entryLabelFor(state, lang) {
62
+ const l = lang ?? resolveLang();
63
+ return copyText(entryBilingualKeyFor(state), l, entryBilingualValuesFor(state));
77
64
  }
78
65
  const UPDATE_ENTRY_CSS = [
79
66
  '.dsh-upd-entry{display:inline-flex;align-items:center;gap:8px;font:13px/1.6 system-ui,"Microsoft YaHei",sans-serif;color:var(--dsh-upd-fg,#1f2937)}',
@@ -159,6 +146,20 @@ function mountUpdateEntry(container, options) {
159
146
  throw new Error(`[dsh-plugin-update] \u9762\u677F\u8F6E\u8BE2\u95F4\u9694\u975E\u6CD5\uFF1A\u4E0D\u5F97\u5C0F\u4E8E 250 \u6BEB\u79D2\uFF08\u6536\u5230 ${JSON.stringify(options.pollMs)}\uFF09`);
160
147
  }
161
148
  const labelOverride = typeof options.label === "string" && options.label ? options.label : null;
149
+ const localeOpt = options.locale ?? void 0;
150
+ if (localeOpt !== void 0 && localeOpt !== null) {
151
+ const isStr = typeof localeOpt === "string";
152
+ const isObj = typeof localeOpt === "object" && typeof localeOpt.getActive === "function";
153
+ if (!isStr && !isObj) {
154
+ throw new Error("[dsh-plugin-update] invalid locale: expected zh / en / BCP47 or { getActive(), subscribe? }");
155
+ }
156
+ if (isStr && !localeOpt.trim()) {
157
+ throw new Error("[dsh-plugin-update] invalid locale: empty string");
158
+ }
159
+ }
160
+ function currentLang() {
161
+ return resolveLang(localeOpt);
162
+ }
162
163
  const profileName = typeof options.profileName === "string" && options.profileName ? options.profileName : null;
163
164
  const onActivate = typeof options.onActivate === "function" ? options.onActivate : null;
164
165
  const call = options.call;
@@ -191,7 +192,7 @@ function mountUpdateEntry(container, options) {
191
192
  return { snapshot, error };
192
193
  }
193
194
  function currentLabel() {
194
- return labelOverride ?? entryLabelFor(stateOf());
195
+ return labelOverride ?? entryLabelFor(stateOf(), currentLang());
195
196
  }
196
197
  function hasUpdate() {
197
198
  return hasUpdateOf(snapshot);
@@ -201,12 +202,13 @@ function mountUpdateEntry(container, options) {
201
202
  }
202
203
  function entryHTML() {
203
204
  const kind = entryStateKind(stateOf());
204
- const labelHTML = labelOverride ? escapeHtml(labelOverride) : activating ? bilingualHTML("entry.action.checking") : entryBilingualHTMLFor(stateOf());
205
- const labelText = labelOverride ?? (activating ? bilingualText("entry.action.checking") : entryBilingualTextFor(stateOf()));
205
+ const lang = currentLang();
206
+ const labelHTML = labelOverride ? escapeHtml(labelOverride) : activating ? copyHTML("entry.action.checking", lang) : copyHTML(entryBilingualKeyFor(stateOf()), lang, entryBilingualValuesFor(stateOf()));
207
+ const labelText = labelOverride ?? (activating ? copyText("entry.action.checking", lang) : copyText(entryBilingualKeyFor(stateOf()), lang, entryBilingualValuesFor(stateOf())));
206
208
  const busyAttr = activating ? ' disabled aria-busy="true"' : "";
207
209
  const themeAttr = theme === "archive" ? ' data-theme="archive"' : "";
208
210
  const control = variant === "badge" ? `<button type="button" class="dsh-upd-entry-dot" ${ENTRY_ATTR}="activate" title="${escapeHtml(labelText)}" aria-label="${escapeHtml(labelText)}"${busyAttr}></button>` : `<button type="button" class="dsh-upd-entry-btn" ${ENTRY_ATTR}="activate"${busyAttr}>${labelHTML}</button>`;
209
- const noteHTML = noteVersion ? `<span class="dsh-upd-entry-note" data-dsh-upd-note="1">${bilingualHTML("entry.note.up-to-date", { version: noteVersion })}</span>` : "";
211
+ const noteHTML = noteVersion ? `<span class="dsh-upd-entry-note" data-dsh-upd-note="1">${copyHTML("entry.note.up-to-date", lang, { version: noteVersion })}</span>` : "";
210
212
  return `<style>${UPDATE_ENTRY_CSS}
211
213
  ${BILINGUAL_CSS}</style>
212
214
  <span class="dsh-upd-entry" data-variant="${variant}" data-state="${kind}"${themeAttr}>${control}${noteHTML}</span>`;
@@ -258,6 +260,7 @@ ${BILINGUAL_CSS}</style>
258
260
  profileName,
259
261
  autoChangelog: options.autoChangelog,
260
262
  changelogMarkdown: options.changelogMarkdown ?? null,
263
+ locale: localeOpt,
261
264
  // 面板点「关闭」即走入口件的完整关闭(收 dialog + 还原按钮 + 重查一次),不再是面板自己停轮询。
262
265
  onCloseRequested: () => close(),
263
266
  call: panelCall
@@ -344,6 +347,10 @@ ${BILINGUAL_CSS}</style>
344
347
  function unmount() {
345
348
  if (!mounted) return;
346
349
  mounted = false;
350
+ try {
351
+ unsubLang();
352
+ } catch {
353
+ }
347
354
  const opened = panel;
348
355
  panel = null;
349
356
  panelMode = null;
@@ -360,6 +367,9 @@ ${BILINGUAL_CSS}</style>
360
367
  } catch {
361
368
  }
362
369
  }
370
+ const unsubLang = subscribeLang(() => {
371
+ render();
372
+ }, localeOpt);
363
373
  try {
364
374
  container.addEventListener?.("click", onClick);
365
375
  } catch {
package/dist/http.d.ts CHANGED
@@ -102,7 +102,12 @@ export interface UpdateEntryHttpOptions extends Omit<UpdateEntryOptions, "call"
102
102
  timeout?: number | HttpTimeouts;
103
103
  timeoutMs?: number | HttpTimeouts;
104
104
  }
105
- /** 入口件 HTTP 直挂:内部源码级禁安装电话,内核复用 mountUpdateEntry。 */
105
+ /** 入口件 HTTP 直挂:调用通道与面板直挂同一条,内核复用 mountUpdateEntry。
106
+ *
107
+ * 通道层不禁安装:入口件核心没有调安装的代码路径(只查 + 打开面板,见 README 铁律),
108
+ * 禁令只会连坐入口件打开的面板(#58:安装键亮着但永远抛错);真正的门禁在宿主侧核心
109
+ *(凭证/排队/安装键/装后校验),面板安装与面板直挂走同一套,不过不少。
110
+ */
106
111
  export declare function mountUpdateEntryHttp(container: UpdatePanelContainer, options: UpdateEntryHttpOptions): UpdateEntryController;
107
112
  export interface UpdateBatchPanelHttpOptions extends Omit<BatchPanelOptions, "call" | "prefix"> {
108
113
  /** 批量电话前缀(与宿主侧一致;五电话名从它算出,不写字面量)。 */
package/dist/http.js CHANGED
@@ -261,15 +261,9 @@ function mountUpdateEntryHttp(container, options) {
261
261
  throw new Error("[dsh-plugin-update] \u6302 HTTP \u5165\u53E3\u4EF6\u7F3A\u5C11\u914D\u7F6E\uFF1A\u63D2\u4EF6\u6807\u8BC6 pluginId \u4E0E\u7F51\u5173\u6839 baseUrl \u5FC5\u586B");
262
262
  }
263
263
  const prefix = options.prefix ?? DEFAULT_PREFIX;
264
- const rawCall = createHttpCall({ prefix, baseUrl: options.baseUrl, routes: options.routes, fetch: options.fetch, fetchImpl: options.fetchImpl, timeout: options.timeout, timeoutMs: options.timeoutMs });
265
- const guarded = async (name, args) => {
266
- if (shortActionOf(name) === "updateInstall") {
267
- throw new Error("[dsh-plugin-update] \u5165\u53E3\u4EF6\u53EA\u5141\u8BB8\u53EA\u8BFB\u7535\u8BDD\uFF0CupdateInstall \u7981\u6B62\uFF08\u6E90\u7801\u7EA7\uFF09");
268
- }
269
- return rawCall(name, args);
270
- };
264
+ const call = createHttpCall({ prefix, baseUrl: options.baseUrl, routes: options.routes, fetch: options.fetch, fetchImpl: options.fetchImpl, timeout: options.timeout, timeoutMs: options.timeoutMs });
271
265
  const { baseUrl: _b, routes: _r, fetch: _f, fetchImpl: _fi, timeout: _t, timeoutMs: _tm, prefix: _p, ...entryRest } = options;
272
- return mountUpdateEntry(container, { ...entryRest, pluginId: options.pluginId, prefix, call: guarded });
266
+ return mountUpdateEntry(container, { ...entryRest, pluginId: options.pluginId, prefix, call });
273
267
  }
274
268
  function mountUpdateBatchPanelHttp(container, options) {
275
269
  if (!options || typeof options !== "object") {
package/dist/lang.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ /** 本包跟随的两种语言:中文(历史行为)与英文(其余语言落英文)。 */
2
+ export type AppLang = 'zh' | 'en';
3
+ /** 显式覆盖的两种形态:稳定码 'zh' | 'en',或带 getActive/subscribe 的服务对象(可注入假信号源,测试不依赖 jsdom)。 */
4
+ export interface LocaleService {
5
+ getActive(): string;
6
+ subscribe?(cb: () => void): () => void;
7
+ }
8
+ /** 入口件/面板/批量挂载点的语言选项:显式覆盖,不传即跟随全局信号。 */
9
+ export type LocaleOption = 'zh' | 'en' | LocaleService | string | null | undefined;
10
+ type LangCallback = (lang: AppLang) => void;
11
+ /**
12
+ * 归一语言标签:zh* → zh;形如 BCP47 的已知非中文标签 → en;空/垃圾 → zh(零回归)。
13
+ * 大小写不敏感,_ 视作 -。
14
+ */
15
+ export declare function normalizeLangTag(tag: unknown): AppLang;
16
+ /**
17
+ * 解出当前语言:显式覆盖 > html[lang] > navigator.languages > zh。
18
+ * 纯函数(除读全局信号外无副作用);无信号永不抛,落 zh。
19
+ */
20
+ export declare function resolveLang(override?: LocaleOption): AppLang;
21
+ /**
22
+ * 订阅语言变化:html[lang] 变化或可选服务源触发时回新语言。
23
+ * 模块级单例:多个挂载件共用一个 MutationObserver;返回 unsubscribe,unmount 后停订。
24
+ * 无 DOM 环境安全降级:没有 document/MutationObserver 时只记回调、永不触发,不抛。
25
+ */
26
+ export declare function subscribeLang(cb: LangCallback, override?: LocaleOption): () => void;
27
+ /** 测试/维护用:清空全部订阅并断开单例观察者(正常业务代码不调它)。 */
28
+ export declare function __resetLangState(): void;
29
+ export {};
package/dist/lang.js ADDED
@@ -0,0 +1,150 @@
1
+ // AUTO-GENERATED by node build.mjs — DO NOT EDIT. Source: src/lang.ts
2
+ function normalizeLangTag(tag) {
3
+ if (typeof tag !== "string") return "zh";
4
+ const s = tag.trim().toLowerCase().replace(/_/g, "-");
5
+ if (!s) return "zh";
6
+ if (s === "zh" || s.startsWith("zh-")) return "zh";
7
+ if (s === "en" || s.startsWith("en-")) return "en";
8
+ if (/^[a-z]{2,3}(-[a-z0-9]+)*$/.test(s)) return "en";
9
+ return "zh";
10
+ }
11
+ function readDocumentLang() {
12
+ try {
13
+ const g = globalThis;
14
+ const v = g.document?.documentElement?.lang;
15
+ if (typeof v === "string" && v.trim()) return v;
16
+ } catch {
17
+ }
18
+ return null;
19
+ }
20
+ function readNavigatorLangs() {
21
+ try {
22
+ const g = globalThis;
23
+ const nav = g.navigator;
24
+ if (!nav) return [];
25
+ const out = [];
26
+ if (Array.isArray(nav.languages)) {
27
+ for (const c of nav.languages) {
28
+ if (typeof c === "string" && c.trim()) out.push(c);
29
+ }
30
+ }
31
+ if (typeof nav.language === "string" && nav.language.trim()) out.push(nav.language);
32
+ return out;
33
+ } catch {
34
+ return [];
35
+ }
36
+ }
37
+ function resolveLang(override) {
38
+ if (override !== null && typeof override === "object") {
39
+ try {
40
+ const fn = override.getActive;
41
+ if (typeof fn === "function") {
42
+ const v = fn.call(override);
43
+ if (typeof v === "string" && v.trim()) return normalizeLangTag(v);
44
+ }
45
+ } catch {
46
+ }
47
+ } else if (typeof override === "string" && override.trim()) {
48
+ return normalizeLangTag(override);
49
+ }
50
+ const docLang = readDocumentLang();
51
+ if (docLang !== null) return normalizeLangTag(docLang);
52
+ const navs = readNavigatorLangs();
53
+ for (const c of navs) {
54
+ return normalizeLangTag(c);
55
+ }
56
+ return "zh";
57
+ }
58
+ const entries = /* @__PURE__ */ new Set();
59
+ let mo = null;
60
+ function ensureGlobalObserver() {
61
+ if (mo !== null) return;
62
+ try {
63
+ const g = globalThis;
64
+ const docEl = g.document?.documentElement;
65
+ const MO = g.MutationObserver;
66
+ if (!docEl || typeof MO !== "function") return;
67
+ const obs = new MO(() => {
68
+ notifyAll();
69
+ });
70
+ obs.observe(docEl, { attributes: true, attributeFilter: ["lang"] });
71
+ mo = obs;
72
+ } catch {
73
+ mo = null;
74
+ }
75
+ }
76
+ function notifyAll() {
77
+ for (const e of [...entries]) {
78
+ try {
79
+ const next = resolveLang(e.override);
80
+ if (next !== e.last) {
81
+ e.last = next;
82
+ e.cb(next);
83
+ }
84
+ } catch {
85
+ }
86
+ }
87
+ }
88
+ function subscribeLang(cb, override) {
89
+ if (typeof cb !== "function") throw new Error("[dsh-plugin-update] subscribeLang needs a function");
90
+ const entry = { cb, override: override ?? void 0, last: resolveLang(override), serviceUnsub: null };
91
+ entries.add(entry);
92
+ ensureGlobalObserver();
93
+ try {
94
+ if (entry.override !== null && typeof entry.override === "object") {
95
+ const sub = entry.override.subscribe;
96
+ if (typeof sub === "function") {
97
+ const un = sub.call(entry.override, () => {
98
+ try {
99
+ const next = resolveLang(entry.override);
100
+ if (next !== entry.last) {
101
+ entry.last = next;
102
+ entry.cb(next);
103
+ }
104
+ } catch {
105
+ }
106
+ });
107
+ if (typeof un === "function") entry.serviceUnsub = un;
108
+ }
109
+ }
110
+ } catch {
111
+ }
112
+ let done = false;
113
+ return () => {
114
+ if (done) return;
115
+ done = true;
116
+ entries.delete(entry);
117
+ try {
118
+ entry.serviceUnsub?.();
119
+ } catch {
120
+ }
121
+ entry.serviceUnsub = null;
122
+ if (entries.size === 0) {
123
+ try {
124
+ mo?.disconnect();
125
+ } catch {
126
+ }
127
+ mo = null;
128
+ }
129
+ };
130
+ }
131
+ function __resetLangState() {
132
+ for (const e of [...entries]) {
133
+ try {
134
+ e.serviceUnsub?.();
135
+ } catch {
136
+ }
137
+ }
138
+ entries.clear();
139
+ try {
140
+ mo?.disconnect();
141
+ } catch {
142
+ }
143
+ mo = null;
144
+ }
145
+ export {
146
+ __resetLangState,
147
+ normalizeLangTag,
148
+ resolveLang,
149
+ subscribeLang
150
+ };
@@ -54,6 +54,11 @@ export interface BatchPanelOptions {
54
54
  * 不传则回退为只停轮询(unmount),DOM 留给调用方处理。入口件打开的 dialog 已内置(收 dialog + 还原按钮)。
55
55
  */
56
56
  onCloseRequested?: () => void | Promise<void>;
57
+ /**
58
+ * 语言覆盖(#60 窗口期占位:本票只加选项不消费,传了暂不生效,消费留给 #61-#66)。
59
+ * 形态与入口件一致:'zh' | 'en' | { getActive(): string; subscribe?(cb): () => void }。
60
+ */
61
+ locale?: unknown;
57
62
  }
58
63
  /** 面板可点的动作(HTML 上 data-act 一一对应;测试走同一条路)。 */
59
64
  export type BatchPanelActionKind = 'check' | 'install' | 'resume' | 'cancel' | 'row-install' | 'row-skip' | 'row-resume-skip' | 'row-cancel-queue' | 'row-copy-manual' | 'row-copy-diag' | 'toggle-details' | 'restart' | 'close';
package/dist/panel.d.ts CHANGED
@@ -68,6 +68,11 @@ export interface UpdatePanelOptions {
68
68
  * 不传则回退为只停轮询(unmount),DOM 留给调用方处理。入口件打开的 dialog 已内置(收 dialog + 还原按钮)。
69
69
  */
70
70
  onCloseRequested?: () => void | Promise<void>;
71
+ /**
72
+ * 语言覆盖(#60 窗口期占位:本票只加选项不消费,传了暂不生效,消费留给 #61-#66)。
73
+ * 形态与入口件一致:'zh' | 'en' | { getActive(): string; subscribe?(cb): () => void }。
74
+ */
75
+ locale?: unknown;
71
76
  }
72
77
  /** 挂载点:只要有 innerHTML 的容器即可(浏览器元素或测试替身都行)。 */
73
78
  export interface UpdatePanelContainer {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-update",
3
- "version": "0.5.6",
3
+ "version": "0.5.8",
4
4
  "description": "可复用的更新系统装成的 npm 包:任何 DSH 插件照文档能集成检查更新等功能(地图 #579)",
5
5
  "license": "MIT",
6
6
  "author": "王辰浩",