feat(i18n): 接入 @huanlin/dsh-plugin-better-locale 支持 ja 覆盖

新增 ja 词典(src/client/locales-ja.ts,326 keys,与 zh key-set-equal),
通过可选 peer @huanlin/dsh-plugin-better-locale 接入第三语言覆盖:

- locales.ts: 新增 attachBetterLocale(store) + ja dict export;t() 先查
  store.getOverride(active, 'betterSidebar', key),命中则返回覆盖文本,
  否则走原 zh/en 链;isZh() 在 override 激活时返回 false
- client/index.tsx: apply 中 ctx.get('betterLocale') 取得 override store
  (optional,未安装时为 undefined),attachBetterLocale 注入 t();同时
  register('betterSidebar', { ja }) 把 ja 词典注册进 better-locale 的
  store,让外部 ctx.locale.lookup 调用者也能拿到 ja 文本
- Sidebar.tsx: 新增 betterLocaleActive uSES 订阅 store.subscribe,覆盖
  切换时全树重渲染(DSH locale 的 active 字段不变,既有 localeRevision
  uSES 不会触发)
- package.json: @huanlin/dsh-plugin-better-locale 加为 optional peer
  + devDep link
- tests/locales.spec.ts: 新增 7 个 better-locale override 测试(命中/
  fallback/detach/interpolate/ja dict 完整性 smoke check)

未安装 better-locale 时 ctx.get 返回 undefined,整段为 no-op,zh/en
行为完全不变。
This commit is contained in:
HuanLinOTO
2026-08-23 00:52:20 +08:00
parent d9b8f15d9e
commit 7ceee777b4
9 changed files with 607 additions and 6 deletions
+31
View File
@@ -188,6 +188,37 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
)
void localeRevision
// better-locale override freshness: when @huanlin/dsh-plugin-better-locale
// is installed and the user picks an override language (e.g. ja), the
// store's `active` changes but the DSH locale's `active` does NOT —
// better-locale keeps the dsh active value (zh/en) unchanged and only
// patches `LocaleRuntime.prototype.lookup`. The localeRevision uSES
// above reads `getSnapshot().active`, so it sees no change and skips
// re-render. This second uSES reads the better-locale store's `active`
// directly, so an override switch fires a full re-render and t() picks
// up the new override text. Optional: ctx.get returns undefined when
// better-locale is absent, in which case this is a no-op uSES.
type BetterLocaleStore = {
readonly active: string | undefined
subscribe(listener: () => void): () => void
}
const betterLocaleStore = (ctx as unknown as {
get(name: 'betterLocale'): BetterLocaleStore | undefined
}).get('betterLocale')
const betterLocaleActive = useSyncExternalStore(
useMemo(() => {
const store = betterLocaleStore
if (store === undefined) return (_cb: () => void) => () => {}
return (callback: () => void) => store.subscribe(callback)
}, [betterLocaleStore]),
useMemo(() => {
const store = betterLocaleStore
if (store === undefined) return () => undefined
return () => store.active
}, [betterLocaleStore]),
)
void betterLocaleActive
// Tab-registry revision: TabContent memo cells must pick up a descriptor
// a plugin registers/disposes after mount (the + menu / icons already read
// the registry at render). Rare events (plugin (un)mount), so one full