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

原始文件一个字没变,为什么还要重新翻译?从 books.yaml 10→12 看语义输入扩展

结构化语义输入扩展示意图:同一份 books.yaml 原始文件保持不变,旧 parser 识别 10 个可本地化单元,当前 parser 识别 12 个,并新增 PDF 和 GitHub,确认旧单元无删除无修改后重新进入 Generation。

作者:

在

系列帖子

Markdown Page 经过 Parse / Protect、Generation 和 Reconstruct / Validate 的流程示意图,强调保护 inline code、链接、代码等机器语义,同时允许译文按照自然语言调整语序。

(1) Markdown 不是纯文本:Learn / Docs 为什么需要新的 parser 与结构保护

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

图 4:GitHub 与 ChatGPT 已成功连接

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

(36) 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 与本地翻译流水线之间的数据交接层。

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

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

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

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

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

【图 2:upstream source preview】

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

【图 1:A Tour of Go 项目首页目前的语言版本列表】

(67) 从 nl-NL 到 tr-TR:A Tour of Go 连续上线三门语言后,我又优化了哪些流程

【图 4:Go local 页面中已经出现 French、German、Korean、Simplified Chinese】

(68) 从邮件无人回复到进入 Go 官方 Go local:我的 4 个 A Tour of Go 翻译站终于被收录

图8:最终只保留一把启用的新 API Key

(69) 腾讯云 EdgeOne API 自动化:从 CAM 子用户到最小权限 API Key 的完整配置

【图 7:Production Release Batch 最终汇总】

(70) 从同步 Go 官方上游到一个命令发布 10 个语言站:A Tour of Go Production 流程的再次收敛

【图 3:Cloudflare Smart Tiered Cache 已启用】

(71) Cloudflare 缓存 HIT 很快,MISS 却不稳定:回源阿里云杭州的一次真实优化

【图 3:简体中文站 Support 页面】

(72) 广告之外,社区项目如何长期活下去?我给 A Tour of Go 多语言项目加了 Support

图5:评论成功发布

(73) 从 Go Weekly 到 Reddit、DEV 和 Go Forum:第一次系统推广 A Tour of Go 多语言项目

【图 4:RTL 桌面课程布局修复后的效果】

(74) 第一次把 A Tour of Go 做成 RTL:阿拉伯语版本上线,以及我踩过的几个坑

多语言项目应该先做哪些语言?我的 66 个语言版本商业优先级排序

(75) 多语言项目应该先做哪些语言?我的 66 个语言版本商业优先级排序

当术语一致性遇到性能瓶颈:Go 多语言翻译项目的一次架构取舍

(76) 当术语一致性遇到性能瓶颈:Go 多语言翻译项目的一次架构取舍

从 A Tour of Go 走向 Go Learning & Documentation:为什么这个多语言项目需要 v2

(77) 从 A Tour of Go 走向 Go Learning & Documentation:为什么这个多语言项目需要 v2

Markdown 明明可以拆成很多段,为什么 v2 仍然坚持一个 Page 就是一个 TranslationUnit

(78) Markdown 明明可以拆成很多段,为什么 v2 仍然坚持一个 Page 就是一个 TranslationUnit

Tour、Learn、Docs 为什么不应该各维护一份术语表:v2 的统一 Glossary 设计

(79) Tour、Learn、Docs 为什么不应该各维护一份术语表:v2 的统一 Glossary 设计

v2 术语表变更后的页面影响范围判断流程:区分 Compatible 与 Affected,只处理真正受影响的页面。

(80) 术语表改了,为什么不应该让所有页面重新翻译?v2 的影响范围判断

CLI 性能优化示意图:通过 Prepare Once、Reuse Many Times,将执行时间从 6m54.554s 降至 10.335s,约提升 40.1 倍。

(81) 从 6 分 54 秒到 10.3 秒:Go 多语言项目一次 CLI 性能优化实战

历史为什么值得保留:复用已经完成的翻译成果,而不是重新做一遍

(82) 历史为什么值得保留:复用已经完成的翻译成果,而不是重新做一遍

历史状态通过 Trusted Checkpoint 建立可信断点,旧记录继续保留用于审计和诊断,后续工作流从当前状态继续向前,不再要求无限重建过去。

(83) 历史应该被保留,而不是被反复重建:v2 为什么最终需要一个“断点”

v2 依赖与失效传播流程图:从内容变化开始,评估依赖关系和真实影响,将结果区分为“仍然有效”和“需要失效”,只让真正受影响的状态重新进入流程。

(84) 一次内容变化,到底应该让哪些状态失效?v2 的依赖与失效传播设计

v2 本地内容覆盖判断流程图:官方英文页面先检查是否已有正式本地覆盖,有覆盖时使用本地 URL,没有覆盖时直接链接 go.dev,并保持本地路径真实 404。

(85) 有英文页面,不代表一定应该有本地页面:v2 如何处理本地内容覆盖、canonical 与真实 404

v2 代码注释完整性审计流程图:从源页面建立代码块清单,分类教学注释、机器语义和无法判断的内容,再与当前译文精确对应,确认 missing、extra 和 protected mutation 均为 0。

(86) 代码块里的注释为什么不能只靠肉眼检查?v2 如何统计并验证代码注释翻译完整性

结构化语义输入扩展示意图:同一份 books.yaml 原始文件保持不变,旧 parser 识别 10 个可本地化单元,当前 parser 识别 12 个,并新增 PDF 和 GitHub,确认旧单元无删除无修改后重新进入 Generation。

(87) 原始文件一个字没变,为什么还要重新翻译?从 books.yaml 10→12 看语义输入扩展

前面几篇一直在讨论一个问题:

当内容发生变化以后,哪些已有结果还能继续复用,哪些必须失效?

但最近 _content/learn/books.yaml 遇到的情况更特别。

这一次,原始文件没有变化。

不是某一行标题被修改了。

不是新增了一本书。

也不是某段英文内容发生了更新。

从文件字节来看,它还是原来的那一份 books.yaml。

可是工作流重新读取以后,却发现:

以前只识别出了 10 个需要进入语言工作流的语义单元,现在变成了 12 个。

新增加的两个,是原来没有进入正式语言语义输入范围的展示文本槽位。它们当前的源值分别是 PDF 和 GitHub。

这里很容易产生一个疑问:

PDF 和 GitHub 本来就很可能在简体中文里保持原样,为什么还要把它们新增到语言输入里?

这个问题其实正好揭示了这次设计真正想解决的事情。

变化的重点不是:

这两个单词现在必须翻译成中文。

而是:

这两个面向用户展示的文本,现在正式进入了语言工作流。

也就是说:

进入语言工作流,不等于文字一定要发生变化。

对于简体中文来说,最终结果完全可能仍然是:

Plaintext
PDF → PDF
GitHub → GitHub

但现在这种“不变”,已经是正式语言流程处理后的结果,而不是因为 parser 根本没有识别到它们,所以偶然原样留了下来。

这篇就整理这个问题,以及后来为它增加的:

结构化语义输入扩展重新进入机制。


最开始,我也把“源文件有没有变化”看得太重了

很多增量工作流都会先判断:

Plaintext
源文件没有变化 → 已有结果继续复用
源文件发生变化 → 重新计算受影响范围

这套逻辑通常没有问题。

例如 Markdown 页面真正发生修改以后,可以比较 source identity,再判断哪些翻译单元(TranslationUnit)需要重新进入流程。

但结构化内容多了一层东西:

原始文件并不直接等于模型真正收到的翻译输入。

中间还有 parser。

以 books.yaml 为例,工作流并不是把整份 YAML 原样扔给模型。

它首先读取结构,然后识别哪些字段属于:

  • 面向用户展示的语言内容;
  • 机器结构;
  • URL;
  • 标识符;
  • 不应该修改的技术值;
  • 可以进入正式语言处理范围的展示文本。

最后,真正进入 Generation 的,是 parser 从原始结构中提取出来的语言语义。

可以简单理解成:

Plaintext
原始 YAML
→ parser 解析结构
→ 提取正式语言语义输入
→ Generation
→ 按原结构重建完整 YAML

所以决定模型任务的,不只有:

原始文件是什么。

还包括:

当前 parser 认为哪些内容属于正式语言输入。


这一次变化的不是 raw source,而是 semantic input

books.yaml 这次的实际状态很清楚。

旧状态可以概括成:

Plaintext
原始文件:未变化
旧语义单元:10
当前语义单元:12
新增:2
删除:0
旧单元变化:0

新增的不是两个“必须翻译成别的语言的单词”。

更准确地说,是:

两个以前没有进入正式语言语义输入、现在被 parser 识别出来的展示文本槽位。

它们当前的源值分别是:

Plaintext
PDF
GitHub

原来的 10 个语义单元全部还在。

它们的:

  • Unit ID;
  • locator;
  • source;
  • source SHA;
  • 坐标;
  • protection metadata;

都没有发生变化。

真正发生的事情只是:

当前 parser 比以前识别出了更多应该由语言工作流显式处理的展示内容。


PDF 和 GitHub 为什么也值得进入语言流程

这是这次最容易让人困惑的地方。

对于简体中文来说:

Plaintext
PDF
GitHub

确实大概率都不需要修改。

那为什么不能继续让它们留在语言流程之外?

原因是:

parser 应该根据字段的结构语义判断它是否属于面向用户的语言内容,而不是根据当前英文值“看起来需不需要翻译”来决定。

假设某个结构化字段今天的值是:

Plaintext
PDF

明天上游可能把它改成:

Plaintext
Download PDF

另一个今天显示:

Plaintext
GitHub

以后可能变成:

Plaintext
View on GitHub

如果 parser 的逻辑是:

“这个字段现在看起来像技术词,所以不进入语言流程。”

那么以后字段内容变化以后,就有可能继续被遗漏。

更稳妥的设计应该是:

这个字段本身是不是用户可见的展示文本?

如果答案是“是”,那它就应该进入语言语义输入。

至于最终目标语言是否改变文字,则是后面的语言处理决定。

所以:

Plaintext
进入语言工作流 ≠ 必须改写字符串

这是这次设计里很重要的一个区别。


“保持原样”本身也可以是正确的本地化结果

以前容易把“本地化”理解成:

英文一定要变成另一种语言。

但实际上并不是所有用户可见内容都必须改变。

例如:

  • 品牌名;
  • 产品名;
  • 缩写;
  • 标准名称;
  • API 名;
  • 某些通用技术术语;

在很多 locale 中,本来就应该保持原样。

所以:

Plaintext
source = PDF
target = PDF

并不自动代表:

没翻译。

它也可能代表:

这条展示文本已经进入正式语言流程,并被确认应该保持原样。

真正的问题不是字符有没有变化。

而是:

它有没有被正式处理。

这和上一篇代码注释完整性里遇到的问题其实很相似。

最终页面“看起来没问题”,不等于流程已经证明:

所有应该处理的内容都真正进入过流程。


如果只看 source SHA,会得到错误结论

假设系统只检查:

Plaintext
old source SHA == current source SHA

结果成立。

那么最容易做出的结论就是:

文件没有变化,所以旧翻译继续有效。

但旧 Generation 实际上只面对 10 个正式语义输入。

当前正式输入已经变成 12 个。

于是:

Plaintext
当前正式语义输入:12
旧 Generation 覆盖:10
当前新增输入:2

这时候,如果仍然把旧 Generation 判定为当前完整结果,相当于在说:

一份没有覆盖全部当前正式输入的输出,可以继续被认定为 current。

这显然不成立。

所以:

currentness 不能只绑定原始文件,还必须绑定 parser 产生的正式语义输入。


原始文件没变,为什么 parser 结果却会变化

因为 parser 本身也是工作流的一部分。

早期实现可能认为某些字段是:

技术标签,不需要进入本地化。

后来在真实页面和结构语义检查中发现:

这些字段实际上也是用户会看到的展示文本。

于是 parser 的正式语义规则发生了变化。

同一份:

Plaintext
_content/learn/books.yaml

旧 parser 得到:

Plaintext
10 个正式语言语义单元

当前 canonical parser 得到:

Plaintext
12 个正式语言语义单元

所以:

raw source bytes 没变,不代表 Generation 的 semantic inputs 没变。

更进一步说:

parser 如何解释 source,本身就是模型任务的一部分。


这不是普通的“文件失效”

一开始,这种情况很容易被简单归类成:

Plaintext
stale

这当然不完全错。

旧结果相对于当前语义输入,确实已经失效。

但只说“失效”还不够。

因为旧结果失效可能有很多不同原因:

  • source 变化;
  • glossary 变化;
  • Unified Review 变化;
  • parser 保护规则变化;
  • authority 变化;
  • 正式语义输入范围扩大。

这些情况不能全部使用同一种恢复方式。

这一次最重要的事实是:

旧 10 个语义单元没有错,也没有发生变化。

它们只是:

不再构成当前完整输入的全部内容。

这就需要比“全部作废”更准确的表达。


但也不能直接把旧结果 carry 成 current

既然旧 10 个单元全部没有变化,另一个很自然的想法是:

那就把旧 10 个结果直接继承,再补上新增的 2 个不就好了?

听起来很合理。

但正式工作流里还有一个重要边界。

books.yaml 是一个完整的 structured asset。

正式 Generation 对应的是:

当前完整 structured asset 的语言输出。

内部 10 个、12 个 slot,主要用于:

  • parser;
  • protection;
  • comparison;
  • reconstruction;
  • impact analysis。

它们不是 12 个独立的正式 TranslationUnit。

所以不能偷偷把流程变成:

Plaintext
旧 10 个结果
+ 新生成 2 个结果
→ 自动拼成新的正式 Generation

否则就会出现一个问题:

新的正式结果没有对应一次真实、完整的 Generation 输入与输出关系。

这样 provenance 就不再准确。

所以:

旧结果可以作为 predecessor context,但不能冒充当前完整 Generation。


这也不是普通 structured recovery

项目里已经有 structured recovery。

它适合处理的是另一类问题。

例如:

模型其实已经针对正确、完整的语义输入完成了 Generation,但后来 Bundle 因 transport、authority 表示或兼容性变化而 stale。

如果可以严格证明:

  • 原模型输入真实存在;
  • 原输出完整;
  • 当前 semantic inputs 没有发生变化;
  • 当前 validator 仍然接受;
  • 变化只是 transport 或兼容 authority;

那么可以恢复那次已经完成的真实工作。

这种恢复的本质是:

证明过去已经做完的工作今天仍然成立。

但 books.yaml 10→12 不是这种情况。

因为旧模型调用根本没有面对新增的两个正式语义输入。

所以:

不能使用 structured recovery,把真实缺少当前输入的旧 Generation 修补成 current。

这不是 transport 问题。

这是:

semantic input expansion。


所以后来增加了一个新的正式状态:语义输入扩展

最终需要表达的是这样一种情况:

Plaintext
raw source 没变
→ 旧语义单元全部精确保留
→ 当前语义输入是旧集合的严格超集
→ 至少新增一个正式语言语义单元
→ 需要重新执行完整 structured Generation

项目后来把它明确成:

Structured semantic-input-expansion reopen

也就是:

结构化语义输入扩展重新进入。

这里的“重新进入”非常重要。

它不是宣布:

旧翻译错了。

也不是宣布:

旧 Generation 当时不合法。

它真正表达的是:

旧 Generation 对当时的 10 个正式输入仍然是合法历史事实,但已经无法证明今天 12 个正式输入的完整结果。


为什么必须证明是“严格超集”

假设旧状态是:

Plaintext
A
B
C

当前状态变成:

Plaintext
A
B
C
D

这可以认为是单纯扩展。

旧内容全部保留,只新增 D。

但如果当前变成:

Plaintext
A
B'
C
D

情况就不同了。

因为 B 已经发生变化。

再比如当前变成:

Plaintext
A
C
D

那么旧的 B 已经消失。

这些都不能再叫:

单纯的语义输入扩展。

所以正式判断要求的是:

旧集合必须全部 exact preserved,当前集合只能在原基础上增加。

用集合关系表达就是:

Plaintext
old semantic inputs ⊂ current semantic inputs

而且必须是真正的严格超集。

不能只是:

  • ID 算法变化;
  • 坐标变化;
  • parser remap;
  • 旧单元重新编号;
  • 无法证明的新旧映射。

如果不能精确证明,就停止。


为什么必须先做只读 preflight

这种重新进入不能看到:

Plaintext
10 → 12

就立即创建新任务。

因为首先要证明:

这真的是语义输入扩展,而不是别的变化被误判成扩展。

所以正式机制先做只读 preflight。

至少需要确认:

  • historical completion 仍然可验证;
  • baseline Generation 仍然可验证;
  • baseline closure 仍然可验证;
  • accepted output 仍然存在;
  • locale、package、asset、path 绑定正确;
  • raw source bytes 没有变化;
  • contributor identity 没有变化;
  • 非 slot 的 Document context 和 dependencies 没有变化。

最关键的是:

旧的每一个 Unit 都必须原样存在。

包括:

  • ID;
  • locator;
  • source;
  • source SHA;
  • coordinate;
  • protection metadata。

同时必须确认:

Plaintext
removed = 0
changed = 0
added > 0

只有这样才能把它正式分类成:

semantic-input-expansion。


books.yaml 为什么符合这个条件

这次实际结果正好满足这些要求。

可以压缩成:

Plaintext
raw source bytes changed:no
旧语义单元:10
当前语义单元:12
新增:2
删除:0
旧单元变化:0

新进入正式语言语义输入的两个展示文本槽位,当前源值分别是:

Plaintext
PDF
GitHub

需要特别强调:

这里的“新增”指新增进入语言工作流,不是新增到原始 YAML 文件。

也不是说:

PDF 和 GitHub 必须翻译成中文。

对于 zh-CN,它们最终完全可以继续保持:

Plaintext
PDF
GitHub

真正变化的是:

它们现在由正式语言流程显式负责,而不再处于 parser 未覆盖的区域。


这种“不变”为什么比以前更可靠

旧状态里,PDF 和 GitHub 最终可能同样出现在页面上。

表面看起来:

页面没有任何问题。

但当时系统真正能够证明的只是:

parser 没有把它们送进语言流程。

它无法回答:

它们是不是本来就应该保持原样?

它们是不是用户可见文本?

它们是不是漏掉了?

新的 parser 把这些展示文本纳入正式语义输入以后,系统终于可以表达:

Plaintext
这个字段属于语言工作流
→ 当前目标语言处理后仍保持原值

这与:

Plaintext
这个字段从未进入语言工作流
→ 因此碰巧保持原值

是两个完全不同的状态。

最终字节可能一样。

但证据强度完全不一样。


为什么还要生成一份 reopen receipt

只读 preflight 能证明:

当前确实符合 semantic-input-expansion。

但后续 Generation Bundle、closure 和其他 reader 还需要知道:

为什么这个原本已经 stale 的 structured asset,现在被正式允许重新进入 Generation?

所以 apply 会生成一份正式 reopen receipt。

它会绑定:

  • 旧 Document identity;
  • 当前 Document identity;
  • structure/source/raw SHA;
  • 旧 Unit inventory;
  • 当前 Unit inventory;
  • 完整新增 semantic atoms;
  • accepted target SHA;
  • baseline Generation;
  • baseline closure;
  • baseline Surface;
  • 当前 glossary;
  • 当前 Unified Review。

这份 receipt 不是一句人工备注:

“这里可以重新翻译。”

而是一份机器可验证的因果证据:

为什么需要重新进入,以及当前新增的正式输入到底是什么。


receipt 不会替你翻译任何东西

这一点也很重要。

有了 reopen receipt,不等于:

Plaintext
PDF 已处理完成
GitHub 已处理完成

它不会:

  • 写 target;
  • 自动补 slot;
  • 创建语言质量等级;
  • 创建 package closure;
  • 创建 Surface PASS;
  • 执行 activation;
  • 进入 Production。

它只提供一种正式 authority:

这个 structured asset 现在确实需要重新进入 Generation,而且重新进入的原因已经被严格证明。

之后真正的语言工作仍然要继续。


新 Generation 为什么仍然要求完整输出 12 个单元

这里还有一个容易产生的疑问:

既然旧 10 个都没有变化,为什么新的 Generation 不能只返回 PDF 和 GitHub 两项?

原因还是正式资产边界。

books.yaml 的正式对象是:

完整 structured asset。

不是两个 patch。

所以新的 Generation Bundle 会携带:

  • 当前完整 Document;
  • 当前 raw source;
  • baseline accepted output;
  • predecessor context;
  • reopen receipt;
  • 当前 glossary;
  • 当前相关 authority。

模型可以看到旧状态作为上下文。

但最终仍然需要输出:

完整当前 asset 对应的 target。

这样才能确保:

  • provenance 真实;
  • reconstruction 完整;
  • schema 仍然成立;
  • current exact set 被完整覆盖。

为什么不把 12 个 slot 都升级成正式 TranslationUnit

这和上一篇代码注释完整性里提到的问题很相似。

内部粒度很细,不代表正式治理单位也应该跟着变细。

对于 books.yaml:

Plaintext
12 个 semantic slots

主要用于:

  • parser;
  • protection;
  • exact comparison;
  • reconstruction;
  • stale detection;
  • impact analysis。

但正式资产仍然是:

Plaintext
_content/learn/books.yaml

如果把每一个展示字段都升级成独立 TranslationUnit,就会引入:

  • 更多 Generation 状态;
  • 更多审核状态;
  • 更多 closure 组合;
  • 更多恢复分支;
  • 更复杂的 currentness 关系。

而这些字段本来就是同一个结构化资源的一部分。

所以当前设计仍然坚持:

内部粒度用于证明,正式资产边界用于治理。


重新 Generation 以后,后面的门禁一个都不能跳

semantic-input-expansion reopen 很容易被误解成:

既然已经证明只是多了两个字段,那么重新生成以后应该可以直接上线。

实际上不是。

新的完整 Generation 完成以后,仍然要继续经过:

Plaintext
Structured Generation
→ import
→ current validation
→ package closure
→ Integrated Surface Review
→ activation
→ preview
→ Production

因为新输出毕竟是一份新的正式 target。

它必须重新证明:

  • 当前 schema 正确;
  • glossary 合法;
  • reconstruction 正确;
  • structured output 完整;
  • 页面组合后没有整合站点问题;
  • activation 绑定当前 authority;
  • Preview 行为正确;
  • Production 展示正常。

所以:

“重新进入 Generation 的理由已经证明”不等于“新的 Generation 已经证明正确”。


这次也让我重新理解了 stale

以前看到 stale,很容易理解成:

旧东西已经错了。

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

旧证据已经不足以证明当前状态。

这两个概念差别很大。

books.yaml 的旧 Generation 并没有突然变成错误翻译。

它当时面对的是:

Plaintext
10 个正式语义输入

对于那 10 个输入,它仍然是合法历史事实。

今天不能继续把它当成 current,是因为:

Plaintext
当前正式输入 = 12
旧 Generation 证明范围 = 10

所以旧结果不是:

错了。

而是:

证明范围不够了。


历史合法,不代表今天仍然完整

这也是 v2 一直在强调的另一个边界。

今天 parser 变得更完整以后,不能回头说:

旧 Generation 当时就是非法的。

这是拿今天的规则去反向改写历史。

旧 Generation 当时绑定的是:

当时正式认可的 semantic inputs。

它应该继续作为历史事实存在。

新的 parser 只能说明:

从当前状态开始,旧结果已经不能满足 current eligibility。

所以需要区分:

historical validity:历史上是否合法。

和:

current eligibility:今天是否仍然足以作为当前结果。

历史合法,不代表今天仍然完整。

今天不完整,也不代表应该删除历史。


这和数据库 schema migration 很像

后来我觉得,可以用数据库迁移来理解这个问题。

假设一条旧记录以前只有:

Plaintext
name
url
description

系统当时正确处理了这三个字段。

后来业务模型正式增加:

Plaintext
pdf
github

旧记录不会因此突然变成:

错误数据。

但它已经不能完整满足新的 schema。

这时候系统需要:

明确识别 schema expansion,然后进入迁移。

而不是:

Plaintext
数据库原始文件没变
→ 所以什么都不用做

结构化翻译里的 semantic input expansion,本质上也是类似的问题。


parser 本身其实也是正式依赖

这次问题进一步暴露了一个以前不够明显的事实:

翻译结果依赖的不只是 source 和 glossary,也依赖 parser 如何解释 source。

同一份 source bytes:

Plaintext
parser v1 → 10 个正式语义输入
parser v2 → 12 个正式语义输入

对于模型来说,这已经是两个不同的任务。

所以 currentness 不能只看:

Plaintext
source SHA

还必须能够回答:

当前 canonical parser 对这份 source 的正式语义投影,是否仍然和旧 Generation 完全一致?

这也是为什么现代增量工作流里的“依赖”,不能只理解成:

文件依赖。

还包括:

解释这些文件的规则依赖。


但也不能因为 parser 有任何代码变化就全部重做

这里也不能走到另一个极端。

如果规则变成:

Plaintext
parser code changed
→ 所有 structured translations 全部失效

同样太粗暴。

parser 内部可能只是:

  • 性能优化;
  • 重构;
  • 错误信息改善;
  • 不影响某个 asset 的实现调整。

真正应该判断的是:

最终正式 semantic inputs 有没有变化。

如果 parser 更新以后,对某个 asset 的:

  • Unit set;
  • source;
  • coordinates;
  • protection metadata;

全部保持完全相同,那么就没有必要因为实现代码发生变化而强制重新翻译。

真正应该触发失效传播的是:

正式语义结果发生了变化。

而不是:

parser 文件有 diff。


所以真正重要的不是“文件变没变”,而是“模型任务变没变”

写到这里,这篇其实可以浓缩成一个问题:

今天要求模型完成的任务,和当初那个模型任务还是不是同一个?

对于这次 books.yaml,答案是:

不是。

虽然:

Plaintext
raw source = same

但:

Plaintext
old semantic input count = 10
current semantic input count = 12

模型真正面对的正式输入范围已经发生变化。

所以旧 Generation 无法继续作为 current。


一个更准确的 currentness 模型

如果非常简化地理解,最开始可能是:

Plaintext
current =
source 没变

后来逐渐变成:

Plaintext
current =
source 正确
+ semantic inputs 正确
+ glossary 正确
+ review authority 正确
+ output 对应正确
+ current validator 仍然接受

其中任何一层发生真正的语义变化,都可能让旧结果失去当前有效性。

这种设计确实更复杂。

但它能避免很多:

页面看起来没问题,所以就默认流程完整。

这样的假阳性。


这次最值得保留的几条原则

经过 books.yaml 10→12,我觉得可以把这次设计浓缩成几条原则。

第一:

raw source 没变,不等于 Generation semantic inputs 没变。

第二:

进入语言工作流,不等于目标字符串一定要发生变化。

像 PDF 和 GitHub 这种展示文本,在简体中文中完全可以正式处理后仍保持原样。

第三:

parser 应该根据字段的结构语义判断是否属于语言输入,而不是根据当前英文值看起来是否需要翻译来决定。

第四:

当前语义输入如果是旧输入的严格超集,可以明确识别成 semantic-input-expansion,而不是笼统归入普通 stale。

第五:

旧单元必须全部 exact preserved;存在删除、修改或歧义映射时,不能冒充单纯扩展。

第六:

旧 Generation 保留为历史事实,但历史上合法,不代表今天仍然足以证明完整。

第七:

reopen receipt 只负责证明重新进入流程的 authority,不负责生成翻译,也不授予 closure、Surface、activation 或 Production 权限。

第八:

structured recovery 只能恢复已经针对完整正确输入完成的真实工作,不能拿来修补真正新增的 semantic inputs。

第九:

内部 semantic slots 用来证明和重建,不因此升级成新的正式 TranslationUnit。

最终,我觉得这次最值得记住的一句话是:

判断一份翻译还能不能复用,不能只问“源文件有没有变化”,还必须问“今天要求模型完成的任务,和当初是不是同一个任务”。

而 PDF 和 GitHub 这个看起来很小的案例,恰好证明了:

即使最终文字一个字符都没有改变,只要它以前没有进入正式语言流程、今天已经正式进入,那么工作流语义本身就已经发生了变化。

有英文页面,不代表一定应该有本地页面:v2 如何处理本地内容覆盖、canonical 与真实 404

Go 学习与文档多语言本地化项目

本系列完整记录 Go 学习与文档多语言本地化项目从 A Tour of Go 起步,逐步扩展到站点首页、项目说明页、学习内容、文档以及未来内容包的实际开发过程,包括架构设计、统一术语治理、整页翻译、结构保护、自动校验、独立质量审核、生产发布与长期维护。

项目源码:
✅ GitHub:shuijingwan/go-tour-i18n

语言版本:

当前跨项目目标语言池共包含 66 个语言版本。Go 项目本身目前提供 65 个社区 Production 语言版本,下面每一门 Go 语言版本都直接链接到其当前公开站点。语言列表沿用项目首页现有的展示顺序。

英语(美国)仅作为跨项目目标语言纳入列表,并不是单独的 Go Production 语言版本。当前 Go 的英文源内容仍为官方 generic English,因此英语(美国)链接到 Go 官方站点,而不是不存在的本地社区部署。

  1. 阿姆哈拉语
  2. 阿拉伯语
  3. 孟加拉语
  4. 葡萄牙语(巴西)
  5. 保加利亚语
  6. 加泰罗尼亚语
  7. 克罗地亚语
  8. 捷克语
  9. 丹麦语
  10. 荷兰语
  11. 英语(美国)(Go 官方英文站点)
  12. 英语(澳大利亚)
  13. 英语(加拿大)
  14. 英语(印度)
  15. 英语(新加坡)
  16. 英语(南非)
  17. 英语(英国)
  18. 爱沙尼亚语
  19. 菲律宾语
  20. 芬兰语
  21. 法语(法国)
  22. 法语(加拿大)
  23. 德语(德国)
  24. 德语(奥地利)
  25. 德语(瑞士)
  26. 希腊语
  27. 古吉拉特语
  28. 希伯来语
  29. 印地语
  30. 匈牙利语
  31. 印度尼西亚语
  32. 意大利语
  33. 日语
  34. 卡纳达语
  35. 哈萨克语
  36. 韩语
  37. 西班牙语(拉丁美洲)
  38. 拉脱维亚语
  39. 立陶宛语
  40. 马来语
  41. 马拉雅拉姆语
  42. 马拉地语
  43. 书面挪威语(Bokmål)
  44. 波斯语
  45. 波兰语
  46. 葡萄牙语(葡萄牙)
  47. 旁遮普语(果鲁穆奇文,印度)
  48. 罗马尼亚语
  49. 俄语
  50. 塞尔维亚语(西里尔文)
  51. 简体中文(中国大陆)
  52. 简体中文(新加坡)
  53. 斯洛伐克语
  54. 斯洛文尼亚语
  55. 西班牙语(西班牙)
  56. 斯瓦希里语(坦桑尼亚)
  57. 瑞典语
  58. 泰米尔语
  59. 泰卢固语
  60. 泰语
  61. 繁体中文(台湾)
  62. 繁体中文(香港)
  63. 土耳其语
  64. 乌克兰语
  65. 乌尔都语
  66. 越南语

项目正在持续扩展新的 Go 学习与文档内容,并长期维护各语言版本的术语一致性、翻译质量、独立审核、生产发布与后续更新。

本项目属于非官方社区多语言本地化项目,与 Go 官方无隶属、授权或背书关系。