最近这段时间,我在继续推进自己的 Go Learning & Documentation 多语言项目。
做到现在,我越来越强烈地意识到一个问题:
基础设施是为了翻译服务的,而不是让翻译项目最后变成一个基础设施项目。
可惜,这一次我还是踩进去了。
而且比以前踩得更深。
v2 最初预计 5 个工作日,现在超过 30 个工作日仍然没有上线
现在回头看,最让我难以接受的,其实不是 v2 本身变复杂了。
而是它与我最初预期之间的差距,已经大到了几乎无法理解的程度。
一开始决定做 Site v2 的时候,我其实想得非常简单。
v1 已经上线,翻译流程、术语表、Generation、Reviewer、validation、断点恢复等大量基础能力都已经存在。
所以我最初认为:
v2 主要就是在现有基础上增加一些页面的翻译。
当时甚至比较乐观地估计:
大约 5 个工作日,就应该能够完成简体中文 v2。
后来真正开始做以后,我才发现事情没有这么简单。
v2 不只是增加几个页面。
范围逐渐扩大到了:
- Learn;
- Docs;
- Site shell;
- Homepage;
- Translation;
- Integrated Surface;
- activation;
- preview;
- Production;
同时还涉及内容包、representation、跨 surface 一致性等问题。
到了这个阶段,我已经意识到:
5 个工作日肯定是不现实了。
于是我的心理预期也不断调整。
如果最终需要十几个工作日,我可以接受。
甚至如果需要接近 20 个工作日,考虑到这是第一次建立完整的 Site v2 流程,我也觉得勉强可以接受。
因为其中一些工程能力,理论上后续还能复用到其它语言。
但现在的问题是:
已经超过 30 个工作日了。
而更糟糕的是:
直到现在,我甚至还不能确定简体中文 v2 到底什么时候能够真正上线。
这才是最让人难受的地方。
如果只是预计 20 天,最后花了 22 天或者 25 天,至少还能说这是项目估算出现了偏差。
但现在已经不是简单的估算偏差了。
最初:
预计 5 个工作日。
后来:
20 个工作日也可以勉强接受。
现在:
超过 30 个工作日,仍然没有上线,而且上线日期依然无法确定。
这说明项目流程本身已经出现了问题。
更让我无法接受的是:
这并不是因为翻译了几十倍的内容。
真正用于“翻译内容本身”的时间,我感觉甚至不到整个项目耗时的 10%。
大量时间都消耗在:
- workflow;
- currentness;
- authority;
- recovery;
- representation;
- historical proof;
- successor;
- Surface;
- producer;
- 以及为了修复这些机制而继续增加的新机制。
换句话说:
一个原本预计 5 个工作日完成的翻译项目,最后不是因为“翻译太慢”拖到了 30 多个工作日,而是因为围绕翻译建立的基础设施越来越复杂。
这已经不能简单解释成:
“v2 比 v1 范围更大,所以耗时更长。”
如果只是范围扩大,那么投入增加到原来的两倍、三倍,也许还能理解。
但当一个已经拥有 v1 经验和大量现成基础设施的 v2,最终花费的时间竟然比当初简体中文 v1 从零做到上线还长,而且到了 30 多个工作日仍然看不到明确终点,
那就说明:
基础设施已经没有在帮助项目加速,而是在拖慢整个项目。
一个标点问题,最后变成了几天的基础设施工程
这几天发生的事情尤其典型。
最开始面对的并不是什么大规模翻译错误。
也不是整个上游文档全部发生变化。
其中相当一部分问题只是:
- 中英文标点;
- representation;
- 历史 decoder 与当前实现之间的差异;
- 当前程序无法重新证明历史 installed proof。
而实际上,在此之前已经存在一个非常重要的历史断点。
当时对应的一批 Learn / Docs 翻译最终已经完成:
32/32 validation PASS
之前成功的 TranslationUnit 被完整保留。
失败的那个 Unit 经过 retry 后也已经通过 validation。
也就是说:
翻译成果本身已经通过了正式验证。
如果按照我原本已经建立过的“历史断点恢复”思路,这个时候首先应该考虑的问题其实是:
这个合法的历史 PASS 能不能直接作为恢复断点?
然后再进一步判断:
到底有没有证据证明这些翻译内容本身已经失效?
如果没有,就应该尽量保留原成果,只处理真正受到影响的部分。
但这一次没有这样走。
问题逐渐演变成:
historical construction bytes
→ installed-derived transport recovery
→ mixed-role attribution
→ role-separated producer
→ semantic IR
→ source-to-role closure
→ per-Unit dependency proof
→ producer3
→ currentness integration
做到这里的时候,我突然发现:
我已经不再是在恢复一个翻译任务了。
我是在建设一整套系统,用当前代码重新证明过去程序是如何生成历史结果的。
这两件事情的工程复杂度完全不是一个量级。
原本一天左右可能解决的问题,最后花了四天左右
这是这次最让我后悔的地方之一。
现在回过头重新看,我认为:
如果当时直接采用已有的历史断点,先接受当时合法的 32/32 validation PASS,再判断有没有真正的内容语义失效,最后只 reopen 真正 affected 的 TranslationUnit,
这一次的问题很可能一天左右就可以解决。
至少不会发展成连续几天都在建设 recovery infrastructure。
但实际情况是:
为了证明为什么可以继续使用一个已经 PASS 的历史成果,又花了大约四天时间继续完善基础设施。
而且到了现在,相关 implementation 还没有完全结束。
这就很荒谬。
历史断点存在的目的,本来就是:
不要重新做已经证明过的事情。
结果这一次,为了重新证明“这个历史断点为什么可以继续使用”,又造出了一套比原恢复流程复杂得多的机制。
Fail-closed 被理解过头了
我的多语言项目一直强调一个原则:
fail closed。
如果不能确定一个 mutation 是安全的,就不要盲目执行。
这个原则本身没有问题。
甚至我认为它是这个项目能够长期维护的重要基础。
真正的问题是:
这一次,fail-closed 被理解过头了。
它逐渐变成了:
如果当前 implementation 无法重新证明历史结果,就必须继续完善基础设施,直到可以重新证明。
现在回头看,这两件事情其实完全不是一回事。
Fail-closed 应该意味着:
不能证明新的操作安全,就不要执行新的操作。
但它并不意味着:
过去每一个已经合法 PASS 的结果,都必须能够被未来版本的程序重新形式化证明。
如果一个历史结果当时已经具备:
- 正式 Generation;
- validation PASS;
- identity;
- SHA;
- provenance;
- 后续没有与它冲突的 mutation;
- 没有证据证明内容语义已经失效;
那么这个历史 PASS 本身就应该是一个可信恢复断点。
implementation changed
不应该自动等价于:
historical content invalid
这是两个完全不同的概念。
每一个 blocker 都能继续造下一层,这是最危险的地方
这次真正让我警惕的,并不是某一个具体设计特别糟糕。
相反,很多设计单独拿出来看,其实都很合理。
比如:
历史 construction bytes 不够?
那就补 historical construction authority。
发现 declaration 同时承担多个职责?
那就设计 mixed-role attribution。
发现 approvedResponseInputs 无法安全归类?
那就设计 role-separated producer。
发现 role equality 不够严格?
那就设计 semantic IR。
发现 batch-level delta 无法准确决定 affected Unit?
那就继续设计 per-Unit dependency proof。
每一步都能给出很充分的技术理由。
每一步都能解释:
“为什么做这件事是合理的。”
但真正缺失的问题是:
我们为什么还在继续做这件事?
这个问题反而没有不断重新检查。
于是就形成了一条非常危险的路径:
修一个 blocker。
发现另一个 blocker。
为了让上一层设计成立,再建设下一层。
下一层又出现新的 formal gap。
继续补。
技术上每一步都能自洽。
整体上却已经完全偏离了项目真正的目标。
自己造轮子的一个危险:什么都可以继续完善
自己开发基础设施有一个非常明显的优势:
可控。
我可以控制:
- schema;
- identity;
- retry;
- QC;
- recovery;
- currentness;
- provenance;
- CAS;
- activation;
- historical evidence。
当项目出现特殊情况时,我也可以继续修改系统,让它覆盖新的场景。
这当然很有吸引力。
但这次让我更加明确:
自己造轮子的最大问题,恰恰也是:
什么都可以继续完善。
没有一个天然边界告诉你:
“够了。”
成熟框架往往会迫使开发者接受一些已有设计。
自己写的时候则很容易产生另一种思维:
既然这个问题理论上可以解决,那就把它解决掉。
问题是:
理论上可以解决,不代表在当前项目里值得解决。
工程设计如果没有停止线,就很容易无限向下展开。
这不是第一次发生
更让我难受的是:
这并不是我第一次遇到基础设施过度设计的问题。
以前在博客网站的历史文章翻译中,我就经历过一次类似情况。
当时同样是为了:
- 翻译一致性;
- 自动化;
- 恢复能力;
- 状态管理;
不断完善翻译基础设施。
后来才发现:
大量时间都被基础设施本身吃掉了。
正因为有过那次经历,所以这次做多语言项目时,我其实已经下意识地尽量避免再次发生。
项目中甚至已经形成了不少原则:
- trusted outputs 不重做;
- failure 先 recover;
- 有历史断点就从断点继续;
- Generation 与 Reviewer 严格隔离;
- 不为了极限性能无限优化;
- Codex 配额优先用于 correctness blocker;
- 合法 PASS 不重复验证。
理论上,这些原则本来应该能够防止同样的问题再次出现。
结果还是出现了。
而且我感觉:
这一次比博客历史文章翻译时更加严重。
因为这一次基础设施不仅消耗了开发时间。
它还直接阻塞了 v2 上线。
Codex 的周额度让这个问题变得更加严重
如果只是本地开发时间增长,问题可能还没有这么明显。
但现在还有一个非常现实的约束:
Codex 是有额度的。
我使用的是 ChatGPT Plus。
不仅有 5 小时窗口额度,更重要的是还有周额度。
5 小时额度用完,过几个小时还能恢复。
但如果周额度被大量基础设施工作消耗掉,整个项目真正需要 Codex 的时候就会直接停下来。
而最近大量额度消耗在:
- 复杂 workflow 的理解;
- formal contract;
- historical reconstruction;
- static analysis;
- semantic IR;
- projection;
- dependency proof;
- design review;
- successor review;
- recovery infrastructure implementation。
这些工作当然不是毫无价值。
但问题是:
真正的翻译、修订、审核和上线,同样需要这些额度。
于是出现了一个非常荒谬的结果:
为了让翻译流程变得更加可靠,最后把能够继续推进翻译项目的模型额度消耗掉了。
基础设施本来应该提高吞吐量。
如果一个基础设施持续降低整个项目的吞吐量,那它就应该被重新审视。
5 小时额度不是唯一的问题,周额度才是真正的瓶颈
前面我一度更多关注 Codex 的 5 小时额度。
后来才越来越明显地意识到:
真正重要的其实是周额度。
如果只是一个 5 小时窗口用完,等下一轮恢复以后还可以继续。
但如果一个复杂基础设施任务连续消耗几个窗口,那么真正受到影响的是整个星期的开发能力。
而我现在还有:
- zh-CN Site v2;
- 后续其它 locale;
- Generation;
- Reviewer;
- activation;
- Production;
- 其它多语言项目研究;
这些都需要 Codex。
如果大量周额度都花在一个本来可以从历史断点恢复的问题上,那么从整个项目组合来看,投入产出是非常差的。
所以以后 Codex quota 不能再被当成“工具内部限制”。
它应该被当作:
正式的项目资源。
就像开发时间一样,需要预算。
Co-op Translator 为什么开始显得越来越有价值
这也是为什么最近我越来越觉得 Microsoft 的 Co-op Translator 可能很有价值。
不是因为它一定能够满足我的全部需求。
也不是因为它一定比我自己设计的方案更严格。
而是因为成熟框架有一个经常被忽略的优势:
它给基础设施复杂度设置了一个外部上限。
如果使用现成框架,遇到问题的时候,更容易考虑:
怎样在这个框架已有能力范围内解决?
而自己造轮子时,更容易考虑:
我要不要再增加一个新的 schema?
要不要再增加一个 authority?
要不要再增加一种 currentness proof?
要不要再设计一套 projection?
要不要再建立一个 IR?
每一个答案都可能是“可以”。
然后项目就越来越重。
这次让我更加明确地认识到:
可控性不是免费的。
过度追求可控性,本身可能变成项目最大的不可控因素。
这不意味着现在马上迁移到 Co-op Translator
当然,这个教训也不能走向另一个极端。
如果现在因为觉得基础设施太重,就马上启动:
“把整个 go-tour-i18n 迁移到 Co-op Translator”
那很可能又会变成一个持续几周的大型基础设施工程。
这就只是换一种方式重复同样的问题。
所以我现在更倾向于:
现有 go-tour-i18n 不立即迁移。
已经存在的成熟能力继续使用。
但未来新的多语言项目,在从零设计 infrastructure 之前,要优先问:
Co-op Translator 或其它成熟方案,能不能直接承担 70%~80% 的基础能力?
如果可以:
就尽量复用。
项目自己只维护薄薄的一层特殊逻辑。
而不是默认重新造一整套 translation infrastructure。
历史 PASS 应该真正成为断点
这次以后,我认为仓库中需要正式强化一个规则:
Historical PASS First
一个合法、完整、没有后续冲突 mutation 的历史 PASS,默认应该被视为可信恢复断点。
除非有明确证据证明:
- canonical source 已发生语义变化;
- membership 已发生相关变化;
- protected representation 已失效;
- dependency 已真正改变;
- validation 已明确判定历史输出不合法;
否则:
不要要求当前 implementation 重新证明 historical implementation semantics。
尤其不能因为:
- implementation SHA 变化;
- decoder implementation 变化;
- producer implementation 变化;
- workflow code 变化;
就自动认为历史翻译成果需要重新证明。
这应该成为真正的默认恢复策略。
新建基础设施必须成为最后一个选项
以后遇到 recovery blocker,决策顺序应该变成:
第一步:
有没有合法历史 PASS 可以作为断点?
第二步:
有没有明确证据证明历史内容已经语义失效?
第三步:
如果确实失效,能不能只 reopen exact affected scope?
第四步:
已有 recovery / retry / successor 机制能不能解决?
只有这些都无法安全继续时,
才允许考虑:
新增基础设施。
也就是说:
新增基础设施应该成为最后一个选项。
而不是看到 current proof 构建失败以后,第一反应就是继续完善 proof system。
必须增加“基础设施复杂度停止线”
这一次之后,我准备把相关规则正式加入仓库文档和项目指令。
尤其需要增加一条:
Mandatory Complexity Reassessment
如果一个局部翻译或者 recovery 问题出现以下任一情况:
- 预计工程时间超过 1 个工作日;
- 需要增加新的 formal schema;
- 需要增加新的 authority;
- 需要修改 currentness;
- 需要修改 mutation / CAS semantics;
- 需要建设 AST / IR / static analysis;
- 需要增加新的 historical reconstruction mechanism;
- 一个 blocker 连续产生第二层新的 infrastructure blocker;
就必须强制停止。
然后重新检查:
有没有历史断点可以恢复?
不能继续沿着当前基础设施路线一路向下补。
这一步以前缺少真正的硬停止线。
这次必须补上。
已经花了很多时间,不是继续做下去的理由
另外还需要明确一个原则:
沉没成本不能成为继续开发某个 subsystem 的理由。
比如:
“已经做了三天了,再做两天做完整吧。”
这种想法非常危险。
因为它很容易把一次已经成本失控的技术路线,继续拖成更大的损失。
如果新的评估发现:
- 历史 PASS 可以安全恢复;
- 未完成的 infrastructure 对未来多个 locale 没有明显收益;
那么就应该允许:
冻结未完成的基础设施。
而不是因为已经投入很多,就一定要把它做完。
Codex 也必须重新划分职责
这次还有一个很明显的问题:
一些本来可以由 Local terminal 完成的 deterministic 工作,也消耗了 Codex 额度。
以后分工必须更加严格。
Codex 主要用于:
- 必须修改 repo code/docs 的 engineering;
- correctness blocker;
- 能够跨 locale 复用的真实缺陷;
- activation / Production blocker。
Maintainer Local terminal 负责:
- read-only inspection;
- inventory;
- currentness reassessment;
- proof;
- contract verification;
- first-real-use gate;
- deterministic workflow;
- formal command execution;
- Git checkpoint;
- push。
特别是:
不能再让 Codex 大量消耗额度做可以由 Local terminal 完成的只读审核。
Codex 周额度本身就是项目资源。
必须留给真正需要代码推理和代码修改的工作。
Review 也必须有成本上限
这次甚至连 design review 都出现了过重的问题。
第一次高风险 formal design 做完整独立审核,我认为是合理的。
因为第一次 review 确实找到了真实问题。
但是 successor review 不应该每次都重新完整审整个体系。
以后默认应该只检查:
- 上一轮 findings;
- 本次修改区域;
- bounded regression surface。
已经合法 PASS,而且没有被本次 revision 触碰的部分,不应该重新做 full review。
否则“为了确保质量”的审核本身,也会成为新的资源黑洞。
翻译项目最终还是应该以内容吞吐量衡量
这次让我重新思考了一个最基本的问题:
一个翻译项目,到底应该如何衡量进展?
以前我很容易关注:
- workflow 是否完整;
- authority 是否严格;
- currentness 是否可证明;
- recovery 是否足够可靠;
- provenance 是否完整。
这些都重要。
但现在我觉得还必须增加一个更简单、更直接的指标:
到底有多少内容真正完成了翻译、审核并上线?
如果一个翻译项目连续几个星期都在完善 infrastructure,
而真正上线的翻译内容没有明显增长,
这本身就应该被视为一个 architecture warning。
不是“项目正在认真打基础”。
而是:
项目可能已经偏离主目标。
翻译项目首先应该是翻译项目
一个翻译项目最核心的主流程,最终还是应该非常简单:
翻译 → 验证 → 审核 → 上线
基础设施存在的意义,是让这四个步骤:
- 更可靠;
- 更快;
- 更容易恢复;
- 更容易跨 locale 复用。
而不是让这四个步骤变得越来越难发生。
如果一个基础设施系统最终需要花几天时间证明:
“为什么一个已经 validation PASS 的翻译成果还可以继续使用”,
那么就应该停下来重新思考:
我们到底是在解决什么问题?
这次 Site v2:
最开始我预计:
5 个工作日。
后来发现范围扩大以后:
20 个工作日,我也勉强可以接受。
现在:
已经超过 30 个工作日。
而且简体中文 v2 仍然没有上线。
甚至什么时候能够上线,现在依然无法确定。
这个结果对我来说已经足够说明:
基础设施设计已经失控。
这次教训的成本非常高。
不只是时间。
还有 Codex 的 5 小时额度。
还有更加宝贵的周额度。
还有被推迟的其它语言和其它项目。
所以这一次不能只是写一篇博客总结一下,然后以后继续照旧。
我准备把这次教训真正写进:
- 仓库文档;
- workflow;
- 项目指令;
- recovery 决策原则;
- Codex 使用规则。
因为我已经在博客历史文章翻译中遇到过一次类似问题。
如果这一次还不形成真正的停止线,
以后仍然可能第三次发生。
而第三次,我不想再付同样的代价了。
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 官方无隶属、授权或背书关系。
