22 KiB
Localization Specification
1. Why This Document Exists
本文件不是翻译指南,也不是打包教程。
本文件的目标是把 Trail Mate 的“本地化系统”定义成一个有边界的对象,防止后续开发再次把以下东西混在一起:
- 固件里的 i18n 运行时
- SD / Flash 上的 runtime pack 载荷
- 仓库中的
packs/<bundle-id>/源包 - GitHub Pages 发布出来的 zip 分发包
- 远程 catalog 元数据
- 已安装索引
installed.json - “换语言”这个用户动作
如果这些对象不被区分,系统就会退化成“改了几行英文字符串,再补几条 tsv”的低解释力状态。
2. Current Distinctions
2.1 当前必须切开的对象
Trail Mate 本地化系统至少由八类对象组成:
- 固件本地化运行时
- Runtime pack 载荷
- 仓库源包
- 分发包
- 远程 catalog
- 已安装索引
- locale 质量状态
- 输入法运行时后端
它们彼此相关,但不是同一个东西。
2.2 非法混淆
以下切法在本规格下视为非法:
- 把
strings.tsv直接当成“本地化系统本体”。 - 把 zip 分发包当成运行时直接消费的对象。
- 把“安装了某语言包”误认为“该字体已经加载进 RAM”。
- 把固件版本和语言包版本绑成同一个版本号。
- 把英文源串当成“只是文案”,而不是翻译查找 key。
- 把页面私有字符串补丁当成合法的本地化实现。
- 把
docs/ui_localization_plan.md这种历史方案继续当成现行契约。 - 把繁简转换当成繁中本地化。
- 把
zh-Hant当成一个不需要地区语境的抽象繁体中文。 - 把拼音当成所有中文使用者的默认输入法。
- 把“机器翻译已经填满 TSV”当成可发布质量。
- 把英文 key 原样复制到翻译列,当成合法的缺省翻译。
3. Object Model
3.1 固件本地化运行时
固件运行时负责:
- 持有内建英文基线。
- 扫描、编目并解析可用的 locale / font / IME pack manifest。
- 根据
display_locale选择活动 locale。 - 维护 UI font chain 与 content font chain。
- 在需要时惰性加载外部
font.bin。 - 提供
tr()、format()、set_label_text()等统一入口。 - 在外部 pack 缺失、不可用或超出设备能力时安全回退。
它不负责:
- 读取 zip 分发包。
- 直接理解网站 catalog。
- 把“语言包安装”与“当前 locale 激活”混成同一动作。
3.2 Runtime Pack 载荷
Runtime pack 载荷是运行时真正扫描和消费的已解包目录树。
当前形态包括:
- Flash 安装根
/fs/trailmate/packs/...,用于 Arduino/ESP32 上的安装器写入
- SD 安装根
/trailmate/packs/...,用于手工拷贝或可移动介质安装
运行时理解的是“解包后的 manifest + strings/ranges/font.bin”,而不是 zip。
3.3 仓库源包
仓库中的 packs/<bundle-id>/ 是源码形态,不是运行时形态。
它可以包含:
package.iniREADME.mdDESCRIPTION.txt- runtime manifests
strings.tsvbuild.inicharset.txtranges.txt- 可选的本地
font.bin
其中 build.ini、charset.txt 这类文件属于构建期元数据,运行时不读取。
3.4 分发包
分发包是面向安装器与网站分发的 zip 产物,不是运行时直接扫描的对象。
它当前由 scripts/build_pack_repository.py 生成,并包含:
- 顶层包元数据
payload/fonts/...payload/locales/...payload/ime/...
安装器的职责是下载 zip、校验 SHA-256、解出 payload/,再写入 runtime pack 根目录。
3.5 远程 Catalog
远程 catalog 是“可下载 bundle 的索引”,不是运行时 registry。
它负责提供:
- package id
- package version
min_firmware_versionsupported_memory_profiles- 提供了哪些 locale/font/IME
- archive 路径、大小、SHA-256
- Extensions 页面所需的展示元数据
它不负责:
- 字体加载
- locale 激活
- zip 内文件解析
3.6 已安装索引
已安装索引记录“安装过哪些分发包”,而不是“当前有哪些 locale 被激活”。
当前安装索引的职责是记录:
- package id
- package version
- archive SHA-256
- storage
- install time
在当前 Arduino/ESP32 实现中,主安装索引默认写入 Flash 安装根下的 installed.json。
历史 SD 路径可以作为迁移兼容来源存在,但不应再被当成新的规范落点。
它不能替代 runtime registry,也不能替代 locale persistence。
3.7 Locale 质量状态
locale 质量状态描述的是“这个语言包现在能不能作为用户可选语言出现”,不是描述字体是否存在,也不是描述包是否可以被 catalog 发现。
当前状态值固定为:
release- 可以出现在语言选择器中。
- 必须完成 native 或项目负责人认可的质量审查。
- 不得包含英文补偿项,除非该项是明确允许保留的技术名词、协议名、单位或格式占位串。
review- 字符串表结构完整,通常已经被批量翻译或人工初审过。
- 仍然需要母语审校、地区用语审校、输入法语义审校。
- 运行时必须跳过,不得出现在普通用户语言选择器中。
draft- 可以缺 key、混用语言、缺少地区用语审查。
- 运行时必须跳过。
为了兼容旧包,省略 translation_status 的历史 locale 视为 release。新包不应再省略该字段。
3.8 输入法运行时后端
IME pack manifest 只是输入法后端的声明,不是后端本身。
一个输入法要成为可启用输入法,必须同时满足:
- 固件中存在真实后端。
- 候选词、组合逻辑或直接键盘布局、提交行为与目标语言/地区习惯匹配。
- UI widget 知道如何展示该输入模式。
- 需要作为 locale 默认输入能力时,locale manifest 显式依赖它。
translation_status 仍然独立决定 locale 是否进入普通用户语言选择器。
review locale 可以依赖真实 IME 以便运行时完整校验和安装该能力,但不能因此被当成 release UI locale。
因此,不允许用 backend=builtin-kana、backend=builtin-hangul、backend=builtin-arabic 这类尚未实现的名字提前占位。未实现输入法可以写进规划文档,但不能作为 runtime IME pack 进入 payload。
4. Core Contracts
4.1 English Source String Is API
英文源串不是随手写的文案,它是翻译查找 key。
这意味着:
ui::i18n::tr(const char* english)的输入是稳定 key,而不是“仅供显示的默认英文”。- 修改英文源串,会直接改变 key。
- 一旦 key 改变,所有 locale pack 中对应条目都会失配并回退到英文。
strings.tsv中同一 English key 不允许承载两套不同语义。strings.tsv的行顺序不构成语义;运行时按 English key 查找,而不是按文件顺序查找。
因此,下列操作都属于本地化契约变化:
- 修改现有英文源串文本
- 拆分一条英文源串为多条
- 把一条英文源串合并为另一条
- 改变 format string 结构,例如
%s/%u/ 换行符位置
它们都不能被当成“只改 UI 文案”的低风险改动。
4.2 Locale / Font / IME Dependency Graph
依赖图固定为:
Locale Pack -> UI Font PackLocale Pack -> Content Font PackLocale Pack -> Optional IME PackContent Text -> Optional Supplement Font Packs
Settings 选择的是 locale,不是 font,不是 IME。
页面或设置页不允许绕过 locale 直接定义另一套“选字体/选输入法”主流程。
4.2.1 Locale ID 必须表达地区语境
locale id 必须按 BCP-47 思路表达语言、脚本与地区语境,而不是只表达字形。
推荐形态:
zh-Hans-CN- 简体中文,中国大陆用语。
zh-Hant-TW- 繁体中文,台湾地区用语。
zh-Hant-HK- 繁体中文,香港地区用语。
pt-PT- 葡萄牙语,葡萄牙地区用语。
pt-BR- 葡萄牙语,巴西地区用语。
当前仓库中的 zh-Hant 是历史兼容 id,语义必须按 zh-Hant-TW 处理;后续如果引入香港、澳门或其他繁中地区包,必须新增独立 locale,而不是继续复用 zh-Hant。
禁止把 OpenCC 或其他繁简转换结果直接作为 release 级繁中包。转换最多用于生成初稿;发布前必须检查地区用词、标点、语气、技术名词和输入法习惯。
4.2.2 翻译质量闸门
运行时 locale catalog 必须执行质量闸门:
translation_status=release- 允许进入运行时 locale 列表。
- 缺省
translation_status- 为兼容旧包,暂按
release处理。
- 为兼容旧包,暂按
translation_status=review- 可以被安装、索引、更新,但运行时必须跳过。
translation_status=draft- 可以作为开发资料存在,但运行时必须跳过。
- 其他未知状态
- 运行时必须按不可发布处理并跳过。
一个 release 语言包不得出现以下情况:
- 空翻译。
- 缺失当前 English key 集合中的 key。
%s、%u、%ld、\\n等占位符或转义符不一致。- 大量英文 key 原样复制到翻译列。
- 混入另一种语言的翻译文本,例如日文包中出现简体中文句子。
- 地区用语明显不匹配,例如台湾繁中使用大陆软件术语作为主用语。
允许保持英文的项目只限于明确的专名、协议名、单位、测量标签、格式骨架,例如 GPS、RSSI、dBm、RNode、LXMF、ID: !%08lX。
4.2.3 输入法必须跟 locale 习惯绑定
输入法不是“有一个能打字的键盘”这么简单。它属于 locale 的语言、脚本和地区习惯。
当前可启用输入法只有:
zh-Hans/zh-Hans-CNzh-hans-pinyin- 后端:
builtin-pinyin - 适用范围:简体中文拼音输入。
ruru-cyrillic-keyboard- 后端:
builtin-keyboard-layout - 布局:
ru-cyrillic - 适用范围:俄文 Cyrillic 直接键盘布局;不包含候选词转换,也不承诺物理 QWERTY 到俄文的硬件重映射。
当前不得启用的输入法包括:
zh-Hant/zh-Hant-TW- 不得默认拼音。
- 未来优先级应是注音(Bopomofo / Zhuyin)。
- 可选扩展可考虑仓颉、速成、台湾常见拼音输入,但必须明确标注,不得冒充默认习惯。
ja- 需要假名/罗马字到假名与汉字候选的日文输入模型。
- 不得复用中文拼音候选器。
ko- 需要韩文两벌式等韩文输入模型。
- 不得复用中文或日文输入模型。
ar- 需要阿拉伯键盘、RTL 编辑语义与字体 shaping 一致性。
- 欧洲 Latin 语言
- 通常不需要 IME pack;现有
EN/123输入路径足够。
- 通常不需要 IME pack;现有
如果某 locale 还没有真实输入法,它可以作为 display-only review 包存在,但不得把假 IME 写进 manifest,也不得让设置页显示一个无法工作的输入法选项。
4.3 Built-In Baseline
固件必须始终保留最小内建基线:
- 内建 locale:
en - 内建字体:
builtin-latin-ui - 内建 IME:可为空
English 必须在没有任何外部 pack 的情况下仍可工作。
这条基线不能被 removable pack 破坏。
4.4 Installed Is Not Loaded
Installed 与 Loaded 是两个不同状态:
- Installed
- pack manifest 存在于 Flash/SD
- runtime registry 能发现它
- Loaded
- 外部
font.bin已经实际加载进 RAM - 会产生即时内存占用
- 外部
禁止把“安装成功”实现成“开机即把所有外部字体全部加载”。
4.5 UI Scope vs Content Scope
本地化运行时必须继续维持两条字体链:
- UI chain
- 页面 chrome
- 菜单、按钮、标题、设置项等
- Content chain
- 联系人名
- 节点名
- 聊天内容
- 其他外部文本
这两条链不能被简化成“只要非 ASCII 就统一切换某个 CJK 字体”的旁路实现。
4.6 Persistence Contract
当前 locale 的持久化 key 是:
- namespace:
settings - key:
display_locale - value:locale id string
display_language 只是历史迁移键,只允许用于一次性迁移,不得再作为现行设计继续扩展。
4.7 Discovery Contract
运行时 registry 只扫描“已解包 runtime pack”。
它不扫描:
- zip
- 远程 JSON catalog
- 仓库里的
build.ini - 构建期 charset 生成元数据
如果一个 pack 只存在于网站 zip 中、还没解到运行时目录,那它对运行时来说就是不存在。
4.8 Installation Contract
安装器层与运行时层必须分离。
安装器负责:
- 拉取 catalog
- 下载 zip
- 校验 SHA-256
- 解压
payload/ - 更新 installed index
- 触发
reload_language()
运行时负责:
- 重新扫描可用 pack
- 解析 manifest / strings / ranges
- 重新选择 active locale
- 按需加载字体
当前 Arduino/ESP32 安装器默认把下载得到的 payload 解到 Flash pack 根。 与此同时,手工把 runtime payload 拷贝到 SD 仍然是合法安装方式,因为运行时会同时编目当前支持的 Flash/SD 根。
禁止让运行时 registry 直接承担网络下载或 zip 解释职责。
4.9 Failure And Fallback Contract
本地化失败时必须退回到可解释状态,而不是把系统带入半失效状态。
当前允许的失败行为包括:
- locale 缺少依赖 font pack
- 跳过该 locale
- locale 缺少依赖 IME pack
- 跳过该 locale
- 当前设备 memory profile 无法承受 locale 成本
- 跳过该 locale
- 持久化的 locale id 不再可解析
- 回退到
en
- 回退到
- 某个 content supplement 无法加载
- 页面继续存活
- 个别文字可出现缺字
禁止因为一个外部包损坏而让整个 UI 失去最小英语可用能力。
5. Versioning And Compatibility Contract
5.1 Firmware Version 与 Package Version 必须分离
固件版本与语言包版本不是同一个版本号体系。
它们的职责不同:
- 固件版本
- 描述运行时代码能力
- package version
- 描述 bundle 载荷与元数据版本
不能因为固件升级一次,就机械把所有语言包 version 同步改掉。 也不能因为语言包换了翻译文本,就伪造一次固件版本升级。
5.2 min_firmware_version 的语义
min_firmware_version 是下界,不是“必须完全相等”的绑定版本。
它表示:
- 低于这个固件版本,当前 bundle 语义可能无法被正确理解
- 高于或等于这个版本,可以认为运行时至少理解这份 bundle 契约
它不表示:
- 这个 bundle 只能给某一个固件版本使用
- 每次小翻译更新都必须改
min_firmware_version
5.3 supported_memory_profiles 的语义
supported_memory_profiles 描述的是设备能力兼容边界,不是语言偏好。
它影响:
- Extensions 页展示是否兼容
- 某设备是否应被允许安装/更新该 bundle
它不能被拿来表达:
- 用户喜欢哪种语言
- 当前 locale 是否被激活
5.4 更新判定
当前“是否有更新”以 package record 为单位判定,至少比较:
- version
- archive SHA-256
因此,哪怕 version 不变,只要 archive 内容变了,也可能被视为不同包。
这意味着偷偷改 zip 内容而不更新包版本,是危险的发布行为。
5.5 兼容性 gating 的归属
兼容性 gating 当前属于“安装/发现层”的职责,主要体现在 catalog 解析和 Extensions UI。
但运行时仍必须对手工拷贝到 Flash/SD 的 payload 保持鲁棒:
- 不兼容 pack 可以被发现
- 依赖不满足时可以跳过
- 系统仍需回退到 English 基线
禁止把系统安全性完全建立在“用户一定只会通过 Extensions 正规安装”这个假设上。
5.6 语言包版本必须表达用户可见变化
语言包版本不是装饰字段。只要用户安装后看到的文字、可用 locale、可用 IME、字体覆盖或质量状态发生变化,就必须 bump package version。
最低要求:
- 只修正少量错别字或补少量漏翻
- bump patch,例如
1.0.0 -> 1.0.1。
- bump patch,例如
- 大量补齐字符串、改写地区用语、改变
translation_status、移除假 IME、改变默认输入法依赖- bump minor,例如
1.0.0 -> 1.1.0。
- bump minor,例如
- 改变 payload 布局或 manifest 语义,导致旧运行时可能错误理解
- bump
min_firmware_version,并视情况 bump minor 或 major。
- bump
发布语言包更新时必须同时重建:
- zip archive。
site/data/packs.json。- archive SHA-256。
- installed/update comparison 所需的 package metadata。
否则用户看不到更新,或者看到更新却无法解释更新内容。
6. Change Classification
6.1 固件改动但通常不要求语言包跟随变化
以下改动通常不要求语言包更新:
- 纯逻辑修复,不引入新的用户可见文本
- 不改变 manifest schema 的运行时重构
- 不改变翻译 key 的内部实现优化
- 不改变字体需求的布局调整
6.2 固件改动后通常要求更新语言包字符串
以下改动通常要求至少更新一个或多个 locale pack:
- 新增用户可见英文源串
- 删除旧源串并引入新源串
- 修改旧英文源串文本
- 修改 format string 参数结构
- 修改带转义字符的字符串结构,例如
\n
6.3 固件改动后可能要求更新字体包
以下改动不仅可能改 strings.tsv,还可能要求重建 font pack:
- 新翻译文本引入当前字体未覆盖的新字形
native_name改动引入新字形- 新 IME 字典或候选字集引入新字形
- 新增 locale/self-name/设置项文本导致 charset 扩展
一旦字形覆盖变化,至少要同步处理:
charset.txtranges.txtfont.binestimated_ram_bytes
6.4 固件改动后必须提高 min_firmware_version
以下改动属于 bundle 契约变化,必须考虑提高 min_firmware_version:
- manifest schema 新增运行时必需字段
- 现有 manifest 字段语义改变
- runtime 对 bundle 布局的期望发生变化
- catalog / archive / installed index 语义发生不兼容变化
- locale/font/IME 依赖规则发生不兼容变化
6.5 只改语言包也必须 bump package version
以下情况即使不改固件,也应 bump package version:
strings.tsv变化manifest.ini变化ranges.txt变化font.bin变化package.ini中影响安装或兼容性的字段变化
否则 installed index 与 update check 将失去解释力。
7. Release Obligations
7.1 固件改动合入前必须回答的问题
- 这次是否新增了用户可见英文源串?
- 这次是否改动了现有英文 key?
- 这次是否改变了 format string 结构?
- 这次是否引入了新字形需求?
- 这次是否改变了 manifest / package / catalog 契约?
只要上述任一答案为“是”,就不能只提交固件代码而不审视语言包。
7.2 语言包发布前必须回答的问题
package version是否已反映 payload 变化?- 如有契约变化,
min_firmware_version是否已提高? font.bin是否与charset.txt/ranges.txt/ manifest 保持一致?estimated_ram_bytes是否仍真实反映生成结果?site/data/packs.json与 zip 产物是否已重建?translation_status是否符合真实质量?- release 包是否已经通过空值、缺 key、重复 key、占位符一致性检查?
- release 包是否已经清除英文补偿项?
- 该 locale 的地区用语是否经过明确审查?
- 该 locale 的 IME 依赖是否真实可用,而不是占位后端?
7.3 新 locale 合入前必须满足的最小条件
- English 基线不被破坏。
- locale manifest 依赖完整。
- 对应 font pack 的 RAM 成本可被当前目标 memory profile 解释。
- 运行时目录布局、分发包布局、catalog 元数据三者一致。
- 手工安装与 Extensions 安装都不会把系统带离 English 回退能力。
- 默认
translation_status不得伪装成 release;未审校语言必须先进入review或draft。 - 输入法要么真实可用,要么不写进 locale manifest。
- 如果 locale 是地区敏感语言,例如繁体中文或葡萄牙语,必须明确地区语境。
8. Prohibited Implementations
以下实现方式在本规格下明确禁止:
- 继续引入新的整数语言枚举作为主持久化键。
- 页面直接绕过
ui::i18n写死另一套翻译逻辑。 - 通过“检测到非 ASCII 就随便切某个字体”来替代 locale/font chain。
- 把 zip 当运行时 pack 根目录。
- 改了英文源串却不把它当成 key 变化处理。
- 改了翻译文本所需字形,却不重建相关字体包。
- 改了 bundle payload,却不 bump package version。
- 改了 bundle 契约,却不调整
min_firmware_version。 - 让不兼容或损坏的 pack 破坏 English 最小基线。
- 用机器翻译或繁简转换结果直接发布 release 包。
- 在没有真实后端时发布 IME manifest 或 locale
ime_pack依赖。 - 给台湾繁中默认启用拼音输入法。
- release 语言包中保留大面积英文补偿项。
9. Relationship To Other Documents
本文件是规范性文档。
如果与其他文档冲突,优先级如下:
docs/LOCALIZATION_SPEC.mddocs/specs/LOCALE_PACK_RELEASE_SPEC.mddocs/LOCALE_PACKS.md- 各
packs/<bundle-id>/README.md docs/ui_localization_plan.md
其中:
docs/LOCALE_PACKS.md- 负责解释 pack 机制、布局和字段
docs/specs/LOCALE_PACK_RELEASE_SPEC.md- 负责解释打包、发布、版本、catalog、archive 与更新可见性
docs/ui_localization_plan.md- 仅保留历史演进价值,不再作为当前设计依据
10. One-Sentence Baseline
Trail Mate 的本地化不是“固件里几条翻译表”,而是“以 English key 为稳定接口、以 locale/font/IME pack 为显式依赖、以安装层与运行时层分离为前提、并允许固件与语言包独立演进但受兼容契约约束”的完整系统。