系列帖子
前面几篇一直在讨论一个问题:
当内容发生变化以后,哪些已有结果还能继续复用,哪些必须失效?
但最近 _content/learn/books.yaml 遇到的情况更特别。
这一次,原始文件没有变化。
不是某一行标题被修改了。
不是新增了一本书。
也不是某段英文内容发生了更新。
从文件字节来看,它还是原来的那一份 books.yaml。
可是工作流重新读取以后,却发现:
以前只识别出了 10 个需要进入语言工作流的语义单元,现在变成了 12 个。
新增加的两个,是原来没有进入正式语言语义输入范围的展示文本槽位。它们当前的源值分别是 PDF 和 GitHub。
这里很容易产生一个疑问:
GitHub本来就很可能在简体中文里保持原样,为什么还要把它们新增到语言输入里?
这个问题其实正好揭示了这次设计真正想解决的事情。
变化的重点不是:
这两个单词现在必须翻译成中文。
而是:
这两个面向用户展示的文本,现在正式进入了语言工作流。
也就是说:
进入语言工作流,不等于文字一定要发生变化。
对于简体中文来说,最终结果完全可能仍然是:
PDF → PDF
GitHub → GitHub但现在这种“不变”,已经是正式语言流程处理后的结果,而不是因为 parser 根本没有识别到它们,所以偶然原样留了下来。
这篇就整理这个问题,以及后来为它增加的:
结构化语义输入扩展重新进入机制。
最开始,我也把“源文件有没有变化”看得太重了
很多增量工作流都会先判断:
源文件没有变化 → 已有结果继续复用
源文件发生变化 → 重新计算受影响范围这套逻辑通常没有问题。
例如 Markdown 页面真正发生修改以后,可以比较 source identity,再判断哪些翻译单元(TranslationUnit)需要重新进入流程。
但结构化内容多了一层东西:
原始文件并不直接等于模型真正收到的翻译输入。
中间还有 parser。
以 books.yaml 为例,工作流并不是把整份 YAML 原样扔给模型。
它首先读取结构,然后识别哪些字段属于:
- 面向用户展示的语言内容;
- 机器结构;
- URL;
- 标识符;
- 不应该修改的技术值;
- 可以进入正式语言处理范围的展示文本。
最后,真正进入 Generation 的,是 parser 从原始结构中提取出来的语言语义。
可以简单理解成:
原始 YAML
→ parser 解析结构
→ 提取正式语言语义输入
→ Generation
→ 按原结构重建完整 YAML所以决定模型任务的,不只有:
原始文件是什么。
还包括:
当前 parser 认为哪些内容属于正式语言输入。
这一次变化的不是 raw source,而是 semantic input
books.yaml 这次的实际状态很清楚。
旧状态可以概括成:
原始文件:未变化
旧语义单元:10
当前语义单元:12
新增:2
删除:0
旧单元变化:0新增的不是两个“必须翻译成别的语言的单词”。
更准确地说,是:
两个以前没有进入正式语言语义输入、现在被 parser 识别出来的展示文本槽位。
它们当前的源值分别是:
PDF
GitHub原来的 10 个语义单元全部还在。
它们的:
- Unit ID;
- locator;
- source;
- source SHA;
- 坐标;
- protection metadata;
都没有发生变化。
真正发生的事情只是:
当前 parser 比以前识别出了更多应该由语言工作流显式处理的展示内容。
PDF 和 GitHub 为什么也值得进入语言流程
这是这次最容易让人困惑的地方。
对于简体中文来说:
PDF
GitHub确实大概率都不需要修改。
那为什么不能继续让它们留在语言流程之外?
原因是:
parser 应该根据字段的结构语义判断它是否属于面向用户的语言内容,而不是根据当前英文值“看起来需不需要翻译”来决定。
假设某个结构化字段今天的值是:
PDF明天上游可能把它改成:
Download PDF另一个今天显示:
GitHub以后可能变成:
View on GitHub如果 parser 的逻辑是:
“这个字段现在看起来像技术词,所以不进入语言流程。”
那么以后字段内容变化以后,就有可能继续被遗漏。
更稳妥的设计应该是:
这个字段本身是不是用户可见的展示文本?
如果答案是“是”,那它就应该进入语言语义输入。
至于最终目标语言是否改变文字,则是后面的语言处理决定。
所以:
进入语言工作流 ≠ 必须改写字符串这是这次设计里很重要的一个区别。
“保持原样”本身也可以是正确的本地化结果
以前容易把“本地化”理解成:
英文一定要变成另一种语言。
但实际上并不是所有用户可见内容都必须改变。
例如:
- 品牌名;
- 产品名;
- 缩写;
- 标准名称;
- API 名;
- 某些通用技术术语;
在很多 locale 中,本来就应该保持原样。
所以:
source = PDF
target = PDF并不自动代表:
没翻译。
它也可能代表:
这条展示文本已经进入正式语言流程,并被确认应该保持原样。
真正的问题不是字符有没有变化。
而是:
它有没有被正式处理。
这和上一篇代码注释完整性里遇到的问题其实很相似。
最终页面“看起来没问题”,不等于流程已经证明:
所有应该处理的内容都真正进入过流程。
如果只看 source SHA,会得到错误结论
假设系统只检查:
old source SHA == current source SHA结果成立。
那么最容易做出的结论就是:
文件没有变化,所以旧翻译继续有效。
但旧 Generation 实际上只面对 10 个正式语义输入。
当前正式输入已经变成 12 个。
于是:
当前正式语义输入:12
旧 Generation 覆盖:10
当前新增输入:2这时候,如果仍然把旧 Generation 判定为当前完整结果,相当于在说:
一份没有覆盖全部当前正式输入的输出,可以继续被认定为 current。
这显然不成立。
所以:
currentness 不能只绑定原始文件,还必须绑定 parser 产生的正式语义输入。
原始文件没变,为什么 parser 结果却会变化
因为 parser 本身也是工作流的一部分。
早期实现可能认为某些字段是:
技术标签,不需要进入本地化。
后来在真实页面和结构语义检查中发现:
这些字段实际上也是用户会看到的展示文本。
于是 parser 的正式语义规则发生了变化。
同一份:
_content/learn/books.yaml旧 parser 得到:
10 个正式语言语义单元当前 canonical parser 得到:
12 个正式语言语义单元所以:
raw source bytes 没变,不代表 Generation 的 semantic inputs 没变。
更进一步说:
parser 如何解释 source,本身就是模型任务的一部分。
这不是普通的“文件失效”
一开始,这种情况很容易被简单归类成:
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。
所以不能偷偷把流程变成:
旧 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。
所以后来增加了一个新的正式状态:语义输入扩展
最终需要表达的是这样一种情况:
raw source 没变
→ 旧语义单元全部精确保留
→ 当前语义输入是旧集合的严格超集
→ 至少新增一个正式语言语义单元
→ 需要重新执行完整 structured Generation项目后来把它明确成:
Structured semantic-input-expansion reopen
也就是:
结构化语义输入扩展重新进入。
这里的“重新进入”非常重要。
它不是宣布:
旧翻译错了。
也不是宣布:
旧 Generation 当时不合法。
它真正表达的是:
旧 Generation 对当时的 10 个正式输入仍然是合法历史事实,但已经无法证明今天 12 个正式输入的完整结果。
为什么必须证明是“严格超集”
假设旧状态是:
A
B
C当前状态变成:
A
B
C
D这可以认为是单纯扩展。
旧内容全部保留,只新增 D。
但如果当前变成:
A
B'
C
D情况就不同了。
因为 B 已经发生变化。
再比如当前变成:
A
C
D那么旧的 B 已经消失。
这些都不能再叫:
单纯的语义输入扩展。
所以正式判断要求的是:
旧集合必须全部 exact preserved,当前集合只能在原基础上增加。
用集合关系表达就是:
old semantic inputs ⊂ current semantic inputs而且必须是真正的严格超集。
不能只是:
- ID 算法变化;
- 坐标变化;
- parser remap;
- 旧单元重新编号;
- 无法证明的新旧映射。
如果不能精确证明,就停止。
为什么必须先做只读 preflight
这种重新进入不能看到:
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。
同时必须确认:
removed = 0
changed = 0
added > 0只有这样才能把它正式分类成:
semantic-input-expansion。
books.yaml 为什么符合这个条件
这次实际结果正好满足这些要求。
可以压缩成:
raw source bytes changed:no
旧语义单元:10
当前语义单元:12
新增:2
删除:0
旧单元变化:0新进入正式语言语义输入的两个展示文本槽位,当前源值分别是:
PDF
GitHub需要特别强调:
这里的“新增”指新增进入语言工作流,不是新增到原始 YAML 文件。
也不是说:
GitHub必须翻译成中文。
对于 zh-CN,它们最终完全可以继续保持:
PDF
GitHub真正变化的是:
它们现在由正式语言流程显式负责,而不再处于 parser 未覆盖的区域。
这种“不变”为什么比以前更可靠
旧状态里,PDF 和 GitHub 最终可能同样出现在页面上。
表面看起来:
页面没有任何问题。
但当时系统真正能够证明的只是:
parser 没有把它们送进语言流程。
它无法回答:
它们是不是本来就应该保持原样?
它们是不是用户可见文本?
它们是不是漏掉了?
新的 parser 把这些展示文本纳入正式语义输入以后,系统终于可以表达:
这个字段属于语言工作流
→ 当前目标语言处理后仍保持原值这与:
这个字段从未进入语言工作流
→ 因此碰巧保持原值是两个完全不同的状态。
最终字节可能一样。
但证据强度完全不一样。
为什么还要生成一份 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,不等于:
PDF 已处理完成
GitHub 已处理完成它不会:
- 写 target;
- 自动补 slot;
- 创建语言质量等级;
- 创建 package closure;
- 创建 Surface PASS;
- 执行 activation;
- 进入 Production。
它只提供一种正式 authority:
这个 structured asset 现在确实需要重新进入 Generation,而且重新进入的原因已经被严格证明。
之后真正的语言工作仍然要继续。
新 Generation 为什么仍然要求完整输出 12 个单元
这里还有一个容易产生的疑问:
既然旧 10 个都没有变化,为什么新的 Generation 不能只返回
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:
12 个 semantic slots主要用于:
- parser;
- protection;
- exact comparison;
- reconstruction;
- stale detection;
- impact analysis。
但正式资产仍然是:
_content/learn/books.yaml如果把每一个展示字段都升级成独立 TranslationUnit,就会引入:
- 更多 Generation 状态;
- 更多审核状态;
- 更多 closure 组合;
- 更多恢复分支;
- 更复杂的 currentness 关系。
而这些字段本来就是同一个结构化资源的一部分。
所以当前设计仍然坚持:
内部粒度用于证明,正式资产边界用于治理。
重新 Generation 以后,后面的门禁一个都不能跳
semantic-input-expansion reopen 很容易被误解成:
既然已经证明只是多了两个字段,那么重新生成以后应该可以直接上线。
实际上不是。
新的完整 Generation 完成以后,仍然要继续经过:
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 并没有突然变成错误翻译。
它当时面对的是:
10 个正式语义输入对于那 10 个输入,它仍然是合法历史事实。
今天不能继续把它当成 current,是因为:
当前正式输入 = 12
旧 Generation 证明范围 = 10所以旧结果不是:
错了。
而是:
证明范围不够了。
历史合法,不代表今天仍然完整
这也是 v2 一直在强调的另一个边界。
今天 parser 变得更完整以后,不能回头说:
旧 Generation 当时就是非法的。
这是拿今天的规则去反向改写历史。
旧 Generation 当时绑定的是:
当时正式认可的 semantic inputs。
它应该继续作为历史事实存在。
新的 parser 只能说明:
从当前状态开始,旧结果已经不能满足 current eligibility。
所以需要区分:
historical validity:历史上是否合法。
和:
current eligibility:今天是否仍然足以作为当前结果。
历史合法,不代表今天仍然完整。
今天不完整,也不代表应该删除历史。
这和数据库 schema migration 很像
后来我觉得,可以用数据库迁移来理解这个问题。
假设一条旧记录以前只有:
name
url
description系统当时正确处理了这三个字段。
后来业务模型正式增加:
pdf
github旧记录不会因此突然变成:
错误数据。
但它已经不能完整满足新的 schema。
这时候系统需要:
明确识别 schema expansion,然后进入迁移。
而不是:
数据库原始文件没变
→ 所以什么都不用做结构化翻译里的 semantic input expansion,本质上也是类似的问题。
parser 本身其实也是正式依赖
这次问题进一步暴露了一个以前不够明显的事实:
翻译结果依赖的不只是 source 和 glossary,也依赖 parser 如何解释 source。
同一份 source bytes:
parser v1 → 10 个正式语义输入
parser v2 → 12 个正式语义输入对于模型来说,这已经是两个不同的任务。
所以 currentness 不能只看:
source SHA还必须能够回答:
当前 canonical parser 对这份 source 的正式语义投影,是否仍然和旧 Generation 完全一致?
这也是为什么现代增量工作流里的“依赖”,不能只理解成:
文件依赖。
还包括:
解释这些文件的规则依赖。
但也不能因为 parser 有任何代码变化就全部重做
这里也不能走到另一个极端。
如果规则变成:
parser code changed
→ 所有 structured translations 全部失效同样太粗暴。
parser 内部可能只是:
- 性能优化;
- 重构;
- 错误信息改善;
- 不影响某个 asset 的实现调整。
真正应该判断的是:
最终正式 semantic inputs 有没有变化。
如果 parser 更新以后,对某个 asset 的:
- Unit set;
- source;
- coordinates;
- protection metadata;
全部保持完全相同,那么就没有必要因为实现代码发生变化而强制重新翻译。
真正应该触发失效传播的是:
正式语义结果发生了变化。
而不是:
parser 文件有 diff。
所以真正重要的不是“文件变没变”,而是“模型任务变没变”
写到这里,这篇其实可以浓缩成一个问题:
今天要求模型完成的任务,和当初那个模型任务还是不是同一个?
对于这次 books.yaml,答案是:
不是。
虽然:
raw source = same但:
old semantic input count = 10
current semantic input count = 12模型真正面对的正式输入范围已经发生变化。
所以旧 Generation 无法继续作为 current。
一个更准确的 currentness 模型
如果非常简化地理解,最开始可能是:
current =
source 没变后来逐渐变成:
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 这个看起来很小的案例,恰好证明了:
即使最终文字一个字符都没有改变,只要它以前没有进入正式语言流程、今天已经正式进入,那么工作流语义本身就已经发生了变化。
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 官方无隶属、授权或背书关系。

