ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

BongoCat 多语言本地化架构实战:基于 rust-i18n 的 JSON catalog 设计与 RFC 4647 语言回退

BongoCat 多语言本地化架构实战:基于 rust-i18n 的 JSON catalog 设计与 RFC 4647 语言回退 桌面应用【免费下载链接】BongoCat BongoCat — A cross-platform interactive desktop pet that brings fun to your desktop!项目地址https://gitcode.com/gh_mirrors/bong/BongoCat点击查看免费下载本篇技术指南以 BongoCat 的 ADR-0012JSON 本地化 为骨架结合bongocat-i18n、bongocat-config的源码与测试讲解这套以rust-i18n编译期嵌入 JSON 资源为核心的多语言方案从语言命名规则、RFC 4647 回退语义、zh简繁分流的特殊处理到新增语言的标准操作流程与工程化门禁。读完你将掌握这套 catalog 的结构约定、text/format_text/platform_text三个查找门面以及如何在 BongoCat 仓库中正确添加一种新语言。决策概览一次数据加注册的改动ADR-00122026-09-10 接受的核心决策是BongoCat 使用rust-i18n 4.2.2加载编译期嵌入的 JSON 语言资源应用层资源由独立的bongocat-i18ncrate 管理默认语言为en-US已落地的语言为zh-CN、zh-TW、ar-SA、vi-VN与pt-BR。这一决策在源码中有三层直接体现工作区依赖锁定在 Cargo.tomlrust-i18n 4.2.2精确版本不带^保证生态行为可复现bongocat-i18n/src/lib.rs 通过rust_i18n::i18n!(locales, fallback en-US)声明 catalog 目录与回退语言bongocat-i18n/Cargo.toml 声明build build.rs即 catalog 编译期嵌入由 build 脚本驱动。ADR 特别强调新增一种语言是一次“数据加注册”的改动不改变本 ADR 的任何结构决定。后续第 5 节会给出完整的注册清单。语言命名地区子标签提名制ADR 规定语言命名采用带地区子标签的提名式与en-US一致一份语言只发一份 catalog地区子标签只提名这份文案所依据的主要变体并不声称 catalog 未携带的地区特化。en-US本身也是这种名字——它同样服务en-GB。因此已发布语言清单是六份 catalog每个地区子标签都只是“提名”Catalog 文件名服务的语言标签说明en-US.jsonen、en-GB、en-AU等全部英语变体英语是默认语言zh-CN.json简体中文zh、zh-Hans、zh-CN等与zh-TW是两个 catalogzh-TW.json繁体中文zh-TW、zh-HK、zh-MO、zh-Hant等书写体系不同ar-SA.jsonar、ar-EG、ar-MA、ar-SA等全部阿拉伯语变体一份文案服务整个语言vi-VN.jsonvi、vi-VN同上pt-BR.jsonpt、pt-PT、pt-BR葡萄牙语只发一种变体该清单在 bongocat-i18n/src/lib.rs 中有常量定义pub const SHIPPED_LOCALES: [str; 6] [en-US, zh-CN, zh-TW, ar-SA, vi-VN, pt-BR];配套的还有两个命名约束语言枚举的变体名按语言本身命名不带地区Arabic、Vietnamese、Portuguese各自服务整个语言地区子标签只是提名。PortugueseBrazil这类名字会声称 catalog 并不携带的地区特化因此被明确排除。在 bongocat-config 的 Language 枚举 中变体名确实是Arabic、Vietnamese、Portuguese序列化时通过#[serde(rename ar-SA)]等映射到带地区的配置值。语言下拉用 endonym本地人自称命名同样不附地区后缀——Português而不是Português (Brasil)العربية而不是العربية (السعودية)对应的 i18n key 也只写portuguese、arabic。在 en-US.json 的settings.appearance.language.options中可以看到完整写法简体中文、繁體中文、English、العربية、Tiếng Việt、Português。endonym 的动机是看不懂当前窗口语言的人也能在语言列表里认出自己的语言带变音符的Tiếng Việt、Português在每个 catalog 中都保持原样。唯一保留限定词的名字是ChineseSimplified因为它命中的确实是脚本分叉zh下的简繁是两种书写体系而非地区变体详见下一节。RFC 4647 language-subtag fallback让机器报告的 tag 收敛到同一份 catalog解析遵循 RFC 4647 的 language-subtag fallback浏览器Intl、CLDR 与操作系统自身的做法按primary subtag匹配已发布 catalog。因此ar/ar-EG/ar_MA/ar-SA全部落到ar-SAvi与vi-VN全部落到vi-VNpt/pt-PT/pt-BR全部落到pt-BRen/en-GB全部落到en-US。机器实际上报的 tag 几乎总不是 catalog 自身的名字而 primary subtag 匹配让它们收敛到同一份文案而不是各自回退英文。pt-BR尤其说明这件事的必要性葡萄牙语没有单一写法catalog 只发一种变体报pt-PT的机器读到的也是它。bongocat-i18n侧的实现是 locale_code它会先把_归一化为-并转为小写再取第一个子标签匹配pub fn locale_code(code: str) - str { let normalized code.replace(_, -).to_ascii_lowercase(); let language normalized.split(-).next().unwrap_or(); if language zh { return if is_simplified_chinese(normalized) { zh-CN } else { zh-TW }; } SHIPPED_LOCALES .iter() .copied() .find(|shipped| { shipped .split_once(-) .is_some_and(|(shipped_language, _)| shipped_language language) }) .unwrap_or(DEFAULT_LOCALE) }zh是唯一不能只看 primary subtag 的情形简繁是不同书写体系而非地区变体因此两种写法各发一份 catalogzh-CN简体、zh-TW繁体并在zh分支内按书写体系子标签直接分流不进入下面的 subtag 循环。hant与TW/HK/MO判为繁体hans与其余zh标签判为简体——两种拼写都要覆盖因为平台只报地区子标签时TW/HK/MO是唯一可用的线索。判定的实现是 is_simplified_chinese脚本子标签hant/hans存在时以它为准不存在时以tw/hk/mo地区子标签为准都不是则判简体fn is_simplified_chinese(normalized: str) - bool { !normalized .split(-) .any(|subtag| matches!(subtag, hant | tw | hk | mo)) }配置层与 i18n 层各做一次同样的判定bongocat-config的 Language::from_system_locale 与bongocat-i18n::locale_code各做一次同样的判定——两个 crate 之间不存在依赖方向不能共用一个 helper行为必须由测试对齐。from_system_locale同样先归一化 tag再看zh分支的hant|tw|hk|mo然后依次匹配ar、vi、pt的 primary subtag其余一律落到English。resolve方法appearance.rs则处理配置里显式选了语言的情形System会代入系统语言解析非System直接返回自身。对齐行为由测试保证catalog.rs 的a_region_variant_reaches_the_catalog_that_serves_its_language覆盖了ar/ar-EG/ar_EG/AR-sa、vi/vi-VN、pt/pt-PT/pt_BR、en/en-GB/en_AU全部落点并断言它们渲染出的文案与对应 catalog 一致catalog.rs 的each_chinese_script_reaches_its_own_catalog把zh-TW/zh-HK/zh-MO/zh-Hant/zh-Hant-HK/zh_Hant_TW判为繁体、zh/zh-CN/zh-Hans/zh-Hans-CN/zh_CN判为简体并断言两个 catalog 的文案确实不同catalog.rs 的a_language_the_product_does_not_ship_falls_back_to_the_default验证de-DE/fr/ja-JP/ko-KR/ru-RU等未发布语言落到en-US配置侧的 bongocat-config/src/tests/validation.rs 用Language::from_system_locale(zh-Hans-CN)、zh_Hant_HK、en-GB、de-DE做了同样的断言。catalog 文件嵌套 JSON、_version与领域分层语言文件放在crates/bongocat-i18n/locales/每种语言一个 JSON 文件使用_version: 1和真正嵌套的领域结构。rust-i18n在编译期将嵌套路径解析为查找 keyJSON 源文件本身不得使用点号分隔的扁平 key。以 en-US.json 开头为例{ _version: 1, navigation: { settings: { title: BongoCat Settings }, ... }, ... }key 命名约束翻译 key 使用小写snake_case按领域分层navigation设置窗口导航settings设置项标签、选项、描述models模型库、导入、校验、行为shortcuts快捷键作用域、命令名、行为名diagnostics诊断相关about关于窗口actions通用操作Cancel / Confirm / Closestatus状态文案errors错误消息其中errors.settings携带数十条面向用户的错误文案字段名必须表达具体上下文例如update.status.downloading、models.validation.package_safety_limits_exceeded。Rust UI 在文案实际使用处直接引用稳定的领域路径不内嵌翻译文本也不维护 enum 到 key 的集中映射——这是与“集中映射表”式方案的关键区别查找发生在调用点。locale_source_uses_nested_snake_case_keys测试catalog.rs机械保证递归遍历所有 locale 的 JSON禁止任何 key 包含.扁平 key禁止任何非snake_case字符仅允许小写字母、_、数字_version除外。使用点门面text / format_text / platform_textUI 不得直接调用t!宏或依赖全局 locale——bongocat-i18n是唯一调用rust_i18n::i18n!的 catalog owner。UI 使用三个门面函数text(locale, key)text 先用locale_code把任意 tag 归一为已发布 catalog再以{locale}\0{key}为键查询一个OnceLockRwLockHashMap缓存未命中时通过t!宏取编译期嵌入值并用Box::leak转为static后缓存。返回值是static str且 key 不存在时rust-i18n会原样返回 key 本身——这让缺失 key 在开发期直接可见而不是静默返回空串。format_text(locale, key, values)format_text 提供%{name}命名插值让 UI 代码无需语言分支pub fn format_text(locale: str, key: str, values: [(str, String)]) - String { let mut message text(locale, key).to_owned(); for (name, value) in values { message message.replace(format!(%{{{name}}}), value); } message }插值语法为rust-i18n的%{name}所有语言必须保持相同的占位符集合。实际用例来自 format.rs 的测试format_text(en-US, update.current_version, [(version, 1.2.3)])得到Current version 1.2.3同样的 key 在zh-CN得到当前版本 1.2.3、在ar-SA得到الإصدار الحالي 1.2.3、在pt-BR得到Versão atual 1.2.3。platform_text(locale, base_key)平台差异化文案由 platform_text 处理对应 ADR-0028 的配套约定。查找顺序先试base_key.platform_id例如settings.app_system.status_icon.label.macos若该 key 不存在rust-i18n返回 key 本身据此判断回退到base_key共享文案。current_platform_idlib.rs在 macOS 上返回macos、Windows 上返回windows。这样新增平台覆盖是纯 locale 改动在..label旁边放一份..label.platform即可UI 层零分支。platform.rs 的测试 验证了平台 key 优先、无覆盖时回退 base key 两条路径。新增一种语言完整注册清单ADR 明确新语言是“数据加注册”改动涉及以下位置以新增语言xx-YY为例写 JSON在 crates/bongocat-i18n/locales/ 新建xx-YY.json与en-US同 key、同占位符build.rs 的CATALOGSbuild.rs 的CATALOGS数组加入locales/xx-YY.jsonLanguage枚举appearance.rs 的Language增加变体并同步ALL、code、from_system_locale、resolve四个方法SettingsLanguage枚举bongocat-ui-protocol的SettingsLanguage及其双向投影projection.rs 的settings_language/config_languagesettings_language_display_name语言下拉显示名与settings.appearance.language.options里的 endonym key如xx_yy对应tools/validate-locales.py的EXPECTED_LOCALESvalidate-locales.py 加入xx-YY测试清单LOCALEStests/mod.rs 的LOCALES加入xx-YY。build.rs编译期嵌入与失效检测bongocat-i18n/build.rs 对每份 catalog 执行cargo:rerun-if-changed并计算 FNV-1a 指纹注入环境变量BONGOCAT_I18N_CATALOG_REVISIONfor catalog in CATALOGS { println!(cargo:rerun-if-changed{catalog}); let bytes fs::read(Path::new(catalog)).expect(read localization catalog); for byte in bytes { fingerprint ^ u64::from(byte); fingerprint fingerprint.wrapping_mul(0x100000001b3); } } println!(cargo:rustc-envBONGOCAT_I18N_CATALOG_REVISION{fingerprint:016x});该值被 lib.rs 以env!读取为CATALOG_REVISION常量。注释解释了动机rust_i18n::i18n!通过 proc macro 在展开时读取 locale 文件编译器不会自动记录对这些文件的依赖因此catalog 只改内容时消费方必须因这份注入的 revision 而重编译。配套的防陈旧测试是 catalog.rs 的compiled_catalog_matches_the_files_on_disk它故意不用include_str!而是从磁盘读取 JSON 逐 key 与text()结果比对发现不一致时报错提示cargo build -p bongocat-i18n重建。注释里记录了一个真实事故2026-09-21 模型删除确认框渲染了改名前的模板%{status} · %{confirm_deletion}而所有门禁通过——这正是本测试要堵住的洞。测试清单的单一来源语言清单只在 tests/mod.rs 的LOCALES一处枚举各条比较型测试都遍历它因此新增语言不会让某条比较悄悄少覆盖一种语言。source()函数tests/mod.rs对LOCALES与include_str!的 match 做了联动locale 出现在一边而缺席另一边是messages里的编译错误而不是静默漏测。工程化门禁五类自动检查与文案强制规则ADR 要求测试/CI 必须检查JSON 可解析所有 locale语言 key 集合相同以en-US为基准值为非空字符串占位符集合一致语言清单单一来源前述LOCALES。双向 key 覆盖检查crates/bongocat-i18n/src/tests/coverage.rs 提供两个互为镜像的扫描source_referenced_keys_exist_in_the_catalog扫描crates/下全部.rs源码凡是bongocat_i18n::text/format_text/platform_text的字面量参数、或形似 catalog key 的点分字面量必须能在每个 locale 中解析resolve逻辑与运行时一致包括平台相对 key 规则catalog_keys_are_referenced_by_source反向检查每个 catalog key 都至少被源码引用一次防止“发了但永远不展示”的死文案。注释记录了两个真实事件2026-09-20 设置窗口清理了 22 个无引用 key后续一次清理 commit 又误删了一个仍被使用的 key——双向检查正是为此而生。由于rust-i18n对未知 key 返回 key 本身改名/删 key 不会让构建失败单测和 smoke 也不报错窗口只会悄悄渲染原始 key。这两条扫描把“悄悄坏掉”变成显式失败。validate-locales.py 与省略号强制规则tools/validate-locales.py 是独立的 Python 门禁README 与 CI 都会调用校验locale 文件集合与EXPECTED_LOCALES完全一致缺一多一都失败每个值非空字符串、_version只允许出现在顶层key 集合与en-US一致、占位符集合正则%\{([A-Za-z][A-Za-z0-9_]*)\}一致。UI 文案的书写约定见 docs/localization-copy-conventions.md。当前已落地的一条关键规则省略号一律写单个…U2026视觉上是三个点禁止中文排版习惯的……六个点与拉丁写法的 ASCII...——同一个 key 由所有语言共用写法必须与语言无关。该规则由 validate-locales.py 的ELLIPSIS_RUN正则\u2026{2,}|\.{2,}机械强制不依赖 review 记忆error: zh-CN: models.import.step.importing spells an ellipsis as ……; use a single … (U2026) in every locale新增/修改文案后本地跑一次python3 tools/validate-locales.py即可。约束边界与运行时行为ADR 还划定了以下约束源码均有对应体现找不到语言或 key 时回退到en-USlocale_code的unwrap_or(DEFAULT_LOCALE)与i18n!的fallback en-US双重保证system在配置/平台层先解析为受支持语言Language::resolve把System展开为系统语言配置层先于 UI 完成解析UI 拿到的永远是具体语言GPUI 语言切换通过已有带 revision 的设置 snapshot 触发重绘本地化查询不进入 overlay frame loop——翻译查找只在设置变更的重绘路径上发生不会成为桌面宠物渲染热路径的负担新文案必须先加入 JSON再由 Rust 使用 key禁止在.rs中新增自然语言翻译文本即禁止硬编码用户可见字符串bongocat-i18n是唯一 catalog ownerUI 通过text(locale, key)facade 取文案并在使用点写出 key不得为 UI crate 再初始化同一份 catalog 或依赖全局 locale。取舍与生态一致性ADR 的取舍部分说明了三个选择gpui-kit的底层gpui-component也使用rust-i18n因此依赖生态一致同一套编译期机制贯穿 UI 组件库与业务层JSON 比 YAML/TOML 更适合现有前端资源、翻译工具和跨语言校验本方案不引入 Fluent 的复数/选择语法若后续产品需要复杂 ICU/Fluent 消息复数、性别、选择语法应另行提交 ADR不在业务代码中混用第二套格式——这是明确的演进边界当前%{name}插值 编译期 JSON 的简单模型保持不变复杂度留给未来的正式决策。小结BongoCat 的本地化方案可以概括为一句话一套编译期嵌入的 JSON catalog RFC 4647 的 primary subtag 收敛 简体/繁体脚本分流 五层自动化门禁。新增语言是纯粹的“数据加注册”无需改动任何结构决策缺失 key、死文案、占位符漂移、陈旧 catalog 都有对应的自动化检查兜底。若要在该仓库中实践建议从 ADR-0012 原文、bongocat-i18n/src/lib.rs、en-US.json 与 validate-locales.py 四个文件入手即可完整掌握从结构到门禁的全链路。赞分享桌面应用【免费下载链接】BongoCat BongoCat — A cross-platform interactive desktop pet that brings fun to your desktop!项目地址https://gitcode.com/gh_mirrors/bong/BongoCat点击查看免费下载相关推荐Unity 多语言国际化实战基于 JSON 语言文件与 LitJson 的本地化方案Unity 多语言国际化实战基于 JSON 语言文件与 LitJson 的本地化方案 导读 本篇文章以仓库中 I18N_Localization/I18N_B示例工程Go Web 应用国际化i18n实战指南语言包设计、go-i18n 落地与多语言站点构建Go Web 应用国际化i18n实战指南语言包设计、go i18n 落地与多语言站点构建 本篇以《Build Web Application with G文档教程Bananas多语言支持国际化i18n架构设计与实现Bananas多语言支持国际化i18n架构设计与实现 Bananas作为跨平台屏幕共享工具其国际化i18n架构设计让全球用户都能使用熟悉的语言进行屏幕共音视频即时通讯创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表