在维护 Go Learning & Documentation Translations 多语言项目时,我一直有一个基本原则:
已经经过独立审核、并且内容没有真正受到影响的翻译,不应该因为后续项目扩展而机械地重新翻译、重新审核。
这个原则本身没有问题。
问题出在:当项目从最初的 A Tour of Go,逐渐扩展到 Site shell、Learn、Docs,以及未来更多 content packages 后,要证明“一份旧翻译仍然可以安全沿用”,机器所需要维护的证据链也越来越复杂。
最近在处理 zh-CN 时,这个问题终于从“架构复杂”变成了一个非常现实的性能问题。
一个只需要审核 1 个页面的任务,为什么要跑 6 分钟?
这次 zh-CN 的术语表经过统一刷新和独立审核后,大多数原有 Tour 翻译仍然可信。
项目当前采用的是 glossary compatibility 机制。
简单来说,当 glossary 发生变化时,不是直接宣布所有旧审核结果失效,而是计算:
旧 glossary
↓
semantic delta
↓
当前所有 translation contexts
↓
哪些 context 真正受到影响
↓
只重新审核 affected scope这个设计的目标非常明确:
glossary 改了一点,不应该导致整个语言包重新审核。
在当前 zh-CN evidence 中,这套机制实际上也确实发挥了作用。
整个 compatibility inventory 有 470 个 context,其中只有 32 个被判断为 affected;如果只看 TranslationUnit,则只有 3 个 TU affected,119 个 TU compatible。
从语言审核成本来看,这非常理想。
但随后出现了一个完全不可接受的问题:
quality-check scope
quality-check reviewer-bundle
quality-check reviewer-bundle-check
quality-check finalize
retranslation export这些本来应该属于确定性机械操作的命令,开始频繁需要 6 分钟左右才能完成。
一次真实的 quality-check scope 测试耗时:
wall time: 5:58.68
user CPU: 400.28s而最终输出不过是 122 个 TranslationUnit 的 QC scope。
这已经不是“CLI 稍微有点慢”的问题。
如果每一次 revision、Reviewer Bundle、finalization 都要等待几分钟,那么随着 65 个 locale、Learn/Docs 和 future content packages 继续扩大,这套工作流本身就会逐渐失去可维护性。
于是出现了两个选择。
Option 1:保留 glossary compatibility,但彻底优化实现
第一个方案,是保留当前 semantic-delta / compatibility architecture。
也就是说,仍然坚持:
glossary changed
↓
机器计算真正 affected scope
↓
可信 unchanged A 继续 carry
↓
只重新审核 affected TranslationUnits最开始我担心,6 分钟可能就是这套精确 compatibility 模型本身不可避免的成本。
如果是这样,继续优化就没有太大意义。
但进一步检查代码以后,情况比预想中乐观。
当前 BuildQualityCheckScope 在遍历 TranslationUnit 时,会按 Unit 调用:
ResolveGlossaryCompatibility(
root,
locale,
previousGlossarySHA,
currentGlossarySHA,
"tu:"+unitID,
catalog,
)问题是,这个 API 表面上像是在查询:
tu:concurrency/6是否 compatible?
实际内部做的却接近:
重新构建整个 glossary compatibility 世界,然后最后查询
concurrency/6。
每次跨 glossary SHA 的调用都会重新验证 current Glossary Review、重新构建完整的 470-context inventory、重新扫描 TranslationUnit source/candidate、重新读取 compatibility evidence、重新加载 archived glossaries、重新计算 semantic delta 和 classification,最后才返回一个 scope 的结果。
于是一个本来应该类似:
构建一次 compatibility state
+
122 次 map lookup的问题,变成了近似:
122 × 完整 compatibility reconstruction当前复杂度被评估为近似:
O(N × M)而如果引入 invocation-local prepared state,则可以接近:
O(M + N)也就是完整 compatibility world 在一次命令里只建立一次,之后所有 Unit 共用。
这意味着当前的分钟级延迟,很大一部分并不是 compatibility 理念本身的固有代价,而是同一个昂贵计算被重复执行了一百多次。
因此 Option 1 仍然值得尝试。
而且这项优化必须是全语言共享的。
zh-CN 只是因为当前 evidence 和 lineage 足够复杂,率先把问题暴露了出来。底层 implementation 本身是 generic shared code path,并不需要为中文、西班牙语、德语分别优化。
正确的设计应该是:
locale
old glossary SHA
new glossary SHA
current inventory identity
↓
一次 prepare
↓
所有 TranslationUnits 共用而不是:
if locale == "zh-CN" {
fastPath()
}这一点现在已经可以明确下来:如果最终保留 compatibility,它的优化也是一次性的 repository-level 优化,所有 locale 自动受益。
Option 2:退回最简单的 glossary SHA gate
第二个方案则激进得多。
直接放弃 glossary change 后的精确 affected-scope carry。
规则变成:
glossary SHA 没变
→ unchanged A 可以正常 carry
glossary SHA 变了
→ 所有旧 QC A 不再自动 carry
→ full QC
→ Reviewer 重新审核全部当前 TranslationUnits
→ 只有真正 B/C/D 的 Unit 才 revision这里有一个很重要的区别:
full QC 不等于 full regeneration。
假设 glossary 改变后有 122 个 Tour TranslationUnits。
并不是重新翻译 122 个。
而是重新审核 122 个。
如果其中 119 个仍然正确,Reviewer 继续给 A;只有真正受到新术语政策影响的 3 个,再进入 targeted revision。
这个方案最大的好处是简单。
机器不再需要努力证明:
为什么三个月前的这个 A,在经过两次 glossary change 以后,现在仍然可以相信?
它只需要判断:
SHA same?如果不是,就重新审核。
从 fail-closed 和 recovery 的角度看,这种设计非常容易理解,也很难产生 false carry。
但进一步评估以后发现,Option 2 并没有想象中那么便宜。
当前 compatibility 不只服务于 TranslationUnit QC carry。
Course SEO、Surface Review、sitecontent package、historical closure、protected-input recovery 等流程也依赖相似的 compatibility / provenance 机制。
所以如果切到 hash-only,并不能简单删除整个 compatibility subsystem。
更现实的结果可能是:
新的 QC workflow
→ 使用简单 SHA gate
历史 evidence / Course SEO / Surface / recovery
→ 继续保留 compatibility legacy path最终形成一套“新规则简单、旧规则仍然复杂”的双轨系统。
这也是为什么当前评估认为 Option 2 的 migration complexity 仍然很高。
真正重要的共同前提:先把 glossary 稳定下来
在讨论两个方案的过程中,我反而越来越确定另一件事情:
无论最终保留 compatibility,还是退回 glossary SHA gate,都应该先最大程度稳定 glossary。
现在项目已经不只是 Tour。
一个 locale 永久只有一份:
locales/<locale>/glossary.yaml它服务于整个 Go 学习和文档体系,而不是某个单独版本。
如果 glossary 的建立方式仍然是:
做 Tour
→ 补 Tour 术语
做 Learn
→ 再补 Learn 术语
做 Docs
→ 再补 Docs 术语
增加 future package
→ 又补一轮术语那么 glossary SHA 必然频繁变化。
这对于两个方案都不好。
对于 Option 1,它意味着 compatibility assessment 和 lineage 会不断增长。
对于 Option 2,它意味着不断触发 full QC。
更合理的方向,是把 glossary generation 的 terminology discovery 范围提前扩大。
不是把 glossary 变成一本庞大的英中技术词典,而是:
完整 Go / go.dev terminology discovery corpus
↓
发现整个生态中可能需要长期治理的术语
↓
筛选
↓
只保留真正高价值的项目级术语决定也就是说:
扩大 corpus,不扩大 glossary 的定义。
项目现有原则仍然保持不变:
glossary 应该“尽量少,但必须有价值”。
真正值得进入 glossary 的,仍然应该是这类词:
- Go 特有、容易产生技术误解的概念;
- 存在多个合理译法,需要项目统一选择的词;
- 跨 Page / surface 反复出现、容易产生术语漂移的概念;
- 必须保持原文 identity 的技术名称;
- UI、navigation、project naming 等高影响表达。
而普通、自然、没有明显歧义的词,不应该因为它出现在 corpus 中,就机械地进入 glossary。
目前 frozen corpus 已经覆盖 Tour、Site shell、Learn YAML、Learn/Docs Pages、Course SEO 等当前正式内容,共有 403 个 contributor、3,004 个 contexts;但它仍然不是完整的 Go/go.dev terminology discovery corpus。
未来更理想的 discovery scope,可以继续覆盖 language specification、memory model、modules、workspace、toolchain、testing、benchmarking、fuzzing、profiling、tracing、PGO、安全与供应链,以及标准库中的高价值跨生态概念。
这些 discovery-only source 不一定意味着对应页面已经被本地化。
它们只是提前帮助 glossary 做长期术语决策。
这样 future content package 真正加入时,就不会出现:
“今天新增一篇页面,所以 glossary 又要增加 5 个词。”
新增页面本身,不应该成为修改 glossary 的理由。
只有出现新的技术领域、真正的术语冲突,或者发现某个概念确实值得长期统一治理时,才应该修改 glossary。
当前决定:先给 Option 1 一次机会
综合这次评估,我暂时选择了 Option 1。
不是因为 semantic compatibility 一定比简单 SHA gate 更正确,而是因为现在已经有比较明确的证据表明:
当前 6 分钟性能问题主要属于实现问题,而不是 architecture 本身必然需要 6 分钟。
如果一次 invocation 中可以把:
122 次完整 compatibility reconstruction变成:
1 次 prepared compatibility state
+
122 次 lightweight lookup那么 compatibility 带来的 Reviewer 成本节省仍然非常有价值。
尤其是在当前真实案例里:
122 TranslationUnits
3 affected
119 compatible如果只是因为 implementation 写得低效,就放弃 119 个可信 carry,未免太早。
但这次决定带有一个明确的止损条件。
我不会为了保住 compatibility,再继续引入:
persistent cache
cache invalidation protocol
新的 receipt
新的 schema
locale-specific tuning这次只接受:
locale-agnostic、invocation-local、无新增持久状态的 prepared-state refactor。
如果完成这次干净重构以后:
quality-check scope
reviewer-bundle
reviewer-bundle-check
finalize
retranslation export仍然明显处于分钟级,那么就重新打开 Option 2。
到那时候,问题就不再是“某个地方重复计算了一百次”,而是这套 architecture 本身的长期成本已经超过了它节省的 Reviewer 成本。
比性能更重要的是长期可维护性
这次问题让我重新意识到一点。
本项目最大的规模挑战,并不是某一个 locale 有 122 个 TranslationUnits。
真正的规模是:
65 locales
×
Tour
×
Site shell
×
Learn
×
Docs
×
future content packages
×
长期 upstream evolution在这个规模下,一个设计不能只回答:
“理论上能不能精确复用?”
还必须回答:
“三年以后还能不能理解、恢复、验证和维护?”
因此这次最终留下来的原则,其实比 Option 1 或 Option 2 本身更重要:
第一,先尽可能稳定 glossary。
第二,机器优化应该服务所有 locale,而不是给某种语言打补丁。
第三,不为了省少量 Reviewer 成本无限增加 evidence state machine 的复杂度。
第四,已经投入很多开发成本,并不是继续保留复杂设计的理由。
如果 compatibility 能被干净地优化到秒级,那么继续使用它。
如果不能,就退回更简单、更保守、更容易恢复的 glossary SHA gate。
架构不是越精确越好。
在一个需要长期维护几十种语言的项目里,可解释、可恢复、可验证,本身就是质量的一部分。
Go 学习与文档多语言本地化项目
本系列完整记录 Go 学习与文档多语言本地化项目从 A Tour of Go 起步,逐步扩展到站点首页、项目说明页、学习内容、文档以及未来内容包的实际开发过程,包括架构设计、统一术语治理、整页翻译、结构保护、自动校验、独立质量审核、生产发布与长期维护。
项目源码:
✅ GitHub:shuijingwan/go-tour-i18n
语言版本:
当前跨项目目标语言池共包含 66 个语言版本。Go 项目本身目前提供 65 个社区 Production 语言版本,下面每一门 Go 语言版本都直接链接到其当前公开站点。语言列表沿用项目首页现有的展示顺序。
英语(美国)仅作为跨项目目标语言纳入列表,并不是单独的 Go Production 语言版本。当前 Go 的英文源内容仍为官方 generic English,因此英语(美国)链接到 Go 官方站点,而不是不存在的本地社区部署。
- 阿姆哈拉语
- 阿拉伯语
- 孟加拉语
- 葡萄牙语(巴西)
- 保加利亚语
- 加泰罗尼亚语
- 克罗地亚语
- 捷克语
- 丹麦语
- 荷兰语
- 英语(美国)(Go 官方英文站点)
- 英语(澳大利亚)
- 英语(加拿大)
- 英语(印度)
- 英语(新加坡)
- 英语(南非)
- 英语(英国)
- 爱沙尼亚语
- 菲律宾语
- 芬兰语
- 法语(法国)
- 法语(加拿大)
- 德语(德国)
- 德语(奥地利)
- 德语(瑞士)
- 希腊语
- 古吉拉特语
- 希伯来语
- 印地语
- 匈牙利语
- 印度尼西亚语
- 意大利语
- 日语
- 卡纳达语
- 哈萨克语
- 韩语
- 西班牙语(拉丁美洲)
- 拉脱维亚语
- 立陶宛语
- 马来语
- 马拉雅拉姆语
- 马拉地语
- 书面挪威语(Bokmål)
- 波斯语
- 波兰语
- 葡萄牙语(葡萄牙)
- 旁遮普语(果鲁穆奇文,印度)
- 罗马尼亚语
- 俄语
- 塞尔维亚语(西里尔文)
- 简体中文(中国大陆)
- 简体中文(新加坡)
- 斯洛伐克语
- 斯洛文尼亚语
- 西班牙语(西班牙)
- 斯瓦希里语(坦桑尼亚)
- 瑞典语
- 泰米尔语
- 泰卢固语
- 泰语
- 繁体中文(台湾)
- 繁体中文(香港)
- 土耳其语
- 乌克兰语
- 乌尔都语
- 越南语
项目正在持续扩展新的 Go 学习与文档内容,并长期维护各语言版本的术语一致性、翻译质量、独立审核、生产发布与后续更新。
本项目属于非官方社区多语言本地化项目,与 Go 官方无隶属、授权或背书关系。
