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:
+70
-3
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Minimal zh/en copy for the sidebar. The copy follows the DSH i18n system:
|
||||
* Minimal zh/en/ja copy for the sidebar. The copy follows the DSH i18n system:
|
||||
* the client apply attaches the locale service (`ctx.locale`, provided by
|
||||
* `@deepseek-ai/dsh-client-locale`) through {@link attachLocale}, and
|
||||
* `t()`/`isZh()` resolve the active locale from it — the Host-backed
|
||||
@@ -7,6 +7,18 @@
|
||||
* Without an attached service (standalone/test compositions) the browser
|
||||
* language is used, matching the previous behavior. The dictionaries are
|
||||
* also registered into the DSH locale registry under {@link LOCALE_NS}.
|
||||
*
|
||||
* ja (Japanese) is opt-in through `@huanlin/dsh-plugin-better-locale`: when
|
||||
* that plugin is installed, the client apply also calls
|
||||
* {@link attachBetterLocale} with the override store. `t()` then consults
|
||||
* the store's active override id first; if it is `'ja'` (or any id whose
|
||||
* dict has the requested key) the ja text wins, otherwise the existing
|
||||
* zh/en chain runs unchanged. better-locale itself patches
|
||||
* `LocaleRuntime.prototype.lookup` so DSH's own translate chain also
|
||||
* returns ja where the `betterSidebar` namespace has a ja entry — that
|
||||
* path covers external callers of `ctx.locale.bind('betterSidebar')`,
|
||||
* while the override-aware `t()` here covers better-sidebar's own
|
||||
* components (which bypass `ctx.locale` and call `t()` directly).
|
||||
*/
|
||||
|
||||
/** The zh dictionary (also registered into the DSH locale registry under {@link LOCALE_NS}). */
|
||||
@@ -675,9 +687,28 @@ export const en: Record<keyof typeof zh, string> = {
|
||||
*/
|
||||
export const LOCALE_NS = 'betterSidebar'
|
||||
|
||||
// The ja dictionary lives in a sibling file (326 keys) so this module
|
||||
// stays readable. Type-checked against the zh key set: a missing or extra
|
||||
// ja key is a compile error.
|
||||
import { ja as jaDict } from './locales-ja.ts'
|
||||
|
||||
/** The ja dictionary (key-set-equal to zh, enforced by the type annotation). */
|
||||
export const ja: Record<keyof typeof zh, string> = jaDict as Record<keyof typeof zh, string>
|
||||
|
||||
/** The DSH locale service attached by the client apply (absent → browser detection). */
|
||||
let localeService: { getSnapshot(): { active: string } } | undefined
|
||||
|
||||
/**
|
||||
* The better-locale override store attached by the client apply
|
||||
* (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".
|
||||
*/
|
||||
let betterLocaleStore: {
|
||||
readonly active: string | undefined
|
||||
getOverride(dshActive: string, ns: string, key: string): string | undefined
|
||||
} | undefined
|
||||
|
||||
/**
|
||||
* Attach (or detach, with undefined) the DSH locale service. The sidebar
|
||||
* mounts its own React root outside the slot system's locale seat, so the
|
||||
@@ -689,6 +720,23 @@ export function attachLocale(service: { getSnapshot(): { active: string } } | un
|
||||
localeService = service
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach (or detach, with undefined) the better-locale override store.
|
||||
* When attached with an active override, `t()` consults the store's
|
||||
* `getOverride(active, LOCALE_NS, key)` first; if it returns a string,
|
||||
* that text wins over the zh/en chain. Detaching (or the store's active
|
||||
* being `undefined`) restores the zh/en chain unchanged.
|
||||
*
|
||||
* The Sidebar root subscribes to the store separately (see Sidebar.tsx)
|
||||
* so an override change re-renders the whole tree — the locale service's
|
||||
* own revision bump (which better-locale triggers via `publish(active, true)`)
|
||||
* does NOT fire the existing `localeRevision` uSES because that snapshot
|
||||
* reads `getSnapshot().active` (unchanged) rather than `revision`.
|
||||
*/
|
||||
export function attachBetterLocale(store: typeof betterLocaleStore): void {
|
||||
betterLocaleStore = store
|
||||
}
|
||||
|
||||
/**
|
||||
* The active locale id ('zh' | 'en'): the DSH locale service's snapshot when
|
||||
* attached, else the browser language.
|
||||
@@ -704,8 +752,24 @@ 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 {
|
||||
const dict = activeLocale().toLowerCase().startsWith('zh') ? zh : en
|
||||
let text = dict[key]
|
||||
// 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.
|
||||
const dshActive = localeService?.getSnapshot().active ?? ''
|
||||
const override = betterLocaleStore?.getOverride(dshActive, LOCALE_NS, key)
|
||||
let text: string | undefined = override
|
||||
// 2. Fall back to the zh/en chain when no override matched.
|
||||
if (text === undefined) {
|
||||
const dict = activeLocale().toLowerCase().startsWith('zh') ? zh : en
|
||||
text = dict[key]
|
||||
}
|
||||
if (text === undefined) {
|
||||
// Key missing from every dict (should not happen — zh is the source of
|
||||
// truth and en/ja are checked against it). Return the key itself so the
|
||||
// UI shows something identifiable rather than `undefined`.
|
||||
text = key
|
||||
}
|
||||
if (params !== undefined) {
|
||||
for (const [name, value] of Object.entries(params)) {
|
||||
text = text.replaceAll(`{${name}}`, String(value))
|
||||
@@ -716,6 +780,9 @@ 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
|
||||
// the non-zh branch (e.g. date format, pluralization).
|
||||
if (betterLocaleStore?.active !== undefined) return false
|
||||
return activeLocale().toLowerCase().startsWith('zh')
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user