为 SILO 提供多语言支持
Eduardo Andrade Machado 的 Console PR #83 提供了巴西葡萄牙语翻译。siyahkut 在 silo#277 中请求土耳其语。这两项贡献推动 Console 扩展为可配置的十语言界面。
**截至 2026-11-09 的状态:**十语言界面、审校后的译文、逐语言压缩、静态全量加载和长期缓存已合入 Console main。最终复核保留了紧凑标签要求,并检查了相关控件在手机、平板和桌面宽度下的显示。源码合入与产品发布是不同阶段。
决定
每种翻译语言生成一个预压缩 JavaScript 资源,全部词典静态导入,各自通过内容哈希独立缓存。 分包不意味着懒加载。应用加载完成后,切换语言同步完成,不再请求网络,离线也能切换。
九份翻译词典逐语言执行 gzip -9 后合计 555,105 字节,合并压缩则为 549,712 字节:分包只增加 5,393 字节,约 1%。这个体积对管理控制台可以接受。我们承担包括英文用户在内的首次下载成本,换取无需语言加载状态、超时、重试和迟到响应协调的简单实现;独立文件则让单种译文更新后能复用其他语言的缓存。
英文始终是默认语言,也是唯一的缺词回退语言。中文由第一方维护,必须完整。其他语言更新期间可以暂时回退英文。只翻译界面,服务端消息和日志内容原样展示。本次决策覆盖 Console,不扩展为服务端、命令行工具或整套文档的多语言工程。
为什么增加语言
管理控制台要求用户理解访问策略、保留设置、复制和危险操作。熟悉的语言有助于理解这些控件,也能让更多运维人员使用产品。葡萄牙语和土耳其语已经有具体的社区贡献或需求;统一注册表可以让后续语言沿同一条路径接入。
持续成本比增加一个菜单项更大。英文文案变更需要补译,存储术语需要保持一致,长文本和 RTL 文字影响版面。如果译文把保留或删除警告的意思翻反,危害比清晰的英文回退更大。词典条目齐全并不能证明语言质量合格。
因此,设计需要限制范围、区分维护承诺,并自动检查结构问题。安装和运行 SILO 不需要新增翻译服务、下载模型或访问外部运行时依赖。
已定决策
- 先合并 #83,再提交维护者调整,保留原作者署名。
- 统一使用下拉语言菜单,并提供 KBar 语言命令。
- 支持英语、中文、法语、德语、俄语、西班牙语、巴西葡萄牙语、日语、阿拉伯语、土耳其语。
- 默认英文,不探测浏览器语言,保留用户显式选择。
- 中文覆盖率阻断 CI,其他语言报告覆盖率。
- 全部词典启动时加载,构建产物按语言分包并预压缩,通过内容哈希与长期缓存独立复用。
- 阿拉伯语同时实现 RTL 布局,协议数据仍从左到右展示。
原有设计
英文原文就是词典 key。原中文词典分为 zh、zhHelp、zhScreens,合并优先级为外壳文案 > 功能页面 > 帮助。原先所有用户都会下载登录页和 Console 包中的中文词典。
保留英文原文 key,可以直接得到可读的回退文案,也无需仅为引入消息 ID 而重写数千处调用。代价是修改英文措辞就意味着迁移 key:审查时必须有意识地更新词典,不能把英文润色视为与翻译无关的改动。
代码中还有数字/日期格式、链接、间隔和语言按钮的专用分支。五处数量显示按英文单复数选择,不能覆盖俄语、阿拉伯语的复数规则。
PR #83
贡献包含 2,249 个词条和葡语专用的异步加载器,将双语切换改为英语 → 中文 → 葡语循环。翻译工作值得接纳,但选择器和加载方式需要成为通用能力,不能持续增加葡语专用分支。因此先合并 #83、保留贡献者原始提交,再调整注册表和界面。
原加载器在功能页面词典失败时,可能已注册公共词典却不通知已挂载组件。最终静态实现彻底消除了这类失败路径,同时删除 catalogRevision、首屏等待、重试状态和葡语专用加载器。贡献中的组件文案回归测试保留,改为验证同步切换与离线行为。
体积与打包
测量口径
以下数据来自 2026-11-09 的生产构建,基于完成译文及紧凑标签复核后的 Console 提交 b0371b6。未来文案变化后需要重新测量。十种可选语言对应九份翻译词典:英文直接使用源码 key,不需要重复词典。KB 按 1,000 字节、MB 按 1,000,000 字节计算。
九个生产词典资源压缩前合计 1,863,188 字节。生产流水线固定使用 fflate 0.8.3、level: 9、mtime: 0,得到 568,470 字节。优化后的完整嵌入 GUI 为 2,357,925 字节,相对合入葡语 PR 后的 1,905,708 字节增加 452,217 字节(23.7%)。这里统计的是嵌入 Web 资源,不是整个 Go 可执行文件。
登录首屏 JavaScript 合计 794.08 KB gzip,预算为首屏依赖图 850 KB、全部语言数据 630 KB、每个分包 350 KB。检查器会累加所有语言包,要求每个语言包都属于首屏静态依赖图,同时执行单包上限;不能靠拆成多个小文件绕过体积限制。
合成一包,还是每种语言一包
对比保持生产源码、压缩混淆方式和静态导入不变,只调整构建分组:全部词典一个组,或者每个语言目录一个组。每个语言组都包含 chrome、help、screens 三部分。然后分别用本机 gzip -n -9 命令和现有固定版本压缩器处理生成的 JavaScript。
| 打包方式 | gzip -n -9,字节 |
fflate level 9,字节 |
冷启动语言资源请求数 |
|---|---|---|---|
| 所有语言合并 | 549,712 | 562,448 | 1 |
| 每种语言独立,合计 | 555,105 | 568,470 | 9 |
| 分包额外成本 | 5,393(0.98%) | 6,022(1.07%) | 8 |
相同压缩级别不保证不同实现输出相同字节。系统命令用于对比,固定生产压缩器用于保证不同构建主机上的结果可复现。两种实现得到相同的打包结论,因此不需要为此更换压缩器。
| 语言 | gzip -n -9,字节 |
|---|---|
| 中文 | 58,319 |
| 法语 | 60,684 |
| 德语 | 60,717 |
| 俄语 | 70,315 |
| 西班牙语 | 59,296 |
| 巴西葡萄牙语 | 57,689 |
| 日语 | 63,159 |
| 阿拉伯语 | 64,095 |
| 土耳其语 | 60,831 |
合并文件并不会自动消除跨词典重复的所有英文 key。DEFLATE 只能引用前面 32 KiB 窗口内的文本,而单种语言的完整词典远大于这个窗口。这解释了为什么不能预设跨语言合并会带来巨大压缩收益;上表给出了实际构建的结果。参见 RFC 1951 第 2 节。
缓存收益与请求代价
另一次构建只修改了一句土耳其语 UI 译文。分包后,只有土耳其语语言资源的哈希变化,其他八个保持不变;合并时,整个语言包的哈希变化。若浏览器保留了有效缓存,这次修订后需要下载的语言数据按生产压缩器计算约为 63 KB,而不是 562 KB。入口模块也可能变化,因此这不是整个应用更新流量的比较。如果所有语言一起修改,独立复用的收益也会明显下降。
代价是冷启动多八个请求。全部语言仍在应用使用前下载和初始化;分包不会减少首次传输总量、词典内存分配或 JavaScript 解析工作。高延迟 HTTP/1.1 连接可能放大额外请求的成本。本轮测量证明的是字节体积与哈希稳定性,没有测出加载时间或内存占用的改善。
独立缓存还需要明确的服务端策略。早期合并语言包的实现设置了 Content-Encoding: gzip 和 Vary: Accept-Encoding,但没有明确的长期 Cache-Control。现已落实以下策略:
- 对确实存在、带内容哈希且与用户无关的 JavaScript 和 CSS 资源设置
Cache-Control: public, max-age=31536000, immutable。 - HTML 入口设置
Cache-Control: private, no-cache,重新验证以发现新资源名称。不存在的资源路径回退到 HTML 时不会继承长期缓存;API 的缓存策略继续由各自处理器控制。 - 构建时预压缩,服务时直接发送已有 gzip 内容。
.js.gz是采用 HTTP 内容编码的 JavaScript 资源,不是要求用户手动解包的归档文件。
这一策略遵循带版本资源的 HTTP 缓存原则。使用实际 Go 静态资源处理器的浏览器检查确认:首次访问加载九个生产语言文件,刷新时九个全部复用缓存;十种语言可离线切换,并保留未保存输入。该资源检查的 API 响应使用受控测试数据,不是整套 SILO 部署的验收测试。
为什么仍然全量加载
约 0.57 MB 的压缩词典对当前 Console 可以接受。随应用加载全部词典,可以消除切换语言时的另一组失败路径:无需协调部分词典到达、加载提示、首屏宽限时间、重试循环和过期响应。应用加载完成后也能离线切换语言。这不意味着首次加载前即可离线使用 Console,也不意味着存储操作不再依赖服务端。
成本依然存在:英文用户会下载可能永远不用的翻译,所有用户都要解析并保留这些词典。如果以后语言语料显著增长、首屏依赖图超过预算,或目标网络与设备的实测发现问题,再讨论选择性加载。当前仅为了更小的字节数,还不足以引入这套运行时复杂度。
设计
范围
翻译静态 UI 文案、帮助主题、控件无障碍名称、日期和数字格式、图表图例前缀。服务端错误、日志、审计、Trace、健康报告数据、对象名称、路径、配置标识符、策略 JSON 和 RSS 内容保持原样。文档站仍为中英双语:中文使用中文链接,其他语言回退英文链接。
语言注册表
src/i18n/locales.ts 管理 LOCALES 与 CATALOGS。元数据记录语言自称、BCP 47 标签、方向和维护级别。CATALOGS 的类型为 Record<Lang, Dictionary>,新增注册语言后,类型检查会要求补上词典映射。
这里的可配置是源码中的构建配置,不是管理后台设置,也不是远程语言包服务。组件从注册表读取菜单、格式和方向,避免继续增加语言专用的条件分支。例如:
| ID | 菜单名称 | 方向 | 级别 |
|---|---|---|---|
| en | English | LTR | source |
| zh | 简体中文 | LTR | core |
| fr | Français | LTR | maintained |
| de | Deutsch | LTR | maintained |
| ru | Русский | LTR | maintained |
| es | Español | LTR | maintained |
| pt-BR | Português (Brasil) | LTR | maintained |
| ja | 日本語 | LTR | maintained |
| ar | العربية | RTL | maintained |
| tr | Türkçe | LTR | maintained |
已有 zh 偏好不变。早期测试版的 pt 映射为 pt-BR;未知或未启用的值回退英文。未完成的语言可以通过 enabled: false 从菜单排除,但隐藏菜单项本身不会从构建中移除静态导入。
词典布局
每种语言在 src/i18n/locales/<id>/ 下保留 chrome.ts、help.ts、screens.ts。这三个文件用于组织翻译,不代表加载阶段。字面量 TypeScript 格式保留译者注释,支持静态校验。合并优先级仍为 chrome > screens > help。
加载
每种语言的三个部分全部静态导入。浏览器在应用启动时加载这些 JavaScript 依赖,渲染和后续语言切换不用等待另一轮语言请求。没有语言请求 API、Promise 缓存或启动计时器。共享下拉菜单也静态导入,因此应用首次加载完成后,第一次点击菜单即可离线选择语言。既有路由拆包,以及编辑器、图表、PDF 等大型组件的加载方式,与语言切换相互独立。
格式与链接
组件读取注册表中的格式化标签和站点语言字段。整句模板决定标点和间隔。MDS ControlLabelsProvider 提供 Actions 标题,删除 CSS 生成中文的做法。
语言菜单
登录、匿名页面和 Console 复用地球按钮与下拉菜单。每项用语言自称,并带对应 lang、dir;标记当前语言,键盘选择后将焦点还给按钮。KBar 提供同一组语言命令。
十种语言继续循环切换,会迫使用户经过多个看不懂的界面才能找到自己的语言。下拉菜单允许直接选择,也能随语言数量增长。使用语言自称,即使当前界面陌生,用户也能认出目标语言;无需用国旗代表语言。
复数
tn(count, one, other) 使用 Intl.PluralRules。多形式译文放在英文复数 key 下,包含该语言使用的全部类别。缺词时按英文规则选择英文原文,不将俄语或阿拉伯语规则应用到英文回退文案。
字体
中文和日语避免拉丁大写字距,阿拉伯语使用正常字距。非拉丁文字依靠系统回退字体,不增加字体下载。德语标题和控件允许断词。
紧凑措辞是界面要求。提高准确性时,不能把已经解决的长标签溢出问题带回来。例如,西语 Tiempo activo、法语 Temps actif 和葡语 Tempo ativo 保留 Uptime 的时长含义;纠删码健康标签可用 EC 缩写;生命周期过期标签仍需指明历史版本。应在真实指标卡和表单中审核这些文案,不能只看词典表格。
从右到左布局
间隔、对齐、边框和移动抽屉改用逻辑 CSS 属性。方向图标、标签页方向键和开关滑块随 RTL 改变。对象路径、桶名、ARN/JSON、策略编辑器和原始 Trace/日志数据保持 LTR。图表与物理视口坐标不做机械镜像。
维护分级与检查
中文完整覆盖是硬门槛。其他词典缺词时报告并回退英文,暂时的缺词不阻断无关发布,但仍需要持续审校和修正,所有词典也都必须通过结构校验。审校后的词典每种语言三部分合计 2,252 个词条,对应 2,235 个不同的 key;这是 key 覆盖统计,不是语言熟练程度证明。
两级翻译维护策略反映维护责任。核心团队承诺中文完整,其他语言通过贡献持续演进,不暗示团队有能力亲自保证每种语言的母语质量。英文是唯一回退语言,法语缺词时不会突然显示中文。
占位符错误、空译文、复数类别错误、重复或错分段 key、无效语言标签、注册表与目录不匹配都会导致校验失败。原有中文孤立词条和重复项基线是历史例外,不允许扩充以掩盖回归。
翻译生产
新增词典采用 AI 辅助草稿,再按术语、UI 上下文和危险操作说明校对。术语表覆盖核心存储与访问控制概念,审核重点包括否定关系、权限、不可逆操作、保留期限、占位符和协议标识符。完整覆盖不等于母语审校,AI 复核也不能证明已经达到这一标准。首轮审核与随后的紧凑标签复核已完成,较长帮助段落继续接受修正。
贡献指南说明了 i18n:report:导出缺词及中文参考、源码位置;以及 i18n:rename:在词典间迁移经过确认的英文 key 改动。翻译工具和服务不是构建或运行时依赖。
版面检查
真实语言在 350、768、1440 像素宽度检查登录页和密集的站点复制表单。额外覆盖键盘操作、偏好持久化、所有语言离线切换、未保存表单、RTL 抽屉/标签页,以及日志和路径原文不变。
同样的三种宽度也覆盖每种语言的真实指标行组件和批量生命周期对话框。检查拒绝标签与数值重叠、字段文字裁切或横向溢出,并保留紧凑指标布局,为后续译文调整守住版面约束。
开发专用 en-XA 将文案拉长约 40%,ar-XB 检查 RTL。两者不进入公开菜单或生产词典。词典校验、类型检查、全量前端单元测试和生产包预算共同补充浏览器验收。
考虑过的替代方案
| 方案 | 收益 | 代价与决定 |
|---|---|---|
| 只保留英文与中文 | 翻译维护负担最低 | 无法回应葡语贡献和土耳其语需求,不采用。 |
| 合并一包,全量加载 | 只有一个语言请求,gzip 总量最小 | 任一语言修改都会使所有翻译数据的缓存失效;是合理的简单基线,但分包只多约 1%。 |
| 按语言分包,全量加载 | 独立复用缓存,保留现有同步运行方式 | 冷启动多八个请求,首次词典工作量不减少;采用。 |
| 按语言分包,懒加载 | 英文可以不加载词典,其他用户只取需要的语言 | 增加部分加载、失败、重试与状态协调路径,暂缓,等实测证明有必要再引入。 |
| 引入完整翻译框架 | 更丰富的消息工具和生态集成 | 增加依赖与迁移工作,现有原文 key 查找、Intl、React/Redux 已满足需求。 |
按语言懒加载曾在评估中实现,维护者选择静态方案后将其移除。独立 gzip 文件仍然保留了以后调整加载方式的自然边界,不必现在就引入相关状态。
交付
PR #83 已保留原始历史合并,初版十语言实现与嵌入 GUI分别提交。最终交付包含审校译文与紧凑布局检查、逐语言资源与缓存策略,以及重新生成的嵌入 GUI。
最终源码通过 789 项前端单元测试、Go API 测试、类型检查、格式检查、未使用代码检查和生产包预算。词典修订既要进入 TypeScript 文件,也必须进入实际发布的 GUI。
验收覆盖缺词回退、占位符和复数、语言偏好持久化、首次加载后的离线切换、未保存表单、阿拉伯语布局、原始数据不变,以及只修改一种语言后的缓存复用。双语设计记录由伴生站点承载;源码合入、生成资源、文档发布和产品发布仍是不同交付事项。