在 Go Learning & Documentation v2 的开发过程中,“历史”后来变成了一个非常重要的话题。
但如果回到最开始,我保留历史的目的其实很简单。
不是为了建立一套越来越复杂的审计系统。
也不是为了让每一个状态都能够无限向前追溯。
最初真正想解决的问题只有一个:
已经翻译和审核好的内容,为什么还要重新做一遍?
A Tour of Go 已经积累了大量正式翻译成果。
其中很多内容已经经历过:
Generation
↓
Machine validation
↓
Independent QC
↓
必要时 revision
↓
re-QC
↓
最终通过如果进入 v2 以后,只因为项目版本变了、工作流增加了新的能力,就把这些结果全部作废,再重新翻译、重新审核一次,我觉得这不仅浪费时间,甚至可能降低最终结果的稳定性。
所以历史最初真正的价值,其实就是:
让已经高质量完成的工作继续发挥作用。
v2 不应该意味着“从零开始”
软件开发中,一个大版本升级经常会给人一种直觉:
v1 结束了,现在开始 v2。
如果把这种版本概念直接带到翻译系统里,很容易形成这样的做法:
v1 Translation
↓
Archive
v2 Translation
↓
重新生成全部内容从工程实现来看,这非常干净。
新版本只处理自己的数据。
旧版本全部留在历史目录。
新旧之间没有复杂关系。
但对于翻译来说,这种“干净”未必合理。
假设某个英文 Page:
- source 没有变化;
- 正式术语规则没有影响它;
- candidate 没有问题;
- 已经通过 Independent QC;
- 新的 parser 仍然能够验证它;
- 当前内容和用户最终看到的内容都没有发生语义变化。
那么仅仅因为:
现在进入 v2 了
就要求重新翻译一次,没有什么实际价值。
版本号是工程 milestone。
它不应该自动成为语言成果失效的理由。
翻译成果和普通缓存并不完全一样
表面来看,这件事情很像缓存复用。
旧结果仍然有效,就直接继续使用。
但我觉得正式翻译成果其实比普通 cache 更值得谨慎保护。
普通缓存通常可以删除以后重新计算。
例如:
source
↓
deterministic function
↓
cachecache 丢了,再运行一次,理论上应该得到完全一样的结果。
AI 翻译不是这样。
同一篇英文 source,再次交给模型生成,可能出现:
- 不同的句式;
- 不同的词汇选择;
- 不同的段落衔接;
- 不同的语序;
- 一些新的细微语言变化。
很多变化并不代表质量提高。
它们只是:
另一种同样可能成立的表达。
因此一份已经经过独立审核的 TranslationUnit,并不能简单理解成:
“反正重新生成一次就好了。”
它已经是一份经过验证的语言成果。
如果没有真实原因,重新生成反而会制造新的变量。
重复翻译还会带来重复审核
重新 Generation 的成本也不只是模型调用本身。
只要 candidate 发生变化,就意味着后面的可信链条也需要重新建立。
例如:
new Generation
↓
Machine validation
↓
Candidate Snapshot
↓
Independent QC如果 Reviewer 发现新问题:
B / C / D
↓
exact revision
↓
re-QC于是一个原本已经完成的 Page,因为没有必要的重新生成,再次消耗:
- Generation 时间;
- Reviewer 时间;
- validation;
- revision;
- evidence;
- workflow coordination。
更重要的是,这些成本并没有带来新的用户价值。
因为那个 Page 一开始可能就已经是正确的。
所以避免重复翻译,本质上也在避免:
为了验证没有必要发生的变化,再进行一整轮质量流程。
历史首先应该回答:“这个成果还能不能继续用?”
因此我后来越来越倾向于把历史的第一个职责定义得非常简单:
帮助系统判断已经存在的成果能不能继续复用。
这里真正需要关注的是当前事实。
例如:
Source changed?
Glossary affected?
Representation still valid?
Candidate still current?
Previous QC still trustworthy?如果这些条件仍然成立,那么历史给出的最好答案应该是:
继续使用。
而不是:
“因为工作流升级了,所以重新跑一遍更安心。”
这也是整个 v2 中逐渐形成的一个核心方向:
只处理真正发生变化的部分。
Source 没变,是最基础的一层
最直接的历史复用依据当然是 source。
如果英文 source 已经变化,那么旧译文是否仍然有效就必须重新判断。
但如果 source 完全没有变化,就至少说明:
翻译对象本身没有改变。
当然,这还不能直接证明旧 candidate 一定可以继续使用。
因为其他依赖也可能变化。
例如 glossary。
前面的文章已经写过:
glossary 文件发生变化,并不等于所有 TranslationUnit 自动失效。
需要进一步判断 semantic delta 是否真正影响这个 Page。
因此 source identity 和 glossary compatibility 可以组合起来:
Source unchanged
+
Glossary compatible
↓
旧译文可能继续有效这里的“可能”很重要。
因为还需要考虑 parser、representation 或其他相关规则。
但至少已经没有理由直接从 Generation 重新开始。
Parser 升级,也不应该自动让语言成果归零
上一篇刚刚写过 Learn / Docs 的 parser。
Parser 会继续演进。
今天可能增加对某一种 teaching comment 的识别。
明天可能修复某种 link binding。
以后还可能完善新的 Markdown representation。
如果每次 parser 实现发生变化,就把历史 TranslationUnit 全部重新生成,那么 parser 的每一次工程改进都会产生巨大的语言工作量。
但真正应该问的是:
旧 candidate 在新的 parser 和 validator 下还能不能成立?
如果答案是:
Parse = PASS
Reconstruction = PASS
Validation = PASS并且没有发现这个 Page 实际受到 parser change 影响,那么旧译文本身没有理由失效。
这也是“复用历史成果”非常重要的一层含义:
基础设施升级,不应该自动推翻语言成果。
已经通过 QC 的结果尤其值得保护
历史中还有一种特别有价值的信息:
Independent QC。
Generation 自己生成了一份 candidate,只能证明:
有了一份候选翻译。
Machine validation PASS,只能证明:
机器结构没有发现问题。
真正完成 Independent QC 以后,才多了一层:
有独立 Reviewer 对这个 TranslationUnit 做过语言和技术质量判断。
如果最终得到 A,那么这份结果已经积累了相当有价值的质量证据。
所以当未来工作流发生变化时,应该尽量保护这样的成果。
当然,这并不意味着:
“以前是 A,所以永远都是 A。”
如果 source 改了。
Glossary 真正影响了它。
Parser 发现过去漏掉了结构问题。
或者其他 authority 变化使旧结论不再成立。
那当然应该重新进入流程。
但如果这些事情都没有发生,仅仅因为:
milestone changed
workflow gained new features就把 A 全部重置,我觉得是非常浪费的。
历史复用还有一个容易忽略的价值:减少语言漂移
成本以外,还有一个我后来越来越重视的问题:
语言稳定性。
一个长期运行的文档站点,如果没有必要就不断重新生成翻译,会产生一种很奇怪的现象。
Source 明明没有变化。
用户今天读到:
“运行该命令即可……”
过一段时间重新生成以后,可能变成:
“可以通过执行该命令……”
再下一次又变成:
“执行此命令后……”
三句话都可能没有错。
但对于一个技术文档站点来说,这种变化其实没有意义。
它只是在制造:
translation churn。
更糟糕的是,每次这样的语言变化都可能触发:
- diff;
- QC;
- revision;
- Surface change;
- 其他下游判断。
所以可信历史的复用,还能帮助保持:
已经稳定下来的语言尽量稳定。
这对于术语一致性、文体一致性以及长期维护都很有价值。
新内容应该新增,而不是把旧内容一起卷回来
v2 的一个重要变化,是项目从 Tour 扩展到:
- Site shell;
- Learn;
- Docs;
- future content packages。
如果采用“新 milestone 全量重做”的方式,那么每新增一种内容,都很可能把以前的成果重新卷入。
例如:
加入 Learn
↓
重新处理 Tour + Learn
加入 Docs
↓
重新处理 Tour + Learn + Docs
未来加入 package X
↓
全部重新处理这种扩展方式很快就会失控。
更合理的模式应该是:
已有稳定内容
→ carry
新增内容
→ Generation / QC
真正受影响的旧内容
→ exact revision / re-QC也就是说:
新工作量应该尽量接近“新增和真实变化”,而不是接近“整个历史 corpus”。
这对长期扩展非常重要。
Milestone 不应该成为新的治理 namespace
因此 v2 以后,我也越来越明确另外一个设计原则:
v1、v2、v3 是 milestone,不应该自动变成新的语言治理 namespace。
如果每个 milestone 都建立:
v1 glossary
v1 translation history
v1 QC
v2 glossary
v2 translation history
v2 QC
v3 ...那么同一个 locale 的语言历史就会被人为切成很多段。
之后再想复用时,又需要不断解决:
v1 的 A 能不能迁移到 v2?
v2 glossary 和 v3 glossary 怎么对应?
这个 candidate 到底属于哪一代?
但从语言本身来看:
简体中文仍然是同一个简体中文 locale。
项目升级了。
Parser 进化了。
Content package 增加了。
但已经经过验证的语言成果,并不会因为工程 milestone 改名而天然过期。
所以更合理的方向是:
同一个 locale 延续自己的 glossary、Generation / Reviewer provenance 和可信语言历史。
真正发生变化时再处理变化。
Carry-forward 不是“复制旧文件”
不过,“复用历史”也很容易被理解成另一种危险的做法:
旧文件存在,那就直接拷贝过来。
这并不是我所说的可信复用。
文件存在,只能证明:
磁盘上还有东西。
它不能证明:
这份东西仍然符合当前 authority。
真正的 carry 至少应该建立在明确条件上。
例如:
source unchanged
glossary compatible
representation valid
candidate unchanged
previous QC trustworthy然后系统才能得出:
trusted unchanged carry也就是说:
复用不是绕过验证,而是证明“不需要重新做语言工作”。
这两者差别非常大。
Reuse 和 Revalidate 也不是同一个概念
这里还有一个很有用的区分。
一份历史 candidate 可以:
不重新 Generation
但仍然:
经过新的 deterministic machine validation。
例如 parser 升级以后,可以把旧 candidate 放到新 validator 中重新检查。
如果仍然 PASS,那么我们得到了一条新的机器事实。
但没有制造新的语言文本。
这往往比:
parser changed
↓
regenerate translation合理得多。
因此历史复用并不意味着:
“以前通过过,以后什么都不看。”
而更接近:
能确定性重新验证的东西重新验证;没有必要重新生成的语言不要重新生成。
这也是我觉得机器工作和语言工作的一个重要分界。
真正需要重新处理的,应该是异常集合
长期运行以后,一个比较健康的系统不应该每次面对:
all units而应该越来越接近:
missing
restore_failed
validation_failed
stale
affected也就是说,正常情况是:
已经稳定的内容继续保持稳定。
系统主要处理的是异常和变化。
这和很多长期运行的软件系统非常相似。
如果每一次部署都需要重建全部历史状态,规模一大就会变得越来越难维护。
翻译系统也一样。
随着 locale 越来越多、Page 越来越多、content package 越来越多:
全量重新处理应该越来越少,而不是越来越多。
历史的价值,本质上是减少重复劳动
做到这里以后,我觉得可以把这篇文章的核心压缩成一句很简单的话:
历史的价值,不是证明过去发生过多少事情,而是避免把已经做对的事情再做一遍。
它应该帮助系统复用:
- 已经翻译好的 candidate;
- 已经通过的 QC;
- 已经确定的 glossary decision;
- 已经验证的稳定内容。
然后把有限的时间和模型资源用在:
真正新增、真正改变、真正出现问题的地方。
这才是历史最初加入这个项目时真正想解决的问题。
但后来,历史开始变得越来越重
不过,这次 v2 也让我意识到另外一面。
当一个系统越来越重视 history、identity、predecessor、Snapshot、promotion 和各种 evidence 以后,很容易慢慢发生一个变化:
最开始:
保存历史,是为了更快向前。
后来却可能变成:
为了向前,必须先把越来越多的历史重新还原出来。
想恢复一个 current state,需要找到 predecessor。
找到 predecessor以后,又需要恢复更早的 Snapshot。
为了证明那份 Snapshot,又需要继续追溯更早的 evidence。
最后工程团队开始花大量时间:
不是在翻译新内容,而是在考古。
这就说明历史的角色已经开始偏离最初的目标。
而这次 v2 后期,一个非常重要的架构调整就是:
历史继续保留,但未来工作不再要求无限向前追溯和重建所有过去状态。
这也是下一篇准备整理的主题。
因为如果说这一篇是在回答:
为什么历史值得保留?
那么下一篇要回答的就是另外一个同样重要的问题:
什么时候应该停止还原历史?
而这个问题,恰恰也是这次 v2 实际开发时间远超最初预期的重要原因之一。
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 官方无隶属、授权或背书关系。
