最近维护 Go 多语言翻译项目时,我遇到了一次看起来非常小、实际却远比预期复杂的变更。
原来的 learn-docs-v1 内容包已经包含:
49 Page而且这 49 个页面并不是“刚刚翻译完”。
它们已经走完了一整套正式生命周期:
翻译生成
→ 机器验证
→ 候选快照
→ 独立质量审核
→ 精确修订
→ 独立复审
→ A-only finalization从当时的状态看,这个内容包已经非常接近“完成”。
后来我才发现,还有两个页面本来就应该属于这套 Learn / Docs 内容:
/doc/
/doc/install于是当前完整成员从:
49 Page变成:
51 Page表面上看,这似乎只是:
再补两个页面。
但真正处理起来,我很快发现:
49 → 51 并不是“增加两个翻译任务”,而是一次正式内容包成员迁移。
这两者的成本完全不是一个数量级。
如果一开始就是 51 页,事情其实会简单很多
假设项目一开始就已经确认:
learn-docs-v1 = 51 Page那么整个流程其实非常直接。
例如新语言可以从一开始就按照完整成员拆分:
Generation:
1–30
31–51然后对应进行:
Initial QC:
1–30
31–51两批翻译生成完成以后,建立一次完整候选快照。
两组初次独立审核全部记录以后,再统一汇总需要修订的问题。
最后集中修订、独立复审、A-only finalization。
整个生命周期从一开始就知道:
我要完成的是 51 个页面。
所有正式身份、候选快照、审核范围和最终确认,都是围绕这 51 个页面建立的。
没有迁移。
也没有“旧 49 页已经完成以后,再把第 50、51 页塞进去”的问题。
真正昂贵的是:前 49 页已经拥有历史
问题就在这里。
当我发现缺少两个页面的时候,原来的 49 页已经不是普通文件。
它们已经拥有正式历史。
例如:
- 已有翻译生成结果;
- 已有机器验证结果;
- 已有候选快照;
- 已有独立审核结果;
- 已有精确修订记录;
- 已有独立复审;
- 已有最终确认;
- 已有当前授权关系;
- 已有正式 SHA 和身份标识。
所以不能简单地:
49 + 2 = 51然后假设原来的所有状态自动继续成立。
因为很多正式产物真正表达的并不是:
这 49 个页面翻译得很好。
而是:
对于当时那个正式内容包成员集合,这 49 个页面已经完成了相应阶段。
一旦内容包成员发生变化,问题就变成:
哪些旧结论仍然可以继承?
哪些必须重新建立?
新的完整内容包身份是什么?
旧候选快照还能不能继续作为当前快照?
新增页面应该怎样进入正式审核范围?
这就是成员迁移开始变复杂的地方。
“只是新增两个页面”并不会自动继承旧生命周期
最容易犯的错误是:
旧 49 页都已经 A 了,新加两页单独审核一下,不就结束了吗?
从语言质量角度看,这句话似乎没有问题。
但正式工作流还要处理另一层事情:
当前完整内容包已经变了。
原来是:
learn-docs-v1
49 Page现在是:
learn-docs-v1
51 Page这意味着完整成员关系需要新的正式权威状态。
候选快照也必须能够表达当前完整 51 页。
历史 49 页可以继承的部分,需要明确证明它们为什么仍然有效。
新增的两个页面则必须进入当前生命周期,而不能因为旁边 49 页已经完成,就跳过自己的 Generation、QC 和最终确认。
也就是说:
旧内容不是重新做一遍。
但:
新内容也不能偷偷并入一个旧结果。
真正需要的是一个可以解释:
旧 49 页为什么继续有效
+
新增 2 页为什么经过了完整流程
=
当前 51 页为什么构成一个新的完整内容包的后继状态。
迁移成本往往远大于新增内容本身
这也是这次最值得记录的经验。
如果只计算翻译工作量:
新增页面 = 2看起来几乎没有什么成本。
但内容包已经进入正式生命周期以后,新增成员带来的实际工作可能包括:
- 更新正式成员权威;
- 建立新的完整候选快照;
- 判断哪些历史页面可以继承;
- 判断哪些页面需要重新审核;
- 更新当前授权;
- 重新建立完整包身份;
- 处理后续审核和封存对成员变化的依赖;
- 确保旧的正式历史没有被覆盖。
于是:
2 个新页面并不意味着:
只增加 2 个页面的工作量更准确的关系反而是:
新增内容工作
+
成员迁移工作
+
历史继承证明
+
后续生命周期重新衔接这也是为什么有时“后来再补几个页面”会比一开始一次准备完整贵得多。
越晚发现遗漏,成本越高
如果成员遗漏发生在最开始,成本其实很低。
例如 Generation 尚未开始时发现:
49其实应该是:
51这时候直接修正成员清单即可。
还没有候选快照。
没有审核结果。
没有最终确认。
没有需要解释的历史继承关系。
但如果是在翻译生成结束以后才发现,已经开始产生迁移成本。
如果是在独立审核以后发现,成本继续增加。
如果已经 A-only finalization,再发现遗漏,处理难度就会明显更高。
如果甚至已经进入:
内容包封存
→ 整合站点审核
→ 激活
→ 生产环境以后才发现成员缺失,那么影响范围还会继续扩大。
所以成员遗漏的成本并不是线性的。
它更接近:
越靠近生命周期后端,修复成本越高。
以后新增一批页面,我更愿意先做一次完整成员审计
这次之后,我开始重新看待“新增一批全新页面”这件事。
以前可能会先问:
这些页面怎么翻译?
现在我觉得更应该先问:
这一批到底完整包含哪些页面?
正式 Generation 开始之前,应该尽量把成员清单整理完整。
尤其需要检查那些很容易遗漏的页面,例如:
- 栏目入口页;
- 索引页;
- 安装页;
- 概览页;
- 顶层
/doc/一类入口; - 导航可以访问、但容易被误认为“不是正文”的页面;
- 与主要内容位于不同路径、但实际上属于同一产品面的页面。
原因并不是追求形式上的完美。
而是:
生命周期一旦开始,成员清单就不再只是一个待办列表。
它会逐渐被:
- 翻译生成;
- 候选快照;
- 独立审核;
- 最终确认;
- 内容包封存;
这些正式状态消费。
越往后,修改它就越像正式迁移,而不是普通编辑。
“先做一部分,以后再补”并不总是更敏捷
在普通内容项目里,“先做一部分,以后继续补”通常是非常合理的方法。
但正式翻译工作流有一个不同点:
每完成一个阶段,系统都会积累新的历史状态。
因此如果已经可以合理预见这一批页面最终属于同一个完整内容包,那么:
先做 49 页,过几天再补 2 页
未必比:
一开始确认 51 页,然后一起进入生命周期
更省时间。
甚至可能恰好相反。
第一种方式看起来更快启动。
但后面会支付一次成员迁移成本。
第二种方式前期多花一点时间核对范围,却可能让后面的整个生命周期保持稳定。
这也是我现在越来越看重“前置完整性”的原因。
当然,也不可能永远一次预测完所有未来内容
这里也有一个边界。
我并不是说:
一个内容包建立以后,永远不能新增页面。
真实项目一定会持续发展。
Go 官方文档以后也可能继续出现真正的新页面。
这种未来新增本来就无法在今天完整预测。
它和这次 49 → 51 的情况不完全一样。
我更想避免的是:
当前已经存在、而且逻辑上明显属于同一批正式内容的页面,因为前期盘点不完整而被遗漏。
这类遗漏是可以通过开始前的成员审计尽量减少的。
所以目标并不是:
一次把未来几年所有页面预测出来。
而是:
对“当前已经存在的 canonical source”,尽量在生命周期开始前把本轮正式成员找完整。
未来真正新增的内容,当然仍然可以按照正式 successor 流程进入。
这两件事情并不冲突。
成员清单本身就是架构
这次还有一个认知变化。
以前很容易把内容成员列表理解成:
一张文件清单。
但随着整个多语言工作流越来越正式,我越来越觉得:
成员清单其实也是架构的一部分。
因为它决定:
- 什么属于当前内容包;
- 什么不属于;
- 翻译生成应该覆盖什么;
- 审核应该判断什么;
- 最终确认到底确认了什么;
- 内容包什么时候才算完整;
- 后面的站点审核建立在哪些正式内容之上。
所以 membership 并不是生命周期开始以后随手调整的辅助数据。
它更接近:
当前产品面正式包含什么内容的机器定义。
一旦它被多个后续阶段消费,它本身就应该受到和其他正式权威状态相似的约束。
新语言尤其应该避免复制这次 49 → 51 的历史
这次迁移至少还有一个好处:
它发生在简体中文第一门完整建设语言里。
这里付出的工程成本,可以转化成后续语言的固定流程。
所以未来新的 locale 不应该再照着旧历史:
先做 49
→ 完成
→ 再发现 2
→ 做 49 → 51 迁移而应该直接从当前完整权威开始:
learn-docs-v1 = 51 Page然后按稳定批次执行。
这样简体中文这次付出的迁移成本,就变成了一次性架构成本,而不是每种语言都重复付一次。
这也是我一直希望第一门语言承担的角色:
不只是把中文翻译出来。
更重要的是:
把后续几十门语言需要复用的流程跑稳定。
如果再做一次,我会把顺序反过来
如果重新开始这批 Learn / Docs 页面,我现在会采用完全不同的顺序。
以前更像是:
找到主要页面
→ 开始翻译
→ 完成审核
→ 后来发现还有遗漏
→ 再做成员迁移现在我会优先:
盘点 canonical source
→ 建立完整候选成员清单
→ 检查入口页和特殊页面
→ 固定本轮正式 membership
→ 再开始 Generation这一步可能只多花一些前期检查时间。
但它保护的是后面整条生命周期。
从投入产出来看,通常非常划算。
最后
这次 learn-docs-v1 从 49 Page 变成 51 Page,让我真正意识到:
内容范围错误越早修,越像普通编辑;越晚修,越像正式迁移。
当一个内容包还没有进入生命周期时:
49 → 51可能只是修改两个数字。
但当 49 页已经拥有翻译结果、候选快照、独立审核和最终确认以后,同样的:
49 → 51就已经是另一件事情。
所以以后再新增一整批全新页面时,我会尽量先把当前正式成员整理完整。
不是因为以后绝对不能增加内容。
而是因为:
如果今天已经存在的两个页面本来就属于这一批,那么最便宜的处理时机,就是第一次 Generation 开始之前。
真正需要避免的,不是所有未来迁移。
而是那些本来可以通过一次完整盘点避免的迁移。
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 官方无隶属、授权或背书关系。

