From 9d61ab6756fe8f8595f84e72853832be117aac65 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Sun, 23 Aug 2026 20:03:26 +0800 Subject: [PATCH] docs(locale): define extensible language fallbacks --- ...07-30-client-locale-full-rollout.i18n.yaml | 4 +-- .../2026-07-30-client-locale-full-rollout.md | 2 ++ ...026-07-30-client-locale-full-rollout.zh.md | 2 ++ packages/client/locale/README.i18n.yaml | 4 +-- packages/client/locale/README.md | 27 ++++++++++++++++++- packages/client/locale/README.zh.md | 27 ++++++++++++++++++- 6 files changed, 60 insertions(+), 6 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-07-30-client-locale-full-rollout.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-30-client-locale-full-rollout.i18n.yaml index 66dff1b8ec..470b9ab737 100644 --- a/.agents/notes/implemented/architecture/2026-07-30-client-locale-full-rollout.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-30-client-locale-full-rollout.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-30-client-locale-full-rollout.md -2026-07-30-client-locale-full-rollout.md: aeb4deae28b0dfdb9ab75fd64fe3143958cd6910 -2026-07-30-client-locale-full-rollout.zh.md: a642b6062cb3dc7a2dfa22dd5d8cf7d9a02e3104 +2026-07-30-client-locale-full-rollout.md: 890312bb7c0825763eb16416939ff4930014f54b +2026-07-30-client-locale-full-rollout.zh.md: 78d1f0aa8e3b6da8be3e2c640414cb4325337891 diff --git a/.agents/notes/implemented/architecture/2026-07-30-client-locale-full-rollout.md b/.agents/notes/implemented/architecture/2026-07-30-client-locale-full-rollout.md index aeb4deae28..890312bb7c 100644 --- a/.agents/notes/implemented/architecture/2026-07-30-client-locale-full-rollout.md +++ b/.agents/notes/implemented/architecture/2026-07-30-client-locale-full-rollout.md @@ -14,6 +14,8 @@ After the typed locale standard seat landed (`locale:` on register → framework **Component copy rides the standard `t` seat; deep children take `t` as a plain prop** typed `XxxProps['t']`. The dictionary canon is unchanged: `zh satisfies Record` is the key source and `en satisfies Record` locks bilingual balance. +**The built-in locale set is closed; the language catalog is extensible.** The package contributes only `zh` and `en`, and typed namespace registration continues to require that bilingual pair. An external client plugin adds a language through `ctx.effect(() => ctx.locale.addLanguage({ id, label, fallback }))` and contributes partial translations through the existing single-locale dictionary registration. An external language id is its BCP 47 tag for preference storage, dictionary lookup, browser matching, and ``; the built-in `zh` definition retains its internal `zh-CN` document tag. Every added language names a registered fallback whose own definition supplies the next fallback, and the chain must terminate at `en`; unknown targets and cycles fail at registration. For each key, lookup walks that chain in the requested namespace, then repeats it in `common`, before displaying the key itself. The Host stores an open string preference; an unavailable saved id remains pending until its language registers, while removal returns an active selection to the available browser match or `en`. Catalog changes advance the `LocaleFace` revision so the Language row follows registration and disposal. + **Zero-Cordis atoms (ui-primitives) take copy as required props.** `HoverCard`, structured Tool blocks, JSON/Markdown renderers, `ConnectionBanner`, and modal chrome remain runtime-independent; localized plugins pass complete dictionary-driven label objects from their own `t` seat and memoize cache-sensitive objects on the `t` identity. The removal of language-bearing defaults and the complete prop inventory are owned by the [locale-owned copy decision](2026-08-23-locale-owned-client-ui-copy.md). **Every product-authored UI phrase is translated.** Client fallbacks, design labels, trajectory inspection, accessibility names, and formatter units are dictionary-owned under the [locale-owned copy decision](2026-08-23-locale-owned-client-ui-copy.md). User/model/provider/wire text and protocol or code tokens remain verbatim data. Framework-free boot markup still runs before the locale service; the localized application replaces its product copy after activation. diff --git a/.agents/notes/implemented/architecture/2026-07-30-client-locale-full-rollout.zh.md b/.agents/notes/implemented/architecture/2026-07-30-client-locale-full-rollout.zh.md index a642b6062c..78d1f0aa8e 100644 --- a/.agents/notes/implemented/architecture/2026-07-30-client-locale-full-rollout.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-30-client-locale-full-rollout.zh.md @@ -14,6 +14,8 @@ typed locale 标准席位(`locale:` 注册声明 → 框架注入强类型 `t` **组件文案走标准 `t` 席位;深层子组件用 prop 下传**,类型写 `XxxProps['t']`。字典规范形态不变:`zh satisfies Record` 为 key 源、`en satisfies Record` 锁双语平衡。 +**内置 locale 集合封闭,语言目录可扩展。** 本包只提供 `zh` 与 `en`,类型化命名空间注册仍要求这对双语字典。外部 client 插件通过 `ctx.effect(() => ctx.locale.addLanguage({ id, label, fallback }))` 增加语言,并通过既有的单 locale 字典注册贡献不完整翻译。外部语言 id 同时是偏好存储、字典查找、浏览器匹配和 `` 使用的 BCP 47 标签;内置 `zh` 定义继续使用内部 `zh-CN` 文档标签。每个新增语言都声明一个已注册的 fallback,fallback 自身的定义给出下一层 fallback,整条链必须终止于 `en`;未知目标和循环在注册时失败。每个 key 先在请求的命名空间中沿链查找,再在 `common` 中重复同一条链,最后显示 key 本身。Host 存储开放字符串偏好;不可用的已保存 id 会保持待采用,直至对应语言注册;定义移除后,正在使用的选择会回落到可用的浏览器匹配或 `en`。目录变更推进 `LocaleFace` revision,使语言设置行跟随注册和 dispose。 + **zero-Cordis 原子组件(ui-primitives)通过必填 prop 接收文案。** `HoverCard`、结构化工具块、JSON/Markdown 渲染器、`ConnectionBanner` 和 modal chrome 均保持运行时独立;已本地化插件从自己的 `t` 席位传入完整的字典驱动 label 对象,对缓存敏感的对象按 `t` 身份 memo。移除带语言默认值以及完整 prop 清单由 [locale 归属文案决策](2026-08-23-locale-owned-client-ui-copy.zh.md)负责。 **所有产品编写的 UI 短语都翻译。** client 兜底文案、设计 label、trajectory 检查面、无障碍名称和格式化单位均按 [locale 归属文案决策](2026-08-23-locale-owned-client-ui-copy.zh.md)进入字典。用户/模型/提供方/wire 文本以及协议或代码 token 仍作为数据原样呈现。不依赖框架的 boot 标记仍早于 locale 服务运行;本地化应用激活后会替换其中的产品文案。 diff --git a/packages/client/locale/README.i18n.yaml b/packages/client/locale/README.i18n.yaml index dca2ac21ab..a8ac680439 100644 --- a/packages/client/locale/README.i18n.yaml +++ b/packages/client/locale/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/locale/README.md -README.md: 1fa0262e7c1e8aa50f12fd2b7533d97ea4737199 -README.zh.md: 45cf657d5f8455fe128f378c2a2b77d919a441da +README.md: 8736c656b2cd0d2ca3ec9f298e6e7edc7dc0ea40 +README.zh.md: 84114900b389f394ee27c5e325c7d427e2dd657c diff --git a/packages/client/locale/README.md b/packages/client/locale/README.md index 1fa0262e7c..8736c656b2 100644 --- a/packages/client/locale/README.md +++ b/packages/client/locale/README.md @@ -2,7 +2,31 @@ English | [中文](README.zh.md) -Locale plugin: LocaleRuntime — the `zh`/`en` preference stored as `locale.preference` in `$DSH_HOME/settings.yaml`; when that explicit Host value is absent, a fresh browser starts provisionally in the language `navigator` asks for (primary-subtag matching, with `en` when it asks for no language this app ships). The Host read runs after plugin activation so an unavailable settings service cannot block the page; its result replaces the provisional browser value live. The Client keeps Host settings persistence disabled on non-loopback pages, so their locale selection remains process-local even though Connection authenticates every API method. `locale/change` fires on switches, and the plugin points `` at the active locale (`zh-CN`/`en`) on activation and on every switch. The service also owns the ns×locale dictionary registry (typed `register(ns, {zh, en})` checked against `LocaleNamespaceMap`, `bind(ns)`→`TranslateNS`; lookup chain ns → common → en → key), implements the slot system's `LocaleFace`, and installs itself through `ctx.slots.installLocale`, backing the framework-injected `t` standard seat (`Translate`/`TranslateNS` are ui-slots types; import them from there — this package only re-exports for dictionary owners' convenience). Product-authored Client UI text must enter through these typed dictionaries or an already-localized primitive prop; `verify-client-ui-i18n` enforces that source ownership ([decision](../../../.agents/notes/implemented/architecture/2026-08-23-locale-owned-client-ui-copy.md)). The [Host-backed preferences decision](../../../.agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.md) owns the persistence boundary. +Locale plugin: LocaleRuntime — the preference stored as `locale.preference` in `$DSH_HOME/settings.yaml`; when that explicit Host value is absent, a fresh browser starts provisionally in the first registered language `navigator` asks for (full-tag then primary-subtag matching, with `en` when none match). The Host read runs after plugin activation so an unavailable settings service cannot block the page; its result replaces the provisional browser value live. A saved external locale waits for its definition to register rather than becoming active while unavailable. The Client keeps Host settings persistence disabled on non-loopback pages, so their locale selection remains process-local even though Connection authenticates every API method. `locale/change` fires on switches, and the plugin points `` at the external language id or the built-in language's document tag on activation and on every switch. Product-authored Client UI text must enter through these typed dictionaries or an already-localized primitive prop; `verify-client-ui-i18n` enforces that source ownership ([decision](../../../.agents/notes/implemented/architecture/2026-08-23-locale-owned-client-ui-copy.md)). The [Host-backed preferences decision](../../../.agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.md) owns the persistence boundary. + +The package ships only `zh` and `en`. External client plugins add a selectable language with `ctx.effect(() => ctx.locale.addLanguage({ id, label, fallback }))` and add its dictionaries through the existing `register(ns, locale, dict)` form; unloading the definition removes it from the selector and returns an active selection to the available browser/default locale. An external id is a non-empty ASCII BCP 47-style tag used for persistence, dictionary lookup, browser matching, and ``. Its fallback must already be registered, and the resulting chain must terminate at `en`; unknown targets, duplicate ids, and cycles fail at registration. For each key, lookup walks the chain in the requested namespace, repeats it in `common`, then displays the key. The typed `register(ns, { zh, en })` form remains checked against `LocaleNamespaceMap` and requires both built-in dictionaries. LocaleRuntime implements the slot system's `LocaleFace` and installs itself through `ctx.slots.installLocale`, backing the framework-injected `t` standard seat (`Translate`/`TranslateNS` are ui-slots types; import them from there — this package only re-exports them for dictionary owners). + +## Language-pack registration + +Register the definition and each translated namespace as effects owned by the language-pack plugin: + +```js +export const inject = ['locale'] + +export function apply(ctx) { + ctx.effect( + () => ctx.locale.addLanguage({ id: 'ja', label: '日本語', fallback: 'en' }), + 'my-locale: language', + ) + ctx.effect( + () => ctx.locale.register('common', 'ja', { + cancel: 'キャンセル', + close: '閉じる', + }), + 'my-locale: common dictionary', + ) +} +``` ## Model Experience @@ -15,3 +39,4 @@ None; this package neither assembles nor sends a provider request. ## Known Limitations and Deferred Work - **Registry-held text reads its translation once** — copy captured at registration time outside the slot render path (e.g. the `/model` command description in the command registry) keeps the language it was registered under until re-registration; slot-rendered copy follows switches live. +- **Language packs own language-specific behavior** — the registry supplies selection, persistence, browser matching, key fallback, and ``; it does not add plural rules or bidirectional layout. diff --git a/packages/client/locale/README.zh.md b/packages/client/locale/README.zh.md index 45cf657d5f..84114900b3 100644 --- a/packages/client/locale/README.zh.md +++ b/packages/client/locale/README.zh.md @@ -2,7 +2,31 @@ [English](README.md) | 中文 -locale 插件:LocaleRuntime——`zh`/`en` 偏好以 `locale.preference` 存储在 `$DSH_HOME/settings.yaml` 中;若没有显式 Host 值,全新浏览器会暂时使用 `navigator` 请求的语言(按主子标签匹配;若其请求的语言本应用都不提供,则使用 `en`)。Host 读取在插件激活后执行,因此 settings 服务不可用不会阻塞页面;读取结果会实时替换浏览器暂定值。Client 在非 loopback 页面禁用 Host settings 持久化,因此这些页面的 locale 选择仍只保留在进程内,尽管 Connection 会认证每个 API 方法。`locale/change` 仅在切换语言时触发;插件会在激活时以及每次切换时把 `` 指向当前 locale(`zh-CN`/`en`)。该服务还拥有 ns×locale 字典注册表(类型化 `register(ns, {zh, en})` 按 `LocaleNamespaceMap` 校验,`bind(ns)`→`TranslateNS`;查找链 ns → common → en → key),实现 slot 系统的 `LocaleFace`,并经 `ctx.slots.installLocale` 自行安装,支撑框架注入的 `t` 标准席位(`Translate`/`TranslateNS` 是 ui-slots 的类型;请从那里导入——本包的再导出仅为字典所有者提供便利)。产品编写的 Client UI 文本必须经这些 typed 字典或已本地化原子组件 prop 进入展示;`verify-client-ui-i18n` 会强制这项源码归属([决策](../../../.agents/notes/implemented/architecture/2026-08-23-locale-owned-client-ui-copy.zh.md))。该持久化边界由[Host settings 支撑的偏好决策](../../../.agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.zh.md)拥有。 +locale 插件:LocaleRuntime——偏好以 `locale.preference` 存储在 `$DSH_HOME/settings.yaml` 中;若没有显式 Host 值,全新浏览器会暂时使用 `navigator` 请求的第一个已注册语言(先匹配完整标签,再匹配主子标签;若都不匹配,则使用 `en`)。Host 读取在插件激活后执行,因此 settings 服务不可用不会阻塞页面;读取结果会实时替换浏览器暂定值。已保存但尚未注册定义的外部 locale 会等待该定义注册,不会在不可用时成为当前语言。Client 在非 loopback 页面禁用 Host settings 持久化,因此这些页面的 locale 选择仍只保留在进程内,尽管 Connection 会认证每个 API 方法。`locale/change` 仅在切换语言时触发;插件会在激活时以及每次切换时把 `` 指向外部语言 id 或内置语言的文档标签。产品编写的 Client UI 文本必须经这些 typed 字典或已本地化原子组件 prop 进入展示;`verify-client-ui-i18n` 会强制这项源码归属([决策](../../../.agents/notes/implemented/architecture/2026-08-23-locale-owned-client-ui-copy.zh.md))。该持久化边界由[Host settings 支撑的偏好决策](../../../.agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.zh.md)拥有。 + +本包只内置 `zh` 与 `en`。外部 client 插件通过 `ctx.effect(() => ctx.locale.addLanguage({ id, label, fallback }))` 增加可选语言,并通过既有 `register(ns, locale, dict)` 形式增加该语言的字典;定义卸载后,它会从选择器移除,正在使用的选择则回落到当前可用的浏览器语言或默认语言。外部 id 是用于持久化、字典查找、浏览器匹配和 `` 的非空 ASCII BCP 47 风格标签。其 fallback 必须已注册,形成的链必须终止于 `en`;未知目标、重复 id 和循环会在注册时失败。每个 key 先在请求的命名空间中沿链查找,再在 `common` 中重复该链,最后显示 key。类型化 `register(ns, { zh, en })` 形式仍按 `LocaleNamespaceMap` 检查,并要求两份内置字典。LocaleRuntime 实现 slot 系统的 `LocaleFace`,并经 `ctx.slots.installLocale` 自行安装,支撑框架注入的 `t` 标准席位(`Translate`/`TranslateNS` 是 ui-slots 的类型;请从那里导入——本包的再导出仅供字典所有者使用)。 + +## 语言包注册 + +语言包插件把语言定义和每个已翻译命名空间注册为自身拥有的 effect: + +```js +export const inject = ['locale'] + +export function apply(ctx) { + ctx.effect( + () => ctx.locale.addLanguage({ id: 'ja', label: '日本語', fallback: 'en' }), + 'my-locale: language', + ) + ctx.effect( + () => ctx.locale.register('common', 'ja', { + cancel: 'キャンセル', + close: '閉じる', + }), + 'my-locale: common dictionary', + ) +} +``` ## 模型体验 @@ -15,3 +39,4 @@ locale 插件:LocaleRuntime——`zh`/`en` 偏好以 `locale.preference` 存 ## 已知限制与暂缓事项 - **注册表持有的文本只读取一次翻译**——在 slot 渲染路径之外于注册时捕获的文案(例如 command 注册表中的 `/model` 命令描述)在重新注册前保持注册时的语言;slot 渲染的文案随切换实时更新。 +- **语言包负责语言特有行为**——注册表提供选择、持久化、浏览器匹配、逐 key 回退和 ``;它不增加复数规则或双向布局。