· engineering· 约 25 分钟精读
端侧离线教育应用的本地化资源治理:QubitLab 内嵌字典的键位对等、逐字段回退与手工语言解析
一款不联网、不注册、没有服务端的教学应用,要同时面对八种语言的用户,文案从哪里来、缺译时怎么办、语言选择在什么优先级下生效?本文以密旋科技 QubitLab 的运行形态为样本,拆开端侧本地化的三条资源通道、四级回退链、四种语言输入源的解析次序,以及一个常被忽略的事实——界面的字翻译了,数字与格式还没有。
一、一款没有服务端的八语言应用,语言从哪里来
密旋科技的 QubitLab 是一款面向 App Store 的移动端量子物理与 STEM 启蒙沙盒:96 关闯关、布洛赫球面态矢演示、双比特逻辑门拼装与测量坍缩教学,全部在本机运行,不注册、不联网、没有服务端。2026 年 10 月的行业报道引用麦肯锡《量子技术监测报告》与中国信通院《量子计算发展态势研究报告(2026 年)》,提到全球量子计算企业数量已超过 400 家。对一款教学应用来说,这句话翻译成工程约束只有一句:它必须能被不说中文、也不说英语的孩子用起来。
于是问题变得很具体:这款应用要在包内自带八种语言,而它没有任何一台服务器可以用来下发文案、读取区域偏好或在发布后纠正一句错译。语言在这里不是运营配置,而是随包发布的只读资源,它必须同时满足三条约束——装到设备上立刻可用、切语言后当前界面立即重绘不需要重启、每一句文案在缺译时有确定且可预期的行为,而不是空白、崩溃或者一串看不懂的占位符。
本文不讨论翻译本身好不好,只拆开这台设备里的本地化资源链路:文案存在哪几个地方、缺译沿哪条路回退、语言选择按什么优先级生效、哪些部分其实还没有被本地化。
二、资源分三条通道,各自回答不同的语言问题
把整仓代码翻一遍会发现,本地化资源并不是一张大表,而是三条彼此独立的通道,它们的规模、载体和缺译行为都不一样。
通道一是界面字符串字典。 它是一张编译期常量表,键是带域前缀的字符串,值是各语言文案,八种语言各 190 个键,负责所有界面骨架:按钮、标题、卡片说明、量表刻度、结果提示。
通道二是关卡内容覆写。 96 个关卡的标题、副标题、任务简报、目标、物理事实、测验题干与解析,在各语言文件里逐字段覆写;英语不走这条通道,它就是关卡模型里的正典内容。
通道三是教师指南覆写。 八个章节的课堂讨论问题、常见误解、动手活动与家长陪伴问题,按章节整体覆写,八种语言齐全。
| 通道 | 代码载体 | 规模 | 英语是否在通道内 | 缺译时的行为 |
|---|---|---|---|---|
| 界面字符串字典 | L10n.translations 静态常量表 | 190 键 × 8 语言 = 1520 条 | 是,英语为母版 | 回落英语母版,再缺则输出键名本身 |
| 关卡内容覆写 | OdysseyL10n.content(forLevel:lang:) | 96 关 × 7 语言(韩语 64 关) | 否,英语为关卡正典 | 该关返回空,由调用点传入的英语原文兜底 |
| 教师指南覆写 | CurriculumGuideL10n.guide(forChapter:lang:) | 8 章 × 8 语言 | 是,英语为母版 | 整章回落英语指南 |
这张表里最值得注意的不是规模,而是第三条里那个“否”。关卡内容不复写英语,意味着英语不是这张覆写表的一行,而是被覆写表所指向的原始数据。这个选择决定了后面所有的回退语义:字典层的缺译是“去别处找”,覆写层的缺译是“什么都不给,让调用者拿原件”。这两件事看起来相似,处理方式却完全不同。
三、界面字典:190 个键位,逐语言完全对等
字典层最值得保留的性质是键位对等。把八种语言的键集合取出来做差集,结果是零:没有任何一种语言缺少某个键,也没有任何一种语言多出某个键。190 键在八种语言里是同一份“词表骨架”。
这件事的价值不在当下,而在维护期。只要键集合保持相等,任何一次新增文案都必须同时补齐八种语言,否则集合比对立刻不成立。它把一个主观问题(“这次改动要翻译几种语言”)变成了一个可以机器判断的客观问题:集合相等即对等。这不是严谨的翻译流程,但它是一道成本极低的护栏——尤其在没有人专职盯着本地化的时候,护栏比流程更管用。
键名本身也承担了一部分治理职责:全部键带域前缀,前缀划分了文案的归属页面,看键名就能知道这句文案属于哪一块界面,删除页面时也容易找到该清理的整段词条。
对等的代价同样明确:字典是编译期常量,编译进二进制。这意味着文案不能与包分离。 改一个错别字,也要走一次完整的发版与审核;没有任何办法在不更新应用的前提下修正一句话。反过来,它换来了两点确定性:查表不涉及文件读取和解析,缺文件、编码损坏、路径不一致这类问题在结构上不存在。对于一款宣称数据不出设备的应用,文案随包也是一种姿态——它不需要连接到任何地方去换取可读性。
四、覆写层:逐字段回退与英语正典
关卡内容覆写是三条通道里最容易出错的一条,因为它的回退不是一张表查到底,而是逐字段发生的。
覆写函数按语言取一张关卡字典,命中了就返回一条结构化的本地化内容;没有命中就返回空。调用方拿到的永远是“可能为空”的结果,因此每个调用点都要显式给出一份英语原文作为兜底:关卡标题、副标题、任务简报、目标、物理事实、测验题干、选项、解析,各有一行独立的回退表达式。回退是调用点的责任,不是中心的统一行为。 出题人写漏一处兜底,那一处就会显示空白。
这套结构还有一个二级回退。任务简报按学段分三档:小学、初中、高中。覆写内容里如果只有通用简报,没有分学段简报,那么小学与高中在取简报时会回落到通用那份;只有命中了对应学段,才用学段版本。于是同一关的简报最多有三层来源:本语言本学段、本语言通用、英语原文。
覆盖率因此成为一个可以数出来的数字。除韩语外,七种语言各有 96 关覆写,与关卡总数一致;韩语是 64 关。这不是缺陷,而是一个必须被承认的状态:有 32 关在韩语环境下会用英语文案展示。 关卡依然可玩,逻辑、物理参数、判分完全一致,只有文字换了语言。页面不会报错,也不会留空,但它确实是一屏混语内容。
| 语言 | 字典层键数 | 关卡覆写关数 | 教师指南章数 |
|---|---|---|---|
| 英语 | 190 | 正典(不参与覆写) | 8 |
| 简体中文 | 190 | 96 | 8 |
| 繁体中文 | 190 | 96 | 8 |
| 西班牙语 | 190 | 96 | 8 |
| 法语 | 190 | 96 | 8 |
| 德语 | 190 | 96 | 8 |
| 日语 | 190 | 96 | 8 |
| 韩语 | 190 | 64 | 8 |
这张表的读法是:字典层八种语言完全对等;覆写层只有韩语欠缺三成关卡;教师指南层八种语言齐全。把覆盖率写进文档、写进发布说明,比在界面上悄悄混语要诚实。对一款面向课堂的工具来说,教师有权知道哪一部分还没有翻译完。
五、语言选择不是设置项,而是一次带优先级的解析
语言切换在界面上只是一个列表,但在代码里,它是一次有明确优先级的解析,四种输入来源按次序争夺同一个结果。
最高优先级是启动参数:命令行里指定语言时,直接锁定该语言,并且不再跟随系统。其次是自动化测试标记:命中时强制英语输出,这样截图与测试用例的文字永远是确定的,不会因为跑测试的那台设备设置了什么语言而变。再次是用户的手动选择:用户点过某种语言后,选择写入本机偏好并持久化。最低优先级、也是默认状态,是跟随系统:读取系统偏好语言列表,取第一个能匹配上的。
四种来源共享一个结果变量,一旦它变化,界面语言即被同步,并触发一次触感反馈作为确认。整个链路的输入与产物如下。
| 输入来源 | 触发时机 | 是否持久化 | 生效后果 |
|---|---|---|---|
| 启动语言参数 | 进程启动时读取命令行 | 否 | 锁定该语言,跟随系统被关闭 |
| 自动化测试标记 | 进程启动时读取命令行 | 否 | 强制英语,保证截图与断言文字稳定 |
| 手动选择 | 用户在设置页点选 | 是,写入本机偏好 | 关闭跟随系统,切换为该语言 |
| 跟随系统 | 首次启动默认值 | 是,记录开关状态 | 依系统偏好语言匹配,无匹配则英语 |
这里有一个容易被忽略的细节:手动选择的持久化是“语言值 + 是否跟随系统”两条记录,而不是一条。用户先手动选了德语,之后又打开“跟随系统”,此时旧的语言值仍在偏好里躺着,但解析路径不会再读它;如果系统语言随后发生变化,跟随时会重新匹配并切换。两条记录分开存,才让“临时改一下”和“以后都跟着系统走”这两种意图可以被区分开。
同一台设备上还有第三个与语言相邻的开关:教师课堂演示模式,它和语言偏好一样写在本机偏好里,同属设备级状态而非账号级状态。应用没有账号,所有偏好就天然属于设备:换一台设备,语言会重新回到默认匹配。
六、跟随系统时,应用是自己在做语言标签匹配
八种语言要匹配操作系统的语言列表,而系统给出来的是一串标签,不是枚举值。代码没有使用系统的标签匹配工具,而是自己遍历偏好语言列表按前缀逐个判断:繁体中文由繁体标记或地区标记识别,简体中文由简体标记、地区标记或裸标记识别,其余语言按语言子标签前缀匹配;第一轮没有任何命中时,再读取系统当前语言对象做一次二级判断,从语言码与书写系统子标签两条线索里推断;最终仍无法判断,就严格回落到英语,不做二次猜测。
这是一条自定的解析策略,不是标准里的匹配算法。标准语言标签匹配考虑的是“标签能不能互相匹配”,而这里的判断是“这批用户实际会带来哪些标签形态”,属于面向真实分布的经验处理。
第一,区域变体被折叠。应用的目标语言只有八种,因此同一语言的不同区域变体共用一套文案:西班牙语的区域变体与欧洲西班牙语是同一份文字,英语也只有一个版本,不区分不同地区的拼写习惯。对教学应用来说,这通常是可接受的取舍;但它意味着“区域性差异”这件事在资源层根本没有位置。
第二,书写方向与复数形态完全没有被处理。八种目标语言全部从左向右书写,界面骨架也就没有做镜像布局;八种语言里没有一种需要多于两种复数形态的规则,因此文案里也没有复数规则的位置,数量变化靠固定句式表达。这是一个由目标语言集合决定的边界,而不是被忽略的问题——一旦要加入需要镜像布局或复杂复数形态的语言,改动量不在文案层,而在布局与资源结构两处。
七、字翻译了,格式与数字还没有
字典规模是一个容易被看高的指标。190 个键、1520 条译文看起来很整齐,但只要看一眼占位符,就会看到本地化的另一面。
接口文案里有三类占位符:整数、字符串、一位小数。三类都是按出现顺序替换的普通占位符,没有任何一处使用带序号的位置化写法。这带来一个直接后果:译文不能调整参数顺序。 对于参数位置与中文、英文语序差异较大的语言,译者只能通过改变句式来绕开,而不能把参数挪到句子里的任意位置。
更要紧的是数字。带小数位的参数走的是 C 语系的格式化路径,小数点是固定的句点,与用户所在区域无关。也就是说,在德语或西班牙语环境下,界面仍然会显示一位小数的点号写法,而不是这两种语言习惯的逗号。数值本身没错,读起来却不符合当地书写习惯。字符串类的日期占位符同理:占位符里的日期必须先被格式化成字符串,格式化器做得好不好,决定了这一句读起来对不对。
这里有一处做对了的例子,对照起来更清楚。结业证书的导出把日期格式化的区域设置显式绑定到当前所选语言,用的是系统提供的长日期样式,因此证书上的年月日写法会随语言改变。相比之下,每日挑战的标题是把一个固定模式的日期字符串与一个未经翻译的英文单词直接拼接起来的,日期部分随系统区域变化,后半句却始终是英文。同一份资源集合里,一个地方把区域格式化交给系统,另一个地方把它留给了硬编码。
星期缩写则是第四条通道:一张手写的映射表,直接给出七种语言的星期字符,未命中时回落到英文字母。它覆盖完整,读起来也没问题,但它是人工维护的常量表,而不是取自系统日历的区域数据:好处是不依赖运行时环境,代价是新增语言时要记得回来改这一处。
八、还有一条旁路:视图里的字面分支
理想状态下,所有界面文案都应该走字典。实际情况是,视图层里还存在 41 处直接比较当前语言的三元分支:简体走一个分支、繁体走一个分支、其余全部落到英文。
这些分支大多出现在需要同时处理多语言与格式的地方——例如把统计数字嵌进一句带冒号的说明,或者给一段文案配不同语言下的标点。用字面分支写起来最直接:不用去字典里加键,不用同步八种语言,就地就能出效果。但它构成了与字典并存的第二条本地化通道,而且这是一条覆盖不完整的通道:这 41 处只有三种语言的结果,其余五种语言全部读英文。
它的风险不在于功能,而在于一致性:同一个概念,字典里有一份译文,视图里又有一份写法,两处分别演化,久了就会出现同一界面两种措辞的现象。因此这 41 处更适合当作一个可测量的技术债指标——它可以用一次搜索得到确切数量,也就可以被逐处搬回字典,直到这个数字归零。
九、离线本地化换来了什么,又付出了什么
把这条链路合起来看,边界相当明确。
它能做到的:八种语言的界面骨架在同一份词表下保持键位对等;关卡与教师指南按内容逐条覆写,缺哪一条由可数的覆盖率说明;语言在四种输入来源之间有确定的优先级,切换即时生效且不触网;格式化的正确性在证书这类需要区域习惯的地方被显式处理过。所有这些都不需要一次网络请求。
它做不到的,至少有五件。第一,它不能在不发版的情况下修正任何一句文案,因为资源是编译进二进制的常量。第二,它不能让译文自由重排参数顺序,因为占位符没有位置编号。第三,它不处理复数形态与从右向左的书写方向,目标语言集合决定了这一点。第四,它不区分语言内部的区域变体,同一语言的所有地区共用一套文字。第五,它没有任何区域分布数据——语言偏好只写本机,不上报,因此也就无从知道八种语言各自有多少真实使用者。这是隐私优先的直接代价:不收集,就不知道。
最后还要补一句关于“能不能相信”的话。离线应用的资源治理没有服务端背书,也没有云端配置可以随时兜底,它的可信度完全建立在本地可核查的事实上:键集合是否相等、覆盖率是几成、优先级是否如文档所写、哪一处还留着字面分支。这些数字全都可以在仓库里当场数出来。对一款把数据主权留在设备上的工具而言,能被当场数清楚,本身就是一种可信。
十、结语
本地化常被当成一个译稿交付问题:文案发出去,回来贴进应用,就结束了。可一旦应用不联网、没有服务端、资源随包发布,它就变成一个纯粹的端侧资源治理问题——表有几张、键是否对等、缺译沿哪条路走、语言按什么次序解析、哪些格式还留在硬编码里。这些决定都不会出现在截图里,却决定了这款应用走出中文与英语世界之后,究竟是被读懂,还是被猜懂。
参考文献
[1] Internet Engineering Task Force, RFC 5646: Tags for Identifying Languages (BCP 47). https://www.rfc-editor.org/rfc/rfc5646 [2] Internet Engineering Task Force, RFC 4647: Matching of Language Tags. https://www.rfc-editor.org/rfc/rfc4647 [3] Unicode Consortium, Common Locale Data Repository (CLDR) Project. https://cldr.unicode.org/ [4] International Organization for Standardization, ISO 639: Codes for the representation of names of languages. https://www.iso.org/iso-639-language-codes.html [5] Apple Inc., Human Interface Guidelines: Localization. https://developer.apple.com/design/human-interface-guidelines/ [6] Apple Inc., Developer Documentation: String init(format:arguments:) and DateFormatter locale behaviour. https://developer.apple.com/documentation/swift/string/init(format:arguments:) [7] World Wide Web Consortium, Web Content Accessibility Guidelines (WCAG) 2.2, Success Criterion 1.4.4 Resize Text. https://www.w3.org/TR/WCAG22/ [8] 第一财经, 全球量子计算企业已超 400 家,产业化落地提速(2026-10-08). https://www.yicai.com/news/103386613.html