34 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 原样复制到翻译列,当成合法的缺省翻译。
- 把 Symbol / Emoji 文本候选插入面伪装成 IME pack 或 Extensions pack。
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 显式依赖它。
直接键盘类后端必须再拆一层 KeyboardLayoutDescriptor。IME backend 只说明“这是直接提交字符的键盘输入策略”,layout descriptor 才拥有:
- 触摸键盘按键表。
- 触摸键盘标签需要的字体覆盖探针。
- 布局 id 与用户可见输入模式标签。
因此,共享 ImeWidget 不允许按具体 IME id 写展示分支;它只能通过 strategy/descriptor 询问当前输入模式是否有触摸键盘、需要什么字体探针。西里尔、希腊、希伯来、假名等未来布局都必须新增 layout descriptor,而不是继续把字符表写进 widget。
Symbol / Emoji 不属于 IME pack。它们不是 locale 输入习惯,也不是可启用输入法后端,而是内置文本候选插入面。IME 模式按钮只负责切换 EN / IM / 123 等输入模式,不得打开 Symbol / Emoji 候选页。
classDiagram
class ImeWidget {
+setMode(mode)
+handle_key(event)
+refresh_labels()
}
class InputModeDescriptor {
+kind
+ime_id
+display_name
+keyboard_layout
}
class KeyboardLayoutDescriptor {
+layout_id
+mode_label
+touch_hint_key
+touch_map
+font_probe_text
}
class ImePackInfo {
+id
+backend
+layout
}
class InputModeAdapter {
+describe(ime)
}
ImeWidget --> InputModeDescriptor
InputModeDescriptor --> KeyboardLayoutDescriptor
InputModeAdapter --> ImePackInfo
InputModeAdapter --> InputModeDescriptor
translation_status 仍然独立决定 locale 是否进入普通用户语言选择器。
review locale 可以依赖真实 IME 以便运行时完整校验和安装该能力,但不能因此被当成 release UI locale。
因此,不允许用 backend=builtin-kana、backend=builtin-hangul、backend=builtin-arabic 这类尚未实现的名字提前占位。未实现输入法可以写进规划文档,但不能作为 runtime IME pack 进入 payload。
3.9 内置文本候选插入面
内置文本候选插入面负责给用户插入“设备键盘难以直接输入、但又不属于 locale 输入法”的短文本符号。
当前只有两组内置候选:
Symbols- 最多 100 个特殊字符。
- 面向 Wi-Fi 密码、频道名、PSK、聊天文本等通用文本输入。
- 需要一个小型内置 content baseline font:
builtin-symbol-core,只覆盖当前 100 个 Symbol 候选。
Emoji- 最多 100 个精选 emoji。
- 不是全量 emoji 表,也不是可下载扩展包。
- 需要一个小型内置 content baseline font:
builtin-emoji-core,只覆盖当前 100 个 Emoji 候选。
这两组候选的合法入口是文本输入 toolbar 中与 IME 切换按钮并列的 Sym / Emoji 按钮。Chat compose 与 Settings 文本弹窗必须接入同一个 TextCandidatePicker,不得各自实现私有候选窗口。
候选页交互规则固定为:
- 点击
Sym或Emoji打开全屏候选页。 - 候选页最多加载 100 项;打开时一次性创建全量候选,不分页,候选数组顺序就是展示顺序。
- 候选页头部提示硬件键盘可用的选择动作:
WASD移动当前候选,Q关闭,E提交当前候选。 - 触摸点击候选项时直接提交并关闭候选页。
- 提交动作只向目标 textarea 插入 UTF-8 文本并触发 value changed,不改变当前 IME 模式。
- 页面不得绕过
TextCandidatePicker直接维护另一份 Symbol / Emoji 列表。
classDiagram
class ChatComposeScreen {
+textarea
+ime_toggle
+symbol_button
+emoji_button
}
class SettingsTextModal {
+textarea
+ime_toggle
+symbol_button
+emoji_button
}
class ImeWidget {
+switchMode()
+handle_key()
}
class TextCandidatePicker {
+open(textarea, set)
+commit(active)
+close()
}
class BuiltinTextCandidateData {
+symbols[<=100]
+emoji[<=100]
+symbol_core_binfont
+emoji_core_binfont
}
class FontRegistry {
+builtin-symbol-core
+builtin-emoji-core
}
ChatComposeScreen --> ImeWidget
SettingsTextModal --> ImeWidget
ChatComposeScreen --> TextCandidatePicker
SettingsTextModal --> TextCandidatePicker
TextCandidatePicker --> BuiltinTextCandidateData
FontRegistry --> BuiltinTextCandidateData
非法做法:
- 发布
symbol-picker/emoji-picker作为 IME pack。 - 发布或解析
candidates.txt来驱动通用候选列表 IME。 - 把 emoji 候选放回 Extensions catalog,要求用户下载后才能输入。
- 内置全量 emoji 表。
- 在 Wi-Fi、Chat、Settings 等页面各自维护不同候选集。
- 让候选页的打开行为依赖当前 locale 或当前 IME。
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 的语言、脚本和地区习惯。
当前可启用输入法只包括 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;现有
- Symbol / Emoji
- 它们是内置文本候选插入面,不是 IME pack。
如果某 locale 还没有真实输入法,它可以作为 display-only review 包存在,但不得把假 IME 写进 manifest,也不得让设置页显示一个无法工作的输入法选项。
4.3 Built-In Baseline
固件必须始终保留最小内建基线:
- 内建 locale:
en - 内建字体:
builtin-latin-ui - 内建文本候选插入能力:
Symbols与精选Emoji - 内建文本候选内容字体补充:
builtin-symbol-core与builtin-emoji-core
English、基础特殊字符输入与精选 emoji 输入必须在没有任何外部 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.5.1 Content Font Load Is Not A Render-Side Blocking Operation
内容文本缺字检测与外部字体加载是两个不同动作。
ensure_content_font_for_text()可以发现当前内容字体链缺少某些 codepoint。- 它不能在 ESP / display-shared SD-SPI 设备的页面渲染、列表构建、LVGL 事件或 timer 路径里同步加载外部
font.bin。 - ESP 上的内容补充字体加载必须被视为后台/显式动作;在后台 runtime 完成前,页面使用当前已加载字体链并记录缺字诊断。
- 固件内置字体只有在 runtime pack manifest 以
source=builtin显式声明时才算运行时 loaded 状态;ESP 不得隐式注册 CJK 内置字体来替代外部zh-hans-core/font.bin。 - 已加载字体是否覆盖某个 codepoint 必须以实际 glyph lookup 为准;manifest/range 只能用于选择候选 pack,不能替代渲染能力判断。
- 外部
font.bin加载失败后必须进入 backoff,不能因为多条联系人名、聊天消息或节点名重复触发同一个失败文件读取。 - 显式切换 locale 时加载 UI 字体属于 locale 激活流程;这不能被内容文本缺字路径复用成隐式 SD 读。
- 外部
source=binfont字体 pack 必须在 catalog 阶段验证font.bin路径可规范化且可打开;缺少 payload 的 locale 不能进入可选 locale 列表。 - 任何用户可见路径中被运行时明确允许的同步外部字体加载,必须先显示阻塞式 busy modal,加载完成或失败后关闭。这个规则不以字体包大小为条件;纯 deferred/backoff 路径只记录诊断,不显示“正在加载”的假窗口。
- “显示 busy modal” 的代码语义不是只创建 LVGL 对象,而是必须在进入
lv_binfont_create()/ 外部font.bin读取之前,强制把 modal flush 到屏幕。当前绑定点是resource_pack_registry.cpp的ScopedFontLoadOverlay,它是load_font_pack()的唯一同步字体加载 UI 边界。
这条规则的目标是保护 UI 实时域:联系人页、聊天页、地图 overlay、节点详情页等内容页面不得因为遇到中文/日文/韩文/阿拉伯文本而把 UI 线程拖入 SD 阻塞 IO。
Registry-time preferred content supplement preload
ESP 上允许一个非常窄的例外:如果当前 active locale 的 manifest 明确声明了
preferred_content_supplement_packs,registry 可以在 reload_language()、安装完成后的重新编目、
或 locale 激活阶段预加载这些已编目的 content supplement,并把它们加入 content font chain。
这个例外只用于 active locale 显式偏好的、已编目的 content supplement。是否允许加载由当前
memory profile 的 max_content_supplement_packs 与 max_content_supplement_ram_bytes 决定,
不能绕过 supplement 预算。ranges.txt / coverage metadata 只用于候选选择与诊断;如果 coverage
缺失或为空,已加载字体是否覆盖某个 codepoint 仍必须以实际 glyph lookup 为准。
当前固件内置的 builtin-symbol-core 与 builtin-emoji-core 属于 content font baseline,而不是用户安装的 external supplement;它们不读取 SD / Flash 外部 font.bin,只覆盖内置文本候选集,并且不占用 max_content_supplement_packs 名额。这个例外不适用于:
- 大型 locale 字体。
- locale 激活之外的 UI 字体替换。
- 任何需要反复探测缺失文件的失败路径。
- 页面渲染、列表构建、LVGL 事件或 timer 路径中的 SD-backed
font.bin同步读取。
因此,已安装到当前运行时 pack root、并被 active locale 显式偏好的内容扩展可以在 registry/locale
激活阶段进入可用字体链;之后聊天内容第一次遇到对应字符时,内容热路径只选择已经 loaded 的字体链,不再为了该字符直接读取 SD。如果 preferred supplement 没有被 catalog 到,运行时必须记录
not_cataloged 诊断;如果预算不足,必须记录 content_budget 诊断;两者都不得改变 active locale。
4.5.2 Text Candidate Built-In Font Boundary
Symbol / Emoji 支持不是 locale,也不是 Extensions 中的可下载包。
当前固件只内置两类文本候选字体:
Symbols候选集- 最多 100 个 UTF-8 symbol 字符串。
- 由
TextCandidatePicker展示和提交。 - 不通过 IME registry,也不通过 pack manifest。
builtin-symbol-core- 小型内置 content baseline font。
- 只覆盖当前 100 个 Symbol 候选集;不得携带用户不能输入的额外 symbol 字形。
Emoji候选集- 最多 100 个 UTF-8 emoji 字符串。
- 由
TextCandidatePicker展示和提交。 - 不通过 IME registry,也不通过 pack manifest。
builtin-emoji-core- 小型内置 content baseline font。
- 只覆盖当前 100 个精选 emoji 候选集;不得携带用户不能输入的额外 emoji 字形。
- 在 registry 阶段进入 content font chain,不从 SD / Flash 读取
font.bin。
- 与
builtin-symbol-core一样不计入 external content supplement 数量预算;中文、日文、韩文等外部内容字体仍必须能够占用自己的 supplement 名额。
合法依赖方向是:
flowchart LR
ChatText["Chat / content text"] --> FontResolver["Content font resolver"]
FontResolver --> SymbolFont["builtin-symbol-core"]
FontResolver --> EmojiFont["builtin-emoji-core"]
ChatCompose["Chat compose toolbar"] --> TextPicker["TextCandidatePicker"]
SettingsText["Settings text modal toolbar"] --> TextPicker
TextPicker --> EmojiData["Builtin emoji candidates <= 100"]
TextPicker --> SymbolData["Builtin symbol candidates <= 100"]
TextPicker --> TextInsert["UTF-8 text insertion"]
非法做法:
- 在聊天页面硬编码另一份 emoji 替换表。
- 把 emoji 声明成一个假 locale。
- 发布
emoji-pickerIME pack 或emoji-coreExtensions pack。 - 解析
candidates.txt来驱动 emoji 输入。 - 为了显示 emoji 而把所有
1Fxxx字符映射到同一个内置图标。 - 在 UI 热路径同步读取 SD 上的 emoji font。
- 内置全量 emoji 表。
- 在 Wi-Fi、Chat 或 Settings 页面各自实现符号/emoji picker。
数量边界:
- 固件最多内置 100 个 Symbol 候选与 100 个 Emoji 候选。
- 对应内置字体只能覆盖这两组候选;候选集缩小后字体也必须同步缩小。
- 选择原则是高频、通用、跨语言语义稳定。
- 如果未来要扩展到全量 emoji 或大型符号表,必须另立新规格;不得悄悄扩大当前内置表。
Glyph 判定边界:
- Font registry 的 coverage 和 missing-glyph 判定必须忽略 Unicode variation selector(例如 U+FE0F)与 ZWJ(U+200D)。
- 这些 codepoint 只改变组合字符的显示形式,本身不要求字体提供独立 glyph。
TextCandidatePicker与 content font resolver 必须使用一致规则;否则 emoji / symbol 候选会显示通过,但聊天、地图或联系人内容仍被误判为缺字。
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 解到当前平台定义的 pack root。 运行时会同时编目当前支持的 Flash/SD 根,因此手工把 runtime payload 拷贝到任一受支持根仍然是合法安装方式。
禁止让运行时 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 为显式依赖、以安装层与运行时层分离为前提、并允许固件与语言包独立演进但受兼容契约约束”的完整系统。