最近几天,我继续为 A Tour of Go 多语言翻译项目增加新的语言版本。
继意大利语之后,又陆续完成了:
- Dutch — Nederlands
- Brazilian Portuguese — Português (Brasil)
- Turkish — Türkçe
到目前为止,项目已经有 10 个正式上线的社区语言版本:
Brazilian Portuguese、Dutch、French、German、Italian、Japanese、Korean、Simplified Chinese、Spanish 和 Turkish。
English 仍然直接指向 Go 官方的 A Tour of Go。

如果只是从最终结果来看,这几门语言的上线过程已经越来越相似了。
初始化 locale、建立 glossary、翻译 TranslationUnit、Automatic Validation、Quality Check、Locale Surface Review、Preview、Production、搜索引擎收口……
整个流程已经基本固定。
这也是为什么我后来不太想继续采用“每增加一门语言,就单独写一篇博客”的方式。
真正值得记录的,已经不是:
荷兰语是怎么上线的?
或者:
土耳其语又执行了哪些命令?
而是在连续重复执行几轮以后,还有哪些原本不容易发现的边界问题,被真实 Production 使用暴露出来了。
最近这三门语言,正好又让我把几个细节继续往前收了一轮。
一、流程已经稳定,但 Git 提交仍然很多
把最近几天的 Git 提交放在一起看,会很有意思。

里面当然有大量正常的 locale 生命周期记录:
完成 nl-NL TranslationUnit 工作流
完成 nl-NL Locale Surface Review A
完成 nl-NL preview 验收
完成 nl-NL 首次生产收口
完成 pt-BR TranslationUnit 工作流
完成 pt-BR Locale Surface Review A
完成 pt-BR preview 验收
完成 pt-BR 首次生产收口
完成 tr-TR TranslationUnit promotion
完成 tr-TR Locale Surface Review A
完成 tr-TR preview 验收
完成 tr-TR 首次生产收口但夹在这些常规步骤中间的,还有不少 fix:
让首次生产浏览器验收复用 zgocloud SOCKS
修复首次生产 SOCKS 隧道生命周期
强化首次生产审核证据校验
增加 Locale Surface Review 审核包导出
固化 Quality Check revision lineage
修复 production 预渲染目录校验
修复多语言页面视觉对齐与输出换行这正好说明了现在这个项目所处的阶段。
大的架构已经不需要每增加一门语言就重新设计,但是只有不断重复跑真实流程,才会逐渐遇到一些以前样本不够多时很难暴露的小问题。
二、Production 浏览器验收,也要考虑公网链路本身的稳定性
之前我已经处理过一个 Production 公网验收问题。
我的开发机器位于中国大陆,而大部分新增语言站点都使用 Cloudflare。实际使用过程中发现,中国大陆到 Cloudflare 的公网连接并不总是稳定,尤其在晚高峰时期,更容易遇到连接失败、超时等瞬态问题。
这会带来一个麻烦:
页面和 Production 本身可能完全正常,但一次公网验收恰好遇到网络波动,就会被判定失败。
所以后来我把大量公网 machine acceptance 放到了 zgocloud 上直接执行,让真正访问 Cloudflare 的请求尽量走境外公网链路。
但继续上线 nl-NL 后,又暴露出了另一个细节:
Headless Chrome 的浏览器验收也应该使用同一套稳定的公网路径。
如果 machine acceptance 已经通过 zgocloud 验证,而随后 Headless Chrome 又直接使用维护者电脑当前的公网出口访问 Cloudflare,那么同一次 first-production 流程实际上混用了两条不同的网络路径。
本地直接访问验证通过,本身当然也是有效的 Production 验证。
真正的问题是,在我的实际网络环境中,这条中国大陆 → Cloudflare 的链路更容易受到瞬态网络波动影响,从而产生并非站点自身故障导致的 false failure。
所以后来的 first-production 流程又进一步统一:
浏览器验收显式复用当前 first-production invocation 已经建立的 zgocloud SOCKS endpoint。
这样:
public-machine
↓
zgocloud
↓
Cloudflare
browser
↓
zgocloud
↓
Cloudflare公网机器验收和 Headless Chrome 浏览器验收尽量使用一致的境外公网路径。
这里并不是要摆脱维护者电脑。
整个流程仍然由本地机器发起,本机必须能够正常连接 zgocloud。如果本地到 zgocloud 的连接失败,Production acceptance 同样会失败。
真正减少的是另一类不确定性:
避免因为中国大陆到 Cloudflare 的公网链路在某个时刻发生波动,让正常的 Production 被误判为失败。
三、隧道能建立,还要保证它能活完整个 Production 流程
pt-BR 上线时又继续暴露了一个非常实际的问题:
SOCKS 建立成功,并不代表它能一直活到 browser acceptance。
first-production 并不是只运行几秒钟。
中间还有:
preflight
→ infrastructure
→ Playground Origin
→ deploy
→ direct-origin acceptance
→ DNS / CDN
→ public-machine
→ browser某些阶段可能持续比较长。
如果 SSH ControlMaster 或 SOCKS tunnel 自己存在较短的 idle deadline,就可能出现:
前面的阶段都正常,等真正轮到浏览器验收时,隧道已经没了。
所以后来又调整了 ControlMaster 生命周期。
不再依赖较短的 idle 超时自动维持,而是由当前 first-production invocation 明确管理:
创建 → 使用 → 成功、失败或者 signal 退出 → cleanup。
这类问题其实很典型。
单独测试 SOCKS:
能连上完全不等于:
在整个 Production 生命周期里都可靠。只有真的连续跑几轮新 locale,才比较容易发现这种生命周期问题。
四、Quality Check revision:不是重新审核全部 122 个 TranslationUnit
土耳其语这一轮还出现了正式 revision。
最终的 Quality Check scope 是:

locale=tr-TR
snapshot=qc-002
total=122
current=13
carry_forward=109
pending=0
A/B/C/D=122/0/0/0
ready_for_finalization=true这里我觉得很能体现现在 Quality Check 工作流的变化。
第一轮已经是 A,而且 TranslationUnit identity 完全没有变化的 109 个 Unit,并不需要为了 revision 再重新审核一次。
它们可以 carry-forward。
真正重新翻译、重新生成 candidate,从而 identity 发生变化的 13 个 Unit,才进入新的 Quality Check。
最终仍然必须满足:
A = 122
B/C/D = 0
pending = 0才允许进入 machine finalization。
这既没有降低 A-only 的质量门槛,也避免了每次修改十几个 Unit,就机械地把另外一百多个完全没有变化的 Unit 再审一遍。
五、但 carry-forward 又带来了一个新的问题:revision lineage 必须被固定
有了 carry-forward 后,就出现了另一个必须严格定义的问题:
qc-002 到底是在谁的基础上做 revision?
例如:
qc-001
↓
revision
↓
qc-002如果 qc-002 已经开始写 Quality Check result 后,还允许事后把 previous_snapshot_id 换成另外一个 Snapshot,整个 carry-forward 的证据链就会变得不可靠。
所以后来又把这一点正式固化下来:
revision Snapshot 第一次成功写入 Quality Check result 时,lineage identity 就确定。
后续可以省略 predecessor,让系统继续使用已经持久化的 lineage。
但如果显式提供,就必须 exact match。
不能:
原来 qc-002 → qc-001后来又改成:
qc-002 → qc-000也不能靠人工修改 JSON 来补证据链。
甚至 cyclic lineage 也必须 fail closed。
例如:
qc-001 → qc-002
qc-002 → qc-001这种情况不能被 machine finalization 接受。
我现在越来越觉得,这类 workflow 的重点并不是“多生成几个 JSON 文件”,而是确保:
最终 A 是怎么得到的,机器可以沿着一条确定的 lineage 重新解释。
六、Locale Surface Review 也不再靠临时拼审核材料
TranslationUnit Quality Check 解决的是 Page 和 eligible Example 的语言质量。
但一个完整 locale 还有很多东西不属于 TranslationUnit:
- 公共 UI;
- 首页;
/tour/;/tour/list;- 导航;
- runtime message;
- article metadata;
- course SEO metadata;
- 语言选择器;
- 其他组合后的页面表层。
这些一直由独立的 Locale Surface Review 负责。
最近这部分又做了一次比较重要的改进:
增加了一个正式的 deterministic 审核包导出入口。
例如土耳其语:

go run -mod=readonly ./cmd/tour-i18n surface-review export \
--locale tr-TR \
--output /tmp/tr-TR-surface-review.json这次实际导出的 coverage 是:
pages=103
ui=92
articles=7
translation_units=122
other_surfaces=21这个 exporter 本身并不会判断:
土耳其语翻译好不好。
它也不会自动给出 Surface Review A。
它只负责另一件事:
从当前 working tree 中,把正式审核需要看到的材料完整、确定性地提取出来。
包括 glossary、UI source ↔ target、article metadata、Page source/canonical target、course description,以及实际参与运行的 first-party runtime/template source context。
这样 ChatGPT 做 Locale Surface Review 时,不需要再靠临时拼材料,也不容易因为漏掉某个 source 文件而造成审核范围不完整。
这里我比较喜欢的一点是:
自动化负责保证“审核输入完整”,但不假装自动化等于“语言质量判断”。
两者职责仍然是分开的。
七、土耳其语又暴露了一个很有意思的 Validator 边界
tr-TR 上线时,还遇到了一个很典型的 HTML 序列化问题。
土耳其语课程目录里有这样的内容:

struct'lar】例如:
Diğer türler: struct'lar, dilimler ve eşlemeler浏览器显示完全正常。
但是 HTML 在序列化时,普通的单引号 ' 可能表现为:
'于是逻辑上完全相同的:
struct'lar在 raw HTML bytes 中可能变成:
struct'lar如果 validator 的做法是:
在原始 HTML 字节里直接搜索正式的课程标题字符串。
那页面明明是正确的,却可能被判成:
missing localized lesson content这其实和之前我在翻译 validator 中遇到的问题很像:
Validator 不是越严格越好,而是必须在正确的语义层级上严格。
这次修复后,课程目录的验证不再简单依赖 raw bytes。
而是:
解析 HTML
→ 得到 DOM
→ 读取解码后的文本
→ 与正式 title / description 比较与此同时,像:
href
canonical
render marker这些真正属于 HTML 结构的东西,仍然继续按结构验证。
这样既不会因为 ' 这样的等价 HTML entity 产生 false failure,也没有降低正式内容完整性的要求。
八、开始关注一些以前不会阻塞上线的视觉细节
连续做到十门语言以后,还有一种变化也越来越明显:
以前我的主要目标是:
页面有没有坏?
有没有横向溢出?
Run / Format / Reset 能不能工作?
Production 是否通过?
现在这些基本稳定以后,一些以前不会阻塞上线的小问题也开始变得值得处理了。
例如更长的语言标题会让 header 的视觉对齐问题更明显。
后来统一调整了 header 的 flex 对齐,而不是给 Turkish 单独写一个 CSS 特例。
另外一个比较直观的问题,是 Playground 输出。
我专门构造了一段很长、几乎没有自然换行机会的连续输出:

这种输出如果 <pre> 一直保持传统的不可换行行为,就很容易把输出区域甚至整个页面横向撑开。
后来对输出区域补充了类似:
white-space: pre-wrap;
overflow-wrap: anywhere;同时保留原始空格和换行语义。
这个改动本身很小。
但它也说明一个变化:
当核心流程稳定以后,真实多语言页面开始反过来帮助我发现共享 UI 中更细的边界问题。
九、连 GitHub README 也不想再人工维护语言列表了
做到 10 门语言以后,我最近又发现了一个很小、但长期会越来越烦的问题:
GitHub README 原来只有:
A Tour of Go 简体中文
A Tour of Go 日本語如果继续这样下去,每增加一个 locale,就得记得再去 README 手工增加一个链接。
但 Production 本身已经有正式的:
production/identity.json用来记录 locale 当前是否已经:
production_state=live首页 language registry 里也已经有:
EnglishName
Autonym
URL那就没有必要再让 README 变成第三份独立的人工语言清单。
所以现在 README 也改成了自动投影:

最终规则是:
production/identity.json 中 production_state=live
+
language registry 中的 EnglishName / Autonym
↓
GitHub README live locale list并且这个投影直接集成进了首次 Production finalization。
也就是说,新 locale 真正完成:
first-production
→ HUMAN visual gate
→ finalize
→ production_state=live以后,README 才会自动出现这门语言。
first-production 状态不会提前展示成已经正式上线。
如果 README marker 损坏、URL 漂移、registry 缺失或者 identity 无法唯一对应,则直接 fail closed。
这个优化对于现在 10 门语言可能只是少维护几行 Markdown。
但如果以后真的扩展到 20、30 门语言,意义就完全不一样了。
十、我也开始改变这个系列博客的写法
这几天还有一个比较明显的感受。
最开始做多语言时,一门新语言本身就可能带来很多新问题。
所以当时我的想法是:
每新增一门语言,只要有新的流程优化,就写一篇博客。
但现在继续这样写,越来越容易重复。
因为正常的新 locale 已经基本都是:
初始化
→ TranslationUnit
→ Quality Check
→ Locale Surface Review
→ Preview
→ Production
→ 搜索引擎 closeout如果没有新的工程问题,再单独写:
荷兰语上线记录
巴西葡萄牙语上线记录
土耳其语上线记录
实际价值已经不大。
所以后续我更倾向于换一种节奏:
连续上线几门语言以后,再集中整理这段时间真正发生的流程变化。
如果中间遇到一个足够独立的问题,例如之前的 Headless Chrome Production false failure、IndexNow,仍然单独写专题。
这样既不会为了“每门语言都要留一篇记录”制造大量相似文章,也更容易把真正值得长期保留的技术问题讲清楚。
总结
从 nl-NL、pt-BR 到 tr-TR,这三门语言本身的上线过程已经没有太大的架构变化。
真正变化的是一些越来越细的边界:
浏览器验收到底走哪条网络
SOCKS tunnel 能不能活完整个 Production invocation
Quality Check revision 如何固定 lineage
A carry-forward 怎样保证证据可靠
Locale Surface Review 如何获得完整审核输入
HTML entity 应该在什么语义层级验证
多语言长文本如何继续暴露共享 UI 问题
新增 locale 后还有哪些人工同步工作可以消掉这些问题单独看都不像最初设计整个多语言架构时那么“大”。
但我反而觉得,这正是一个流程逐渐进入长期维护阶段后的正常状态。
最开始解决的是:
能不能把一门新语言正确上线。
现在越来越多的问题变成:
怎样让第 10、第 20、第 30 门语言继续使用同一套流程,而且尽可能少依赖人的记忆、临时操作和偶然环境。
截至目前,A Tour of Go 多语言项目已经正式上线 10 个社区语言版本。
接下来我仍然会继续增加新的 locale。
但这个系列的博客更新方式,也会逐渐从“每新增一门语言写一篇”,转向:
出现新的真实问题、形成新的工程改进,或者达到新的阶段节点时,再集中记录。
这样应该更适合这个项目接下来的长期扩展。
A Tour of Go 多语言翻译项目
本系列完整记录 A Tour of Go 多语言翻译项目从架构设计、整页翻译、结构保护、自动校验,到生产发布与后续维护的实际开发过程。
项目入口:
✅ 简体中文:A Tour of Go 简体中文版
✅ 日语:A Tour of Go 日语版
✅ 德语:A Tour of Go 德语版
✅ 法语:A Tour of Go 法语版
✅ 项目源码:GitHub:shuijingwan/go-tour-i18n
当前已上线简体中文、日语、德语和法语版本。项目属于非官方社区多语言翻译项目,与 Go 官方无隶属或授权关系。
