这次对 A Tour of Go 多语言项目的 Production 流程做进一步优化,起因其实很简单:Go 官方终于把我维护的 4 个翻译站链接加入了官方网站。
法国、德国、韩国和简体中文这 4 个站点进入官方 A Tour of Go 的其他语言列表之后,我自然需要再次同步 golang/website 上游。
原本我以为这只是一次普通的 upstream sync。
真正开始执行之后才发现,随着目前已经上线的语言站增加到 10 个,“同步一次上游,然后把所有站点重新发布上线”这件事情,本身已经值得继续标准化。
最终,这次维护把原本分散的 Production 操作进一步收敛到了一个命令。
起点:Go 官方加入了 4 个翻译站链接
Go 官方网站仓库合并了 _content/tour: add translations,新增了 French、German、Korean 和 Simplified Chinese 四个 A Tour of Go 翻译入口。

golang/website commit,新增 4 个翻译站链接】对我来说,这次变化还有另一层意义。
之前这些语言站只是我自己维护和推广的社区翻译站。进入 Go 官方页面之后,官方 upstream 本身开始包含指向这些站点的链接。
因此,我自己维护的 fork 也必须重新同步上游,否则项目中的 source baseline 就会落后于官方。
随后,我的仓库完成了两步:
59b8b55:
chore: 同步官方 Go Tour 上游基线
以及:
852f2cf:
chore: 完成官方上游同步后的多语言发布准备

从图里也能看到,这次同步并不只是改一行链接。
除了 upstream metadata、Tour 页面之外,还涉及不同语言的 Locale Surface Review evidence、Production 相关测试和发布准备。
这也是多语言项目越来越大以后很明显的一个变化:一个很小的 upstream diff,最终可能需要传播到所有正式语言站。
问题很快从“怎么同步”变成了“怎么重新发布全部站点”
同步 upstream 本身并不是这次最麻烦的部分。
真正让我开始继续优化流程的,是后面的 Production 发布。
目前项目已经有 10 个正式 live locale:
简体中文、日语、德语、法语、韩语、西班牙语、意大利语、荷兰语、巴西葡萄牙语和土耳其语。
如果每次共享源码发生变化之后,都要手工逐个完成 publish、deploy、CDN purge、machine acceptance 和 browser acceptance,那么随着 locale 数量继续增长,维护成本会越来越明显。
而且这种工作有一个特点:绝大多数步骤并不需要重新“思考”,它们本质上都是确定性的 Production 操作。
真正需要解决的问题因此变成了:
怎样把已经验证过的 Production gate 保留下来,同时把重复的机械执行收敛成一个正式工作流?
这几天的 Git 历史正好记录了这个演进过程。

从时间线上可以看到:
先同步官方 Go Tour upstream,然后完成多语言发布准备;随后开始自动化 Production CDN purge 和批量维护;再把 Production 发布与 shared assets 纳入批量流程;最后继续完善批量恢复和 shared assets verification。
这不是一开始就设计出来的一套“大而全”自动化。
相反,它是在真实 Production 使用过程中,一层一层收敛出来的。
最终只需要一个正式入口
现在,对于已经 live 的全部语言站,一次完整的 maintenance release 可以从下面这个命令启动:
scripts/production-release-batch.sh \
--all-live \
--output-root /tmp
--all-live 命令启动完整批量发布】今天这次真实运行中,命令首先导出了 shared assets:
files=14 bytes=971973随后进行 assets validation,然后自动进入 publish。
10 个 locale 会依次完成 projection/build、production binary build、Chrome prerender、runtime validation 和 manifest/checksum。
例如每个正式语言版本都会再次确认:
ready=122
pending=0
blocked=0
pages=103
articles=7整个 publish 阶段本身就持续了相当长时间。日志中最终记录:
stage=publish PASS elapsed=1288.3s这也说明,所谓“一键发布”并不是把原来的严格流程删掉。
恰恰相反,是把原本需要维护者反复手工调用的正式步骤组织到了同一个 orchestration 里面。
一个命令,不代表跳过 Production gate
批量发布之后,流程还会继续做正式 Production maintenance。
其中 Phase A 负责 deploy 和 CDN purge。
以中间的韩语站为例,部署过程中仍然会检查 release SHA-256、远端 staging、权限、远端 SHA-256、systemd service health、连续 HTTP 200,以及公网访问。
部署完成后再进行对应 hostname 的 CDN purge。
然后才进入下一个 locale。

这里我后来专门继续优化了一件看起来很小、实际却非常影响使用体验的事情:进度输出。
长时间 Production workflow 如果只不断打印 curl、SSH 或部署细节,维护者很容易不知道:
现在到底在第几个语言?
上一站有没有完成?
还有几个?
当前是 deploy、purge,还是后面的 machine acceptance?
因此现在会明确输出:
[phase A] locale=ko-KR 5/10 stage=purge PASS
[batch] phase=A completed=5/10 remaining=5
[phase A] locale=es-ES 6/10 stage=deploy START这类输出本身不会改变 Production 结果,但对于一个可能持续几十分钟的正式流程来说,可观测性非常重要。
Phase B 仍然逐站进行 machine 和 browser acceptance
完成 deploy 和 purge 并不意味着发布结束。
后面的 Phase B 还要继续执行 machine acceptance 和 browser acceptance。
Machine acceptance 中会检查 release identity、remote identity、正式路由、CDN cache observation、HTML identity、sitemap、socket boundary 等。
Browser acceptance 则继续检查 desktop routes、mobile 页面,以及 Run、Format、Reset、SPA 和广告相关行为。
也就是说,现在的“一键”只是减少维护者重复输入命令,并没有把验收标准简化成一句:
“首页 HTTP 200 就算上线成功。”
这也是我希望这套自动化一直保持的边界。
自动化的目标是减少机械劳动,而不是降低 Production 标准。
一次真实的 curl exit 28,反而证明了重试机制的价值
今天这轮 10 语言正式 Production 还有一个很有意思的小插曲。
轮到荷兰语 nl-NL 执行 sitemap verification 时,其中一个 URL 第一次请求出现:
curl-exit-28
HTTP-000日志中可以看到:
attempt=1/5
reason=curl-exit-28
next=retry
backoff=1s随后第二次请求恢复:
attempt=2/5 PASS最终结果仍然是:
sitemap URLs: 105/105
host mismatch: 0
HTTP failure: 0
PRODUCTION MACHINE ACCEPTANCE: PASS
这次 failure 不是为了测试而人为制造的,而是在真实 Production 中自然出现的网络瞬态问题。
这也是我前面一直在完善 bounded retry 的原因。
公网、CDN、跨境网络和第三方 API 都不可能保证每一个请求永远第一次成功。
如果一个短暂超时就立即让整个十语言批次失败,会产生大量误报;但如果脚本无条件吞掉错误继续往下走,又会破坏 fail-closed 原则。
比较合理的做法是区分:
短暂网络异常可以在有限次数内 retry;
恢复成功需要明确记录 recovered;
如果最终仍然失败,则正式 gate 依然不能通过。
这次真实的 curl exit 28 → recovered → PASS,至少说明当前这个方向是有实际价值的。
最终结果:10 个语言全部完成
这次完整 maintenance release 最后的结果是:

10 个 locale 的:
deploy
purge
machine
browser
全部 PASS。
最终汇总为:
PASS=10
FAILED=0
SKIPPED=0
PENDING=0Shared assets 同样完成:
export PASS
deploy DEPLOYED
purge PASS
verify PASS最后得到:
PRODUCTION RELEASE BATCH: PASS
locale maintenance: COMPLETE日志中的最终汇总还把每个 locale 的 publish、deploy、purge、machine、browser 状态分别列了出来,因此即使未来某一个语言失败,也能够明确知道 failure 位于哪个阶段,而不是只得到一个笼统的“批量发布失败”。
这次优化真正解决的不是“少敲几个命令”
如果只是为了少输入几行 shell,其实没有必要花这么多时间完善这一套流程。
我更看重的是另外几件事。
随着语言站数量不断增加,一个操作能不能线性扩展,会越来越重要。
现在是 10 个 locale。
如果以后变成 20 个、30 个甚至更多,那么任何需要维护者“逐站手工执行”的步骤都会逐渐成为瓶颈。
而且,手工执行越多,越容易出现另一类问题:
不是代码有 bug,而是某个语言忘记 deploy、某个 CDN 忘记 purge、某个 acceptance 没有执行。
把这些步骤收敛成正式 workflow 之后,维护者的职责开始发生变化。
以前更像是:
“记住所有命令,并且一条一条正确执行。”
现在则更像是:
“启动正式 workflow,观察明确的阶段、进度和 failure evidence,在真正需要人工判断的时候介入。”
我认为这才是这次优化更重要的价值。
自动化越多,反而越需要保留清楚的人工边界
这段时间做 A Tour of Go 多语言项目,我越来越倾向于把工作分成两类。
一类是机器非常适合做的事情:
build、checksum、publish、deploy、CDN purge、HTTP verification、browser automation、状态汇总。
另一类是不能因为自动化方便,就让机器替代的判断:
翻译质量审核、Production HUMAN visual gate,以及遇到真正异常之后对 failure evidence 的判断。
所以我现在并不追求“所有事情一个命令全部做完”。
我更希望实现的是:
能机械化的部分尽可能可靠地机械化;
需要人工判断的 gate 则明确保留下来。
这两者并不冲突。
反而只有机械操作越来越稳定之后,我才可以把更多时间放在真正值得人工投入的地方。
从一次 upstream sync,继续推动整个 Production 流程成熟
回过头看,这次事情的起点其实只是 Go 官方新增了 4 条翻译链接。
我同步了一次 upstream。
然后因为需要重新发布全部语言站,开始重新审视现有 Production workflow。
接下来又陆续完善了 CDN purge、批量 maintenance、shared assets、恢复机制、实时进度和 failure diagnostics。
最终,10 个正式语言站可以从一个命令开始,自动完成整套 maintenance release,并在最后给出明确的逐 locale 验收结果。
这应该不会是这套流程最后一次演进。
以后随着新的 locale 继续增加,应该还会出现新的规模化问题。
不过至少现在,“官方 upstream 又更新了,需要重新发布所有正式语言站”这件事,已经不再意味着我要手工重复操作十遍。
对于一个计划长期扩展到更多语言的项目来说,这一步很重要。
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 官方无隶属、授权或背书关系。
