没有不值得去解决的问题,也没有不值得去学习的技术!

从法语 553 次提交到韩语两代离线:A Tour of Go 社区翻译为什么难以长期维护

图 2:法语 A Tour of Go 仓库目前已经归档,页面仍保留 553 Commits 和 61 Contributors 的历史

作者:

A Tour of Go 多语言翻译项目

图3:访问过去的 Go Tour 简体中文站 tour.go-zh.org 时,当前已经无法正常建立连接。

(1) 从「A Tour of Go 中文版」这个搜索词开始:我决定做一个持续维护的 Go Tour 中文版

本地运行的 A Tour of Go「Methods continued」课程页面,左侧为课程说明,右侧为 Go 代码编辑器及运行结果。

(2) A Tour of Go 中文版项目设计冻结:从 101 页到 103 页,从 Gin 转向 Cobra CLI

A Tour of Go 中文版开发实战:英文基线、在线运行与 101 页上游同步体系

(3) A Tour of Go 中文版开发实战:英文基线、在线运行与 101 页上游同步体系

go-tour-i18n 项目完成首个简体中文课程页面 welcome/1 的整页翻译、结构校验和本地预览。

(4) A Tour of Go 多语言翻译项目实录:完成首个 zh-CN 页面翻译闭环

图 1:DeepSeek 官方更新日志显示,本次只更新 DeepSeek-V4-Flash,DeepSeek-V4-Pro API 与 APP、Web 模型均未更新

(5) A Tour of Go 中文翻译项目进展:完成前 8 页、修复 present 语法,并暂缓 DeepSeek 对比

图 1:generics/1 简体中文页面本地预览

(6) 一个页面重试五次:使用 GLM-5.2 翻译 A Tour of Go 时遇到的 Token 与 Present 结构问题

图 1:methods/16 简体中文页面浏览器预览

(7) A Tour of Go 中文翻译实录:如何只翻译教学代码注释,而不破坏 Go 代码

图 2:A Tour of Go methods/20「练习:错误」中文候选页面最终渲染效果

(8) A Tour of Go 多语言翻译:当正确的中文语序被受保护标记顺序校验误判

图 3:methods/24 中文页面,静态代码、Bounds 行内代码和链接内的 image.Rectangle 均正确渲染

(9) A Tour of Go 中文翻译完成 7 个代表页校准:最后 3 页又发现了哪些真实问题

图 2:首批 10 个普通页面经过试跑与流程校准后全部进入 ready

(10) A Tour of Go 多语言翻译项目:首批 10 个普通页面试跑,从 2 个 blocked 到全部 ready

图 2:flowcontrol/6 最终中文页面,return 与 v 为适应自然中文语序发生整体换位

(11) A Tour of Go 中文翻译第二批实跑:从 Inline Code 顺序误判到历史响应重新验证

图 1:flowcontrol/6 首次原始输入实验直接通过统一自动校验

(12) A Tour of Go 多语言翻译项目:从 swap 问题重新审视 Token 保护,原始输入与最小保护的一次真实实验

图 7:moretypes/1 最终中文页面预览。左侧课程正文、静态代码块、行内代码和教学注释均正常渲染,右侧官方 Go 示例继续保持原样。

(13) A Tour of Go 多语言翻译实战:第三批 10 页全部 Ready,继续校准 Protected Token 的结构角色

图 4:上游源码确认共有 103 个课程页面,zh-CN 最终达到 ready=103、pending=0、blocked=0

(14) A Tour of Go 多语言翻译项目:zh-CN 103 个课程页面全部 ready,课程正文翻译阶段完成

图 4:GitHub 与 ChatGPT 已成功连接

(15) ChatGPT 连接 GitHub 实测:让 AI 直接读取 go-tour-i18n 仓库参与译文审核

图 3:中文 Go 术语校准完成后,将确定的译法写入 zh-CN glossary

(16) 为什么 channel 最终选择“通道”:A Tour of Go 中文术语统一的一次实践

图 1:技术含义基本正确,但“返回一个返回……”已经明显影响阅读,因此仍被列入 C 类修订

(17) 103 页逐页审核后,只返修 16 页:A Tour of Go 中文正文发布前质量审计

图 2:中文课程页面以及已经完成本地化的编辑器控制区

(18) A Tour of Go 多语言翻译项目:公共 UI 本地化完成,从课程译文走向完整中文界面

图 2:完整 zh-CN 正式投影成功生成,103 个课程页面被重新组装为 7 个 .article

(19) 从 103/103 ready 到完整站点验收:A Tour of Go 中文版补齐正式投影与课程元数据本地化

图 3:从 production bundle 独立启动的 A Tour of Go 简体中文页面,课程正文、公共 UI 和官方 Go 示例均已进入最终发布形态。

(20) A Tour of Go 多语言翻译项目:103 页全部完成后,我终于生成了可独立部署的 zh-CN Production Bundle

图 1:A Tour of Go 简体中文版正式生产页面,远程运行已经成功输出“Hello, 世界”。

(21) A Tour of Go 多语言翻译项目:zh-CN 正式上线,从生产发布到 go.dev Playground 的完整部署记录

图 1:新生产域名 go-dev.shuijingwanwq.com 已正常访问,Go 示例远程运行成功

(22) A Tour of Go 多语言翻译项目:从 go-tour 到 go-dev,完成 EdgeOne、Nginx 与 HTTPS 生产域名迁移

图 1:A Tour of Go 多语言翻译项目正式生产首页

(23) A Tour of Go 多语言翻译项目正式上线:从项目首页、公共 Footer 到生产发布元数据的完整收尾

图 3:GA4 g/collect 请求返回 HTTP 204,并携带当前课程页面地址,确认 Google Analytics 已产生真实事件上报

(24) A Tour of Go 多语言翻译项目:接入 Google Analytics 与百度统计,从 systemd 注入到 EdgeOne 与真实上报验证

图 1:生产环境 robots.txt 与 sitemap.xml 已正确对外提供,Sitemap 共包含 104 个规范 URL。

(25) A Tour of Go 中文站上线后:补齐 robots.txt、Sitemap,并完成五大搜索引擎提交

图 4:发布生产并刷新 EdgeOne 缓存后,再次通过真实手机访问,顶栏项目名称已经完整保持在一行,最终移动端验收通过。

(26) A Tour of Go 多语言翻译项目:修复手机端首页顶栏标题换行,并完成真机验证

图 3:生产自动部署脚本第一次真实运行时,systemd 已经显示 active,但首次 localhost 探测仍然得到 HTTP 000;随后连续 3 次同时满足 active + HTTP 200 后,脚本才判定部署成功,并继续完成公网 HTTP 200 验收。这次真实运行验证了连续应用层健康检查的必要性。

(27) A Tour of Go 多语言翻译项目:从一次 203/EXEC 故障到生产自动部署脚本落地

图 1:阿里云生产环境访问 go.dev Playground 时出现 TLS 握手超时

(28) A Tour of Go 多语言翻译项目:为 Playground 搭建独立的 ZgoCloud 执行代理

图 3:清除 /tour/script.js 缓存后,Run 已直接请求 ZgoCloud /compile 并返回 HTTP 200

(29) A Tour of Go 多语言翻译项目:将生产 Run / Format 切换到 ZgoCloud 执行节点

【图 1:微信中运行 Go 示例时出现 Error communicating with remote server.】

(30) A Tour of Go 中文版手机端运行与格式化偶发失败排查:从间歇性报错到补强 Nginx 可观测性

【图 4:向 golang-dev 公开邮件列表发送 Playground 使用告知邮件并投递成功】

(31) A Tour of Go 多语言项目:为 Playground 代理增加唯一 User-Agent,并主动告知 Go 官方

【图 3:百度剩余 9 个核心 URL 提交成功,remain 归零】

(32) A Tour of Go 搜索引擎收录实测:从 Google 自然索引到百度主动提交与 Bing IndexNow

【图 1:当前正式中文 methods/24 页面,右侧示例代码运行成功】

(33) A Tour of Go 中文翻译完成后,我为什么重新评估 minimal-protect 翻译模式

【图 3:5 个页面 Request SHA256 完全一致,但 Response SHA256 全部不同】

(34) A Tour of Go 翻译实验:更多原始上下文为什么没有更好?从 157 个保护标记到 30 次重复盲评

图 3:3 个代表页 × 4 个评审模型,共 12 次匿名评审的完整排名与第一名票数。

(35) A Tour of Go 翻译质量再评估:minimal-protect 未达预期后,我开始比较 ChatGPT、Codex 与 GLM-5.2

图 2:GitHub 中的 chatgpt-zh-CN-008 重译批次。一个 Batch 同时保存 inputs、raw-responses、candidates、validation 和 retry 记录,GitHub 成为 ChatGPT 与本地翻译流水线之间的数据交接层。

(36) 没有 OpenAI API,我如何用 ChatGPT + GitHub 完成 A Tour of Go 103 页全量重译

图 1:103 个正式课程页面完成最终语义质量审核,结果为 A=103、B/C/D=0,同时不存在缺失、重复或额外页面。A/B/C/D 是本轮项目内部语义质量分级,并不是自动结构 validator 的结果。

(37) 103 页翻完还不能发布:A Tour of Go ChatGPT 译文的语义审核与 Canonical Promotion

图 3:7 个 article endpoint 的 production origin 与 EdgeOne 公网响应全部 exact match,同时新的 ChatGPT 译文标记存在,旧版译文标记已经消失。

(38) 103/103 与 7/7 Exact Match:A Tour of Go ChatGPT 新译文的最终线上验收

【图 2:upstream source preview】

(39) A Tour of Go 多语言翻译项目首次实现 upstream 同步机制:从一次性翻译到长期维护

图 2:A/B/C/D 质量评级规则,展示正式质量 rubric 与 Promotion gate。

(40) A Tour of Go 多语言翻译项目引入 Translation Quality Review:从自动验证到 AI 翻译质量控制

图 4:翻译后 Playground Example,展示中文注释与保持不变的 Go 代码结构。

(41) A Tour of Go 多语言翻译项目实现 Playground Example 本地化:从代码注释翻译到完整学习体验

图 5:公网页面验证广告脚本输出

(42) A Tour of Go 多语言翻译项目接入广告系统:从 AdSense 专用配置到通用 HTML 注入架构

图 3:百度统计单页应用设置

(43) go-dev 单页网站接入统计与 Google AdSense Auto Ads 验证记录

图 1:GitHub 仓库中的多语言翻译项目结构

(44) A Tour of Go 多语言翻译项目:选择日语作为第二语言并启动翻译验证

图 5:模型身份描述不一致

(45) A Tour of Go 多语言翻译项目中的 AI 协作异常排查:一次长期工程会话失效分析

【图 1:ChatGPT GitHub 写入流程调整说明截图】

(46) go-tour-i18n 翻译流程调整:从 ChatGPT GitHub 写入到本地 artifact 导入

图 4 展示了本次异常状态。

(47) go-tour-i18n 翻译自动化流程复盘:已验证的 ChatGPT 翻译能力在新任务中的稳定性问题

【图 6:全部评审结束以后才读取 secret key,完成最终揭盲】

(48) A Tour of Go 翻译质量再评估:Codex High 已接近 ChatGPT High,我决定调整默认翻译引擎

图 4:ja-JP 生产课程页实际运行 Go 示例

(49) A Tour of Go 日语版正式上线:第二门语言 ja-JP 完成生产发布

图 1:ja-JP 上线以后,先提交“新增语言标准化流程”

(50) A Tour of Go 多语言扩展复盘:为第三门语言建立标准化流程

图 1:课程正文结束以后,页面左侧仍然存在大块视觉空白,但 AdSense 并没有在这里插入展示广告。

(51) A Tour of Go 明明有大片空白,为什么 AdSense 就是不显示展示广告?

图 2:golang/website 当前的 _content/tour/。课程内容、静态资源和模板都在这个上游目录中持续维护。

(52) 为了 AdSense,要不要放弃 SPA?A Tour of Go 页面架构的一次取舍

图 4:最终的手动课程广告真实展示在课程内容区域内,而不是 footer 之后。

(53) 保留 SPA 以后,广告应该放在哪里?从 AdSense 预览到方案 B

图 1:Google Search Console 中,多个不同的 A Tour of Go 课程 URL 被判断为“重复网页,用户未选定规范网页”。

(54) SPA 的 SEO 老问题:为什么最后还是给 103 个 A Tour of Go 课程页生成了 Prerender HTML

图 2:方案 B 生产环境中,源码已经正常,但课程主体高度异常缩短,footer 提前进入第一屏。

(55) 从源码空白到 AdSense 高度污染:A Tour of Go 方案 B 的生产收尾

图 1:A Tour of Go 德语版 de-DE 已正式运行在生产环境

(56) A Tour of Go de-DE 上线复盘:第三门语言用了约 16 小时,下一门如何压到 8 小时?

图 1:fr-FR 首次 production 验收完成后,我没有立即开始 ko-KR,而是先连续完成新增语言与首次生产流程的优化

(57) 法语版上线后,我没有马上开始韩语:A Tour of Go 新增语言流程的一轮完善与简化

图 1:根据 ko-KR 期间 76 次 Git 提交估算实际工作时间

(58) 原本预计 6 小时,最后用了约 16~18 小时:A Tour of Go 韩语版上线复盘

图 2:英文 source 与当时发生 validation failure 的 ko-KR candidate 对比

(59) Validator 报错,到底该改译文还是改规则?一次 A Tour of Go 韩语翻译实战

图 2:法语 A Tour of Go 仓库目前已经归档,页面仍保留 553 Commits 和 61 Contributors 的历史

(60) 从法语 553 次提交到韩语两代离线:A Tour of Go 社区翻译为什么难以长期维护

这几天完成 A Tour of Go 韩语版以后,我重新回头看了一些历史上的社区翻译项目。

最开始只是因为韩语比较特别。

我之前已经知道,韩语 A Tour of Go 并不是第一次有人翻译。更有意思的是,它甚至经历过一次比较明确的“换代”:旧项目长期没有更新以后,有新的维护者重新建立项目、重新部署站点,希望接替原来的韩语版本。

但几年以后,第二代韩语站最终还是离线了。

继续往下查以后,我发现这并不是韩语自己的故事。

法语、德语、意大利语、西班牙语、俄语、土耳其语、乌克兰语……很多语言都曾经有过自己的 A Tour of Go,有仓库、有域名、有部署,有些项目甚至积累了数百次 Git 提交。

最后,其中相当一部分还是消失了。

这让我开始认真思考一个问题:

对于社区翻译项目来说,真正困难的究竟是第一次翻译完成,还是五年、十年以后仍然有人能够继续维护?


一、一次运行环境淘汰,让大量翻译站同时离线

Go 官方仓库里至今还保留着一个很有代表性的 tracking issue:

golang/tour#1039

标题是:

Plaintext
Tour translations re-deployment after appspot deprecation
of Go1.9 (tracking issue)
图 1:Go 官方为大量社区翻译站重新部署建立的 tracking issue
图 1:Go 官方为大量社区翻译站重新部署建立的 tracking issue

这张图把当年的问题记录得非常清楚。

很多非英语版 A Tour of Go 都部署在 Google App Engine。

后来 App Engine 停止支持 Go 1.9,而一些已经很久没有更新、也没有重新部署的翻译站,开始直接显示:

Plaintext
Go 1.9 is no longer available.

事情并没有很快解决。

2021 年 1 月,一些站点进一步变成:

Plaintext
Error: Server Error

到了 2021 年 3 月,Go 官方开始把已经离线的翻译链接从英文 Tour 中删除。

官方给出的处理原则也很明确:

如果这些翻译以后重新上线,链接还可以再加回来。

2021 年 12 月,法语和乌兹别克语又被记录为 OFFLINE。

到了 2025 年 8 月,第二代韩语 Tour 同样离线,官方随后把韩语链接从 welcome 页面移除。

这件事情特别能说明一个问题:

翻译内容本身没有突然消失。

真正先失效的是:

Plaintext
runtime
→ deployment
→ hosting
→ production site
→ official link

只要其中某一环长期没人维护,即使仓库里的译文还完整存在,对普通读者来说,这门语言实际上也已经“消失”了。


二、法语尤其让我觉得可惜:553 次历史提交,最后还是 archived

其中最让我意外的是法语。

以前看到这个项目时,我记得 GitHub 上显示的参与人数非常多。

这次重新打开:

dupoxy/go-tour-fr

首先看到的已经不是项目介绍,而是 GitHub 顶部的一条黄色提示:

Plaintext
This repository was archived by the owner on Feb 11, 2023.
It is now read-only.
图 2:法语 A Tour of Go 仓库目前已经归档,页面仍保留 553 Commits 和 61 Contributors 的历史
图 2:法语 A Tour of Go 仓库目前已经归档,页面仍保留 553 Commits 和 61 Contributors 的历史

页面显示:

Plaintext
553 Commits
61 Contributors
12 Forks

旁边仍然保留着原来的站点:

Plaintext
go-tour-fr.appspot.com

第一次看见这个数字时,我的直觉和之前一样:

一个有 60 多位参与者、500 多次提交的项目,最后居然也没有继续维护下来?

如果真的是 61 位法语维护者,这确实很让人惊讶。

但是这次为了写这篇文章,我专门重新分析了这些仓库的 Git 历史,结果发现这里还有一个很重要的细节。


三、61 Contributors,并不等于有 61 位法语维护者

A Tour of Go 这些早期翻译项目,很多是在官方 Tour 源码基础上继续开发的。

这意味着它们的 Git history 中同时包含:

Plaintext
官方 Go Tour 的上游提交
+
该语言自己的翻译和维护提交

所以 GitHub 页面显示的:

Plaintext
61 Contributors

并不能直接理解成:

61 个人曾经专门维护法语翻译。

那些 Contributors 中包含了很多 Go Tour 上游代码的作者。

于是这次我把几个历史仓库全部拉到本地,又把当前 golang/tour 中能够识别出来的上游提交排除掉,只统计各翻译 fork 自己独有的 Git 历史。

结果如下。

图 3:排除可识别上游历史后,几门 A Tour of Go 社区翻译的 fork-only 提交统计
图 3:排除可识别上游历史后,几门 A Tour of Go 社区翻译的 fork-only 提交统计

我得到的数据是:

语言 / 项目仓库总提交fork-only 提交fork-only 作者名最早最晚
法语553162142011-10-072023-02-11
德语仓库已不可访问
韩语第一代434322012-02-062014-08-28
韩语第二代47985102020-08-192023-07-23
日语47581262015-11-162020-12-05
简体中文581200252011-11-082022-03-20

这里的 AUTHORS 我也不准备称为“维护者人数”。

它只是:

fork-only commits 中出现的不同 Git author name 数量。

一个人可能更换 Git name,同一个名字理论上也可能不止对应一个人,所以它只能用于观察大致的参与规模。

但即使这样,这个结果依然很有意思。

法语 GitHub 页面显示:

Plaintext
61 Contributors

真正排除可识别的上游历史以后,fork 独有提交中出现的是:

Plaintext
14 个 Git author name
162 次 fork-only commit

这仍然不是一个很小的项目。

而且持续时间非常长。

从:

Plaintext
2011-10-07

一直延续到:

Plaintext
2023-02-11

超过 11 年。

最终还是归档了。

所以我觉得法语真正令人惋惜的地方,并不是:

“61 个维护者最后全都走了。”

这个说法其实并不准确。

真正值得感慨的是:

一个持续十多年、留下 162 次独有提交、实际有多位参与者维护过的翻译项目,最终仍然没有形成能够无限期持续下去的维护机制。


四、韩语更加典型:第一代已经失去维护,于是有人重新做了第二代

如果说法语代表的是“一个长期项目最终停下来”,韩语则更加特殊。

韩语是真的经历过一次比较清晰的接班。

在 2019 年的另一个 Go Tour issue 中,官方人员已经明确提到,当时已有的韩语翻译:

Plaintext
go-tour-kr.appspot.com

已经 outdated。

对应仓库是:

Plaintext
atomaths/go-tour-kr

当时新的贡献者想更新韩语翻译,官方给出的其中一个建议是:

向原来的韩语仓库提交更新,然后请原 owner 重新部署。

问题在于,这需要:

原维护者仍然存在,而且愿意继续合作。

如果联系不上,或者原 owner 不再处理这个项目,那么新的贡献者就只能建立新的 App Engine 项目和新的 URL。

到 2020 年,这件事情真的发生了。

图 4:2020 年新的韩语维护者明确表示旧韩语项目已经长期无人管理,并创建第二代项目
图 4:2020 年新的韩语维护者明确表示旧韩语项目已经长期无人管理,并创建第二代项目

新的维护者在 golang/tour#1027 中写得非常直接。

大意是:

原来的 Korean Go Tour 项目已经很老,而且没有人管理,所以很长时间都没有更新。

于是他做了三件事:

Plaintext
创建 golang-ko/tour
重新部署 go-tour-ko.appspot.com
联系 Go 官方,希望更新韩语链接

这基本就是一次完整的社区项目接班。


五、第一代韩语其实规模非常小

本地统计以后,这种接班为什么会发生,也变得更加容易理解。

第一代:

Plaintext
atomaths/go-tour-kr

一共只有:

Plaintext
43 commits
2 个 fork-only Git author name

最后一次 fork-only 活动:

Plaintext
2014-08-28

换句话说,这个项目的 Bus Factor 本来就非常低。

如果真正知道:

Plaintext
怎么同步上游
怎么翻译
怎么部署 App Engine
谁拥有 production project
谁能重新发布

的人主要只有一两位,那么其中一个人离开以后,整个 production 生命周期就很容易停下来。

代码虽然仍然公开,但代码公开并不等于:

生产环境自动具有可继承性。


六、第二代韩语已经明显扩大参与范围,但最后还是离线

第二代:

Plaintext
golang-ko/tour

规模其实已经比第一代大很多。

我的本地统计结果是:

Plaintext
479 total commits
85 fork-only commits
10 fork-only author names

fork 独有活动从:

Plaintext
2020-08-19

持续到:

Plaintext
2023-07-23

也就是说,这已经不再是一个“两个人随手维护一下”的规模。

而且它由:

Plaintext
golang-ko

这样的组织承载,也比第一代个人仓库看起来更容易长期持续。

但是到了 2025 年,官方 tracking issue 还是出现了:

Plaintext
August 2025 update
the Korean tour is offline

并且被从 welcome 页面移除。

所以:

增加维护人数可以降低风险,但它本身并不能保证一个社区翻译站永远在线。


七、真正困难的是“代码之外的东西”也需要持续有人负责

如果问题只是译文,那么事情其实简单很多。

源文更新以后:

Plaintext
diff
→ 翻译
→ commit

理论上就结束了。

但一个真正在线的 A Tour of Go 远远不止译文。

它还需要:

Plaintext
源码同步
构建工具
Go runtime
依赖
服务器 / App Engine
域名
TLS
Playground
前端兼容
部署权限
搜索引擎
官方外链

这些东西的生命周期完全不同。

翻译可能五年不用改。

但是运行环境可能两年以后就弃用了。

域名可能续费失败。

云平台可能改变 runtime。

旧 build 脚本可能已经无法运行。

即使后来有人愿意继续翻译,也未必拥有原 production 的控制权。

这也是早期韩语 issue 让我印象很深的一点。

当时官方建议新的贡献者:

要么请原 owner 接受更新并重新部署,要么建立新的 App Engine 项目和新的 URL。

也就是说:

GitHub repository 的开源性,并不能自动解决 production ownership 的传承问题。


八、还有一个很现实的问题:长期维护本身需要钱

不过只讨论维护人数、知识传承和 production ownership,我现在觉得仍然漏掉了一层非常重要的原因。

那就是:

一个长期在线的社区项目,本身就是有成本的。

开源源码可以免费放在 GitHub。

翻译者也可以无偿贡献自己的时间。

但 production 并不会因为项目是开源的,就自动变成零成本。

以我现在维护的 A Tour of Go 多语言站为例,背后实际涉及:

Plaintext
ECS / VPS
海外网络节点
域名
公网带宽
CDN
DNS / 网络服务
监控和日志
Playground 代理
服务器存储
生产环境维护
AI 工具

其中有些服务可以使用免费额度。

例如部分 locale 可以使用 Cloudflare 免费版,Let’s Encrypt 证书本身也不收费。

但这些免费能力并不能让整个项目变成零成本。

以我目前实际使用的基础设施和维护工具来看,这已经完全不是:

一年几百元。

服务器、海外节点、域名,以及 ChatGPT / Codex 等长期使用的工具费用叠加以后,一年实际需要承担的是数千元级别的现金支出,全年总成本甚至有可能超过 5000 元。

而且这还没有给维护者自己的时间定价。

所以如果一个项目永远没有任何收入,那么十年下来,意味着维护者不仅要持续无偿投入时间,还要持续拿自己的收入补贴这个项目的生产环境。


九、服务器账单还只是最容易看到的那部分

现金成本其实还不是全部。

另一个可能更大的成本是:

维护者自己的时间。

例如一次上游同步,可能涉及:

Plaintext
检查 upstream diff
→ export TranslationUnit
→ 翻译
→ validation
→ Quality Check
→ revision
→ Final Review
→ promotion
→ preview
→ production
→ 浏览器验收

服务器出故障以后,还可能需要:

Plaintext
排查 systemd
Nginx
CDN
DNS
TLS
Playground
浏览器兼容

这些时间都不会出现在云厂商账单里。

但它们同样是真实成本。

甚至对于开发者来说,时间成本可能比服务器本身更高。

如果一个维护者平时还有:

Plaintext
工作
家庭
其他项目
生活

那么要求他十年如一日地继续无偿投入,并不是一件理所当然的事情。

早期可能因为兴趣非常强:

“这个项目很好,我愿意做。”

几年以后则很容易变成:

“我还有更重要的事情,这个站目前还能访问,就先放着吧。”

再过几年:

runtime 过期了。

但是这时候要恢复 production,已经需要重新花几个晚上研究。

再往后,就很容易变成 OFFLINE。


十、没有经济反馈时,维护优先级会自然越来越低

我以前看到过中文社区维护者表达过类似的无奈:

这种项目很难谈什么商业模式。

我现在没有重新找到那句话的原始出处,所以这里不把它作为直接引文。

但这个意思,我现在越来越能够理解。

一个社区翻译项目通常:

Plaintext
免费访问
不开会员
不卖课程
不卖软件
没有企业合同
没有订阅收入

对于读者当然很好。

但对于维护者来说,就形成了一个非常现实的结构:

Plaintext
收入:0

服务器成本:持续存在
工具成本:持续存在
维护时间:持续投入
升级成本:偶尔突然出现
风险:主要由维护者承担

这种模式可以靠热情运行一年。

甚至可以运行五年。

但如果要问:

能不能稳定运行十五年?

难度就明显不同了。


十一、这也是为什么我现在选择给 A Tour of Go 加广告

这也是我自己的项目和很多早期社区翻译不同的一个地方。

我最终选择了:

在 A Tour of Go 多语言生产站加入广告。

一开始我其实也会顾虑:

一个技术教程站放广告,会不会显得不够纯粹?

尤其是 A Tour of Go 本身属于学习内容。

广告太多当然会影响体验。

所以我这段时间也花了不少时间处理:

Plaintext
Auto Ads
课程页手动广告
SPA 生命周期
广告刷新
课程高度
footer
移动端布局
AdSense 对 Angular DOM 的影响

有时候为了一个广告位,反而增加了不少工程复杂度。

从单纯的技术角度看:

不放广告当然更简单。

但如果从十年的维护周期来看,我现在反而觉得:

完全没有任何收入来源,也是一种长期风险。


十二、广告的第一目标不是赚很多钱,而是让项目逐渐承担自己的成本

我现在对广告收益的预期其实非常保守。

并不是:

靠 A Tour of Go 多语言站赚很多钱。

更现实的第一阶段目标是:

Plaintext
广告收入

覆盖域名
覆盖部分服务器成本
覆盖部分网络与基础设施成本
覆盖部分维护工具成本

只要能够做到:

项目产生的收入,逐渐接近项目自己的长期运行成本。

它的可持续性就已经和完全依赖个人补贴非常不一样。

以我现在的情况来说,感觉一年的投入是超过 5 位数的,那么完全没有收入意味着:

Plaintext
1 年
→ 自己承担数千元

5 年
→ 继续承担数万元级别累计成本的可能性

10 年
→ 仍然要靠个人持续补贴

这还没有计算自己的维护时间。

如果广告至少能够覆盖其中一部分:

Plaintext
项目产生流量
→ 产生少量收入
→ 抵消部分运行成本

那么继续维护时,心理结构都会不同。

它不再完全是:

“我每年还要继续拿自己的钱补贴这个公益项目。”

而会逐渐接近:

“这个项目至少开始拥有一点维持自己运行的能力。”


十三、哪怕每门语言每天只有很少的收入,长期仍然可能有意义

这也是为什么我现在会观察每一门语言的广告收益。

按照目前实际看到的流量,我并不会对单个 locale 做很高的收益预期。

一门 locale 每天可能只有:

Plaintext
0.1 美元
0.2 美元
0.5 美元

而且坦白说:

以目前的流量来看,我觉得每天达到 0.5 美元都已经不是一个容易实现的目标。

单独看,这些数字都非常小。

0.1 美元一天,一年也只有三十多美元。

这当然不可能一下子覆盖整个项目的长期成本。

但如果未来能够稳定维护:

Plaintext
10 门语言
20 门语言
30 门语言

并且每一门语言都逐渐积累一些搜索流量,那么多个 locale 的小额收入才可能慢慢汇总起来,帮助承担整个项目的长期运行费用。

例如并不需要假设:

每一门语言每天都能赚 0.5 美元。

现实很可能是:

Plaintext
有些语言几乎没有收入
有些语言每天 0.1 美元
有些语言稍微高一些
极少数语言可能表现更好

最终看的是:

整个多语言项目加起来,能不能形成一定程度的自我供血能力。

这目前当然仍然只是一个需要长期用真实流量和 AdSense 数据验证的方向。

我不会因为几个 locale 上线以后出现一点收益,就认为商业模式已经成立。

但至少和完全没有任何商业化路径相比:

它开始存在一个长期可验证的方向。


十四、商业化并不等于牺牲所有用户体验

当然,加入广告也有另一面。

如果为了收入:

Plaintext
每页塞满广告
频繁弹窗
影响代码编辑器
遮挡课程
降低页面速度

那么项目即使赚到钱,也可能失去原来的价值。

所以我现在更希望寻找的是一个平衡:

Plaintext
广告存在
+
不破坏课程阅读
+
不破坏 Playground
+
不影响 SPA 下一页
+
不要求必须 filled
+
不为了广告重做整个 Tour

这也是为什么当前最终留下的共享方案比较克制:

Plaintext
Auto Ads
+
课程页手动 course ad
+
Angular SPA mount / unmount
+
局部 AdSense layout protection

新增语言直接复用。

而不是每增加一个 locale,再重新设计一套广告系统。

换句话说:

商业化本身也应该成为可维护基础设施的一部分,而不是另一个不断制造维护成本的系统。


十五、没有收入并不是以前那些项目离线的唯一原因

这里我也不想反过来得出一个过于简单的结论:

法语、韩语、德语离线,就是因为没赚钱。

目前并没有足够 evidence 支持这种直接因果关系。

它们失去维护可能同时受到:

Plaintext
维护者离开
Bus Factor
App Engine runtime 淘汰
部署权限
上游变化
技术债
时间不足
production ownership
个人兴趣变化
经济成本

等多种因素影响。

但经济因素很难完全忽略。

因为只要一个项目长期依赖:

某个人持续贡献时间 + 持续承担真实现金成本

那么这个人一旦工作、家庭或者经济情况发生变化,项目就很容易降低优先级。

所以我现在更愿意把经济收益理解成:

长期维护体系中的一个稳定器。

不是唯一条件。

但值得存在。


十六、而且这并不是只有韩语和法语

官方 tracking issue 中列出的语言很多。

图 5:官方 tracking issue 中,法语、德语、繁体中文、意大利语等多个翻译站都曾被标记为 OFFLINE
图 5:官方 tracking issue 中,法语、德语、繁体中文、意大利语等多个翻译站都曾被标记为 OFFLINE

我截取的这一段中已经可以看到:

Plaintext
Traditional Chinese  OFFLINE
French               OFFLINE
German               OFFLINE
Hebrew               OFFLINE
Italian              OFFLINE

与此同时,同一份 tracking issue 里:

Plaintext
Czech        OK
Indonesian   OK

也就是说,不能简单得出:

“这种社区翻译模式肯定维护不下去。”

事实并不是这样。

一些语言确实能够长期维持。

在 issue 记录中,日语也一直被标记为:

Plaintext
Japanese
Status: OK

真正的问题应该变成:

为什么有一些语言能够持续,而另一些最终掉线?


十七、不同语言的 Git 历史差异非常大

这次本地统计还有一些结果值得记录。

例如日语。

它有:

Plaintext
81 fork-only commits
26 个 fork-only Git author name

相比第二代韩语:

Plaintext
85 commits
10 个 author name

两者提交数量非常接近,但参与分布明显不同。

简体中文更大:

Plaintext
200 fork-only commits
25 个 author name

法语则是:

Plaintext
162 fork-only commits
14 个 author name

但这些数字也不能简单拿来排名:

人越多,就越不容易停。

因为一个项目能否长期生存,还受到 production ownership、维护组织、上游同步方式、部署复杂度,以及经济可持续性等很多因素影响。

不过至少可以得到一个很实际的结论:

长期维护不能只依赖“项目刚上线时的热情”。


十八、德语甚至连原仓库都已经找不到了

这次还有一个让我比较意外的细节。

Go 官方 tracking issue 中记录的德语仓库是:

Plaintext
michivo/go-tour-de

站点:

Plaintext
go-tour-de.appspot.com

状态:

Plaintext
OFFLINE

但是我这次真正准备统计 Git history 时,这个 GitHub repository 已经无法访问。

于是最终数据只能显示:

Plaintext
German
michivo/go-tour-de
UNAVAILABLE

这又比“站点离线”更进一步。

一开始可能只是:

Plaintext
production offline

再过几年,甚至可能变成:

Plaintext
production offline
+
repository unavailable

到了这个时候,再想恢复历史版本的难度就会明显增加。


十九、所以“翻译完成”其实只是最容易保存的一部分

这几天我自己连续增加法语、德语、韩语时,很容易产生一种错觉:

只要 TranslationUnit 全部翻完,以后维护成本应该就很低。

从文本本身看,这个判断并没有完全错。

真正变化的英文课程内容,并不会每天大量更新。

但是看完这些历史项目以后,我觉得需要把维护对象拆开。

译文本身比较容易保存。

Git 就能够很好地保存它。

真正难保存的是:

Plaintext
为什么这样设计
如何同步
如何 validation
哪个版本是 production
怎样重新构建
怎样首次部署
服务器在哪里
CDN 怎样配置
Playground 怎样代理
为什么这个规则不能改
搜索引擎提交过什么
出了问题应该回到哪一步
谁在支付基础设施成本
项目怎样继续承担这些成本

这些如果只存在于原维护者脑子里,就不是真正可继承的项目知识。


二十、这也解释了为什么我现在越来越重视正式 docs

我现在这个 go-tour-i18n 项目里已经有越来越多的文档:

Plaintext
NEW_LOCALE_RUNBOOK
TRANSLATION_WORKFLOW
TRANSLATION_TASK_SPEC
RETRANSLATION_RUNBOOK
LOCALE_SURFACE_REVIEW
PRODUCTION_RUNBOOK
DEFERRED_ISSUES

刚开始写这些东西时,有时候也会觉得:

我自己明明知道下一步怎么做,还需要写这么细吗?

看完这些十年前的项目以后,我现在觉得非常有必要。

因为文档真正服务的并不只是:

今天的我。

更重要的是:

未来不记得这些细节的我,或者未来可能接手这个项目的另一个人。


二十一、单仓库多 locale,也有这方面的考虑

早期很多 A Tour of Go 翻译采用:

Plaintext
英文 upstream

法语独立 fork

英文 upstream

日语独立 fork

英文 upstream

韩语独立 fork

英文 upstream

中文独立 fork

每个 locale 都有:

Plaintext
自己的仓库
自己的构建方式
自己的部署
自己的维护者
自己的同步历史
自己的 production 成本

这种模式最大的优点是独立。

但代价也很明显。

如果一个 locale 的维护者离开,那么:

这一门语言的整个工程知识、部署权限和持续投入来源,都可能一起离开。

我现在的项目则更倾向:

Plaintext
一个 upstream 基线
+
统一 TranslationUnit
+
多个 locales
+
统一 validation
+
统一 Quality Check / Final Review gate
+
统一 production tooling
+
共享广告能力

当然,这种方案也不是天然不会失维护。

如果我自己哪天完全不再维护,这个项目同样会遇到风险。

我并不认为写几套脚本、加上广告以后就解决了 Bus Factor。

真正希望降低的是:

Plaintext
接班需要重新考古的成本
+
每门 locale 重复基础设施的成本
+
长期运行完全依赖个人补贴的程度

二十二、自动化真正重要的地方可能不是省时间,而是保存知识和降低成本

以前我做自动化时,更关心的是:

下一门语言能不能从 9 小时压到 6 小时?

现在看完这些历史项目以后,我觉得还有两个更长期的价值。

第一:

脚本本身也是维护知识的一种编码。

例如:

Plaintext
locale init

不仅让我少创建几个文件。

它还保存了:

一个合法 locale 的机械骨架究竟是什么。

first-production 不只是少执行十几条服务器命令。

它还保存了:

第一次 production 到底应该按照什么顺序推进。

validator 不只是帮我发现错误。

它还保存了:

哪些技术结构是这个项目认为绝对不能被翻译破坏的。

第二:

共享基础设施能够降低每门语言的边际成本。

如果每一门语言都需要:

Plaintext
单独服务器
单独部署体系
单独广告实现
单独验收脚本
单独监控

那么语言越多,维护成本越容易线性上涨。

如果可以共享:

Plaintext
代码
流程
deployment baseline
广告能力
Playground 代理
validation

那么新增 locale 才有机会变成:

主要增加语言成本,而不是重新增加一整套平台成本。


二十三、真正需要担心的并不是“某一天没有人翻译”

A Tour of Go 的历史让我觉得,一个翻译项目真正进入危险状态,往往不是:

今天没人提交译文。

真正危险的是:

Plaintext
没人知道 upstream 已经变了
没人知道 production 为什么还能跑
没人敢重新部署
没人拥有原云项目
没人知道哪个分支是真实版本
没人知道站点为什么离线
没人愿意继续支付服务器和工具成本

到了这个阶段,即使突然出现一个愿意贡献翻译的人,也很难立刻恢复项目。

第一代韩语就是一个很典型的例子。

2019 年已经有人愿意重新翻译。

但问题很快就从:

“我有新译文。”

变成:

“原来的仓库 owner 是否愿意合并?”

然后又变成:

“谁能够重新 deploy?”

最后干脆建立第二代项目。

这其实已经不是翻译问题了。


二十四、所以我现在对“维护”这个词的理解也变了

以前说:

维护一门 A Tour of Go 翻译。

我首先想到的是:

Plaintext
上游更新
→ 补翻译

现在我更愿意把它理解成:

Plaintext
语言质量维护
+
上游同步维护
+
验证规则维护
+
构建维护
+
部署维护
+
基础设施维护
+
production acceptance
+
搜索生态维护
+
经济可持续性

也只有这些部分能够持续运转,一门 locale 才真正还是“在线”的。

否则 GitHub 上即使仍然躺着完整译文,对普通读者来说也没有太大区别。


二十五、法语的 553 次提交为什么让我觉得特别可惜

这篇文章最开始,其实就是因为重新看到法语仓库。

Plaintext
553 Commits
61 Contributors

第一眼真的会觉得:

这么多人、这么长历史,怎么最后还是没了?

现在分析完以后,数字已经更清楚。

不能说有 61 位专门的法语维护者。

但即使排除上游历史:

Plaintext
162 fork-only commits
14 个 fork-only Git author name
超过 11 年的历史跨度

仍然是一段相当可观的社区投入。

大量人花过真实时间:

Plaintext
翻译
修正
同步
部署
讨论
维护

而今天访问原来的生产站,对新读者来说,这些工作已经很难直接被看到了。

所以我仍然觉得非常可惜。

但这种可惜也让我更明确:

一个社区翻译项目真正的成果,不应该只有“今天已经上线”。

还应该尽可能留下:

Plaintext
几年以后别人仍然能够继续维护它的条件
+
让项目能够承担一部分自身运行成本的可能性

小结

A Tour of Go 的多语言历史里,其实有很多曾经非常认真维护过的项目。

法语:

Plaintext
553 total commits
162 fork-only commits
14 fork-only author names
最终 archived / OFFLINE

韩语第一代:

Plaintext
43 fork-only commits
2 author names
长期无人维护

于是出现第二代。

第二代韩语:

Plaintext
85 fork-only commits
10 author names
重新部署
重新进入官方语言列表

几年以后,又再次 OFFLINE。

德语则更进一步:

Plaintext
站点 OFFLINE
原 tracking issue 中记录的仓库目前也已不可访问

同时,也有日语、捷克语、印尼语等项目在官方 tracking issue 的记录里长期保持过 OK 状态。

所以问题并不是:

社区翻译一定维护不下去。

也不能简单归结为:

以前的维护者不够坚持。

一个长期项目需要同时面对:

Plaintext
维护者时间
Bus Factor
上游同步
技术升级
部署权限
服务器
带宽
CDN
域名
production 运维
工具费用
以及持续的经济成本

如果所有这些成本都长期依赖某个维护者无偿承担,那么时间本身就会不断放大风险。

这也是为什么我的 go-tour-i18n 最终不仅要解决:

Plaintext
怎样翻译
怎样 validation
怎样上线

还希望继续解决两个问题:

Plaintext
怎样让下一个维护者能够接得住

以及:

Plaintext
怎样让这个项目逐渐有能力承担自己的长期运行成本

所以我现在选择了广告。

并不是因为 A Tour of Go 一上线就能带来多少收入。

按照目前的真实流量,我甚至觉得:

单个 locale 每天达到 0.5 美元,都已经不是一个容易实现的目标。

但这并不影响我认为“收入来源”本身值得尝试。

因为如果一个需要长期运行十年的项目永远只有成本、没有任何经济反馈,那么它的可持续性天然会弱很多。

广告当然不是唯一答案。

未来也可能有赞助、捐赠或者其他方式。

但至少现在,它给了这个项目一个最现实、也能够通过真实数据持续验证的可能性:

Plaintext
读者使用站点
→ 产生少量广告收入
→ 抵消部分基础设施和维护工具成本
→ 降低长期依赖个人补贴的程度

也许某一门语言每天只有 0.1 美元。

另一门只有 0.2 美元。

甚至有一些语言长期几乎没有收入。

但如果未来能够稳定维护更多语言,并且其中一部分逐渐获得搜索流量,那么这些分散的小额收入才有可能汇总起来,让整个项目开始具备一点自我供血能力。

这并不能保证项目永远不会停止维护。

但它至少让“长期维护”不再完全建立在一个人的热情、时间和钱包之上。

十年前那些已经离线的 Tour,并不是没有人认真做过。

恰恰相反。

正因为能够看到那些仓库里留下来的大量提交,才更能体会到:

第一次把一门语言翻译出来很难。

而现在我越来越觉得:

让它十年以后仍然有人愿意维护、有人能够维护,而且还有钱继续运行,可能才是真正更难的事情。

A Tour of Go de-DE 上线复盘:第三门语言用了约 16 小时,下一门如何压到 8 小时?

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 官方无隶属或授权关系。