Files
trail-mate/docs/specification/LOCALIZATION_SPEC.md
T
2026-06-13 20:34:04 +08:00

22 KiB
Raw Blame History

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 本地化系统至少由八类对象组成:

  1. 固件本地化运行时
  2. Runtime pack 载荷
  3. 仓库源包
  4. 分发包
  5. 远程 catalog
  6. 已安装索引
  7. locale 质量状态
  8. 输入法运行时后端

它们彼此相关,但不是同一个东西。

2.2 非法混淆

以下切法在本规格下视为非法:

  1. strings.tsv 直接当成“本地化系统本体”。
  2. 把 zip 分发包当成运行时直接消费的对象。
  3. 把“安装了某语言包”误认为“该字体已经加载进 RAM”。
  4. 把固件版本和语言包版本绑成同一个版本号。
  5. 把英文源串当成“只是文案”,而不是翻译查找 key。
  6. 把页面私有字符串补丁当成合法的本地化实现。
  7. docs/ui_localization_plan.md 这种历史方案继续当成现行契约。
  8. 把繁简转换当成繁中本地化。
  9. zh-Hant 当成一个不需要地区语境的抽象繁体中文。
  10. 把拼音当成所有中文使用者的默认输入法。
  11. 把“机器翻译已经填满 TSV”当成可发布质量。
  12. 把英文 key 原样复制到翻译列,当成合法的缺省翻译。

3. Object Model

3.1 固件本地化运行时

固件运行时负责:

  1. 持有内建英文基线。
  2. 扫描、编目并解析可用的 locale / font / IME pack manifest。
  3. 根据 display_locale 选择活动 locale。
  4. 维护 UI font chain 与 content font chain。
  5. 在需要时惰性加载外部 font.bin
  6. 提供 tr()format()set_label_text() 等统一入口。
  7. 在外部 pack 缺失、不可用或超出设备能力时安全回退。

它不负责:

  1. 读取 zip 分发包。
  2. 直接理解网站 catalog。
  3. 把“语言包安装”与“当前 locale 激活”混成同一动作。

3.2 Runtime Pack 载荷

Runtime pack 载荷是运行时真正扫描和消费的已解包目录树。

当前形态包括:

  1. Flash 安装根
    • /fs/trailmate/packs/...,用于 Arduino/ESP32 上的安装器写入
  2. SD 安装根
    • /trailmate/packs/...,用于手工拷贝或可移动介质安装

运行时理解的是“解包后的 manifest + strings/ranges/font.bin”,而不是 zip。

3.3 仓库源包

仓库中的 packs/<bundle-id>/ 是源码形态,不是运行时形态。

它可以包含:

  1. package.ini
  2. README.md
  3. DESCRIPTION.txt
  4. runtime manifests
  5. strings.tsv
  6. build.ini
  7. charset.txt
  8. ranges.txt
  9. 可选的本地 font.bin

其中 build.inicharset.txt 这类文件属于构建期元数据,运行时不读取。

3.4 分发包

分发包是面向安装器与网站分发的 zip 产物,不是运行时直接扫描的对象。

它当前由 scripts/build_pack_repository.py 生成,并包含:

  1. 顶层包元数据
  2. payload/fonts/...
  3. payload/locales/...
  4. payload/ime/...

安装器的职责是下载 zip、校验 SHA-256、解出 payload/,再写入 runtime pack 根目录。

3.5 远程 Catalog

远程 catalog 是“可下载 bundle 的索引”,不是运行时 registry。

它负责提供:

  1. package id
  2. package version
  3. min_firmware_version
  4. supported_memory_profiles
  5. 提供了哪些 locale/font/IME
  6. archive 路径、大小、SHA-256
  7. Extensions 页面所需的展示元数据

它不负责:

  1. 字体加载
  2. locale 激活
  3. zip 内文件解析

3.6 已安装索引

已安装索引记录“安装过哪些分发包”,而不是“当前有哪些 locale 被激活”。

当前安装索引的职责是记录:

  1. package id
  2. package version
  3. archive SHA-256
  4. storage
  5. install time

在当前 Arduino/ESP32 实现中,主安装索引默认写入 Flash 安装根下的 installed.json。 历史 SD 路径可以作为迁移兼容来源存在,但不应再被当成新的规范落点。

它不能替代 runtime registry,也不能替代 locale persistence。

3.7 Locale 质量状态

locale 质量状态描述的是“这个语言包现在能不能作为用户可选语言出现”,不是描述字体是否存在,也不是描述包是否可以被 catalog 发现。

当前状态值固定为:

  1. release
    • 可以出现在语言选择器中。
    • 必须完成 native 或项目负责人认可的质量审查。
    • 不得包含英文补偿项,除非该项是明确允许保留的技术名词、协议名、单位或格式占位串。
  2. review
    • 字符串表结构完整,通常已经被批量翻译或人工初审过。
    • 仍然需要母语审校、地区用语审校、输入法语义审校。
    • 运行时必须跳过,不得出现在普通用户语言选择器中。
  3. draft
    • 可以缺 key、混用语言、缺少地区用语审查。
    • 运行时必须跳过。

为了兼容旧包,省略 translation_status 的历史 locale 视为 release。新包不应再省略该字段。

3.8 输入法运行时后端

IME pack manifest 只是输入法后端的声明,不是后端本身。

一个输入法要成为可启用输入法,必须同时满足:

  1. 固件中存在真实后端。
  2. 候选词、组合逻辑或直接键盘布局、提交行为与目标语言/地区习惯匹配。
  3. UI widget 知道如何展示该输入模式。
  4. 需要作为 locale 默认输入能力时,locale manifest 显式依赖它。

translation_status 仍然独立决定 locale 是否进入普通用户语言选择器。 review locale 可以依赖真实 IME 以便运行时完整校验和安装该能力,但不能因此被当成 release UI locale。

因此,不允许用 backend=builtin-kanabackend=builtin-hangulbackend=builtin-arabic 这类尚未实现的名字提前占位。未实现输入法可以写进规划文档,但不能作为 runtime IME pack 进入 payload。


4. Core Contracts

4.1 English Source String Is API

英文源串不是随手写的文案,它是翻译查找 key。

这意味着:

  1. ui::i18n::tr(const char* english) 的输入是稳定 key,而不是“仅供显示的默认英文”。
  2. 修改英文源串,会直接改变 key。
  3. 一旦 key 改变,所有 locale pack 中对应条目都会失配并回退到英文。
  4. strings.tsv 中同一 English key 不允许承载两套不同语义。
  5. strings.tsv 的行顺序不构成语义;运行时按 English key 查找,而不是按文件顺序查找。

因此,下列操作都属于本地化契约变化:

  1. 修改现有英文源串文本
  2. 拆分一条英文源串为多条
  3. 把一条英文源串合并为另一条
  4. 改变 format string 结构,例如 %s / %u / 换行符位置

它们都不能被当成“只改 UI 文案”的低风险改动。

4.2 Locale / Font / IME Dependency Graph

依赖图固定为:

  1. Locale Pack -> UI Font Pack
  2. Locale Pack -> Content Font Pack
  3. Locale Pack -> Optional IME Pack
  4. Content Text -> Optional Supplement Font Packs

Settings 选择的是 locale,不是 font,不是 IME。

页面或设置页不允许绕过 locale 直接定义另一套“选字体/选输入法”主流程。

4.2.1 Locale ID 必须表达地区语境

locale id 必须按 BCP-47 思路表达语言、脚本与地区语境,而不是只表达字形。

推荐形态:

  1. zh-Hans-CN
    • 简体中文,中国大陆用语。
  2. zh-Hant-TW
    • 繁体中文,台湾地区用语。
  3. zh-Hant-HK
    • 繁体中文,香港地区用语。
  4. pt-PT
    • 葡萄牙语,葡萄牙地区用语。
  5. pt-BR
    • 葡萄牙语,巴西地区用语。

当前仓库中的 zh-Hant 是历史兼容 id,语义必须按 zh-Hant-TW 处理;后续如果引入香港、澳门或其他繁中地区包,必须新增独立 locale,而不是继续复用 zh-Hant

禁止把 OpenCC 或其他繁简转换结果直接作为 release 级繁中包。转换最多用于生成初稿;发布前必须检查地区用词、标点、语气、技术名词和输入法习惯。

4.2.2 翻译质量闸门

运行时 locale catalog 必须执行质量闸门:

  1. translation_status=release
    • 允许进入运行时 locale 列表。
  2. 缺省 translation_status
    • 为兼容旧包,暂按 release 处理。
  3. translation_status=review
    • 可以被安装、索引、更新,但运行时必须跳过。
  4. translation_status=draft
    • 可以作为开发资料存在,但运行时必须跳过。
  5. 其他未知状态
    • 运行时必须按不可发布处理并跳过。

一个 release 语言包不得出现以下情况:

  1. 空翻译。
  2. 缺失当前 English key 集合中的 key。
  3. %s%u%ld\\n 等占位符或转义符不一致。
  4. 大量英文 key 原样复制到翻译列。
  5. 混入另一种语言的翻译文本,例如日文包中出现简体中文句子。
  6. 地区用语明显不匹配,例如台湾繁中使用大陆软件术语作为主用语。

允许保持英文的项目只限于明确的专名、协议名、单位、测量标签、格式骨架,例如 GPSRSSIdBmRNodeLXMFID: !%08lX

4.2.3 输入法必须跟 locale 习惯绑定

输入法不是“有一个能打字的键盘”这么简单。它属于 locale 的语言、脚本和地区习惯。

当前可启用输入法只有:

  1. zh-Hans / zh-Hans-CN
    • zh-hans-pinyin
    • 后端:builtin-pinyin
    • 适用范围:简体中文拼音输入。
  2. ru
    • ru-cyrillic-keyboard
    • 后端:builtin-keyboard-layout
    • 布局:ru-cyrillic
    • 适用范围:俄文 Cyrillic 直接键盘布局;不包含候选词转换,也不承诺物理 QWERTY 到俄文的硬件重映射。

当前不得启用的输入法包括:

  1. zh-Hant / zh-Hant-TW
    • 不得默认拼音。
    • 未来优先级应是注音(Bopomofo / Zhuyin)。
    • 可选扩展可考虑仓颉、速成、台湾常见拼音输入,但必须明确标注,不得冒充默认习惯。
  2. ja
    • 需要假名/罗马字到假名与汉字候选的日文输入模型。
    • 不得复用中文拼音候选器。
  3. ko
    • 需要韩文两벌式等韩文输入模型。
    • 不得复用中文或日文输入模型。
  4. ar
    • 需要阿拉伯键盘、RTL 编辑语义与字体 shaping 一致性。
  5. 欧洲 Latin 语言
    • 通常不需要 IME pack;现有 EN / 123 输入路径足够。

如果某 locale 还没有真实输入法,它可以作为 display-only review 包存在,但不得把假 IME 写进 manifest,也不得让设置页显示一个无法工作的输入法选项。

4.3 Built-In Baseline

固件必须始终保留最小内建基线:

  1. 内建 localeen
  2. 内建字体:builtin-latin-ui
  3. 内建 IME:可为空

English 必须在没有任何外部 pack 的情况下仍可工作。

这条基线不能被 removable pack 破坏。

4.4 Installed Is Not Loaded

InstalledLoaded 是两个不同状态:

  1. Installed
    • pack manifest 存在于 Flash/SD
    • runtime registry 能发现它
  2. Loaded
    • 外部 font.bin 已经实际加载进 RAM
    • 会产生即时内存占用

禁止把“安装成功”实现成“开机即把所有外部字体全部加载”。

4.5 UI Scope vs Content Scope

本地化运行时必须继续维持两条字体链:

  1. UI chain
    • 页面 chrome
    • 菜单、按钮、标题、设置项等
  2. Content chain
    • 联系人名
    • 节点名
    • 聊天内容
    • 其他外部文本

这两条链不能被简化成“只要非 ASCII 就统一切换某个 CJK 字体”的旁路实现。

4.6 Persistence Contract

当前 locale 的持久化 key 是:

  1. namespacesettings
  2. keydisplay_locale
  3. valuelocale id string

display_language 只是历史迁移键,只允许用于一次性迁移,不得再作为现行设计继续扩展。

4.7 Discovery Contract

运行时 registry 只扫描“已解包 runtime pack”。

它不扫描:

  1. zip
  2. 远程 JSON catalog
  3. 仓库里的 build.ini
  4. 构建期 charset 生成元数据

如果一个 pack 只存在于网站 zip 中、还没解到运行时目录,那它对运行时来说就是不存在。

4.8 Installation Contract

安装器层与运行时层必须分离。

安装器负责:

  1. 拉取 catalog
  2. 下载 zip
  3. 校验 SHA-256
  4. 解压 payload/
  5. 更新 installed index
  6. 触发 reload_language()

运行时负责:

  1. 重新扫描可用 pack
  2. 解析 manifest / strings / ranges
  3. 重新选择 active locale
  4. 按需加载字体

当前 Arduino/ESP32 安装器默认把下载得到的 payload 解到 Flash pack 根。 与此同时,手工把 runtime payload 拷贝到 SD 仍然是合法安装方式,因为运行时会同时编目当前支持的 Flash/SD 根。

禁止让运行时 registry 直接承担网络下载或 zip 解释职责。

4.9 Failure And Fallback Contract

本地化失败时必须退回到可解释状态,而不是把系统带入半失效状态。

当前允许的失败行为包括:

  1. locale 缺少依赖 font pack
    • 跳过该 locale
  2. locale 缺少依赖 IME pack
    • 跳过该 locale
  3. 当前设备 memory profile 无法承受 locale 成本
    • 跳过该 locale
  4. 持久化的 locale id 不再可解析
    • 回退到 en
  5. 某个 content supplement 无法加载
    • 页面继续存活
    • 个别文字可出现缺字

禁止因为一个外部包损坏而让整个 UI 失去最小英语可用能力。


5. Versioning And Compatibility Contract

5.1 Firmware Version 与 Package Version 必须分离

固件版本与语言包版本不是同一个版本号体系。

它们的职责不同:

  1. 固件版本
    • 描述运行时代码能力
  2. package version
    • 描述 bundle 载荷与元数据版本

不能因为固件升级一次,就机械把所有语言包 version 同步改掉。 也不能因为语言包换了翻译文本,就伪造一次固件版本升级。

5.2 min_firmware_version 的语义

min_firmware_version 是下界,不是“必须完全相等”的绑定版本。

它表示:

  1. 低于这个固件版本,当前 bundle 语义可能无法被正确理解
  2. 高于或等于这个版本,可以认为运行时至少理解这份 bundle 契约

它不表示:

  1. 这个 bundle 只能给某一个固件版本使用
  2. 每次小翻译更新都必须改 min_firmware_version

5.3 supported_memory_profiles 的语义

supported_memory_profiles 描述的是设备能力兼容边界,不是语言偏好。

它影响:

  1. Extensions 页展示是否兼容
  2. 某设备是否应被允许安装/更新该 bundle

它不能被拿来表达:

  1. 用户喜欢哪种语言
  2. 当前 locale 是否被激活

5.4 更新判定

当前“是否有更新”以 package record 为单位判定,至少比较:

  1. version
  2. archive SHA-256

因此,哪怕 version 不变,只要 archive 内容变了,也可能被视为不同包。

这意味着偷偷改 zip 内容而不更新包版本,是危险的发布行为。

5.5 兼容性 gating 的归属

兼容性 gating 当前属于“安装/发现层”的职责,主要体现在 catalog 解析和 Extensions UI。

但运行时仍必须对手工拷贝到 Flash/SD 的 payload 保持鲁棒:

  1. 不兼容 pack 可以被发现
  2. 依赖不满足时可以跳过
  3. 系统仍需回退到 English 基线

禁止把系统安全性完全建立在“用户一定只会通过 Extensions 正规安装”这个假设上。

5.6 语言包版本必须表达用户可见变化

语言包版本不是装饰字段。只要用户安装后看到的文字、可用 locale、可用 IME、字体覆盖或质量状态发生变化,就必须 bump package version。

最低要求:

  1. 只修正少量错别字或补少量漏翻
    • bump patch,例如 1.0.0 -> 1.0.1
  2. 大量补齐字符串、改写地区用语、改变 translation_status、移除假 IME、改变默认输入法依赖
    • bump minor,例如 1.0.0 -> 1.1.0
  3. 改变 payload 布局或 manifest 语义,导致旧运行时可能错误理解
    • bump min_firmware_version,并视情况 bump minor 或 major。

发布语言包更新时必须同时重建:

  1. zip archive。
  2. site/data/packs.json
  3. archive SHA-256。
  4. installed/update comparison 所需的 package metadata。

否则用户看不到更新,或者看到更新却无法解释更新内容。


6. Change Classification

6.1 固件改动但通常不要求语言包跟随变化

以下改动通常不要求语言包更新:

  1. 纯逻辑修复,不引入新的用户可见文本
  2. 不改变 manifest schema 的运行时重构
  3. 不改变翻译 key 的内部实现优化
  4. 不改变字体需求的布局调整

6.2 固件改动后通常要求更新语言包字符串

以下改动通常要求至少更新一个或多个 locale pack

  1. 新增用户可见英文源串
  2. 删除旧源串并引入新源串
  3. 修改旧英文源串文本
  4. 修改 format string 参数结构
  5. 修改带转义字符的字符串结构,例如 \n

6.3 固件改动后可能要求更新字体包

以下改动不仅可能改 strings.tsv,还可能要求重建 font pack

  1. 新翻译文本引入当前字体未覆盖的新字形
  2. native_name 改动引入新字形
  3. 新 IME 字典或候选字集引入新字形
  4. 新增 locale/self-name/设置项文本导致 charset 扩展

一旦字形覆盖变化,至少要同步处理:

  1. charset.txt
  2. ranges.txt
  3. font.bin
  4. estimated_ram_bytes

6.4 固件改动后必须提高 min_firmware_version

以下改动属于 bundle 契约变化,必须考虑提高 min_firmware_version

  1. manifest schema 新增运行时必需字段
  2. 现有 manifest 字段语义改变
  3. runtime 对 bundle 布局的期望发生变化
  4. catalog / archive / installed index 语义发生不兼容变化
  5. locale/font/IME 依赖规则发生不兼容变化

6.5 只改语言包也必须 bump package version

以下情况即使不改固件,也应 bump package version

  1. strings.tsv 变化
  2. manifest.ini 变化
  3. ranges.txt 变化
  4. font.bin 变化
  5. package.ini 中影响安装或兼容性的字段变化

否则 installed index 与 update check 将失去解释力。


7. Release Obligations

7.1 固件改动合入前必须回答的问题

  1. 这次是否新增了用户可见英文源串?
  2. 这次是否改动了现有英文 key
  3. 这次是否改变了 format string 结构?
  4. 这次是否引入了新字形需求?
  5. 这次是否改变了 manifest / package / catalog 契约?

只要上述任一答案为“是”,就不能只提交固件代码而不审视语言包。

7.2 语言包发布前必须回答的问题

  1. package version 是否已反映 payload 变化?
  2. 如有契约变化,min_firmware_version 是否已提高?
  3. font.bin 是否与 charset.txt / ranges.txt / manifest 保持一致?
  4. estimated_ram_bytes 是否仍真实反映生成结果?
  5. site/data/packs.json 与 zip 产物是否已重建?
  6. translation_status 是否符合真实质量?
  7. release 包是否已经通过空值、缺 key、重复 key、占位符一致性检查?
  8. release 包是否已经清除英文补偿项?
  9. 该 locale 的地区用语是否经过明确审查?
  10. 该 locale 的 IME 依赖是否真实可用,而不是占位后端?

7.3 新 locale 合入前必须满足的最小条件

  1. English 基线不被破坏。
  2. locale manifest 依赖完整。
  3. 对应 font pack 的 RAM 成本可被当前目标 memory profile 解释。
  4. 运行时目录布局、分发包布局、catalog 元数据三者一致。
  5. 手工安装与 Extensions 安装都不会把系统带离 English 回退能力。
  6. 默认 translation_status 不得伪装成 release;未审校语言必须先进入 reviewdraft
  7. 输入法要么真实可用,要么不写进 locale manifest。
  8. 如果 locale 是地区敏感语言,例如繁体中文或葡萄牙语,必须明确地区语境。

8. Prohibited Implementations

以下实现方式在本规格下明确禁止:

  1. 继续引入新的整数语言枚举作为主持久化键。
  2. 页面直接绕过 ui::i18n 写死另一套翻译逻辑。
  3. 通过“检测到非 ASCII 就随便切某个字体”来替代 locale/font chain。
  4. 把 zip 当运行时 pack 根目录。
  5. 改了英文源串却不把它当成 key 变化处理。
  6. 改了翻译文本所需字形,却不重建相关字体包。
  7. 改了 bundle payload,却不 bump package version。
  8. 改了 bundle 契约,却不调整 min_firmware_version
  9. 让不兼容或损坏的 pack 破坏 English 最小基线。
  10. 用机器翻译或繁简转换结果直接发布 release 包。
  11. 在没有真实后端时发布 IME manifest 或 locale ime_pack 依赖。
  12. 给台湾繁中默认启用拼音输入法。
  13. release 语言包中保留大面积英文补偿项。

9. Relationship To Other Documents

本文件是规范性文档。

如果与其他文档冲突,优先级如下:

  1. docs/LOCALIZATION_SPEC.md
  2. docs/specs/LOCALE_PACK_RELEASE_SPEC.md
  3. docs/LOCALE_PACKS.md
  4. packs/<bundle-id>/README.md
  5. docs/ui_localization_plan.md

其中:

  1. docs/LOCALE_PACKS.md
    • 负责解释 pack 机制、布局和字段
  2. docs/specs/LOCALE_PACK_RELEASE_SPEC.md
    • 负责解释打包、发布、版本、catalog、archive 与更新可见性
  3. docs/ui_localization_plan.md
    • 仅保留历史演进价值,不再作为当前设计依据

10. One-Sentence Baseline

Trail Mate 的本地化不是“固件里几条翻译表”,而是“以 English key 为稳定接口、以 locale/font/IME pack 为显式依赖、以安装层与运行时层分离为前提、并允许固件与语言包独立演进但受兼容契约约束”的完整系统。