A Tour of Go 法语版完成以后,我原本已经可以马上开始下一门韩语。
毕竟到了这个阶段,“新增一门语言应该怎么做”已经不是未知问题。
在此前的日语、德语和法语过程中,TranslationUnit、automatic validation、Quality Check、Final Review、promotion、Locale Surface Review、production 部署等主要流程都已经逐渐固定下来。
尤其是在上一篇《A Tour of Go de-DE 上线复盘:第三门语言用了约 16 小时,下一门如何压到 8 小时?》里,我已经把下一阶段的目标写得比较明确:
不是继续降低质量标准,而是减少没有判断价值的人工操作。
随后完成的法语版,也进一步验证了这一方向。
按照 Git 记录,从 2026 年 8 月 30 日 11:59 左右出现 fr-FR 第一笔初始化与首轮翻译提交,到当天 21:37 完成首次 production 验收,墙钟跨度大约 9 小时 39 分钟。
我当时大体上把它记作:
约 9 小时。
这个结果已经非常接近此前给下一门语言设定的 8 小时目标。
按正常思路,接下来似乎应该马上开始韩语,看看能不能继续压缩到 6~8 小时。
但我没有这么做。
法语完成以后,我又停下来专门处理了一轮流程问题。

从 Git 时间线看,这一段非常清楚。
2026 年 8 月 30 日 21:37:
docs: 完成 fr-FR 首次生产验收一个多小时以后,23:15:
feat: 优化新增语言与首次生产流程第二天上午 09:35:
feat: 自动化新增语言首次生产流程下午 14:16:
docs: 建立 deferred issue 记录机制直到 14:58,才真正出现:
feat: 初始化 ko-KR locale也就是说,法语已经完成以后,我没有继续追求“下一门语言马上开始”。
相反,我先把法语和前几门语言已经验证过的经验,再往工具和正式规则里收了一层。
这一次的重点已经不是:
流程应该怎么设计?
而变成了:
既然流程已经基本确定,为什么还要让我每增加一门语言都重复做这些事情?
从“标准流程”继续往前走一步
这两个阶段其实很容易混在一起。
所谓流程标准化,解决的是:
下一门语言应该按照什么顺序做?
例如:
locale 准备
→ glossary
→ 公共 UI 与 metadata
→ TranslationUnit 翻译
→ automatic validation
→ Quality Check
→ revision
→ Final Review
→ promotion
→ SEO metadata
→ preview
→ Locale Surface Review
→ publish
→ first production
→ production acceptance这些步骤在前面的几门语言中已经逐渐明确下来。
但有标准流程,并不代表执行成本已经足够低。
举个最简单的例子。
假设每增加一门语言,我都已经知道需要准备:
locale.json
glossary.yaml
UI catalog
article metadata
course metadata inventory
status.tsv
语言 registry
……那么“知道这些文件需要存在”,属于流程标准化。
但是如果第五门、第六门语言开始时,我仍然需要:
复制旧目录
→ 修改 locale
→ 删除旧语言内容
→ 检查有没有遗漏
→ 创建 status
→ 检查 UI key
→ 再检查有没有把旧 locale 内容带过来这部分仍然是重复劳动。
而且这种重复劳动不只是慢。
它还有另一个问题:
非常容易把上一门语言的状态、内容或者历史假设一起复制过去。
所以法语完成以后,我首先处理的,就是新增 locale 的“机械骨架”。
新 locale 不再从“复制上一门语言”开始
现在新增语言的正式入口已经变成了一个明确命令:
go run -mod=readonly ./cmd/tour-i18n locale init \
--locale <locale> \
--language-name <autonym> \
--english-name <English-name> \
--html-lang <html-lang>
locale init 命令这一点看起来只是增加了一个 CLI 命令,但实际上它改变了新增语言的起点。
以前更接近:
找一门现有语言作为参考,然后人工搭出下一门语言。
现在则变成:
从正式 catalog 和英文 source 出发,由工具生成一套明确处于“未完成”状态的机械骨架。
目前这个初始化过程会准备包括:
locale.json
glossary.yaml 骨架
UI catalog
article-metadata.json
course-metadata.todo.json
status.tsv
.locale-init-incomplete其中有一个我很喜欢的设计:
.locale-init-incomplete它的意义不是告诉开发者:
“这里还有点东西没写完,记得以后回来看看。”
而是直接把“还没有完成语言内容”变成机器能够识别的状态。
只要这个标记仍然存在,完整 build、preview 和 publish 就应该 fail closed。
换句话说:
TODO 不再只是人的记忆,而成为发布流程的一部分。
这也避免了一种很危险的情况:
刚创建完语言骨架,里面其实还是 TODO 或英文占位内容,但因为目录结构已经齐全,误以为它已经可以 build 或发布。
自动生成的是骨架,而不是翻译
这里还有一个边界很重要。
locale init 并没有试图“顺便把语言也翻译了”。
它解决的是机械问题:
哪些文件应该存在
哪些 key 应该存在
TranslationUnit inventory 是什么
status 初始状态是什么
哪些内容还没有完成但是下面这些事情仍然需要正式语言判断:
glossary 应该怎样制定
公共 UI 应该怎样翻译
article metadata 应该怎样本地化
术语在全站应该怎样统一
TranslationUnit 的正式译文是什么也就是说,我现在越来越倾向于一个原则:
能机械生成的东西尽量由工具生成,但有语言判断价值的东西不要假装自动化。
这也是这一轮“简化”与简单追求“一键生成所有东西”的区别。
我的目标不是让系统帮我猜。
而是尽量让系统不要再逼我做它自己完全可以确定完成的事情。
首次 production 也不应该每门语言重新手工拼命令
另一个明显的重复区域,是首次 production。
新增 locale 的第一次生产部署,和已有语言的日常发布并不是一回事。
第一次需要建立或者确认:
production identity
hostname
data root
service
port
Nginx
TLS
CDN
Playground Origin
AdSense production 配置
首次 release 激活
production acceptance早期这些东西之所以大量依赖 Runbook 和人工命令,是因为流程本身还没有完全稳定。
如果服务器目录结构、service 命名、Playground 代理、CDN 方案都还可能变化,那么太早把它们全部写死进脚本,反而容易增加维护成本。
但到了法语完成以后,情况已经不同。
中文、日语、德语、法语连续几门语言走完以后,哪些部分是真正共享的,哪些部分只是 locale-specific identity,已经越来越明确。
所以这时候继续让第五门、第六门语言复制几十条命令,就开始显得没有必要了。
法语完成后的第二轮大改动,就是:
feat: 自动化新增语言首次生产流程
从提交本身也能看出,这不是只添加一个几行的 shell wrapper。
这一轮涉及 17 个文件:
2085 insertions
135 deletions其中新增了:
scripts/first-production.py
scripts/first-production.sh
scripts/first-production-test.py
scripts/production-identity.py
scripts/production-identity.sh
scripts/production-identity-test.py
production/identity.json
scripts/verify-production-browser.py并继续调整:
deploy-production.sh
verify-production.sh
PRODUCTION_RUNBOOK.md
NEW_LOCALE_RUNBOOK.md其中 first-production.py 本身就有 800 多行。
这也是我认为这轮优化值得单独记录的原因之一。
如果只是把几条常用命令包装成:
./deploy.sh那很难称得上流程变化。
真正有意义的是:
production identity、首次部署和验收边界开始从“人脑理解 Runbook”继续进入机器可执行状态。
自动化不是把所有 HUMAN GATE 都删除
当然,把首次 production 自动化,也不等于:
从此运行一个脚本,什么都不用管,直接上线。
这一点我反而越来越谨慎。
例如 DNS、CDN、真实浏览器页面、某些生产环境操作,本身就可能存在外部状态。
真正应该自动化的是:
确定性检查
重复命令
固定文件生成
identity 校验
机器能够明确判断 PASS / FAIL 的验收而不是为了追求“一键部署”这个听起来漂亮的目标,把需要人确认的生产边界偷偷跳过去。
所以我现在对“自动化”的理解更接近:
让机器把问题推进到真正需要人判断的位置。
而不是:
让人彻底退出流程。
这和 TranslationUnit Quality Check 的思路其实是一样的。
我可以自动准备:
source
candidate
glossary
validation evidence
TranslationUnit identity但最后:
A / B / C / D仍然属于语言质量判断。
production 也是如此。
命令输出本身也开始成为优化对象
除了减少命令数量以外,还有一个之前比较容易忽略的问题:
命令输出太多,也会消耗人工时间。
CLI 如果每执行一步都打印几百行 JSON,看起来似乎“信息非常完整”。
但如果我真正需要判断的只是:
是否通过
失败在哪个 stage
哪些 TranslationUnit 需要处理
下一步应该执行什么那么把所有内部信息默认全部倾倒到终端,反而会增加查找成本。
这也是项目后来持续调整的一个方向:
默认输出尽量服务于下一步决策,完整 evidence 则继续保留在正式文件中。
这个方向在后面的韩语实战中还会继续推进,例如一些 retranslation 命令的默认输出后来又继续精简。
所以这里我没有把它理解成:
少打印一点,看起来干净。
而是:
终端是执行界面,evidence 文件才是长期证据。
两者没有必要承担完全相同的职责。
还有一种浪费:发现一个问题,就被迫马上解决
法语完成以后,我还专门增加了一项看起来和“自动化”关系不大的东西:
Deferred Issues它解决的是另一个长期项目里很实际的问题。
开发、测试和 production 验收过程中,经常会遇到这种情况:
我发现了一个真实问题。
但这个问题:
- 不阻塞当前 locale;
- 不属于当前任务;
- 暂时没有必要立即修复;
- 或者现在解决反而会把当前流程带偏。
以前这种时候通常只有两个选择。
第一种:
现在就解决。
缺点是很容易 scope 膨胀。
本来正在部署一门语言,最后却因为顺手发现的另一个问题,又改了半天完全不同的代码。
第二种:
先不解决。
但问题往往只留在 ChatGPT / Codex 对话里。
几天以后会话换了、上下文变了,这个真实问题也就跟着消失了。
于是我增加了:
docs/DEFERRED_ISSUES.md
这个文件的边界其实定得比较严格。
不是所有“以后可能优化一下”的想法都往里面扔。
必须同时满足:
- 问题已经在真实工作中被发现,有现象、失败结果或者其他 evidence;
- 已经明确决定当前任务暂不处理或者延后处理。
普通 TODO、未经验证的怀疑、未来架构设想,都不属于 Deferred Issue。
状态也明确限制为:
open
resolved
accepted
obsolete这意味着问题即使以后不修,也需要有明确结论。
例如:
resolved表示已经修复,并且有验证证据。
accepted表示问题仍然存在,但已经明确接受这个影响,不准备继续修。
obsolete则代表相关前提已经不存在,问题自然失效。
Deferred Issues 实际上也是一种流程简化
最开始我只是担心问题遗忘。
但后来再看,我觉得 Deferred Issues 对效率还有另一层价值:
它允许当前任务保持当前任务。
以前发现一个问题以后,因为害怕以后忘记,很容易产生一种心理压力:
要不现在顺手解决了吧。
结果一个原本 10 分钟记录就能结束的问题,可能把当前任务拖进另外两个小时的分析。
有正式 Deferred Issues 以后,可以明确做出:
确认问题真实存在
→ 记录 evidence
→ 标记 open
→ 当前任务继续这并不是逃避问题。
恰恰相反,它比:
“这个以后再说。”
可靠得多。
因为“以后再说”如果没有正式记录,大部分时候其实就等于:
“以后很可能忘掉。”
所以这一轮的“简化”,已经不只是命令数量的简化。
它还包括:
减少不必要的上下文切换。
到这个阶段,我越来越不希望新增 locale 依赖记忆
如果回头看前几门语言,会发现项目一直有一个变化。
最开始很多事情依赖的是:
我记得上一次是这样做的。
随后变成:
文档里已经写清楚应该怎么做。
再往后则开始变成:
只要这是机器能够确定完成的事情,就不应该要求我每次重新读一遍文档再手工执行。
这三种状态其实差别很大。
第一种依赖人的记忆。
第二种依赖人的执行纪律。
第三种才真正开始依赖工具本身保证边界。
例如新 locale:
以前可能需要记得:
不要漏 status.tsv
不要直接复制旧 glossary
别忘了 article metadata
别忘了 UI catalog
正式 metadata 还没生成
现在不能 publish现在越来越多的东西可以变成:
locale init
→ 明确生成机械骨架
→ 明确留下 incomplete marker
→ 未完成就 fail closed人的工作就可以集中到:
真正需要判断的语言内容。
“简化流程”不能变成“减少质量 gate”
这里我仍然想特别强调一个边界。
这一轮做了很多:
自动生成
自动初始化
自动部署
自动验收
减少命令
减少默认输出
减少上下文切换但是有一类东西我并没有打算因为效率而减少:
逐 TranslationUnit Quality Check
A-only
独立 Final Review
Locale Surface Review
production acceptance
必要的 HUMAN GATE也就是说,我想减少的是:
操作成本。
而不是:
判断成本。
如果一门语言真的出现 20 个 B,那么它就应该 revision 20 个。
如果 Final Review 又发现新的问题,就应该重新回 revision。
如果真实浏览器发现页面异常,就应该修。
这些都不能因为:
“我这一门语言计划 6 小时完成。”
就强行跳过去。
所以真正理想的 happy path 应该是:
没有问题时非常快;有问题时能够正确变慢。
这比“无论任何语言都必须 6 小时上线”重要得多。
然后我才开始韩语
到 2026 年 9 月 1 日 14:58:
feat: 初始化 ko-KR locale韩语才正式开始。
当时我的预期其实很乐观。
德语已经把新增 locale 的边际成本降下来。
法语又只用了大约 9 小时。
而且法语结束以后,我还专门继续完成了:
locale 机械骨架初始化
首次 production 自动化
production identity
验收入口继续收敛
Deferred Issues
减少重复操作按这个趋势,我当时认为:
韩语大约 6 小时左右完成,并不是一个特别激进的目标。
甚至从逻辑上来说,它应该比法语更快。
但实际结果完全不是这样。
韩语最终明显比法语慢得多。虽然中文和日文同样属于非拉丁语系,但韩语这次又暴露出了一批新的语言边界问题,尤其是 Go 标识符与韩语助词直接连接时,validator 会产生误判。
也正因为如此,我后来才越来越清楚一件事:
流程越来越成熟,并不意味着每增加一门语言,耗时就一定线性下降。
工具只能消掉已经理解的问题。
新的自然语言仍然可能带来以前从来没有遇到过的问题。
而这部分,就是下一篇韩语版复盘真正值得记录的内容。
小结
法语完成以后,我没有马上开始韩语。
从时间线上看,中间只有很短的一段间隔。
但就在这段时间里,项目又完成了一轮比较集中的流程调整:
fr-FR production acceptance
→ 新增 locale 初始化能力
→ 首次 production 进一步自动化
→ production identity 收敛
→ Deferred Issues 正式化
→ ko-KR 初始化这一次和之前“为第三门语言建立标准化流程”最大的区别在于:
标准化解决的是“应该怎么做”。
而这一轮继续解决的是:
既然已经知道怎么做,还有多少事情根本不应该再由人重复做。
我现在越来越觉得,一个长期维护的多语言项目,真正能产生复利的并不是:
下一门语言翻译得比上一门快 10%。
而是:
上一门语言已经解决过的问题,不要再让下一门语言重新支付一次成本。
如果 locale 骨架已经固定,就让工具生成。
如果 production identity 已经有明确 schema,就不要再到多个脚本里分别写一遍。
如果首次 production 的机械步骤已经验证稳定,就把它们收进正式入口。
如果问题已经发现但本轮不处理,就留下可追踪的 evidence,而不是让它消失在聊天记录里。
如果机器能够判断 PASS / FAIL,就不要每次让人重新阅读十几条命令输出。
但与此同时,真正属于语言质量和生产风险判断的 gate,仍然应该保留。
这才是我现在理解的“流程简化”:
不是把流程砍短,而是把流程中没有判断价值的部分拿掉。
法语之后的这一轮优化,本来是为了让韩语更快。
事实证明,韩语最终并没有更快。
但也正因为韩语后来遇到的问题更多,我反而进一步确认:
这轮优化并没有白做。
如果连这些已经可以消掉的重复操作都还保留着,那么韩语最终花费的时间只会更长。
而下一篇,我准备专门整理:
为什么原本预计大约 6 小时完成的 A Tour of Go 韩语版,最终实际投入达到了约 16~18 小时。
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 官方无隶属或授权关系。
