前面的流程已经越来越接近上线。
Generation 完成。
机器校验通过。
独立审核完成。
修订后的内容再次经过独立复审。
最终只保留 A 级结果。
内容包完成封存。
整合站点层(Surface)也已经审核通过。
走到这里,很容易产生一种感觉:
语言内容和完整站点都已经检查过了,Preview 应该只剩下“打开看看页面长什么样”。
但实际推进 zh-CN Site v2 时,我遇到过一个非常典型的反例。
当时 Surface gate 已经通过,完整 locale Preview 也成功启动。
然后自动化 Preview 验收直接失败:
[preview-browser] FAILED:
stage=Site shell
check=/
expected=one Site shell main without legacy homepage or Tour SPA
actual=wrong renderer问题不是哪句话翻译错了。
也不是哪个翻译单元没有通过审核。
甚至不是 Surface Reviewer 漏掉了一个语言问题。
真正的问题是:
运行时使用了错误的 renderer。
这次失败让我更加明确了一件事:
每一层 PASS,只能证明这一层负责的事情。
Surface PASS 不能替 Preview 提前证明真实运行结果。
这篇就整理为什么 Preview 不能被 Surface 替代,以及一次 wrong renderer 为什么反而证明了分层验证的价值。
Surface 检查的是“内容组合”,Preview 检查的是“真实运行”
先把两个阶段分开。
整合站点审核主要回答的是:
这些已经完成的语言内容,放到完整 locale 站点上下文里以后,组合关系是否正确?
它会关注很多单独翻译单元看不到的问题。
例如:
- 页面之间的链接是否正确;
- 本地内容覆盖是否一致;
- structured content 结构化内容是否和页面配套;
- local / official fallback 是否符合当前覆盖规则;
- canonical 与 route 的关系是否合理;
- 新内容和继承复用内容放在一起以后有没有冲突;
- glossary、导航和站点上下文有没有整合问题。
所以 Surface 很重要。
它解决的是:
单个页面都正确,不代表把这些页面组合成站点以后仍然正确。
但是 Preview 检查的是另一件事:
把当前正式状态真正交给运行时以后,HTTP、renderer、route、DOM 和浏览器行为是不是我们预期的样子。
这已经不是单纯的“内容关系”。
而是:
系统实际执行出来的结果。
一个简单例子:内容全对,也可能被错误 renderer 展示
假设当前已经有正确的几类内容:
Site shell对应的站点外壳;Homepage对应的首页内容;- Learn 页面内容;
- Docs 页面内容;
- Tour 内容;
- structured data 结构化数据。
Surface 可以证明:
这些内容之间的组合关系正确。
但真正启动站点以后,还要经过一条完整的运行链:
收到 HTTP 请求
→ 进入运行时
→ 执行路由解析
→ 选择页面 renderer
→ 生成 HTML
→ 交给浏览器显示只要这条链中的某一层选错实现,最终页面仍然会错。
例如首页本来应该执行:
Site v2 首页内容
→ 使用 Site shell renderer 生成页面但实际运行时却进入:
错误路径:legacy homepage或者继续进入:
错误路径:Tour SPA那么即使 source 正确、翻译正确、Surface 也正确,用户看到的仍然是错误页面。
这就是 wrong renderer 这种问题的特点。
为什么 Surface Reviewer 很难发现这种错误
因为这不是 Reviewer 应该承担的职责。
Surface Reviewer 接收到的是:
当前正式整合范围、source、target、链接、structured content 和相关上下文。
它应该判断:
语言和整合层有没有问题。
如果要求 Reviewer 顺便证明:
Go runtime 最后到底调用哪个 handler;
根路径请求最终进入哪个 renderer;
浏览器拿到的 DOM 是不是那个 renderer 生成的;
那实际上是在让一个语言与内容审核阶段承担 runtime integration test 的职责。
不仅很难做,而且会让边界越来越混乱。
所以更合理的是:
Surface 阶段
→ 负责证明内容整合正确
Preview 阶段
→ 负责证明实际运行正确各自解决不同的问题。
当时最关键的失败不是“页面不好看”
wrong renderer 这个错误特别有代表性,因为它不是普通视觉问题。
自动化检查期待的是:
根页面中存在且只存在一个正确的 Site shell
<main>。
同时还要求:
不能继续出现 legacy homepage。
也不能出现:
Tour SPA 被错误当成完整站点首页。
结果返回:
实际结果:wrong renderer这说明系统发现的不是:
页面某个样式有问题。
而是:
我们正在看的根本不是预期那个产品实现。
这和字体稍微偏了一点、margin 不一致、某张图片大小不理想,完全不是一个等级的问题。
如果 renderer 都选错了,那么后续继续检查页面文案细节,意义已经很小。
这也说明 Preview 不应该只是人工“看一眼”
提到 Preview,很多时候容易理解成:
启动一个本地服务,然后浏览器打开页面,人工看看。
但这种方式有一个很明显的问题:
人很容易被“页面大致能打开”欺骗。
例如旧 renderer 可能同样返回:
请求结果:HTTP 200页面也可能:
- 有标题;
- 有导航;
- 有正文;
- CSS 能正常加载;
- 看起来不像完全坏掉。
如果只凭:
页面打开了。
很可能会把错误实现误认为正确。
所以 Preview 更需要自动化检查明确的产品不变量。
例如应该检查:
首页必须使用当前 Site shell。
而不是只检查:
首页能够返回 HTTP 200。
这两种标准差别非常大。
HTTP 200 是一个很弱的成功条件
这个案例其实和前面“真实 404”那篇有一点类似。
只看 HTTP status 很容易产生假阳性。
例如:
根路径请求:GET /
返回状态:200只能证明:
有一个 handler 对这个请求返回了内容。
它不能证明:
返回的是正确 handler。
同样:
Learn 路径请求:GET /learn/
返回状态:200也不能单独证明:
- 页面来自当前 activated content;
- 当前 renderer 使用正确;
- canonical 正确;
- navigation 正确;
- local coverage 正确;
- structured data 正确。
所以 Preview 验收需要检查的不只是:
有没有响应。
而是:
实际响应是不是当前正式产品模型要求的那个响应。
为什么在 Surface 通过以后才做 Preview
这里还有一个顺序问题。
既然 Preview 能发现真实运行错误,为什么不早点做?
因为如果前面的内容状态还不稳定,Preview 很容易变成:
每改一句翻译,都重新执行整站 runtime 和 browser 检查。
这样成本会非常高。
更合理的顺序是:
先完成 TranslationUnit
→ 再完成独立质量审核
→ 再完成内容包封存
→ 再完成 Integrated Surface Review
→ 再执行 Activation
→ 最后进入 Preview也就是先把:
内容本身是否正确
逐层证明清楚。
然后再把已经稳定的 activated state 交给 runtime。
Preview 这时承担的是:
最终集成执行检查。
而不是替前面的语言工作流重复做一次审核。
Preview 失败,也不等于应该重新翻译
这个边界特别重要。
当时看到:
实际错误:wrong renderer如果没有清楚的层次划分,很容易出现一个错误反应:
Preview 失败了,那是不是 TranslationUnit 或 Surface 要重新做?
实际上完全不一定。
wrong renderer 首先指向的是:
runtime / rendering integration。
如果已经能够证明:
- TranslationUnit 候选没有变化;
- QC 结果没有变化;
- finalization 没有变化;
- package closure 没有变化;
- Surface 输入没有变化;
- Surface 结论仍然成立;
那么不应该因为 renderer 工程缺陷,就把语言成果全部重新打开。
正确思路应该是:
保留可信语言结果
→ 保留可信 Surface 证据
→ 定位 renderer 问题
→ 修复 repository engineering
→ 运行 targeted tests
→ 启动 fresh Preview
→ 重新执行 Preview acceptance也就是:
失败发生在哪一层,就先修哪一层。
否则很容易把工程错误误诊成翻译错误
假设首页看起来完全不对。
原因可能有很多。
第一种:目标译文本身错误
这种情况应该回到:
TranslationUnit revision → validation → independent re-QC。
第二种:单页正确,但站点整合关系错误
这种情况应该回到:
受影响的 TranslationUnit 或 Surface integration correction。
第三种:内容和 Surface 都正确,但 runtime 选错 renderer
这种情况应该进入:
repository engineering。
如果所有问题都统一处理成:
页面显示不正确
→ 重新执行翻译结果会非常糟糕。
不仅浪费 Generation 和 Reviewer 额度,还可能破坏原本可信的翻译成果。
所以失败信息必须足够具体
一个好的自动化验收,不能只输出:
Preview 结果:FAILED因为这几乎无法指导恢复。
更有价值的是:
失败阶段:Site shell
检查路径:/
预期行为:只有一个正确的 Site shell main,并且不能出现 legacy homepage 或 Tour SPA
实际行为:wrong renderer这几项信息分别回答了不同问题。
失败阶段
说明问题出现在哪一层。
检查对象
说明具体检查了哪个 route 或行为。
预期结果
说明正确产品行为应该是什么。
实际结果
说明实际违反了哪个不变量。
这样 Coordinator 才能判断:
是语言问题、结构问题、runtime 问题,还是 browser 行为问题。
好的 gate 不只是“拦住错误”。
还应该:
帮助把错误送回正确的责任层。
Preview 实际上是在验证整个运行链
到了 Preview 这一步,检查对象已经不再只是文件。
而是一条完整运行链:
当前激活权威状态
→ 计算本地内容覆盖
→ 加载正式内容
→ 组合完整站点
→ 解析请求路由
→ 选择页面 renderer
→ 生成 HTTP 响应
→ 生成 HTML
→ 浏览器构建 DOM
→ 执行真实交互这也是为什么 Preview 会发现 Surface 不可能发现的问题。
例如:
- HTTP handler 是否选择正确;
- 页面 renderer 是否选择正确;
- 本地 route 是否真实存在;
- 本应返回 404 的路径是否错误返回 200;
- 页内 fragment target 是否真实存在;
- breadcrumb 面包屑链接是否正确;
- navigation 导航点击是否有效;
- desktop 与 mobile 的 DOM 行为是否一致;
- images 图片是否真正加载;
- shared Site chrome 是否符合要求。
这些已经全部进入:
运行时事实。
source 和 target 都正确,不代表 Compose 一定正确
这里再举一个更细的例子。
假设已经能够证明:
Homepage source:正确
Homepage target:正确
Site shell target:正确这只能证明相关输入正确。
真正运行时还要执行:
加载正式内容
→ 执行 Compose
→ 选择 renderer
→ 生成最终页面如果 Compose 没有使用当前 activated package authority,而是:
- 读取旧 projection;
- 进入 legacy fallback;
- 调用了旧 Tour renderer;
那么最终输出仍然可能错误。
因此:
验证输入正确,不能替代验证执行路径正确。
这也是集成测试一直存在的意义。
为什么 Preview 必须使用真实 activated scope
如果 Preview 自己再做一套:
“我觉得应该显示这些页面。”
那它也可能产生另一套真相。
因此 Preview 最重要的输入之一应该来自:
当前 activated scope 和已经完成的内容包。
而不是:
- 某个旧 Snapshot;
- 人工列出的 route;
- 文件系统里碰巧存在的页面;
- 尚未激活的 candidate。
只有这样,Preview 验证的才是:
如果现在准备进入 Production,当前正式 activated state 实际会组成什么站点。
这和“文件存在 ≠ 页面存在”又连起来了
前面本地内容覆盖那篇已经讨论过:
一个翻译文件存在,不代表本地 route 就应该存在。
Preview 又进一步证明:
一个 route 能打开,也不代表它由正确内容和正确 renderer 提供。
所以状态可以继续拆得更清楚:
source 已经存在
≠ candidate 已经存在
≠ TranslationUnit 已经完成
≠ package 已经完成
≠ Surface 已经通过
≠ 内容已经激活
≠ runtime 行为正确
≠ Production 已经准备完成看起来层次很多。
但每一层实际上都在消除一种不同的假阳性。
分层验证不是为了把流程做长
如果只看步骤数量,这套流程确实越来越长。
但 wrong renderer 这个案例让我觉得:
真正的问题不是 gate 多,而是每个 gate 有没有明确证明一件事情。
如果 Surface 和 Preview 全部揉成一个超大的 gate,会产生几个问题。
首先:
语言 Reviewer 可能被 runtime failure 阻塞。
其次:
runtime 工程修复以后,很难判断是否必须重新做语言审核。
再次:
一旦失败,很难确定责任边界。
分层以后反而更清楚。
TranslationUnit 负责:
证明单元语言质量。
Package Closure 负责:
证明内容包已经完整完成。
Integrated Surface 负责:
证明完整站点上下文中的内容整合质量。
Activation 负责:
证明正式 activated authority 已经安全切换。
Preview 负责:
证明真实运行行为符合当前产品模型。
Production 负责:
证明正式线上环境最终正确。
每一层都只回答一个范围更加明确的问题。
PASS 不能跨层继承它没有证明过的事情
这是这个案例里我最想强调的一点。
例如:
Surface PASS。
只能说明:
Surface 检查范围通过。
不能自动推出:
runtime 已通过。
也不能自动推出:
browser 已通过。
更不能自动推出:
Production 已通过。
同样:
Preview PASS。
也不能证明:
Production 网络、部署、CDN 和服务进程已经全部正确。
所以:
PASS 应该严格绑定它自己的证据范围。
不能因为:
前面已经有很多 PASS。
就推断:
后面的阶段大概率也没问题,所以可以跳过。
wrong renderer 其实是一个很“干净”的失败
虽然当时看到这个失败会很烦,但从系统设计角度看,它其实是一个很好的结果。
因为自动化在进入 Production 以前明确发现:
首页实际由错误 renderer 提供。
而且没有把这个问题误判成:
- TranslationUnit 的语言质量问题;
- Reviewer 审核失败;
- glossary 问题;
- Surface language defect。
它只是准确地指出:
Preview runtime 不符合当前产品模型。
这说明 gate 至少成功完成了它最重要的职责:
把运行时错误挡在 Production 之前。
如果没有 Preview,这类问题可能直接进入 Production
可以想象一下没有这一层的流程:
Integrated Surface Review 已通过
→ 直接执行 publish
→ 直接执行 deploy
→ 进入 Production如果 runtime 仍然接的是旧 renderer,那么最后可能出现:
新 Homepage 已经翻译完成。
Learn / Docs 已经完成。
Surface 已经审核。
但公网首页仍然显示:
legacy homepage 或 Tour SPA。
这时候前面所有语言工作都是真实完成的。
问题只是在:
最后一公里没有真正消费这些成果。
这是非常典型的 integration failure。
也是为什么 Preview 不能省掉。
Preview 也不能只测几个漂亮页面
另一个风险是:
只挑首页、Learn 首页和一两个 Docs 页面人工看一下。
这种抽样对于视觉观察有用。
但 machine acceptance 仍然应该尽量覆盖正式产品不变量。
例如应该检查:
- 当前 activated route 集合;
- 本地不存在页面应返回的真实 404;
- 页面 canonical 规范链接;
- sitemap 站点地图;
- shared chrome 共享站点框架;
- fragment links 页内锚点;
- breadcrumb 面包屑导航;
- navigation 导航行为;
- images 图片加载状态;
- Tour 交互行为;
- desktop / mobile 浏览器行为。
特别是那些:
不应该存在的东西
也要检查。
例如:
- legacy homepage 不应该重新出现;
- Tour SPA 不应该占据 Site 首页;
- uncovered local route 不应该偷偷返回 200。
很多集成错误恰恰发生在:
错误 fallback。
“没有报错”也不能作为 renderer 正确的证明
旧 renderer 本身可能是一个完全正常、成熟、能够稳定运行的实现。
所以它不会:
- panic;
- 返回 HTTP 500;
- 输出无效 HTML。
从传统运行监控角度看,它甚至可能非常健康。
但产品语义上它仍然是错的。
例如:
HTTP 状态:200
HTML 结构:有效
运行进程:健康
实际 renderer:错误系统依然应该失败。
这说明:
技术健康和产品正确不是一回事。
这个案例后来也影响了 Production 设计
Preview 发现 wrong renderer 以后,一个自然的问题就是:
即使 Preview 修好了,Production 会不会又走另一套旧 runtime?
如果 Preview 和 Production 使用完全不同的:
- 内容模型 content model;
- 路由解析 route resolver;
- 页面渲染 renderer;
- 浏览器验证 browser contract;
那么 Preview PASS 仍然不能给 Production 太多信心。
所以后面的 release-path 收敛也越来越强调:
Preview 和 Production 应尽可能共享同一个 Site 内容模型和运行契约。
Delivery 可以不同。
例如:
Preview 环境 → localhost
Production 环境 → public origin / CDN但核心的产品规则不应该各自维护一套真相,例如:
- 本地内容覆盖 coverage;
- 页面 renderer;
- route 路由;
- canonical 规范链接;
- sitemap 站点地图;
- 404 路由行为。
这些应该尽可能共享。
Preview 的价值不只是“最后再检查一次”
如果只是把 Preview 理解成:
前面做过的事情再检查一遍。
那确实会显得重复。
但它真正做的是:
把前面已经证明正确的静态状态,放进实际运行系统里,再证明动态结果。
前面的阶段主要证明:
这些输入和状态是正确的。
Preview 则进一步证明:
系统真正消费这些输入以后,产生的行为也是正确的。
前者关注的是输入和状态的正确性。
后者关注的是执行结果的正确性。
这两个层次不能互相替代。
Preview 失败时,恢复原则也应该保持 affected-only
即使 Preview 失败,也不能因此退回:
全站全部重做。
还是应该先确定:
哪一层真正受影响。
例如 wrong renderer 这种情况,可以得到类似这样的判断:
TranslationUnit 状态:可信
QC 状态:可信
Package Closure 状态:可信
Surface 状态:可信
Activation 状态:按照当前 authority 单独确认
Preview runtime 状态:FAILED那么主要恢复范围应该集中在:
renderer 和 runtime integration。
修复以后继续:
运行 targeted tests
→ 启动 fresh Preview
→ 重新执行 automated acceptance而不是重新调用 Generation。
这和整个项目一直坚持的原则是一致的:
可信成果不重做。
为什么 fresh Preview 很重要
repository engineering 修复以后,也不能继续使用旧 Preview 服务,然后说:
文件已经改了,刷新浏览器看看就行。
因为旧进程可能:
- 仍然运行旧 binary;
- 仍然持有旧 content;
- 仍然使用旧 handler;
- 仍然缓存旧状态。
所以工程修复完成以后,需要启动:
新的 Preview 实例。
这样才能重新获得:
使用新的 runtime
→ 使用新的进程
→ 读取当前 activated inputs
→ 使用当前 renderer再执行自动化验收。
否则很可能出现:
代码已经正确,但测试的还是旧进程。
自动化 Preview 和 HUMAN gate 也不是重复
即使 automated Preview 全部 PASS,最终 HUMAN gate 仍然有价值。
因为机器很擅长检查:
- exact route;
- DOM structure;
- required links;
- broken fragments;
- 404;
- canonical;
- 固定的产品不变量。
但人更容易发现:
- 页面看起来不自然;
- 某个布局虽然合法但体验很差;
- 字体、行宽、留白明显异常;
- 文案组合后读起来奇怪;
- 某个交互虽然程序判断成功,但实际体验不好。
所以:
automated acceptance
和:
HUMAN gate
属于不同的证明范围。
这次失败让我更认可“不要用一个 PASS 替代另一个 PASS”
回头看整个流程,可以明确写成:
TranslationUnit 通过
→ 不代表 Surface 已通过
Surface 通过
→ 不代表 Activation 已通过
Activation 通过
→ 不代表 Preview 已通过
Preview 通过
→ 不代表 Production 已通过这不是因为系统不信任前一个 gate。
而是因为:
每进入下一层,就会出现一组前一层根本不存在的新变量。
进入 Surface 后,多了:
跨页面关系。
进入 Activation 后,多了:
当前 authority 与并发状态。
进入 Preview 后,多了:
runtime、renderer、HTTP、DOM 和 browser 行为。
进入 Production 后,又多了:
deployment、public origin、CDN、service 和真实网络环境。
所以不能指望某一层一次性证明未来所有变量。
我现在更认可的几条原则
经过这次 wrong renderer,我觉得 Preview 这一层可以浓缩成几条原则。
第一:
Surface PASS 证明的是内容整合质量,不证明 runtime 最终选择了正确 renderer。
第二:
Preview 不是简单“看一眼页面”,而是对真实运行链进行自动化验收。
第三:
HTTP 200 只能证明有响应,不能证明响应来自正确的产品实现。
第四:
source、target 和 Surface 都正确,仍然可能因为 runtime integration 错误而失败。
第五:
Preview failure 必须尽量定位到具体 stage、check、expected 和 actual,才能把问题送回正确责任层。
第六:
repository engineering defect 不应该自动重新打开可信 TranslationUnit 和 Reviewer 结论。
第七:
工程修复以后必须启动 fresh Preview,不能用旧进程证明新代码。
第八:
Preview 应消费 current activated authority,而不是自己重新发明一套页面范围。
第九:
自动化 Preview 和 HUMAN gate 各自证明不同事情,不能互相替代。
第十:
一个 gate 的 PASS,只对它真正检查过的范围负责。
最终我觉得这个案例最值得记住的一句话是:
内容正确,不等于系统真的在使用这些正确内容。
而 Preview 真正要证明的,就是:
前面已经完成并激活的那些成果,终于被真实运行系统按照预期消费了。
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 官方无隶属、授权或背书关系。

