refactor(i18n): 覆盖只在 DSH active=en 时生效(借用英文槽位)

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 方法
This commit is contained in:
HuanLinOTO
2026-08-23 01:42:01 +08:00
parent 7ceee777b4
commit 38dc807d15
4 changed files with 44 additions and 9 deletions
+1 -1
View File
@@ -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-<name>.js`)中,经 `/sidebar/bundle` 路由按需下发;每个脚本把 factory 赋到插件自有全局注册表 `globalThis.__dshChunks__[<name>]`,由 `src/client/chunk-loader.ts` 用自定义 require(externals 经 `__DSH_MODULES__` seed 分支解析)物化——**不经过** `__ModuleLoader__` 注册;**核心 bundle 禁止静态 import `src/client/chunks/*`**(会把库拖回启动路径);对消费插件透明——懒加载只作用于内置 descriptor,`component` 契约(`(props) => ReactNode` 纯渲染函数)不变 |
---
+1
View File
@@ -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<string, Record<string, string>>): () => void
subscribe(listener: () => void): () => void
}
+19 -6
View File
@@ -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, string | number>): 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<string, string | number>): 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')
}
+23 -2
View File
@@ -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<string, Record<string, Record<string, string>>>
constructor(dict: Record<string, Record<string, Record<string, string>>>) {
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')