系列帖子
前面已经写过一篇:
Markdown 不是纯文本:Learn / Docs 为什么需要新的 parser 与结构保护
那篇主要讨论的是一个基础问题:
页面里并不是所有内容都可以直接交给翻译模型。
标题、正文、链接文字可以翻译。
代码关键字、标识符、路径、URL、指令和很多机器语义不能随便改。
代码块里的注释则更特殊。
有些注释只是机器指令,必须保持原样。
有些注释是真正面向学习者解释代码的自然语言,应该翻译。
做到这里以后,看起来问题似乎已经解决了:
能翻译的注释开放出来,不能翻译的代码继续保护。
但后来我发现,这还远远不够。
因为还有一个更难发现的问题:
我们怎么证明,所有本来应该进入翻译流程的代码注释,都真的被找到了?
如果漏掉了一条教学注释,翻译模型根本不会知道它存在。
质量审核者看到的,也可能只是 parser 已经挑出来的内容。
最终页面可以:
- 结构完全正确;
- Go 代码可以运行;
- 所有保护规则全部通过;
- 已经进入翻译流程的文字质量也很好;
但代码块中仍然悄悄残留一条没有翻译的英文说明。
这篇就整理 v2 后来为什么又增加了一层:
代码注释完整性审计。
“可以翻译代码注释”和“没有漏掉代码注释”是两个问题
这两个问题非常容易混在一起。
第一个问题是:
某一条代码注释能不能翻译?
例如:
// Print the result.
fmt.Println(result)这里的:
Print the result.明显是在向学习者解释代码。
这种内容可以作为教学注释进入翻译。
但下面这种:
//go:generate tool不是普通自然语言说明。
它属于 Go 指令,不能因为目标语言是中文,就把里面的内容翻译掉。
所以 parser 首先需要做的是分类:
教学性自然语言注释 → 可以翻译
机器语义注释 → 必须保护
未知或无法可靠判断 → 不猜这一部分解决的是:
翻译边界。
但第二个问题完全不同:
源页面里到底一共有多少条应该翻译的教学注释?
如果 parser 本来应该发现 20 条,却只发现了 19 条,那么后续流程无论做得多严格,都无法发现那第 20 条没有进入翻译。
所以:
“已经发现的注释都处理正确”并不能证明“所有注释都已经被发现”。
最危险的遗漏,往往不会报错
这种问题之所以值得单独做完整性检查,是因为它经常不会造成明显故障。
假设一个页面里有:
func main() {
// Calculate the total.
total := a + b
// Print the result.
fmt.Println(total)
}如果 parser 只识别到了第二条:
Print the result.那么生成出来的中文页面可能变成:
func main() {
// Calculate the total.
total := a + b
// 打印结果。
fmt.Println(total)
}代码完全合法。
页面也能正常渲染。
机器结构校验可能全部通过。
审核者如果主要看翻译候选,也可能不会意识到:
第一条注释其实从来没有进入过翻译流程。
这就是我现在觉得最需要警惕的一种错误:
静默遗漏。
它不像语法错误那样会立即失败。
反而很容易一路跟着内容进入生产环境。
只检查目标译文是不够的
一开始很容易想到一种检查方式:
扫描中文页面,看看代码块里还有没有英文。
但这个办法并不可靠。
因为代码本身本来就会包含大量英文。
例如:
func GetAlbumByID(id string) (*Album, error) {里面的:
GetAlbumByIDidstringAlbumerror
都应该保持原样。
API 名称、字段名、包名、文件名、JSON 字段、操作系统名称等,也可能合法保留英文。
所以不能简单做:
目标页面还有英文 → 一定漏译同样,也不能认为:
目标页面没有明显英文 → 一定完整真正可靠的判断必须从源内容开始。
第一步不是检查译文,而是建立唯一的源注释清单
v2 后来采用的思路是:
先从规范源内容建立完整的代码注释清单,再拿目标语言结果与它比较。
这个顺序非常重要。
源内容负责回答:
页面里到底有哪些代码块?
哪些代码块真正包含注释?
哪些注释属于教学自然语言?
哪些属于机器语义?
哪些代码块当前还无法安全判断?
只有先把这一层证明完整,后面的“有没有漏译”才有意义。
项目现在专门提供了代码注释审计入口,例如:
tour-i18n code-comments unsupported --output-dir /tmp
tour-i18n code-comments coverage --output-dir /tmp
tour-i18n code-comments audit --output-dir /tmp这些命令本身属于只读工程审计。
它们可以输出诊断 JSON 和文本证据,但不会修改:
- 候选译文;
- Candidate Snapshot;
- 质量审核;
- authorization;
- 内容包封存;
- 整合站点状态;
- activation。
也就是说:
先证明问题,再决定是否需要进入正式修订流程。
而不是扫描到什么,就立即开始改内容。
为什么不能直接搜索 //
如果只是 Go 代码,看起来似乎可以用很简单的办法:
搜索 //然后把后面的内容都统计出来。
但这个办法很快就会出问题。
例如字符串里可能有:
url := "https://go.dev/doc/"这里的:
//当然不是注释。
同样,页面里还可能出现:
- JSON;
- go.mod;
- 命令行;
- 终端输出;
- SQL;
- HTML
<pre>; - 文档里的路径示例;
- 普通纯文本代码块。
不同语言对注释的定义完全不同。
例如:
#在某些语言里可能是注释。
但在 shell 参数、URL fragment、普通输出或文本示例里,它又不一定代表注释。
所以完整性检查不能依赖:
“看到某个符号,就猜它是注释。”
而必须依赖:
已经证明的源语言和完整语法。
语言本身也不能靠文件路径猜
另一个很容易出现的捷径是:
这个页面在 Go 教程里,所以代码块应该都是 Go。
或者:
里面出现了
//,所以应该是 Go。
这同样不可靠。
v2 当前使用的原则更保守。
判断代码块语言时,优先使用:
- Markdown fence 明确声明的语言;
- HTML 中明确的
language-*; - 已有 source parser 或 metadata 的正式语义;
- 最后才是严格 parser 对整个代码块的完整接受。
不能使用:
- 目标译文内容;
- locale;
- Preview;
- 文件路径猜测;
- 某几个 ASCII 关键字;
- 宽松正则表达式;
来证明源代码语言。
这一点很重要。
因为:
一旦把语言猜错,后面的“没有注释”也可能是假结论。
“没识别到注释”不能自动等于“这里没有注释”
这大概是整个完整性设计里,我最认可的一条原则。
假设遇到一个代码块。
当前 parser 不认识它的语言。
最简单的实现可能会得到:
发现注释数 = 0但这个 0 到底代表什么?
可能代表:
parser 完整理解了这段代码,并证明其中确实没有注释。
也可能只是:
parser 根本不知道该怎么分析。
这两个状态完全不同。
所以 v2 不允许把:
无法分析
偷偷变成:
已经证明没有注释
未知语言必须明确保留成“不支持”或“存在歧义”。
只要仍然存在无法证明的代码块,就不能声称:
代码注释覆盖已经完整。
这和整个项目一直采用的原则是一致的:
无法证明时,按失败处理,不猜。
每一个代码块都必须得到明确结论
为了避免代码块在统计过程中悄悄消失,v2 会给每一个原始代码块一个明确状态。
可以大致理解成下面几类。
已经提取注释
说明语言受支持,而且已经建立了精确的注释槽位。
通过完整语法证明没有注释
不是“没搜到”,而是 parser 已经证明其中没有注释。
终端会话记录
明确区分命令和输出,不能再把输出中的 #、// 或 -- 当成程序注释。
非代码输出或字面量
源内容本身已经证明这是输出、数据或文字示例,而不是需要继续分析的程序源码。
机器语义整体保护
内容明确属于机器语义,整块保持不变。
存在歧义、暂不支持
无法可靠确定,就明确留下未解决状态。
这里最重要的不是分类名字本身。
而是:
每一个原始代码块必须出现,而且只能得到一个结论。
不能:
- 丢块;
- 多出块;
- 改变顺序;
- 用目标译文重新猜类型;
- 把未知块默认为零注释。
统计的重点不是只有“注释总数”
如果真要避免遗漏,只统计:
一共有多少条注释仍然不够。
更有价值的统计应该同时包括:
- 一共有多少代码块;
- 多少代码块成功提取注释;
- 多少通过语法证明没有注释;
- 多少属于终端记录;
- 多少属于输出或字面量;
- 多少属于机器语义;
- 多少仍然无法可靠分类;
- 每种源语言分别有多少代码块;
- 一共有多少教学注释;
- 目标译文中多少注释仍然与源内容完全相同;
- 是否存在缺失注释;
- 是否存在额外注释;
- 是否存在代码结构或受保护内容变化。
这样统计才真正回答:
整个 source coverage 是不是完整的?
而不是只给出一个看起来很漂亮的数字。
注释身份不能只依赖行号
还有一个容易被忽略的问题:
怎样确认源注释和目标译文中的注释是同一条?
如果只使用行号,会很脆弱。
Markdown 页面里只要前面增加几行内容,后面的所有行号都会变化。
所以 v2 的注释身份会绑定更稳定的上下文,例如:
- 内容包;
- source path;
- parent kind / ID;
- parser block coordinate;
- comment ordinal;
- 前一个词法 token 的位置;
- source semantic kind。
这里的目标不是:
给每条注释做一个永远不变的身份证。
因为源代码真正发生语义变化以后,对应关系本来就可能需要重新建立。
真正需要的是:
在当前这份规范源内容里,能够精确证明这条注释是谁。
Source 变化以后,旧统计也不能自动继续用
这和前面写过的失效传播问题其实又连起来了。
假设昨天完成了:
代码块数量 = X
教学注释数量 = Y
未知代码块 = 0今天规范源内容发生了变化。
那么不能简单认为:
昨天已经统计过了,所以今天继续用。
因为源内容或者代码结构变化以后:
- 可能新增代码块;
- 可能新增注释;
- 可能删除注释;
- 可能改变注释所在结构;
- 可能让旧的对应关系失效。
所以 source inventory 也有自己的当前性。
统计结果只有绑定当前源内容时才有意义。
对目标语言的检查,必须使用当前正式译文
建立完整源清单以后,下一步才是比较目标语言。
这里也不能随便从某个旧目录读取文件。
因为项目里可能同时存在:
- 历史 Snapshot;
- 旧 candidate;
- 已经失效的 promotion;
- 当前正式 candidate;
- Preview 中的渲染结果。
真正的代码注释完整性检查必须读取:
已经由当前正式 authority 证明的目标内容。
如果无法证明某份目标译文仍然是当前版本,就应该明确输出:
CURRENT_TARGET_UNAVAILABLE而不是:
“这里刚好有一个文件,就先拿它比较。”
这也是为什么完整性审计和历史恢复不能混在一起。
对中文来说,教学注释完全没变化,本身就是一个强烈信号
假设英文源注释是:
// Return the vulnerability ID.中文目标页面里仍然是:
// Return the vulnerability ID.如果这条注释已经被正式分类为:
teaching-comment
那么对于中文这种非英语目标语言来说:
完全保留英文源文本,本身就应该被机器视为失败。
因为这时候已经不需要 Reviewer 来判断:
“这句话是不是漏翻了?”
机器已经知道:
- 它是可翻译的教学自然语言;
- 当前 locale 不是英语;
- 目标文本与源文本完全相同。
因此可以确定:
这条教学注释没有真正进入目标语言。
而对于英语变体,例如 en-GB、en-CA,这种 exact carry 又可能完全合理。
所以这里还需要知道目标 locale 的基础语言,而不是简单维护一张“哪些 locale 是英文”的手写名单。
但“文字不同”也不能证明翻译正确
反过来也一样。
如果:
source comment ≠ target comment只能证明:
两边字节不完全相同。
它不能证明:
翻译质量正确。
例如原文:
Return the vulnerability ID.目标内容被改成一句完全错误的中文。
机器只能确认:
它不是原样照搬英文了。
但自然度、准确性和术语仍然需要独立 Reviewer 判断。
所以这里的职责边界很清楚:
机器负责证明完整性和结构对应。
Reviewer 负责判断语言质量。
“有没有漏译”不应该先交给 Reviewer 猜
这一点我觉得特别重要。
如果机器已经能够确定:
- source 有一条教学注释;
- target 对应位置不存在;
- target 多出一条无法对应的注释;
- 受保护内容发生变化;
- 代码块结构不一致;
这些都不应该先拿给 Reviewer 问:
“你觉得是不是漏译了?”
因为这是确定性的结构事实。
Reviewer 更应该把时间花在:
- 翻译是否准确;
- 表达是否自然;
- 术语是否合适;
- 教学意图是否保留;
这些真正需要语言判断的地方。
所以:
能由机器确定的问题,就不要消耗人工语言审核。
完整性失败也不应该让整个内容包全部重做
如果统计发现:
49 个页面里只有 3 个页面存在教学注释遗漏。
最简单的做法仍然是:
整个内容包重新生成
→ 所有页面重新审核但这又回到了前面几篇一直在避免的问题。
更合理的是:
确定受影响页面
→ 只重新打开这些页面
→ 精确修订遗漏的注释
→ 重新执行机器校验
→ 独立复审
→ 建立新的后继正式状态其他已经证明:
- source 没变化;
- 注释完整;
- candidate 当前有效;
- 之前 A 仍然可信;
的页面继续继承。
所以代码注释完整性最终也接入了同一套:
affected-only
思路。
不是再造一套特殊工作流。
这也解释了为什么内部 comment slot 不能变成新的正式翻译单元
为了统计注释,内部一定会生成非常细的记录。
例如:
Page
→ Code Block 3
→ Comment 2
→ teaching-comment但这并不意味着应该把每一条 comment 都变成正式 TranslationUnit。
正式治理边界仍然是:
一个物理 Page,就是一个正式 Page TranslationUnit。
内部 comment slot 的作用是:
- parser;
- 保护;
- 还原;
- 校验;
- 完整性审计;
- 精确影响分析。
它们是为了让机器看得更细。
而不是为了把正式审核流程切得越来越碎。
这和前面“Page 是治理单位,slot 是内部解析单位”的设计仍然保持一致。
未来的新内容,也可以先做源侧预检
这套统计还有一个我很喜欢的用途:
内容还没有正式进入 TranslationUnit 生命周期之前,就可以先检查代码注释覆盖风险。
例如未来准备纳入更多 /doc/ 页面。
可以先从冻结的官方 source 做只读分析:
- 有多少代码块;
- 哪些语言已经支持;
- 是否存在未知语法;
- 是否存在无法分类的注释;
- 当前 parser 是否已经能够完整覆盖。
如果 source 阶段就发现:
有大量 ambiguous code block
那么完全可以先修 parser。
而不是等内容正式进入 Generation 以后,才发现:
原来还有一批代码注释根本没有被识别。
这其实是在把问题尽量提前。
好的完整性检查,应该能够证明“零遗漏”,而不是报告“看起来没问题”
我觉得这篇最核心的区别就在这里。
最弱的检查方式是:
我扫了一遍,好像没有英文注释。
稍微强一点的是:
已经识别的所有 teaching comment 都有目标文本。
而真正希望达到的是:
所有原始代码块都已建立 disposition
→ 不存在 ambiguous unsupported
→ 所有 source teaching comment 都有精确身份
→ 所有 current target comment 都能精确对应
→ 不存在 missing
→ 不存在 extra
→ 不存在 protected mutation
→ 不存在 structure mismatch到了这里以后,才能更有底气地说:
当前支持范围内,没有已知代码注释遗漏。
注意,这句话里最重要的其实是:
当前支持范围内。
如果还有未知 grammar,就必须明确说:
目前仍然是 partial coverage。
不能因为已识别部分全部通过,就把未知部分一起算成 PASS。
这和普通翻译质量审核解决的是两个不同问题
做到这里以后,我觉得可以把整个问题分成两条线。
第一条是质量:
已经进入翻译流程的文字,翻译得好不好?
第二条是完整性:
所有应该进入翻译流程的文字,有没有真的全部进入?
第一条主要依赖 Independent Reviewer。
第二条则更依赖:
- source inventory;
- parser;
- 精确对应;
- currentness;
- deterministic validation。
如果只做第一条,就可能得到:
每一句已审核译文都很好。
但仍然有内容从来没有进入审核。
所以:
翻译质量高,不自动等于翻译完整。
这次设计让我重新理解了“统计”的意义
以前提到统计,很容易想到:
有多少页面
有多少注释
有多少已经翻译后来我越来越觉得:
统计不是为了做报表,而是为了建立完整性证明。
真正有价值的统计不是一个总数。
而是能够回答:
总数是怎么来的?
是否覆盖了全部 source block?
哪些被证明没有注释?
哪些仍然未知?
哪些目标内容缺失?
哪些只是机器语义而不应该翻译?
如果这些问题回答不了,那么:
translated_comments = 100%这样的数字本身并没有太大意义。
最终形成的几条原则
现在回头看,代码注释完整性这部分可以浓缩成几条规则。
第一:
“能够翻译教学注释”不等于“没有遗漏教学注释”。
第二:
完整性检查必须从规范源内容建立唯一 inventory,不能从目标译文反推。
第三:
不能靠
//、#、文件路径或几个关键字猜测代码语言和注释边界。
第四:
unsupported 不等于零注释;无法证明时必须明确保留未知状态。
第五:
每一个原始代码块都必须得到唯一、可追溯的处理结论。
第六:
机器负责 missing、extra、结构和受保护内容完整性;Reviewer 负责翻译质量。
第七:
目标比较只能使用当前正式译文,不能因为历史文件存在就自动复用。
第八:
完整性失败只重新打开真正受影响的 Page 或 Example,不让无关的 A 全部重做。
最终目标并不是:
尽可能找出一些代码注释来翻译。
而是:
能够证明所有应该进入翻译流程的代码注释,都没有被静默遗漏。
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 官方无隶属、授权或背书关系。

