前面已经写过一篇:
术语表改了,为什么不应该让所有页面重新翻译?
那篇主要讨论的是一个比较具体的问题:
当统一术语表发生变化以后,怎样判断真正受到影响的页面,只重新处理确实需要修改的翻译单元,而不是让整个语言版本全部重新翻译和审核。
但随着 Go Learning & Documentation v2 越来越完整,我发现这个问题其实并不只存在于术语表。
整个工作流里,到处都有类似的问题:
一个上游状态发生变化以后,到底应该让哪些下游结果失效?
例如:
一个页面被修订了。
它之前的质量审核还算数吗?
内容包原来的封存结果还能继续使用吗?
整合站点审核是否需要重新做?
其他完全没有变化的页面,要不要一起重新审核?
如果只是一个输入文件发生变化,整个语言版本是否都应该重新走一次流程?
另一方面,如果失效判断过于宽松,又会出现更加危险的问题:
下游继续使用一份其实已经不再对应当前内容的旧结果。
所以 v2 后来需要解决的,已经不只是:
发生变化了吗?而是:
到底发生了什么变化?
↓
哪些状态真正依赖这次变化?
↓
哪些已有结果仍然有效?
↓
哪些状态必须失效?这篇就准备整理这个问题。
最简单的处理方式:上游一变,后面全部清空
如果只考虑安全性,最容易实现的规则其实非常直接。
假设整个流程大致是:
Source
↓
翻译单元
↓
质量审核
↓
内容包
↓
整合站点
↓
Production那么只要 Source 发生变化,就可以:
Source 发生变化
↓
所有下游状态全部失效所有翻译重新生成。
所有质量审核重新做。
内容包重新封存。
整合站点重新审核。
Production 再重新验证。
这种方案很好理解。
而且非常保守。
因为几乎不会留下过期的旧结果。
但问题也非常明显:
工作量会被极度放大。
一个只有几行发生变化的页面,可能让整个语言版本重新进入流程。
术语表中只调整一个、而且只影响两个页面的术语,也可能把上百个已经稳定的翻译结果一起清空。
从安全角度看很简单。
从长期维护角度看,却很难持续。
另一个极端也很危险:只要旧结果还在,就继续使用
既然全部失效的成本太高,另一个极端就是尽可能复用。
例如:
旧质量审核记录存在
→ 直接复用
旧整合站点 PASS 记录存在
→ 直接复用
旧内容包封存记录存在
→ 直接复用这样当然很快。
但它忽略了一个最核心的问题:
这些旧结果审核的,到底是不是现在这份内容?
如果候选译文已经修改,旧质量审核仍然存在,并不意味着它还能证明新的译文是 A。
如果内容包已经变化,一份旧的整合站点 PASS 记录仍然存在,也不意味着它审核过现在这组页面。
所以:
“旧结果存在”与“旧结果仍然有效”是两回事。
这也是 v2 中需要大量身份标识、SHA、范围和当前性检查的原因。
真正困难的部分,就是在两个极端之间找到平衡:
既不能动不动全部清空,也不能因为旧证据还在就继续相信。
判断哪些状态应该失效,首先要理解依赖关系
做到后来以后,我觉得失效判断本质上是在维护一张依赖关系图。
实际系统当然比下面复杂得多,但可以先简单理解成:
Source
↓
翻译单元
↓
候选译文
↓
质量审核
↓
A-only finalization
↓
内容包封存
↓
整合站点审核
↓
激活
↓
预览 / Production与此同时,统一术语表也会影响翻译单元。
Parser 和内容表示方式的变化,也可能影响某些页面。
所以一个东西发生变化以后,真正的问题不是:
“它下面还有多少东西?”
而应该是:
“下面哪些结论真正依赖这次变化?”
这才是需要重新判断的范围。
身份发生变化,不等于语义一定失效
这一点和前面术语表那篇文章中的结论其实是一致的。
SHA 很适合证明:
两份输入是不是完全相同。
但它不能单独回答:
这次变化是否真的让已有结论失效。
例如术语表的 SHA 从 A 变成 B。
这次变化可能只影响两个页面。
也可能影响一百个页面。
甚至可能没有任何已有译文需要修改,因为它们本来就已经符合新的术语规则。
同样,Parser 的实现发生变化,也不代表所有 Markdown 页面都必须重新翻译。
所以 v2 中需要明确区分两层问题:
输入身份是否发生变化?和:
原来的语言或工作流结论是否真的受到影响?第一层适合机器精确判断。
第二层才决定哪些结果需要失效。
翻译单元是第一层正式语言边界
对于 Learn / Docs,前面已经写过:
一个实际存在的 Page,就是一个正式翻译单元(TranslationUnit)。
所以当页面内部某个位置出现问题时,机器可以分析到非常细的范围。
例如:
Page
├── 上下文 A → 仍然兼容
├── 上下文 B → 仍然兼容
├── 上下文 C → 受到影响
└── 上下文 D → 仍然兼容最终正式状态仍然是:
整个 Page
→ 需要重新处理但这并不意味着:
整个内容包都受到影响。
更不意味着:
整个语言版本都受到影响。
这就是范围分层的第一步。
机器内部可以把原因分析得很细。
正式工作流则把影响提升到正确的治理单位。
一个 Page 被修订,不应该让其他 A 一起失效
假设一个内容包有 49 个 Page。
其中 48 个已经通过独立质量审核,都是 A。
另一个 Page 得到 B。
后来针对审核发现的问题做了精确修订。
这时候最简单的方式,是重新建立整批候选状态,然后让 49 个 Page 全部重新审核。
但这样会浪费前面 48 个已经可信完成的结果。
更合理的方式是:
48 个没有发生变化的 A
→ 继续继承
1 个已经修订的 Page
→ 机器校验
→ 独立复审
→ A最后再建立新的 A-only finalization。
这里实际上已经发生了一次精确失效:
只有真正发生修订的翻译单元,其旧语言判断需要失效。
其他没有变化的 A,应该继续成立。
这和前面写过的“历史成果复用”,其实是同一个原则的另一面。
继承旧审核结果的前提,不是“这个文件没人动过”
当然,也不能简单认为:
这 48 个文件我没有手动修改,所以继续用 A。
真正应该证明的是:
候选译文没有变化
+
相关输入没有变化,或者仍然兼容
+
之前的 A 仍然对应当前这份确切译文只有这样,旧审核结果才可以继续继承。
所以“继续复用”和“判断失效”实际上是一对关系:
能证明仍然有效 → 继续继承
无法证明仍然有效 → 重新判断或视为失效
这也是为什么系统需要坚持:
无法确认时,按失败处理。
不能为了减少工作量而猜测旧结果仍然有效。
内容包封存是另一层依赖边界
当所有翻译单元都达到正式要求以后,会进入内容包封存。
假设一个内容包已经完成封存。
后来其中一个 Page 因为整合站点审核发现问题,被重新打开并修改。
那么:
原来的内容包封存还能代表现在这个内容包吗?
显然不能。
因为它封存的是修改之前的确切状态。
所以:
翻译单元发生变化
↓
旧内容包封存不再是当前有效状态等修订完成、机器校验、独立复审和 A-only finalization 全部完成以后,再建立新的后继内容包封存。
这里还有一个重要区别:
旧封存记录并没有被删除。
它仍然是历史的一部分。
只是:
它不再代表当前内容包。
这和前面“历史应该被保留,但不能绑住当前工作流”的思路是完全一致的。
整合站点审核判断的是“组合以后”的结果
单个翻译单元全部得到 A,并不代表整个网站组合起来以后一定正确。
所以 v2 中还有整合站点审核(Surface Review)。
它关注的是更高一层的问题,例如:
- 多个内容包组合以后是否一致;
- 导航是否合理;
- 本地内容覆盖是否正确;
- 页面之间的链接是否正确;
- Site shell 与正文是否协调;
- 用户最终看到的完整站点是否成立。
因此,它依赖的是:
一组当前有效的内容包封存所形成的确切站点范围。
如果这个范围真正发生变化,旧的整合站点审核结论就可能不再成立。
但这里仍然不能简单地认为:
任何地方发生任何变化
↓
所有整合站点审核全部重做真正需要判断的是:
这次变化是否属于那次审核所覆盖的范围。
整合站点发现语言问题,不能直接跳过翻译单元审核
这也是 v2 中后来非常明确的一条边界。
假设整合站点 Reviewer 发现:
某个 Page 的翻译本身存在问题。
这时候不能直接:
整合站点审核发现问题
↓
直接修改 Page
↓
整合站点 PASS因为整合站点审核并不能替代翻译单元的独立质量审核。
真正应该回到对应的语言治理层:
整合站点发现语言问题
↓
重新打开受影响的翻译单元
↓
精确修订
↓
机器校验
↓
独立复审
↓
A-only finalization
↓
建立新的内容包封存
↓
重新进行后继整合站点审核这样依赖关系才是完整的。
问题虽然是在整合站点阶段发现的。
但真正拥有语言判断权的,仍然是翻译单元这一层。
所以:
问题属于哪一层,就回到拥有正式权威的那一层修复。
这样可以避免“高层审核偷偷修改低层事实”
如果不这样分层,会出现一种很危险的情况。
整合站点 Reviewer 发现一个 Page 翻译有问题。
有人直接把 Page 改了。
然后整合站点审核 PASS。
这时候表面上看,页面已经修好。
但内部可能出现:
- 翻译单元的正式质量审核仍然对应旧译文;
- 内容包封存仍然对应旧状态;
- 整合站点记录却已经针对新状态。
整个证据链开始分裂。
也就是说:
用户看到的是新文本,但语言质量记录证明的却还是旧文本。
这种问题非常难从表面发现。
所以失效传播并不只是:
“让哪些东西变成过期状态。”
它还负责保证:
内容发生修改以后,所有依赖旧状态的正式结论,都重新绑定到新的后继状态。
并不是所有整合站点失败都属于语言失败
另一方面,也不能把整合站点阶段的所有失败都理解成翻译质量失败。
例如:
压缩包传输错误。
输入文件不完整。
当前性检查失败。
Schema 不匹配。
这些问题本质上属于:
输入门禁失败。
Reviewer 根本还没有得到一份可以正式审核的当前站点。
那么这时就不应该记录成:
语言审核 FAILED因为真正的语言判断根本还没有开始。
更合理的处理方式是:
输入门禁失败
↓
没有产生状态变更
↓
修复传输或输入
↓
使用新的 review-id
↓
重新进入审核这里非常重要的一点是:
系统故障和语言质量失败必须分开。
否则历史里就会留下很多实际上并不存在的“语言审核失败”。
激活阶段也有自己的当前性要求
再往后进入激活阶段,依赖关系又发生变化。
激活并不是简单地:
整合站点 PASS 以后,把一个开关打开。
它还需要绑定:
- 当前内容包封存;
- 当前整合站点结果;
- 当前正式权威状态;
- exact prior SHA。
激活预检的意义之一,就是重新确认:
现在准备激活的,仍然是不是刚才已经审核过的那组确切状态。
如果预检得到的 prior_sha256 后来已经不再匹配,说明这期间发生了新的状态变化。
这时正确做法不是:
“之前都通过了,那就继续执行吧。”
而应该:
prior SHA 不匹配
↓
STOP
↓
读取当前正式状态
↓
重新执行预检因为旧的预检结论已经不再对应当前状态。
这同样属于一种失效判断。
CAS 保护的是“决定时看到的世界”没有偷偷变化
激活阶段使用的 prior SHA,本质上类似 compare-and-swap(CAS)。
可以把它理解成:
我做预检时看到的是状态 A
只有真正执行变更时
当前仍然还是 A
才允许继续把它变成 B如果真正执行时已经变成:
A'那么之前的变更授权就不能继续使用。
它解决的是一个很实际的问题:
我做决定时看到的状态,和真正执行变更时的状态,还是不是同一个?
如果已经不是,就应该重新判断。
而不是假设中间没有人或没有流程改变过它。
预览和 Production 证明的又是另外一种事实
激活成功以后,也不代表 Production 一定正确。
最终网站还涉及:
- 页面渲染;
- 导航;
- URL;
- canonical;
- 本地内容覆盖;
- 404;
- sitemap;
- 浏览器实际行为。
所以预览验证和 Production 仍然是独立阶段。
语言链条可能完全正确。
整合站点也可能已经 PASS。
但最终页面渲染仍然可能出错。
因此:
翻译单元审核 PASS
≠
整合站点审核 PASS
整合站点审核 PASS
≠
已经可以安全进入 Production每一层证明的事情都不同。
所以每一层的失效条件也不能混在一起。
上游发生变化,不代表失效必须一直传播到底
这里最容易出现一个误解:
既然系统存在依赖关系,那么上游变化是不是必然让所有下游结果一路失效?
并不是。
依赖关系首先说明的是:
需要重新判断。
至于是否真的失效,还要看变化的实际影响。
例如术语表发生变化。
系统首先重新计算兼容性。
但某个 Page 可能发现:
仍然符合新的术语规则那么它的候选译文和质量审核都不需要失效。
既然这个 Page 的正式结果没有变化,内容包的实际内容也可能没有变化。
如果内容包身份也没有变化,整合站点就没有理由仅仅因为术语表文件曾经改过,就重新审核一遍。
理想的传播方式应该是:
发生变化
↓
评估真实影响
↓
如果正式结论没有变化
↓
停止继续传播而不是:
发生变化
↓
所有下游状态全部失效这其实很像增量构建系统
做到后来以后,我觉得整个 v2 越来越像一个增量构建系统。
编译器通常不会因为仓库中任意一个文件变化,就重新编译所有内容。
成熟的构建系统会判断:
哪些目标真正依赖这个输入。
然后只重新构建受影响的部分。
翻译系统也很类似。
它同样存在:
Glossary
Source
Parser 规则
候选译文
质量审核
内容包
整合站点
激活每一层都有自己的依赖关系。
每一层都有自己的身份。
每一层也都可能因为上游变化而失效。
区别只是:
自然语言比普通编译产物更难判断“结果是否仍然等价”。
所以系统需要更多证据,才能安全证明旧结果仍然可以继续使用。
目标不是“让失效越少越好”
这里也要防止走向另一个极端。
如果过度追求:
越少状态失效越好
就会开始想办法把所有旧结果都判定为“仍然有效”。
这是非常危险的。
真正的目标不是:
尽可能减少失效。
而是:
尽可能准确地判断失效。
确实受到影响,就应该失效。
无法可靠证明没有受到影响,也应该按失败处理。
只有真正能够证明旧结果仍然成立时,才应该继续继承。
所以:
减少重复工作永远排在:
保证正确性之后。
“不知道有没有影响”本身也是一个需要停止的状态
这一点在工程上尤其重要。
有时候系统无法确认:
某次状态变更到底有没有真正发生。
或者:
某份证据是否仍然对应当前状态。
这时候最危险的做法就是猜:
大概没变然后继续。
更安全的规则应该是:
状态无法确认
↓
STOP
↓
先进行诊断特别是机器明确给出:
mutation=unknown这种情况更不能盲目重新执行。
因为你甚至不知道上一轮是否已经产生了一部分状态变更。
再次执行,有可能制造第二次变化。
所以:
无法确认时停止,比猜测继续更加可靠。
判断旧结果是否失效,也是故障恢复能力的一部分
以前我更多把失效判断理解成:
防止系统使用旧结果。
后来越来越觉得,它其实也是故障恢复架构的一部分。
因为工作流中断以后,系统真正需要回答的是:
哪些结果已经可信完成?
哪些只是缺少后续记录?
哪些结果真的已经失效?
哪些内容真正受到这次变化影响?如果无法准确回答这些问题,最简单的恢复方式就只剩下:
全部重做。
而失效与继承判断越准确,恢复过程就越可以做到:
保留已经可信完成的结果
↓
只继续处理缺失、失效和真正受影响的部分所以精确失效和快速恢复其实是互相依赖的。
好的失效判断,会让整个系统越来越增量化
长期来看,我希望整个工作流越来越接近:
新增工作
+
真实变化
+
真实故障而不是:
每次都重新处理全部历史工作例如:
新增一个 content package:
主要处理新增内容。
Glossary 调整:
主要处理真正受到影响的 Page。
整合站点发现一个语言问题:
主要重新打开对应的翻译单元。
某个内容包封存发生变化:
重新判断依赖它的整合站点结果。
激活阶段检测到并发状态变化:
重新预检,而不是继续使用旧 prior SHA。
这就是我理解的增量工作流。
历史复用、精确失效和快速恢复其实是同一件事
前面几篇分别写过:
- 为什么历史值得保留;
- 为什么历史不能无限重建;
- 为什么术语表变化不能全量重做。
做到这一篇以后,我觉得这些问题其实已经逐渐汇合成了同一个目标:
让系统知道什么已经完成、什么仍然有效、什么真的发生了变化。
如果能够可靠回答:
是否已经完成?
是否仍然有效?
是否真正受到影响?那么:
历史可以复用。
故障可以增量恢复。
Revision 可以保持精确。
整合站点可以只审核真正当前有效的范围。
Production 也可以避免依赖已经失效的旧状态。
所以依赖与失效传播并不是一个孤立的小功能。
它实际上是整个长期工作流能够稳定扩展的基础之一。
最终形成的几条原则
现在回头看,v2 中关于依赖和失效传播的设计,可以浓缩成几条规则。
第一:
身份发生变化,不自动等于语义结论失效。
第二:
只让真正依赖这次变化的正式状态重新进入流程。
第三:
能够证明仍然有效的可信结果继续继承。
第四:
发生修改以后,原来依赖旧状态的正式结论,必须重新绑定新的后继状态。
第五:
哪一层出现问题,就回到拥有正式权威的那一层修复。
第六:
无法证明没有受到影响时,按失败处理,不猜。
第七:
故障恢复优先处理缺失、失效和真正受影响的部分,而不是默认全部重做。
最终目标不是:
让失效越少越好。
也不是:
让流程越复杂越安全。
而是:
让每一次变化,只产生它真正应该产生的工程成本。
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 官方无隶属、授权或背书关系。

