Skip to content

Web UI 通过一个服务 ctx.locale(@deepseek-ai/dsh-client-locale)实现本地化。它的翻译单元是命名空间 → 按语言字典,扩展点是每个模块合并进去的、带类型的 LocaleNamespaceMap——即 SlotMap 的翻译孪生。

目录结构 ​

字典是扁平的 key → 模板字符串 映射;参数用 {name} 占位。内置两种语言 id:LOCALE_IDS = ['zh', 'en'],FALLBACK_LOCALE: BuiltInLocaleId = 'en'(packages/client/locale/src/client/index.ts:107)——它是每条兜底链的终点语言,而非仅 zh 字典未命中时。一个命名空间的注册会一次性声明全部内置语言(双语对等性在注册时强制):

ts
// package client/index.ts
locale.register(COMMON_NS, { zh, en })
locale.register(SETTINGS_NS, { zh: settingsZh, en: settingsEn })

COMMON_NS = 'common' 是共享的跨功能词汇表。模块字典紧挨着各自的 React 代码——例如 packages/client/locale/src/locales/{en,zh}.ts、packages/client/ui-theme/src/client/locales.ts、packages/client/ui-settings-models/src/client/locales.ts——每个属主都把键集合合并进 LocaleNamespaceMap,从而在带类型注册处缺键/多键即为编译错误。

字典就是字面量 key → 模板 string 的对象。键可以是点分的,用于归组某个界面的文案——主题字典(命名空间 settings.theme)用 appearance.title/appearance.description,外加较新的 fontSize.title、fontSize.description、fontSize.increase、fontSize.decrease 键:

ts
export const en = {
  'appearance.title': 'Appearance',
  'preference.label': 'Theme',
  light: 'Light', dark: 'Dark', system: 'Use system',
  'fontSize.title': 'Font size',
  'fontSize.increase': 'Increase', 'fontSize.decrease': 'Decrease',
}

locale 包自己合并两个命名空间:common(共享词汇)与 settings.locale(语言行的自有文案)。由于 LocaleDictOf<N> 是 Record<LocaleNamespaceMap[N] & string, string>,一个命名空间的键并集是其唯一事实来源:在 en 字典里打错一个键,注册调用会在编译期失败而非在运行时 UI 里出错。

两个字典面:运行时 vs 文档 ​

同一个「locale」一词横跨两种截然不同的人工产物:

产物位置内容
运行时字典packages/*/src/**/locales.ts + LocaleNamespaceMapUI 调用 t(key) 用的活的 key → template 字符串
*.i18n.yaml 配对记录仓库根 + 各包成对文档 EN/ZH 的一致性哈希,而非文本

很容易误以为 README.i18n.yaml 装着译文——它装的是哈希,实际文字居于并排的 README.md / README.zh.md。见下文 .i18n.yaml 约定。

LocaleRuntime ​

LocaleRuntime(packages/client/locale/src/client/index.ts)持有一张 Map<namespace, Map<locale, dict>> 与单调递增的 revision。其按键查找链(translate)沿当前语言的声明兜底链行走——按语言的兜底链以英语为终点:

  1. 入口命名空间在当前语言下的字典,
  2. 该命名空间在当前语言兜底链里每种后续语言下的字典(内置 BUILT_IN_LOCALE_METADATA 固定 zh → en;addLanguage 用必须以英语为终点的规则扩充目录),
  3. 共享 common 命名空间(同一条按语言兜底链),
  4. 原始键本身——缺失文案保持可见(在 UI 里响亮失败而非留白),
  5. 然后做 {name} 模板替换。
ts
private translate(ns, key, params?) {
  const template = this.lookup(ns, key)
    ?? (ns !== COMMON_NS ? this.lookup(COMMON_NS, key) : undefined)
    ?? key
  if (!params) return template
  return template.replace(/\{(\w+)\}/g, (m, name) =>
    name in params ? String(params[name]) : m)
}

bind(ns) 返回稳定的 Translate(已记忆化,因此可骑乘 inject 面);register 递增 revision,让已挂载的出口捕获迟到的字典。

语言解析 ​

resolveInitialLocale() 在构造时运行:浏览器语言优先(detectBrowserLocale() 遍历 navigator.languages 再 navigator.language,按主子标签匹配,故 zh-Hans-CN → zh),否则用 FALLBACK_LOCALE(en)。显式宿主选择随后可能覆盖它:宿主插件注册持久化的 locale.preference 设置区(LocaleSettingsSchema,对两种 id 求 union 的 schemastery schema,可选),客户端通过 SettingsScope<LocaleSettings> 绑定它;当存储的 preference 不同时 adopt() 会切换当前语言。写入一律经 setLocale(id),其会通过 host.set(LOCALE_PREFERENCE_FIELD, id) 持久化。

txt
navigator.languages → detectBrowserLocale() → 临时语言(默认 en)
   └─ 宿主设置区 locale.preference → adopt() 覆盖
   └─ LocaleRuntime.provisional / active  → 以 LocaleSnapshot 发布
   └─ syncDocumentLanguage(snapshot) → document.documentElement.lang = 'zh-CN' | 'en'

当前语言持续同步到文档:syncDocumentLanguage(index.ts:146-150)在每次真实变化时写入 document.documentElement.lang(active 为 zh 时写 zh-CN,否则写 active id)——辅助技术与浏览器自身的语言协商跟随 UI,而非被伺服 shell 上的静态 lang 属性。

切换语言时仅在真正的当前语言变化时发出 ctx.emit('locale/change', snapshot)——字典注册不会轰炸该事件(它们改为递增 LocaleFace 的 revision)。该设置区还承载 ui-theme 的外观行,因此语言与主题都堆在同一设置 General 栈里。

渲染器 LocaleFace 与 t 座 ​

服务本身就是 LocaleFace(bind + getSnapshot/subscribe),只安装一次:

ts
// locale client apply
ctx.slots.installLocale(locale)

装上并激活某 LocaleFace 后,任何在注册时声明了 locale: 命名空间的 slot 注册,都会在组件 props 上得到一个框架合成的 t 座,其类型指向该命名空间的字典并集加上共享 common 词汇表(TranslateNS<N>)。当前语言随后自动跟随——组件调用 props.t('key', {name}),并在 LocaleFace revision 变化时重渲染。

语言偏好行 ​

locale 功能拥有自己的设置界面,正如 ui-theme 拥有外观。apply(locale client)按顺序做三件事:

  1. 绑定持久化作用域:ctx.settingsScope.bind<LocaleSettings>({ namespace: LOCALE_SETTINGS_NAMESPACE }),
  2. 创建 LocaleRuntime 并注册基础字典,
  3. 把服务安装为 LocaleFace,并把语言行贡献进 settings.general.item:
ts
ctx.slots.inject('settings.general.item', () => ctx.slots.register({
  name: 'settings.general.item',
  id: 'language',
  order: 0,
  store,
  locale: SETTINGS_NS,                       // → 框架合成的 `t` 座
  inject: injected,                          // → { setLocale } 业务面
}, LanguageRow))

LanguageRow.tsx / createLanguageRowStore 持有一份 { active, locales, revision } 快照;注入的 setLocale 代理到 locale.setLocale(id)。LOCALE_IDS 为 ['zh', 'en'],行内每个选项的 label 是该种语言的自描述(中文 / English)——因此切换器在知道当前语言之前就可用了。行订阅 locale/change 以重新同步当前值与 revision。

每包字典、每功能命名空间 ​

由于跨插件协作只经服务(客户端 bundle 纯净门),每个包都只触及 ctx.locale、不导入任何东西;它只调用 register/bind。命名空间身份保证字典按命名空间隔离;两个功能即便键相同也能共存而不撞车,但带类型的范式是每个功能一个命名空间:

功能命名空间字典文件注册处
Locale(共享 + 语言行)common、settings.localepackages/client/locale/src/locales/{en,zh}.tslocale apply
主题外观settings.themepackages/client/ui-theme/src/client/locales.tsui-theme apply
设置 General 壳 + chromesettingspackages/client/ui-settings-general/src/client/locales.tsui-settings-general
提供商编辑(Models 页)settings.modelspackages/client/ui-settings-models/src/client/locales.tsui-settings-models

注册一个尚未并入 LocaleNamespaceMap 的命名空间时用第二个、不带类型的 register 重载;带类型注册是已发布组合里的常态。

由于行内没有任何自身的翻译标签——每个选项的 label 都是该语言的自描述(中文 / English)——切换器在获知当前语言之前仍可用。apps/web/index.html 在该固定版本上声明了 lang="en",运行时的 syncDocumentLanguage 则从首屏起持续同步活的 document.documentElement.lang。

.i18n.yaml 约定(文档配对) ​

切勿把运行时字典与仓库的 *.i18n.yaml 文件(CONTRIBUTING.i18n.yaml、README.i18n.yaml,以及各包 README.i18n.yaml)混为一谈。后者是双语配对一致性记录,而非翻译载荷:每条记录某个成对文档的英文与中文两端在「最后一次确认一致」时的 git blob 哈希,供 pnpm run verify-translation-pairing 检测漂移。文档本身是以 EN/ZH 文件对编辑的(例如 README.md / README.zh.md、packages/client/locale/README.md / README.zh.md)。

Web shell 的路由硬件本身也双语:apps/web/index.html 在该固定版本上声明了 lang="en"(客户端起来后由 syncDocumentLanguage 切到 zh)。

本页涉及的包 ​

包
@deepseek-ai/dsh-client-locale
@deepseek-ai/dsh-client-ui-theme
@deepseek-ai/dsh-client-ui-slots
@deepseek-ai/dsh-client-ui-settings
@deepseek-ai/dsh-client-web

延伸阅读 ​

  • 前端:UI 模块 — LocaleNamespaceMap 如何合并、t 座如何接入 slot。
  • 前端:Web 前端 — installLocale、LocaleFace 与启动链。
  • 前端:Schema 表单 — locale 与 theme 两行都要绑定的设置作用域。
  • packages/client/locale/src/client/index.ts — LocaleRuntime、translate、resolveInitialLocale。
  • packages/client/locale/src/locales/{en,zh}.ts — 内置的 common 与 settings.locale 字典。
  • 仓库根 README.i18n.yaml 与 CONTRIBUTING.i18n.yaml — 配对记录格式。