23 KiB
Locale、Font 与 IME Pack
本文档解释 pack 机制与打包细节。
整个本地化系统的规范性规格现在位于
docs/specs/LOCALIZATION_SPEC.md。
如果两份文档之间存在冲突,以 LOCALIZATION_SPEC.md 为准。
语言包打包、发布、版本、archive 与 catalog 更新规则见
docs/specs/LOCALE_PACK_RELEASE_SPEC.md。
目标
pack 系统同时为了解决四个问题而存在:
- 让固件镜像保持尽可能小。English 保持内建,大体积脚本资源移出固件镜像,进入外部 pack 存储。
- 明确区分
installed与loaded。一个 pack 出现在 Flash 或 SD 上,并不意味着它的字体已经加载进 RAM。 - 让混合语言内容正确渲染。即使 UI 是西班牙语,只要对应字体 pack 已安装,系统仍应能显示中文联系人名。
- 给 RAM 使用量设定边界,适配差异很大的设备,包括无 PSRAM 目标。
这已经是一套一等公民的运行时架构,而不是若干基于文本的旁路拼接。
核心模型
Trail Mate 把本地化明确建模为三类 pack:
Locale Pack拥有翻译后的 UI 字符串,并声明它依赖的 UI font pack、content font pack,以及可选的 IME pack。Font Pack拥有字形覆盖元数据以及外部font.bin资源。IME Pack声明输入行为,例如简体中文拼音,或直接提交目标脚本字符的键盘布局。 IME pack 只有在固件里已经有真实后端时才允许进入 runtime payload。
Settings 页选择的是 Locale Pack,而不是直接选择字体或 IME。
内建与外部
固件有意只携带最小内建基线:
- 内建 locale pack:
en - 内建 font pack:
builtin-latin-ui - 内建文本候选插入能力:
Symbols与精选Emoji - 内建文本候选内容字体基线:
builtin-symbol-core与builtin-emoji-core
因此,即使没有任何外部语言包存在,English、基础特殊符号输入和精选 emoji 输入也始终可用。
运行时 payload 可以从外部 pack 根目录中被发现,例如 SD,或者设备上的 Flash 安装存储。 具体运行时根目录由当前固件实现定义;已安装布局保持为:
/trailmate/packs/fonts/<font-pack-id>/manifest.ini
/trailmate/packs/locales/<locale-id>/manifest.ini
/trailmate/packs/ime/<ime-pack-id>/manifest.ini
启动时,registry 分两阶段建立:
- 编目所有内建 pack 以及所有外部 pack manifest。
- 解析依赖,并根据当前 memory profile 决定哪些 locale 被允许使用。
重要说明:发现 manifest 的成本很低。编目阶段不会把外部字体加载进 RAM。
三种布局
同一份本地化资源现在同时存在三种表示形式,刻意把它们分开是设计要求的一部分:
仓库源码 Bundle
Git 中 packs/<bundle-id>/ 下的内容。
- 包含运行时 manifest
- 包含面向人的 package 元数据
- 包含仅构建期使用的文件,例如
charset.txt与build.ini - 可能不跟踪
font.bin,因为 Pages 构建可以重新生成它
CJK 标点基线
CJK 字体 pack 必须把常用中文/全角标点当作 pack 资源处理,而不是在固件、渲染器或消息协议层补旁路。
规则如下:
- 共享标点源只有一处:
packs/common/cjk-punctuation.txt。 - CJK 主字体 pack 的
build.ini必须通过extra_chars_file=packs/common/cjk-punctuation.txt合入这份资源。 - 已有完整
charset.txt的 CJK 主字体 pack 必须同时设置seed_charset_file=charset.txt,防止只因增加 extra chars 而把原字体集缩小成“翻译文本 + 标点”。 charset.txt与ranges.txt必须同时覆盖共享标点源里的所有 codepoint。tools/validate_locale_packs.py是这条规则的发布前护栏,CI 与 Pages 构建都会运行它。
这条规则只定义字体资源覆盖范围,不改变文本内容、消息协议、BLE/MQTT 行为,也不允许 UI 层用字符替换来掩盖 pack 缺字。
已安装运行时布局
固件真正扫描的已安装运行时布局:
/trailmate/packs/fonts/<font-pack-id>/...
/trailmate/packs/locales/<locale-id>/...
/trailmate/packs/ime/<ime-pack-id>/...
这是运行时 registry 唯一理解的布局。
分发包
网站以及未来 Extensions 页面应该分发和消费的对象:
- 每个可安装 bundle 一个 zip
- 一个带版本与兼容性元数据的 package manifest
- 一个用于 UI 展示的描述文件
- 一个用于发现与更新检查的远程 catalog 条目
运行时不会直接扫描 zip。package manager 层负责下载 zip,把其中 payload 解开到已安装运行时布局,然后要求运行时 registry 刷新。
Installed 不等于 Loaded
这是整个设计的中心区分。
Installedmanifest 存在于 SD 上,registry 知道这个 pack 存在。Loaded外部font.bin已经真正传给lv_binfont_create(),现在开始消耗运行时 RAM。
运行时的行为如下:
- 从
settings/display_locale解析当前活动 locale。 - 当该 locale 被激活时,立即加载活动 UI font pack。
- 活动 content font pack 采用惰性加载,只在 content-scope 文本真正需要时才加载。
- ESP 上,如果活动 locale 显式声明
preferred_content_supplement_packs,registry 可以在 locale 激活阶段按 supplement 预算预加载这些已编目的 content supplement。 - 如果当前文本包含活动 content chain 尚未覆盖的 codepoint,则惰性加载额外的 content supplement pack。
- 切换 locale 时,会卸载所有运行时已加载的外部字体,并从头重建整条链。
这意味着,一个设备可以安装很多 pack,但任意时刻真正驻留在 RAM 中的只会是 active locale 需要的 UI/content 字体,以及当前 memory profile 允许的少量 content supplement。
UI Scope 与 Content Scope
运行时有意维持两条不同的 fallback chain。
UI Chain
用于静态应用 chrome:
- 菜单标签
- 设置页标签
- 标题
- 按钮
- 其他翻译后的界面文本
链路如下:
screen-selected Latin base font -> active UI font pack
Content Chain
用于用户生成或外部接收的文本:
- 聊天发送者行
- 聊天预览与正文
- 联系人名
- 队伍成员名
- 节点名与描述
- 选择器中显示的 locale 名称
链路如下:
screen-selected Latin base font
-> active content font pack
-> active UI font pack
-> registry-preloaded or lazily loaded content supplement packs
这就是为什么在非中文 UI 下,只要安装了相应 pack,系统仍然能正确显示中文内容。
Memory Profile
pack 的可用性由共享运行时代码中的 board-specific memory profile 约束。
当前 profile 如下:
constrainedlocale 字体预算128 KiB,禁用 content supplement,解码后的地图 cache 为2张 tile,页面退出后不保留 cache。standardlocale 字体预算768 KiB,content supplement 预算640 KiB,最多2个 supplement pack,解码后的地图 cache 为12张 tile,页面退出后不保留 cache。extendedlocale 字体预算2 MiB,content supplement 预算2 MiB,最多3个 supplement pack,解码后的地图 cache 为12张 tile,页面退出后保留 cache。
当前板级映射:
extended:Tab5、T-Display P4standard:T-Deck、T-Deck Proconstrained:其余所有设备,包括无 PSRAM 和 pager 级目标
locale 预算会基于当前活动 locale 的真实成本进行检查:
unique(UI font pack, content font pack)
supplement 预算与之分离,只作用于后续为混合脚本内容额外拉入的 content pack。
持久化
活动 locale 存储为:
- namespace:
settings - key:
display_locale
旧版 ESP 安装使用的整型 display_language key,会被一次性迁移到新的字符串 key。迁移完成后,旧 key 会被移除。
Manifest Schema
manifest 使用普通的 key=value 文件格式。
Font Pack Manifest
kind=font
id=zh-hans-core
display_name=Simplified Chinese Core
usage=both
estimated_ram_bytes=217456
source=binfont
file=font.bin
ranges=ranges.txt
字段说明:
id稳定的 pack 标识符。display_name用于诊断的人类可读名称。usage取值为ui、content或both。estimated_ram_bytes使用 LVGL binfont loader 加载后的预期运行时 RAM 成本。该值用于 profile 决策与 supplement 规划。source当前外部文件使用binfont,编译进固件的字体别名使用builtin。filefont.bin的相对路径,相对于该 pack 目录。ranges覆盖元数据文件的相对路径,相对于该 pack 目录。它用于 codepoint 规划,而不是直接用于渲染。
如果 estimated_ram_bytes 缺失或为 0,运行时会把该 pack 视为“成本未知”,因而无法准确做预算。仓库中的 pack 应始终提供此字段。
IME Pack Manifest
kind=ime
id=zh-hans-pinyin
display_name=Pinyin
backend=builtin-pinyin
直接键盘布局使用同一类 IME manifest,但必须把后端与布局分开声明:
kind=ime
id=ru-cyrillic-keyboard
display_name=Russian Cyrillic Keyboard
backend=builtin-keyboard-layout
layout=ru-cyrillic
目前可启用的输入法实现是:
zh-hans-pinyin,后端为builtin-pinyinru-cyrillic-keyboard,后端为builtin-keyboard-layout,布局为ru-cyrillic
backend 的真实实现位于固件代码中。IME pack manifest 是把它注册进运行时并暴露出来的那一层。
layout 是直接键盘布局后端的目标布局名;转换型后端例如拼音可以不声明 layout。
builtin-keyboard-layout 必须通过 layout 指向一个固件已知的 keyboard layout descriptor。descriptor 拥有触摸键盘 map、按键标签字体 probe 和模式标签;IME pack 只引用 descriptor,不复制这些展示资源。
硬规则:
- 只有已经有真实输入引擎和正确候选词表的 IME 才能出现在 runtime payload 中。
- Symbol / Emoji 不允许作为 IME pack 发布。它们由固件内置
TextCandidatePicker负责,入口是文本 toolbar 中与 IME 切换按钮并列的Sym/Emoji按钮。 - IME 模式按钮只负责切换
EN/IM/123;不得打开 Symbol / Emoji 候选页。 - 直接键盘布局不得把具体字符表、字体 probe 或 layout id 判断写进
ImeWidget。新增布局时只能扩展 keyboard layout descriptor registry。 - 未实现的假名、韩文、阿拉伯、繁中注音/仓颉和泛 Latin 软键盘,不得用
backend=builtin-*提前占位。 - 西里尔输入只有
ru-cyrillic-keyboard当前可发布;它是直接键盘布局,不是候选词转换引擎。 - 台湾繁中不得默认拼音。未来
zh-Hant-TW的首选输入法应是注音,仓颉/速成可以作为可选扩展。 - 欧洲 Latin 语言通常不需要 IME pack;沿用普通
EN/123输入路径即可。 - locale manifest 只有在真实 IME 可用时才写
ime_pack。display-only locale 不写假依赖。
Locale Pack Manifest
kind=locale
id=zh-Hans
display_name=Simplified Chinese
native_name=简体中文
translation_status=release
ui_font_pack=zh-hans-cjk
content_font_pack=zh-hans-cjk
ime_pack=zh-hans-pinyin
strings=strings.tsv
一个带分层中文覆盖的示例:
kind=locale
id=zh-Hans
display_name=Simplified Chinese
native_name=简体中文
translation_status=release
ui_font_pack=zh-hans-core
content_font_pack=zh-hans-core
preferred_content_supplement_packs=zh-hans-ext
ime_pack=zh-hans-pinyin
strings=strings.tsv
字段说明:
id用于持久化和日志的 locale 标识符。新 locale 应优先使用能表达地区语境的 BCP-47 形态,例如zh-Hant-TW、pt-PT。display_name面向英文语境的名称。native_name在选择器中显示的本语言自称。ui_font_pack该 locale 用于界面 chrome 的 font pack。content_font_pack该 locale 在内容表面优先使用的 font pack。preferred_content_supplement_packs可选的逗号分隔 content supplement font pack 列表;当这个 locale 遇到缺字时,优先尝试它们。ime_pack可选 IME 依赖。stringslocale TSV 文件的相对路径,相对于该 pack 目录。translation_status可选质量闸门。省略时为了兼容旧包暂视为可发布;新包必须显式写出。取值为:release:运行时可选。review:字符串表结构完整但还未通过母语/地区审校,运行时跳过。draft:未完成或可能混入错误语言,运行时跳过。- 其他未知值:运行时按不可发布处理并跳过。
如果省略 content_font_pack,默认回退为 ui_font_pack。
如果省略 ui_font_pack,默认回退为 builtin-latin-ui。
String Table 格式
locale 字符串以 TSV 存储:
English source string<TAB>Localized string
支持的转义包括:
\\n\\t\\r\\\\
示例:
Settings Paramètres
Send this code and compare:\\n Envoyez ce code et comparez:\\n
代码中的稳定查找 key 始终是 English 源字符串。
String Table 质量要求
release 级 strings.tsv 必须满足:
- 与当前基准 key 集合一一对应,不缺 key。
- 不存在重复 key。
- 不存在空翻译。
%s、%u、%ld、%.3f、\\n、\\t等占位符和转义符必须与 English key 一致。- 不允许把 English key 原样复制到翻译列作为补偿。
- 不允许混入其他语言的句子。
- 地区敏感语言必须符合地区用词习惯。
允许保留英文的只有明确专名、协议名、单位、测量标签或格式骨架,例如 GPS、HDOP、RSSI、dBm、RNode、LXMF、ID: !%08lX。
review 包也应尽量满足上述结构校验,但可以因为需要母语审校而暂不进入运行时语言列表。
失败行为
如果某个 locale pack 无法被使用:
- 缺少依赖的 font pack:跳过该 locale
- 缺少依赖的 IME pack:跳过该 locale
translation_status不是release:跳过该 locale- 活动 locale 的字体成本超出当前 memory profile:跳过该 locale
- 持久化的 locale id 不再能解析:运行时回退到
en
不会仅仅因为可移除 pack 当前缺席,就把持久化的 display_locale 值擦掉。如果该 pack 之后重新出现,这个 locale 会再次变得可选。
如果某个 content supplement 无法加载,UI 仍然要继续存活。只有那段具体文本可能显示为缺字,但活动 locale 不会因此改变。
仓库 Bundle
仓库当前提供的源码 bundle 位于:
packs/arpacks/europe-cyrillic-extpacks/europe-latin-extpacks/zh-Hanspacks/zh-Hantpacks/japacks/ko
每个源码 bundle 包含:
package.iniDESCRIPTION.txtREADME.md中的生成说明- 位于
fonts/、locales/与可选ime/下的运行时 payload 树 - 每个外部 font-pack 目录中的
build.ini与charset.txt - 供运行时覆盖规划使用的
ranges.txt
font.bin 有意不作为 source-of-truth 布局的一部分。如果本地已经存在,就直接使用;如果缺失,Pages pack 构建会在生成 zip 之前,基于 bundle 内的 charset.txt 与 build.ini 元数据重新生成它。
Package Manifest
每个源码 bundle 现在都带有 bundle-level 的 package.ini。
示例:
kind=package
id=zh-Hans
package_type=locale-bundle
version=1.0.0
display_name=Simplified Chinese
summary=Full Simplified Chinese locale bundle with external CJK font coverage and the built-in Pinyin IME backend.
description=DESCRIPTION.txt
readme=README.md
author=Trail Mate
homepage=https://vicliu624.github.io/trail-mate/
min_firmware_version=0.1.18-alpha
supported_memory_profiles=standard,extended
tags=language,cjk,chinese,ime
字段说明:
id稳定 package 标识符,供远程 catalog 与未来的 installed-index 文件使用。package_type高层级 package 角色。合法值包括:locale-bundle:至少提供一个 locale,可能同时提供 font/IME。content-bundle:提供内容字体、supplement 或内容相关 IME,但不声明 locale。input-bundle:主要提供 IME runtime payload,可选依赖已有字体。 历史空值按locale-bundle兼容处理。
versionpackage 版本号,独立于固件 tag。字符串、manifest、字体、质量状态或 IME 依赖发生用户可见变化时必须 bump。display_nameExtensions UI 中给用户看的 package 名称。summary用于列表视图的一行简述。description相对于 bundle 根目录的纯文本描述文件。readme相对于 bundle 根目录的面向开发者文档文件。authorpackage 发布者字符串。homepage项目或 package 首页。min_firmware_version正确理解该 bundle 契约所要求的最小固件版本。新增运行时必须理解的 manifest 语义,例如非 releasetranslation_status跳过规则时,必须提高该下界。supported_memory_profiles声明式兼容性提示,可取constrained、standard与/或extended。tags供未来 Extensions 浏览器搜索与分组的元数据。
版本 bump 约定:
- 补少量漏翻或错别字:patch。
- 大量补齐翻译、调整地区用语、改变质量状态、移除假 IME:minor。
- 改变 payload 布局或旧固件会误解释的 manifest 语义:提高
min_firmware_version,并按影响 bump minor/major。
Font 构建元数据
每个外部 font-pack 目录还可以声明一个仅供工具链使用的 build.ini。
示例:
font=tools/fonts/NotoSansCJKsc-Regular.otf
size=16
bpp=2
no_compress=true
字段说明:
font仓库中的源字体文件。size传给 binfont generator 的像素尺寸。bpp生成字体的 bits-per-pixel。no_compress是否让生成的font.bin禁用 lv_font_conv 的 RLE 压缩。
这个文件只属于构建期元数据。运行时永远不会读取它。
分发 Archive 布局
GitHub Pages 构建现在会在以下路径下为每个 bundle 生成一个 zip:
site/assets/packs/<package-id>-<version>.zip
每个 archive 包含:
package.ini
DESCRIPTION.txt
README.md
payload/fonts/<font-pack-id>/...
payload/locales/<locale-id>/...
payload/ime/<ime-pack-id>/...
payload/ 内只包含可安装的运行时文件。像 charset.txt、build.ini 和 .gitignore 这类仅构建期文件都会被排除在 archive 之外。
远程 Catalog
Pages 构建还会生成:
site/data/packs.json
这个 catalog 是未来 Extensions UI 的发现面。每个条目包含:
- package id、version、display name、summary 和长描述
min_firmware_version、supported memory profiles 之类的兼容性提示- bundle 提供的 locale/font/IME 记录列表
- 用于下载与更新检查的 archive 路径、大小和 SHA-256
- 用于安装前规划的预计运行时字体 RAM 总量
这个 catalog 被有意设计在 runtime registry 之上。它描述的是“可下载 bundle”,而不是“当前已经加载的字体”。
已安装 Package 索引
runtime registry 仍然直接从 /trailmate/packs 对已解包资源进行编目。
对于 package 管理,对应的 installer 层还应额外维护:
/trailmate/packs/.index/installed.json
该文件应记录:
- 已安装的 package id
- 已安装的 package version
- 安装时间
- 来源 archive 的 SHA-256
未来的更新提示应把这个 installed index 与 site/data/packs.json 对比,而不是靠目录名猜测。
当前 IME 覆盖
目前仓库 bundle 中只允许声明真实 locale IME:
- locale bundle 可以声明专用于该 locale 的 IME。
- input bundle 可以声明未来新增的真实输入引擎,但必须已经有固件后端支持。
具体约束:
- 欧洲 Latin locale 不应为了“有输入法列表项”而挂虚假的
builtin-latin。 - Symbol / Emoji 是固件内置文本候选插入能力,不属于任何 locale,也不属于任何 pack。
builtin-symbol-core/builtin-emoji-core只覆盖固件内置的 100 个 Symbol / 100 个 Emoji 候选,属于 content font baseline,不占用 external content supplement 数量预算。symbol-picker、emoji-picker、builtin-candidate-picker与candidates.txt都是旧候选列表 IME 模型,不得作为新的 runtime payload 发布。
新增内容 / 输入扩展
内容或输入扩展可以没有 locale,但必须仍然遵守 runtime payload 分层:
- source bundle 位于
packs/<bundle-id>/。 package.ini使用package_type=content-bundle或package_type=input-bundle。- 字体 payload 仍放在
fonts/<font-pack-id>/。 - IME payload 仍放在
ime/<ime-pack-id>/。 README.md必须说明它不是 locale,以及用户如何启用它。supported_memory_profiles必须按 font RAM 与 supplement 策略声明。- 如果它需要新的 runtime IME 后端,必须提高
min_firmware_version。
当前不再提供 emoji 扩展参考实现。精选 Symbol / Emoji 候选与 builtin-symbol-core / builtin-emoji-core 字体由固件内置,数量上限由 LOCALIZATION_SPEC.md 的内置文本候选插入面约束。
新增一种语言
- 先决定这个 locale 能否复用已有 font pack,还是需要新建一个。
- 决定 locale id 是否需要地区后缀,例如
zh-Hant-TW、pt-PT。 - 新增源码 bundle:
packs/<bundle-id>/,并写好package.ini、DESCRIPTION.txt与README.md。 - 为该 bundle 中的每个外部 font pack 生成
charset.txt与ranges.txt。 - 为每个生成型 font pack 写好
build.ini。 - 写运行时 font manifest,包括
usage、estimated_ram_bytes和ranges。 - 写 locale pack manifest,包括
ui_font_pack、content_font_pack、translation_status。 - 初始语言包应先使用
translation_status=review或draft,母语/地区审校完成后才能改成release。 - 只有在真实输入引擎已经实现时,才添加
ime_pack。 - 校验
strings.tsv:缺 key、重复 key、空翻译、占位符不一致、英文补偿项。 - bump
package.ini中的version,必要时提高min_firmware_version。 - 运行
python scripts/build_pack_repository.py --pack-root packs --site-root site。 - 可以手工把 runtime payload 拷贝到 SD 安装,也可以通过 Pages 产出的 zip 提供给未来的 Extensions installer。
- 只有 release locale 会在 Settings 中可选。
设计规则
这套架构有意避免以下做法:
- 使用整数语言枚举
- 开机时急切加载所有已安装字体
- 用一个全局的“只要非 ASCII 就切换字体”快捷规则
- 把 UI chrome 文本与用户内容文本混为一谈
- 让不同平台上的本地化实现逐渐漂离
- 混淆运行时 manifest 与 package 分发元数据
- 让 runtime registry 去理解远程 catalog 或 zip archive
- 用机器翻译、繁简转换或英文补偿伪装 release 包
- 在没有真实输入法后端时发布假 IME
依赖图保持显式:
Locale Pack -> UI Font Pack
Locale Pack -> Content Font Pack
Locale Pack -> IME Pack
Content text -> Optional supplement font packs
这样既让代码更容易推理,也让 Trail Mate 在不强迫所有设备承担同样固件体积与 RAM 成本的前提下,拥有扩展到更广泛语言支持的路径。