外观
国际化与主题
回答的问题:四种语言与三档主题在「共享库」和「各站」之间怎么分工,为什么这样分, 以及那条防止文案回显 key 的自动检查在防什么。
1. 宿主契约
ui/ 里的组件不自带 theme store、不自己决定当前语言。这些归 app 所有,由每个 app 在启动时调一次 setUiHost() 注入:
ts
import { setUiHost } from '$ui';
setUiHost({ locale, theme }); // 传对象引用,不要解构快照未注入时组件仍可渲染(英文、亮色兜底),因此孤立渲染与首帧不会崩——但正常 app 必须注入。
必须传引用而非解构快照。 库侧的 locale / theme 是逐层读穿到宿主对象的 getter,在组件的反应上下文里读 locale.code / theme.mode 才能被追踪。解构会切断 $state 反应性,表现为用户切语言或切主题时,库内的图表不重绘。
2. 字典归库,语言信号归宿主
这条分工是整个设计的核心,两边各有理由:
文案字典自带在库里(ui/i18n.svelte.ts)。因为 topo.*、fstat.*、routing.*、 pflap.* 这些命名空间是组件自有语义,不是应用语义。若改成由宿主注入,每个借用 组件的 app 都得把同一批 key × 四种语言抄进自己的字典——组件搬得越多负担越重。自带则 相反:加一个组件只动一个文件。
当前语言由宿主注入。库和 app 各持一个 locale,会在用户切换语言时不同步。
通用词汇留在 app 字典里,库通过 UiHost.t 回落读取。common.updatedAt、 common.noMatch 这类词 app 自己也在到处用,两边各存一份会出现「同一个词在同屏两处 渲染,只改了一边」的自相矛盾。回落是双向的:库的 t() 查不到就找宿主,app 的 t() 查不到就找库。
3. 各站的语言覆盖并不相同
| 站 | 语言 | 存储键 | 切换入口 |
|---|---|---|---|
| control | en / zh / zh-Hant / ja | dn42.locale | 顶栏 |
| peering | en / zh | natlan.peering.locale | 顶栏 |
| auth | en / zh / zh-Hant / ja | 见 apps/auth/src/lib/i18n.svelte.ts | 页内 |
| home | 固定 en | — | 无 |
首页注入的是两个常量而不是响应式实例——它是纯静态页,既没有语言切换器,也没有 theme store。宿主契约做成可选注入,正是为了容纳这种场景。
4. 完整性检查
npm run check:i18n(scripts/check-i18n.mjs)在 CI 里守两条规则:
- 库组件用到的 key 必须在库字典里,
ALLOW_HOST名单里的通用词汇除外。 - app 用到的 key 必须在「自己字典 ∪ 库字典」里。
动态 key(t(\ns.${x}`)` 形式)只校验前缀:该前缀下有任意条目即算通过,逐个枚举 取值超出静态分析的能力范围。
这条检查防的是一类不会报错的回退:把组件搬进共享库时,它用的 key 还留在原 app 的 字典里,于是另一边整块回显 pflap.xxx。构建不报错,类型检查也不报错,只有真的打开 页面才看得见——这个 bug 咬过两次,两次都是靠截图发现的,因此固化成一条能自动跑的检查。
5. 主题的两条投递路径
设计令牌定义在 ui/styles/app.css,暗色值出现两次,这是有意的:
| 选择器 | 服务谁 | 触发方式 |
|---|---|---|
:root[data-theme='dark'] | control、peering | app.html 的预绘制脚本读 localStorage 后盖标记 |
@media (prefers-color-scheme: dark) { :root:not([data-theme]) } | auth | 系统色,无需 JS |
控制台与门户由 app.html 里的一段预绘制脚本恒定盖上 data-theme(顺带设 color-scheme,让浏览器立刻把画布画成暗色,避免首屏白闪),因此 :not([data-theme]) 在那两站永不命中,媒体查询对它们零影响。
授权页从不盖标记——它是一次性流程,没有导航也没有设置入口,主题直接跟随系统——于是 只走媒体查询这一条。四种「系统色 × 存储偏好」组合都实测过。
⚠️ 两处令牌值必须逐字一致,改一处就得改两处。 纯 CSS 没有不重复的写法(媒体查询 里的选择器合并不进外面的规则)。light-dark() 能消掉这份重复,但需要把整套令牌改写 成双值形式,那是独立的一次改造。
首页不引 ui/styles/app.css:它是自包含单页,令牌内联在 index.html 里,并额外定义 了台地灰阶(--mesa-far/-mid/-near)这组只有它用的令牌。值与共享层保持同源,口径见 reference/design-tokens.md。
6. 主题状态机
ThemePref 是三档 light | dark | system,ThemeMode 是解析后的两档 light | dark。 system 档会持续跟踪 prefers-color-scheme 的变化(注册 change 监听),不需要 刷新页面。注入给库的是解析后的 mode。
图表只把 theme.mode 当重绘信号——颜色本身走 CSS 变量运行时解析,不在 JS 里硬编码。