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

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

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

作者:

,

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 社区翻译为什么难以长期维护

【图 4:es-ES 首次 Production 收口后连续完成的流程优化 commit】

(61) 从 es-ES 首次上线复盘:我把 A Tour of Go 新增语言的 Production 流程又收敛了一轮

图 1:旧公网验收链路连续出现 curl 28 超时,最终导致 public-machine 阶段失败

(62) A Tour of Go Production 公网验收踩坑:从跨境 SOCKS 链路改为 zgocloud direct

图 1:两次 Production Machine Acceptance 都已经通过,但 Headless Chrome 分别在 / 和 /tour/ 判定页面没有完成渲染

(63) 页面明明能打开,为什么 Headless Chrome 仍然判定 Production 失败?

图 3:Bing 从 Google Search Console 中识别到新的 it-IT 站点,并同时发现已有的 1 个 sitemap

(64) GSC → Bing Webmaster Tools:多语言站点如何减少重复验证

图 3:英文站 3 小时内出现 565 个 Cloudflare 来源 URL

(65) IndexNow 实战:手工提交长期不显示,Cloudflare Crawler Hints 却很快出现在 Bing Webmaster Tools

继续推进 A Tour of Go 多语言翻译项目时,我原本只是准备完成下一个代表性校准页面 methods/20

这一页的标题是:

Exercise: Errors

也就是“练习:错误”。

从页面结构来看,它并不算特别复杂:普通正文、行内代码、一个跨章节链接、两个预格式化 Go 代码块、一个加粗的 Note 提示段,以及最后的 .play 示例。

此前项目已经通过多个代表页面逐步完善了行内代码、legacy present 语法、预格式化代码、教学代码注释、强调结构等保护与校验。我原本判断,这一页应该可以直接进入第一次开发翻译。

没想到,第一次翻译就再次失败了。

但这一次,真正出问题的并不是 GLM-5.2 的翻译,而是我之前设计的一条看起来很合理、实际上对多语言翻译过于严格的规则:

所有受保护标记必须严格保持原始全局顺序。

这次排查最终让我重新划清了一个非常重要的边界:

结构校验应该负责机器可以可靠判断的结构安全,而不应该把不同语言之间正常的语序变化误判为结构损坏。


一、methods/20 第一次翻译:API 正常,自动校验却失败

methods/20 对应的官方原文如下:

Plaintext
* Exercise: Errors

Copy your `Sqrt` function from the [[/tour/flowcontrol/8][earlier exercise]] and modify it to return an `error` value.

`Sqrt` should return a non-nil error value when given a negative number, as it doesn't support complex numbers.

Create a new type

	type ErrNegativeSqrt float64

and make it an `error` by giving it a

	func (e ErrNegativeSqrt) Error() string

method such that `ErrNegativeSqrt(-2).Error()` returns `"cannot`Sqrt`negative`number:`-2"`.

*Note:* A call to `fmt.Sprint(e)` inside the `Error` method will send the program into an infinite loop. You can avoid this by converting `e` first: `fmt.Sprint(float64(e))`. Why?

Change your `Sqrt` function to return an `ErrNegativeSqrt` value when given a negative number.

.play methods/exercise-errors.go

第一次开发翻译执行:

Bash
go run ./cmd/tour-i18n translate run --locale zh-CN --id methods/20 --dev

GLM-5.2 请求本身正常完成:

Plaintext
Attempt: 1
finish_reason: stop

但是自动校验失败:

JSON
{
  "attempt": 1,
  "api_success": true,
  "token_valid": false,
  "present_valid": false,
  "failures": [
    "protected token order mismatch at 1",
    "protected token order mismatch at 2",
    "protected token order mismatch at 6",
    "protected token order mismatch at 7",
    "protected token order mismatch at 10",
    "protected token order mismatch at 11"
  ],
  "passed": false
}

页面因此仍然处于:

Plaintext
pending

乍一看,这似乎又是一次模型没有遵守 Protected Token 规则。

但继续检查后发现,情况完全不是这样。


二、16 个受保护标记,一个都没有丢

这一页一共生成了 16 个受保护标记。

GLM-5.2:

  • 没有删除任何标记;
  • 没有重复任何标记;
  • 没有伪造未知标记;
  • 所有 16 个标记都完整保留。

真正发生变化的,只是其中三组标记交换了出现顺序:

  • Sqrt 与链接 target;
  • error 与一个预格式化代码块;
  • fmt.Sprint(e)Error

也就是说,这次失败并不是:

模型破坏了受保护内容。

而是:

模型按照中文自然语序重新排列了部分受保护内容。

这两个问题的性质完全不同。


三、第一组换序:链接先出现,Sqrt 后出现

原文:

Copy your Sqrt function from the earlier exercise…

按照英语语序,大致是:

Plaintext
复制
→ 你的 Sqrt 函数
→ 从之前的练习中

而模型生成的中文是:

从之前的练习中复制你的 Sqrt 函数……

更自然的中文语序显然是:

Plaintext
从之前的练习中
→ 复制
→ 你的 Sqrt 函数

因此,模型输出中链接 target 对应的受保护标记出现在 Sqrt 之前。

旧校验器看到的是:

Plaintext
源顺序:

Sqrt
→ link target

模型输出变成:

Plaintext
link target
→ Sqrt

于是判定:

Plaintext
protected token order mismatch

但从翻译质量来看:

“从之前的练习中复制你的 Sqrt 函数”

不仅完全正确,而且明显比强行维持英文顺序自然。


四、第二组换序:error 被移动到代码块之后

原文这一段比较特殊:

Plaintext
and make it an `error` by giving it a

	func (e ErrNegativeSqrt) Error() string

method such that ...

英语表达的是:

通过给它下面这个方法,使它成为一个 error

模型翻译以后则变成:

Plaintext
并为其实现

	func (e ErrNegativeSqrt) Error() string

方法,使它成为一个 `error`

于是:

Plaintext
`error`
→ 预格式化代码块

变成:

Plaintext
预格式化代码块
→ `error`

如果只看受保护标记的全局顺序,这当然发生了变化。

但进一步分析 present 结构后发现:

  • 第二个代码块仍然位于原来的代码块位置;
  • 代码内容一个字节都没有变化;
  • 两个预格式化代码块没有互换;
  • 只是普通正文里的 error 从代码块之前移动到了代码块之后。

换句话说:

代码块根本没有移动。

改变的只是代码块前后自然语言的组织方式。

最终人工润色后,我将这一段调整为:

并为其实现
func (e ErrNegativeSqrt) Error() string
方法,使其实现 error 接口……

不仅结构正确,而且技术表达也比“使它成为一个 error”更准确。


五、第三组换序:Errorfmt.Sprint(e)

原文:

A call to fmt.Sprint(e) inside the Error method will send the program into an infinite loop.

英语顺序是:

Plaintext
fmt.Sprint(e)
→ Error

而模型翻译成:

Error 方法中调用 fmt.Sprint(e) 会导致程序陷入无限循环。

中文自然变成:

Plaintext
Error
→ fmt.Sprint(e)

旧规则于是再次判断 token 顺序错误。

但这个中文句子没有任何技术问题。

相反,如果为了强行保持英文 token 顺序,写成类似:

调用 fmt.Sprint(e)Error 方法中……

中文反而会明显变差。


六、这不是第一次:generics/1 已经暴露过相同问题

继续回查此前的翻译记录后,我发现,这并不是第一次出现这种情况。

generics/1 的一次历史翻译尝试中,也曾发生:

Plaintext
T
→ comparable

变成:

Plaintext
comparable
→ T

原文大致表达:

type T that fulfills the built-in constraint comparable

自然中文则更适合写成:

满足内置约束 comparable 的任意类型 T

这同样是非常典型的英语和中文语序差异。

当时只把它作为一次 token 换序失败处理。

直到这一次 methods/20 一页连续出现三组类似情况,我才有足够证据确认:

问题不是某一次模型偶然不遵守规则,而是“所有 Protected Token 必须保持全局顺序”这条规则本身不适合作为多语言结构不变量。


七、真正需要保护的到底是什么?

这里出现了一个非常关键的设计问题。

假设两个行内代码在翻译后交换位置,校验器是否能够仅根据 present AST 或 token 顺序确定:

这是正常的中文语序变化?

还是:

技术语义真的发生了错误颠倒?

很多情况下,答案是不能。

例如:

Plaintext
T / comparable

以及:

Plaintext
fmt.Sprint(e) / Error

从 present 结构来看,它们都只是普通正文里的 program span。

仅靠静态结构信息,没有一种通用算法能够同时做到:

  • 允许正确的跨语言语序变化;
  • 又准确拒绝所有技术语义上的错误换位。

如果硬要实现,最后很可能变成越来越多针对英语、中文甚至某个具体句式的启发式规则。

这与项目未来支持更多语言的目标反而背道而驰。

因此,我重新定义了校验器的职责:

校验器负责机器能够可靠验证的结构安全。

跨语言技术语义是否正确,不伪装成静态结构校验能力。


八、恢复前:不再要求 Protected Token 全局顺序一致

修改后的 Protected Token 恢复规则仍然非常严格。

模型输出必须满足:

  • token 总数正确;
  • 不允许未知 token;
  • 每个期望 token 恰好出现一次;
  • 不允许缺失;
  • 不允许重复;
  • token payload 仍然按照确定性映射逐字恢复。

唯一删除的是:

所有 token 必须严格保持一条全局原始顺序。

与此同时,行内 token 的边界规范化也调整为按照:

模型实际输出中的 token 顺序

执行,并根据每个 token 自身的类型判断对应规则。

这样模型可以为了目标语言的自然表达调整位置,但仍然不能:

  • 删除代码;
  • 重复代码;
  • 修改代码;
  • 伪造 token;
  • 偷换受保护内容。

九、恢复后:让 present 结构校验真正承担结构安全职责

取消全局顺序限制,并不意味着放弃严格校验。

恰恰相反,这次同时增强了恢复后的结构验证。

行内代码

以前要求:

内容、数量、顺序全部完全一致。

现在调整为:

内容和数量的多重集合一致,同时仍然必须正确解析为 program span。

因此:

Plaintext
T / comparable

可以因为目标语言语序发生变化。

但是以下情况仍然必须失败:

  • 某个行内代码消失;
  • 多出一个行内代码;
  • 内容发生修改;
  • legacy program span 被拆坏。

十、legacy program span 仍然严格保护

methods/20 中还有一个很典型的 legacy present 写法:

Plaintext
`"cannot`Sqrt`negative`number:`-2"`

它在 present 中实际是一个完整的 program span。

最终页面显示为:

Plaintext
"cannot Sqrt negative number: -2"

这类内容仍然作为一个完整 payload 保护。

模型不能拆分,也不能修改其中的 present 编码。

浏览器最终检查时,这一段正确显示为:

"cannot Sqrt negative number: -2"

没有出现内部反引号泄漏,也没有 program span 断裂。


十一、链接仍保持更严格的规则

链接 target 与普通行内代码不同。

目前仍然严格检查:

  • target 内容;
  • target 数量;
  • target 自身顺序;
  • target 是否仍然处于合法链接结构中。

也就是说,虽然某个:

Plaintext
link target

可以和旁边的 Sqrt 因为中文语序发生跨类别位置变化,但多个链接 target 之间暂时仍不能任意交换。

这是一个有意保持的保守策略。

目前没有真实案例证明多个链接必须为了目标语言语序互换,因此没有必要提前扩大放宽范围。


十二、预格式化代码仍逐块严格校验

对于预格式化代码,规则仍然严格。

继续检查:

  • 代码块数量;
  • 代码块顺序;
  • 静态代码块字节;
  • 可翻译教学注释中的代码结构;
  • 代码块所属 Section。

因此,虽然这次允许:

Plaintext
`error`
→ 代码块

调整为:

Plaintext
代码块
→ `error`

但绝不允许:

Plaintext
第一个代码块
↔ 第二个代码块

互换。

更不能将代码块移动到另一个 Section。

这正是:

自然语言可以重组,但结构对象本身不能被破坏。


十三、directive 也增加了 Section 级保护

.play.image 等 directive 同样继续严格检查:

  • 内容;
  • 数量;
  • directive 自身顺序;
  • 所属 Section。

此外,这次还增加了一条机器可以稳定判断的规则:

如果源 directive 是所属 Section 的最后一个元素,那么候选中也必须保持为最后一个元素。

例如:

Plaintext
.play methods/exercise-errors.go

它本身不仅决定代码文件,也决定教学页面中示例代码出现的位置。

如果模型把原本位于页面末尾的 .play 移到说明文字中间,即使 present 仍然能够解析,也应该被视为结构保护失败。

不过,我没有进一步要求:

directive 必须和所有普通文本段落保持完全相同的相对位置。

因为普通自然语言本来就可能因为翻译而拆分、合并或者调整表达顺序。


十四、固定翻译提示词也必须同步修改

只修改校验器还不够。

之前的 zh-CN 固定翻译提示词同样要求:

Protected Token 的数量、顺序和形式保持一致。

如果校验器已经允许自然语序调整,而提示词仍然要求全局顺序严格一致,两套规则就互相矛盾。

因此,这次同步修改提示词:

  • 所有 Protected Token 必须完整保留;
  • 每个 token 必须恰好出现一次;
  • 不得修改;
  • 不得删除;
  • 不得复制;
  • 不得伪造;
  • 允许为了目标语言自然语序进行必要的位置调整;
  • 但不得破坏链接、代码、directive、预格式化代码等结构关系。

这并不是一条只针对中文的特殊规则。

它更适合作为未来多语言扩展时共同遵守的原则:

保护结构,而不是保护英语语序。


十五、最关键的验证:没有重新调用 GLM-5.2

修改完成后,我没有立即进行第二次翻译。

因为如果重新调用一次 GLM-5.2,即使新结果通过,也无法直接证明:

旧结果真的只是被错误的校验规则拒绝。

因此,我保留了第一次翻译产生的原始:

  • request.json
  • response.json
  • validation.json

然后直接使用修改后的生产代码,对同一份 attempt-001 原始 response 进行确定性回放。

也就是说:

模型输出本身完全没有变化。

改变的只有校验规则。

旧结果:

Plaintext
protected token order mismatch at 1
protected token order mismatch at 2
protected token order mismatch at 6
protected token order mismatch at 7
protected token order mismatch at 10
protected token order mismatch at 11

新规则下,同一份 response:

  • token 完整性:PASS
  • token 恢复:PASS
  • present 解析:PASS
  • inline-code:PASS
  • link target:PASS
  • preformatted block:PASS
  • directive:PASS
  • emphasis / font span:PASS
  • glossary:PASS
  • Section 结构:PASS
  • candidate validation:PASS

这次回放非常关键。

它直接证明:

失败的不是翻译,而是旧校验规则。


十六、因此没有创建 attempt-002

既然原始 attempt-001

  • API 请求正常;
  • finish_reason=stop
  • 16 个 token 全部完整;
  • 修复规则后完整通过;
  • 人工审核也没有发现技术误译;

那么继续为了流程形式重新调用一次 GLM-5.2,就没有实际意义。

最终仍然保留:

Plaintext
methods/20
ready
Attempts=1

并没有虚构一个实际上不存在的第二次模型翻译。

第一次失败的历史记录也没有覆盖。

validation.json 仍然保留当时六项顺序失败,作为这次规则演进的完整历史证据。


十七、人工润色后的最终译文

最终采用的译文如下:

Plaintext
* 练习:错误

从[[/tour/flowcontrol/8][之前的练习]]中复制你的 `Sqrt` 函数,并修改它,让它返回一个 `error` 值。

当传入负数时,`Sqrt` 应当返回一个非 nil 错误值,因为它不支持复数。

创建一个新类型

	type ErrNegativeSqrt float64

并为其实现

	func (e ErrNegativeSqrt) Error() string

方法,使其实现 `error` 接口,这样 `ErrNegativeSqrt(-2).Error()` 就会返回 `"cannot`Sqrt`negative`number:`-2"`。

*注意:* 在 `Error` 方法中调用 `fmt.Sprint(e)` 会导致程序陷入无限循环。可以先转换 `e` 来避免这个问题:`fmt.Sprint(float64(e))`。为什么?

修改 `Sqrt` 函数,使其在传入负数时返回一个 `ErrNegativeSqrt` 值。

.play methods/exercise-errors.go

主要人工调整包括:

  • “前一个练习”改为“之前的练习”;
  • “并修改它使其返回”改为“并修改它,让它返回”;
  • “非 nil 的错误值”改为“非 nil 错误值”;
  • “使它成为一个 error”改为更准确的“使其实现 error 接口”。

随后重新执行正式候选译文校验:

Plaintext
candidate OK: locale=zh-CN page_id=methods/20

十八、浏览器中英文页面实际对照检查

完成候选译文校验后,我又启动了本地 Tour,对 methods/20 的英文原始页面和中文候选页面进行实际浏览器对照。

英文原始页面

图 1:A Tour of Go methods/20「Exercise: Errors」英文原始课程页面
图 1:A Tour of Go methods/20「Exercise: Errors」英文原始课程页面

这里的英文页面可以作为最终视觉结构的参照,包括:

  • 正文;
  • 跨章节链接;
  • 两个预格式化 Go 代码块;
  • Note 提示;
  • 右侧 exercise-errors.go 示例。

中文候选页面

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

中文页面截图尤其能直观说明,这次修改为什么是必要的。

例如,旧校验器曾经拒绝:

Plaintext
fmt.Sprint(e)
→ Error

变成:

Plaintext
Error
→ fmt.Sprint(e)

但是从最终中文页面可以直接看到:

Error 方法中调用 fmt.Sprint(e) 会导致程序陷入无限循环。

这正是正常而自然的中文表达,并没有造成任何 present 结构损坏。

浏览器实际对照检查确认:

  • “练习:错误”标题正常;
  • “之前的练习”链接正常;
  • 两个 Go 代码块的位置和内容正确;
  • "cannot Sqrt negative number: -2" 正常渲染;
  • legacy present 内部反引号没有暴露;
  • “注意:”正确加粗;
  • Errorfmt.Sprint(e) 调整顺序后的中文自然;
  • .play 对应的 exercise-errors.go 正常加载;
  • 页面没有 present 解析、渲染或运行时异常。

因此,浏览器最终结果进一步证明:

那三组被旧规则拒绝的 Protected Token 换序,并没有造成结构问题,反而是正常的跨语言语序调整。


十九、完整测试重新通过

修复完成,并同步 methods/20 的提交状态基线以后,完整测试重新通过:

Plaintext
ok github.com/shuijingwan/go-tour-i18n/internal/i18n 0.499s

git diff --check 也全部通过。

最终提交:

Plaintext
13d9129b3bb0223b5e39af7679868832352e3b42

提交说明:

Plaintext
feat: 完成 methods/20 并修复受保护标记顺序校验

随后已经推送到 origin/main

项目仓库:

https://github.com/shuijingwan/go-tour-i18n

二十、这次最大的收获:结构安全不等于英语顺序不变

这次问题让我更加明确了一件事情。

开发多语言翻译系统时,很容易把:

“原文结构”

和:

“原文表面顺序”

混为一谈,但它们并不是同一回事。比如:

Plaintext
fmt.Sprint(e)
→ Error

在英语里是自然顺序。

中文却更自然地写成:

Plaintext
Error
→ fmt.Sprint(e)

真正需要保持的是:

  • Error 仍然是正确的 program span;
  • fmt.Sprint(e) 没有被修改;
  • 二者都没有丢失;
  • present 仍然能够正确解析;
  • 技术含义仍然正确。

而不是:

两个字符串必须永远按照英语里的出现顺序排列。

同样:

“从之前的练习中复制你的 Sqrt 函数”

也不应该因为链接 target 出现在 Sqrt 之前,就被判定为结构失败。


二十一、自动校验越严格,并不意味着质量越高

这个问题还有一个很值得记录的教训:严格校验本身不是目标。

如果一条规则能够拒绝大量内容,却无法区分:

  • 真正的结构破坏;
  • 正常的跨语言语序调整;

那么规则越严格,产生的误报反而越多。

一个好的自动校验器应该尽可能严格地检查机器真正能够确定的事实。

例如:

  • token 有没有丢失;
  • 有没有重复;
  • 有没有未知 token;
  • 代码有没有改变;
  • link target 有没有被修改;
  • directive 是否仍然属于正确的 Section;
  • 预格式化代码块有没有互换;
  • present 是否仍然能够正确解析。

而对于:

“行内代码换序以后,跨语言技术语义是否仍然正确?”

如果静态结构无法可靠判断,就应该承认这个边界,而不是设计一个看似严格、实际上并不可靠的伪语义规则。


二十二、下一步:完成最后 3 个代表页

目前代表页校准阶段已经完成 4 页:

  1. generics/1
  2. flowcontrol/8
  3. methods/16
  4. methods/20

接下来已经确定继续处理另外 3 个不同类型的代表页。

concurrency/7

官方课程中唯一包含 .image 的正式页面。

主要验证:

  • .image directive;
  • 图片与正文混排;
  • 非尾部 directive;
  • JavaScript target 链接。

concurrency/11

长篇、多段、高密度链接页面。

主要验证:

  • 大量 link target;
  • 跨段链接;
  • slides 强制术语 label;
  • 没有代码和 directive 时的长文本稳定性。

methods/24

API 说明型页面。

主要验证:

  • 较长静态 interface 代码块;
  • 多个 API 链接;
  • 强调段;
  • 尾部 .play

如果这 3 页也稳定通过,总代表页将达到 7 页。

届时就准备结束这一阶段的逐页校准,进入:

10 个普通 pending 页面自动试跑。

如果 10 页试跑继续稳定,再进入剩余页面的批量翻译阶段。


结语

methods/20 原本只是计划中的又一个代表性校准页面。

最终,它却帮助这个项目发现并修掉了一个更加底层的问题:

Protected Token 的全局顺序并不是可靠的多语言结构不变量。

这一次最有说服力的地方,是我没有通过:

“重新翻译一次,直到它通过”

来绕开问题。

而是保留原始失败 response,修复校验器以后,再让同一份模型输出重新经过完整生产校验。

最终证明:GLM-5.2 第一次其实就已经翻译成功。

真正需要修正的,是自动校验系统对于“结构安全”的定义。

对于一个准备从简体中文继续扩展到更多语言的项目来说,这类问题比完成某一个页面本身更加重要。

因为真正需要构建的,并不是一套“尽量让译文保持英语原文表面顺序”的规则。

而是一套允许不同语言自然表达,同时仍然严格保护代码、链接、指令和 present 结构的多语言翻译流程。

A Tour of Go 中文翻译实录:如何只翻译教学代码注释,而不破坏 Go 代码 A Tour of Go 中文翻译完成 7 个代表页校准:最后 3 页又发现了哪些真实问题

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