From 38dc807d153599e641c89355549e0ceeae1b7f74 Mon Sep 17 00:00:00 2001 From: HuanLinOTO Date: Sun, 23 Aug 2026 01:42:01 +0800 Subject: [PATCH] =?UTF-8?q?refactor(i18n):=20=E8=A6=86=E7=9B=96=E5=8F=AA?= =?UTF-8?q?=E5=9C=A8=20DSH=20active=3Den=20=E6=97=B6=E7=94=9F=E6=95=88?= =?UTF-8?q?=EF=BC=88=E5=80=9F=E7=94=A8=E8=8B=B1=E6=96=87=E6=A7=BD=E4=BD=8D?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit better-locale 的覆盖本质上是借用 DSH 的英文槽位来渲染第三语言: - store.getOverride(dshActive, ns, key) 改为只在 dshActive === 'en' 时返回覆盖文本;DSH 在 zh 下覆盖完全惰性(保持原生 zh) - 新增 store.isOverrideActive(dshActive):返回 active !== undefined && dshActive === 'en',供 better-sidebar 的 isZh() 判断 - isZh() 改用 isOverrideActive(dshActive) 判断(仅在 DSH=en 且有 override 时返回 false),zh 下返回 true - LanguageSwitcher 接收 dshActive prop,选了非 native 且 DSH 非 en 时显示「请将 DSH 切换到英文以查看 [语言名]」提示 - 新增 switchToEnHint copy key(zh + en) - 测试:better-locale +3 store tests(en-only / inert on zh / isOverrideActive),+1 patch test(inert on zh);better-sidebar +1 locales test(override inert while DSH on zh),FakeBetterLocale 加 isOverrideActive 方法 --- AGENTS.md | 2 +- src/client/index.tsx | 1 + src/client/locales.ts | 25 +++++++++++++++++++------ tests/locales.spec.ts | 25 +++++++++++++++++++++++-- 4 files changed, 44 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index aa310e3..a9b3836 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -595,7 +595,7 @@ interface OpenTabSeed { | **id 冲突** | `registerTab` / `registerFileViewer` 对重复 id 抛错;建议用包前缀(`my-plugin:xxx`) | | **家族右面板互斥(v0.13.0+)** | 读取 `aionui-panel` 设置命名空间的 `rightPanel`:解析为 `'aionui-panel'` 时整个侧边栏不挂载(`settings.get` 路由返回 `externalDisable: true`,客户端挂载门 + 各类接管一并停用;`settings/document-updated` 推送实时生效,无 `remote` 服务的部署回退为启动时判定)。未安装 aionui 或提供方为其他值时不受影响 | | **i18n 跟随** | 侧边栏界面文案跟随 DSH 的 `ctx.locale`(`@deepseek-ai/dsh-client-locale`):词典注册在 `betterSidebar` 命名空间,语言偏好(Host-backed `locale.preference`)与浏览器语言不一致时以 DSH 为准并实时切换;locale 服务缺失时回退浏览器语言。插件自身的 `t()`(`src/client/locales.ts`)由 `apply()` 挂接服务;消费插件**不要**依赖此内部函数——标题等字段传字符串或 `() => string` 即可(i18n 友好)。⚠️ 渲染 DSH 的 `MarkdownText` 时必须传 `codeLabels={{ copyLabel: t('copy'), copiedLabel: t('copied') }}`——该组件 cordis-free,漏传则代码块复制按钮回退硬编码中文 | -| **第三语言覆盖(ja 等)** | 可选 peer `@huanlin/dsh-plugin-better-locale`(`peerDependenciesMeta.optional: true`)提供 ja/ko/... 等第三语言覆盖。安装后 `apply()` 调 `ctx.get('betterLocale')` 取得 override store 并经 `attachBetterLocale()` 注入 `t()`:`t()` 先查 `store.getOverride(active, 'betterSidebar', key)`,命中则返回覆盖文本(如 ja),否则走原 zh/en 链。Sidebar root 另订阅 store 的 `subscribe()`(uSES on `store.active`),覆盖切换时全树重渲染(DSH locale 的 `active` 字段不变,所以既有 `localeRevision` uSES 不会触发)。本插件同时把 ja 词典 `register('betterSidebar', { ja })` 进 better-locale 的 store,让外部 `ctx.locale.lookup('betterSidebar', key)` 调用者也能拿到 ja 文本。未安装 better-locale 时 `ctx.get` 返回 undefined,整段为 no-op,zh/en 行为不变。新增 zh key 时**必须**同步加 ja 翻译(`src/client/locales-ja.ts`),否则该 key 在 ja 覆盖下回退到 en | +| **第三语言覆盖(ja 等)** | 可选 peer `@huanlin/dsh-plugin-better-locale`(`peerDependenciesMeta.optional: true`)提供 ja/ko/... 等第三语言覆盖。覆盖**借用 DSH 的英文槽位**:`store.getOverride(dshActive, ns, key)` 只在 `dshActive === 'en'` 时返回覆盖文本,DSH 在 zh 下覆盖完全惰性(保持原生 zh)。安装后 `apply()` 调 `ctx.get('betterLocale')` 取得 override store 并经 `attachBetterLocale()` 注入 `t()`:`t()` 先查 `store.getOverride(active, 'betterSidebar', key)`,命中则返回覆盖文本(如 ja),否则走原 zh/en 链;`isZh()` 改用 `store.isOverrideActive(active)` 判断(仅在 DSH=en 且有 override 时返回 false)。Sidebar root 另订阅 store 的 `subscribe()`(uSES on `store.active`),覆盖切换时全树重渲染(DSH locale 的 `active` 字段不变,所以既有 `localeRevision` uSES 不会触发)。本插件同时把 ja 词典 `register('betterSidebar', { ja })` 进 better-locale 的 store,让外部 `ctx.locale.lookup('betterSidebar', key)` 调用者也能拿到 ja 文本。better-locale 的 LanguageSwitcher 在用户选了非 native 且 DSH 非 en 时显示「请将 DSH 切换到英文以查看 [语言名]」提示。未安装 better-locale 时 `ctx.get` 返回 undefined,整段为 no-op,zh/en 行为不变。新增 zh key 时**必须**同步加 ja 翻译(`src/client/locales-ja.ts`),否则该 key 在 ja 覆盖下回退到 en | | **懒加载 chunk** | 内置重依赖(xterm/CodeMirror)在独立 bundle(`lib/client-.js`)中,经 `/sidebar/bundle` 路由按需下发;每个脚本把 factory 赋到插件自有全局注册表 `globalThis.__dshChunks__[]`,由 `src/client/chunk-loader.ts` 用自定义 require(externals 经 `__DSH_MODULES__` seed 分支解析)物化——**不经过** `__ModuleLoader__` 注册;**核心 bundle 禁止静态 import `src/client/chunks/*`**(会把库拖回启动路径);对消费插件透明——懒加载只作用于内置 descriptor,`component` 契约(`(props) => ReactNode` 纯渲染函数)不变 | --- diff --git a/src/client/index.tsx b/src/client/index.tsx index 08bcc06..856fac3 100644 --- a/src/client/index.tsx +++ b/src/client/index.tsx @@ -73,6 +73,7 @@ export function apply(ctx: Context): void { | { readonly active: string | undefined getOverride(dshActive: string, ns: string, key: string): string | undefined + isOverrideActive(dshActive: string): boolean register(ns: string, dicts: Record>): () => void subscribe(listener: () => void): () => void } diff --git a/src/client/locales.ts b/src/client/locales.ts index dd22225..86bf928 100644 --- a/src/client/locales.ts +++ b/src/client/locales.ts @@ -703,10 +703,17 @@ let localeService: { getSnapshot(): { active: string } } | undefined * (absent → no override; the zh/en chain runs). The store's `active` * field holds the user's chosen override id (e.g. `'ja'`); `undefined` * means "no override, use DSH native zh/en". + * + * The override only takes effect when DSH's active locale is `'en'` + * (it borrows DSH's English slot to render a third language). While + * DSH is on `'zh'` the override is inert — `getOverride` returns + * `undefined` and `isOverrideActive` returns `false` — so `t()` and + * `isZh()` fall through to the native zh/en chain unchanged. */ let betterLocaleStore: { readonly active: string | undefined getOverride(dshActive: string, ns: string, key: string): string | undefined + isOverrideActive(dshActive: string): boolean } | undefined /** @@ -752,10 +759,11 @@ export type CopyKey = keyof typeof zh /** Translate a copy key; `{name}` placeholders interpolate from `params`. */ export function t(key: CopyKey, params?: Record): string { - // 1. better-locale override (e.g. ja) wins when an override is active - // and the store has a translation for this (ns, key). The DSH active - // locale is forwarded for forward compatibility (a future mode could - // gate overrides on DSH being 'en'); the current store ignores it. + // 1. better-locale override (e.g. ja) wins when an override is active, + // DSH's active locale is 'en' (the override borrows the en slot), + // and the store has a translation for this (ns, key). The store's + // getOverride returns undefined otherwise (no override, DSH on zh, + // or missing key) and the zh/en chain runs. const dshActive = localeService?.getSnapshot().active ?? '' const override = betterLocaleStore?.getOverride(dshActive, LOCALE_NS, key) let text: string | undefined = override @@ -780,9 +788,14 @@ export function t(key: CopyKey, params?: Record): strin /** Whether the active locale is Chinese (used for selectors). */ export function isZh(): boolean { - // An active override is not Chinese — short-circuit so selectors pick + // An override is only "effectively active" when DSH is on 'en' (the + // override borrows the en slot). While DSH is on 'zh' the override is + // inert — the user sees native zh, so isZh() returns true. When an + // override is effectively active, the rendered text is neither zh nor + // en (it's ja/ko/...), so isZh() returns false to route selectors to // the non-zh branch (e.g. date format, pluralization). - if (betterLocaleStore?.active !== undefined) return false + const dshActive = localeService?.getSnapshot().active ?? '' + if (betterLocaleStore?.isOverrideActive(dshActive) === true) return false return activeLocale().toLowerCase().startsWith('zh') } diff --git a/tests/locales.spec.ts b/tests/locales.spec.ts index 8ec9d7d..56e0750 100644 --- a/tests/locales.spec.ts +++ b/tests/locales.spec.ts @@ -113,17 +113,21 @@ describe('locales (DSH i18n following)', () => { }) }) -/** Minimal fake of the better-locale override store (the subset t() reads). */ +/** Minimal fake of the better-locale override store (the subset t()/isZh() read). */ class FakeBetterLocale { active: string | undefined = undefined private readonly dict: Record>> constructor(dict: Record>>) { this.dict = dict } - getOverride(_dshActive: string, ns: string, key: string): string | undefined { + getOverride(dshActive: string, ns: string, key: string): string | undefined { if (this.active === undefined) return undefined + if (dshActive !== 'en') return undefined return this.dict[ns]?.[this.active]?.[key] } + isOverrideActive(dshActive: string): boolean { + return this.active !== undefined && dshActive === 'en' + } subscribe(_listener: () => void): () => void { return () => {} } @@ -175,6 +179,23 @@ describe('locales (better-locale override)', () => { expect(isZh()).toBe(true) }) + it('override is inert while DSH is on zh (override borrows the en slot)', () => { + const locale = new FakeLocale() + locale.switchTo('zh') + attachLocale(locale) + + const betterLocale = new FakeBetterLocale({ + [LOCALE_NS]: { ja: { explorer: 'エクスプローラー' } }, + }) + betterLocale.active = 'ja' + attachBetterLocale(betterLocale) + + // Override is set to 'ja' but DSH is on zh — override is inert, + // native zh text wins. The user must switch DSH to en to see ja. + expect(t('explorer')).toBe('资源管理器') + expect(isZh()).toBe(true) + }) + it('returns to the zh/en chain after detaching the override store', () => { const locale = new FakeLocale() locale.switchTo('en')