当 Go Learning & Documentation v2 从 A Tour of Go 逐渐扩展到 Learn、Docs 和完整站点以后,我遇到了一个以前不太明显的问题:
go.dev 上存在一个英文页面,是不是就意味着每一个本地语言站点都应该存在对应的本地 URL?
一开始看,这个问题似乎很好回答。
既然英文站点有:
https://go.dev/某个路径那么中文站点似乎也应该有:
https://中文站点/某个路径哪怕中文内容暂时还没有完成,也可以先创建一个页面。
或者访问这个本地 URL 时,再跳转到英文 go.dev。
这样至少不会出现 404。
听起来很合理。
但真正把 Learn、Docs、导航、canonical、sitemap 和搜索引擎收录放到一起以后,我越来越觉得:
“没有 404”并不等于“站点结构正确”。
有时候,一个真实的 404,反而比一个假的本地页面更正确。
这篇就整理 v2 在这个问题上的最终思路。
最容易产生的误区:英文页面存在,所以本地页面也应该存在
假设 go.dev 上有一个正式页面。
本地语言暂时还没有翻译。
最直接的想法可能是:
英文页面存在 → 创建对应的本地 URL至于内容,可以以后再补。
甚至可以先让这个本地 URL 自动跳回英文页面。
这样做的好处非常明显:
用户不会看到 404。
站内链接也不容易“断”。
路由看起来比较完整。
搜索引擎爬到这个 URL 时,也能得到一个正常响应。
但是这里其实混淆了两件事:
官方英文页面存在。
和:
这个本地语言站点已经正式覆盖这个页面。
它们并不是同一件事。
v2 开始把“本地页面存在”当成一种正式承诺
以前如果只是 Tour,站点内容范围相对固定。
但扩展到整个 Go Learning & Documentation 以后,页面种类越来越多。
包括:
- Tour;
- Learn;
- Docs;
- Site shell;
- 安全文档;
- 其他后续可能加入的内容包。
这时,本地 URL 是否存在就不能再只是一个路由层面的决定。
它实际上代表了一项更强的承诺:
这个页面已经属于该语言版本的正式内容覆盖范围。
也就是说,只要本地路由存在,用户很自然地会认为:
这个页面是这个语言站点的一部分。
搜索引擎也会根据实际响应、canonical、sitemap 和站内链接,判断这个页面属于谁。
所以 v2 后来把“有没有这个本地页面”提高成了一个正式状态问题:
只有具备正式本地内容覆盖时,才应该建立对应的本地页面。
什么叫“正式本地内容覆盖”
这里的“覆盖”,并不只是数据库或者文件系统里存在一份内容。
至少从 v2 的工作流来看,一个内容要真正成为本地站点的一部分,需要经过完整的质量链路。
大致包括:
内容进入翻译范围
→ 生成候选译文
→ 机器校验
→ 独立质量审核
→ 修订与复审
→ 全 A 定稿
→ 内容包封存
→ 整合站点审核
→ 激活
→ 预览与生产环境验证所以:
“已经有译文草稿”不等于“已经有正式本地内容覆盖”。
甚至:
“翻译单元已经得到 A”也不自动等于这个 URL 已经可以进入生产环境。
因为页面最终还需要进入内容包、整合站点、激活和生产验证。
这也是上一篇里提到的:
每一层证明的事情不同。
没有本地覆盖时,应该继续链接官方英文页面
最终我更认可的一条规则是:
只有正式存在本地覆盖的页面,才使用本地链接。
如果还没有正式本地覆盖,就继续指向官方英文页面:
https://go.dev/<canonical>例如一个导航项对应的文档尚未翻译完成。
此时不应该为了“让链接看起来都是本地的”,强行改成:
https://本地站点/<canonical>而应该继续链接:
https://go.dev/<canonical>等这部分内容真正完成翻译、审核、整合和激活以后,再把链接切换到本地站点。
这意味着本地站点的内容范围不是:
“英文站点有什么,我就先把 URL 全部复制过来。”
而是:
“我正式完成了哪些内容,就公开哪些本地 URL。”
为什么不应该创建“假的本地页面”
所谓假的本地页面,可能有几种形式。
例如:
页面 URL 存在,但内容实际上还是英文。
或者:
页面只有一句:
本页面暂未翻译。
或者:
页面立即跳转到 go.dev。
这些方案看起来都能避免 404。
但它们都有一个共同问题:
本地 URL 存在了,却没有真正对应的本地内容。
这样会让“URL 存在”与“本地覆盖存在”这两个概念脱离。
以后系统就很难再回答:
哪些页面真的已经完成本地化?
“先创建一个空壳”会让覆盖率失真
假设英文站点有 100 个页面。
真正完成中文翻译的只有 60 个。
如果为了路由完整,先把 100 个本地 URL 全部创建出来,那么从路由角度看:
本地页面数量 = 100但真正的翻译覆盖其实只有:
正式本地内容 = 60这时系统里会同时存在两种“覆盖率”。
一个是路由覆盖。
一个是内容覆盖。
久而久之,很容易出现:
系统认为页面已经存在,所以导航、sitemap、canonical 都开始把它当成本地内容处理。
但实际上,这 40 个页面根本还没有完成本地化。
所以 v2 更希望:
URL 是否存在,本身就尽量反映真实内容覆盖。
自动跳转到英文页面,也不是真正的本地覆盖
另一种常见做法是:
访问本地 URL → 302 跳转到 go.dev 英文页面这样用户至少能看到内容。
但从语义上看,这个本地 URL 仍然不是一个真正的本地页面。
它只是一个中转入口。
这会带来几个问题。
首先,用户看到的是一个本地语言域名。
自然会预期:
点击以后还是这个语言版本。
但结果突然进入英文 go.dev。
这种体验其实并不比直接链接英文页面更自然。
其次,站点内部会产生一个并不存在真实内容的 URL。
以后导航、sitemap、canonical、搜索引擎爬取都需要为这个“虚拟页面”增加额外规则。
最后,最麻烦的是:
系统很难再通过路由是否存在,判断页面是否真的具备本地内容。
所以 v2 选择了更简单、也更诚实的方式:
没有本地内容,就不要伪造本地页面。
真实 404 不是失败,而是在表达真实状态
这也是我这段时间对 404 最大的认识变化之一。
以前很容易认为:
网站出现 404,就是有问题。
但实际上,404 的含义只是:
这个 URL 所代表的资源不存在。
如果某个页面本来就没有正式进入本地内容覆盖,那么访问对应的本地 URL:
https://本地站点/尚未覆盖的路径返回真实 404,反而是正确的。
因为系统是在准确表达:
这个本地页面目前不存在。
真正的问题不是 404 本身。
而是:
明明应该存在的页面却返回 404。
或者:
明明不存在的页面却伪装成存在。
这两个问题要区分开。
这和“软 404”其实是相反的思路
如果一个 URL 返回 HTTP 200,但页面内容实际上只是:
暂无内容。
或者:
请访问英文页面。
那么从用户和搜索引擎角度看,它很可能并不是真正有价值的页面。
相比之下:
没有正式内容 → 真实 404反而更加明确。
所以 v2 不追求:
所有可能的本地 URL 都返回 200。
而是追求:
返回状态应该准确反映内容是否真实存在。
canonical 不能脱离真实内容覆盖
到了这里,就会碰到另一个很重要的问题:
canonical。
canonical 可以简单理解成:
这个页面告诉搜索引擎,哪一个 URL 才是它的正式版本。
如果一个本地页面确实存在,并且属于正式本地覆盖,那么它应该拥有符合当前站点规则的 canonical。
但如果页面本身根本不存在,那么就不能为了“结构完整”提前制造一个 canonical。
否则会出现非常奇怪的状态:
页面不存在
但 sitemap 里有它
canonical 也声明它存在
导航还链接到它这实际上是在同时向不同系统发送相互矛盾的信息。
所以 v2 的原则是:
canonical 必须建立在真实、完整的本地内容覆盖之上。
sitemap 也不能变成“未来计划清单”
同样的道理也适用于 sitemap。
sitemap 的意义并不是:
“这些 URL 将来可能会有内容。”
它应该告诉搜索引擎:
这些是当前真正希望被发现和索引的页面。
所以没有正式本地覆盖的页面,不应该仅仅因为 go.dev 上存在同路径页面,就提前进入本地 sitemap。
否则就会出现:
sitemap 声明页面存在
→ 搜索引擎访问
→ 本地没有真实内容或者:
sitemap 声明本地页面
→ 页面再跳到英文站点这些状态都会让站点的内容边界变得模糊。
所以 v2 更倾向:
sitemap 只包含真正完成本地覆盖的页面。
导航也必须遵守同样的规则
导航是用户最直接接触到的地方。
假设官方英文站点导航里有某个文档。
本地版本尚未翻译。
这时候其实有两种选择:
第一种:
导航 → 本地 URL → 404 或跳转英文第二种:
导航 → 官方英文 go.dev我更认可第二种。
因为它更加直接。
用户点击之前看到的是一个可用的导航入口。
点击以后直接到真正存在的内容。
而不是先进入一个假的本地路径,再由系统替用户决定跳去哪里。
这也让链接状态变得非常清晰:
存在正式本地覆盖 → 链接本地页面
没有正式本地覆盖 → 链接 go.dev 官方页面这样可以避免“路由存在”和“内容存在”脱节
我觉得这一条非常重要。
一个长期维护的多语言站点,最好让下面几个概念尽可能一致:
本地路由存在
本地内容存在
本地内容已经通过正式质量流程
导航可以链接本地页面
sitemap 可以包含这个页面
canonical 可以声明这个页面如果这些状态长期不同步,系统就会越来越复杂。
例如:
路由存在,但是内容不存在。
内容存在,但是还没审核。
审核通过,但是没有激活。
已经激活,但是 sitemap 还没更新。
导航又提前链接了未来页面。
这样每一个地方都需要额外判断。
而且越往后,越难知道:
究竟哪一层才代表真实状态。
所以 v2 更希望建立一个比较清晰的边界:
正式本地覆盖,是本地页面进入站点表面的前提。
/doc/security/** 让我更重视 canonical 规则本身
在 Docs 扩展过程中,还有一个比较具体的例子:
Security 文档的正式 canonical 路径是:
/doc/security/**这类规则看起来只是路径差异。
但它让我进一步意识到:
canonical 并不能靠“看起来差不多”来猜。
一个内容页面真正对应什么 canonical,必须服从当前正式内容结构。
否则本地站点很容易因为:
- 路径拼接;
- 历史旧路由;
- 页面分类变化;
- 内容包扩展;
而生成一个看似合理、实际上并不是正式 canonical 的 URL。
所以 v2 对这类路径规则更倾向于:
读取正式内容权威,而不是根据页面名称推测。
本地覆盖其实也是一种状态机
做到这里以后,我觉得“页面有没有本地版本”已经不能简单用:
true / false来理解。
更接近的是:
英文官方内容存在
→ 尚未进入本地翻译范围
→ 已进入翻译流程
→ 翻译与审核完成
→ 内容包完成
→ 整合站点通过
→ 激活
→ 本地覆盖正式生效其中任何一个中间阶段,都不能提前等价成:
“生产环境已经存在正式本地页面。”
这和前面几篇博客讨论的 current、历史复用、失效传播,本质上其实是一套逻辑。
系统需要知道:
什么已经完成,什么只是正在进行,什么已经真正成为当前生产状态。
Surface PASS 也不代表 Production ready
这一点在这个问题上尤其明显。
假设整合站点审核已经通过。
这证明:
当前组合出来的站点内容,在审核范围内没有发现需要阻止继续推进的问题。
但生产环境还有真实路由、canonical、sitemap 和浏览器行为。
因此:
整合站点审核 PASS ≠ 已经可以宣布生产环境正确生产验证仍然需要确认:
- 应该存在的本地页面确实存在;
- 不应该存在的本地页面确实返回 404;
- 本地覆盖存在时链接到本地页面;
- 没有本地覆盖时链接官方 go.dev;
- canonical 指向正确;
- sitemap 没有提前暴露未完成页面。
所以:
整合站点审核是进入生产之前的重要门禁,但不能代替生产环境验证。
“页面不存在”也应该被测试
以前测试站点时,很自然会重点测试:
哪些页面应该打开成功?
但做到 v2 以后,我觉得还应该测试另外一件事:
哪些页面应该明确不存在?
例如:
某个英文页面尚未进入本地正式覆盖。
那么它对应的本地 URL:
应该返回真实 404而不是:
200 空页面也不是:
302 跳回英文页面这类“负向测试”其实很重要。
因为只有这样,才能确认本地内容边界没有被无意扩大。
一个成熟的本地化站点,不应该假装自己翻译完了所有内容
我觉得这背后还有一个比技术实现更重要的问题。
多语言站点很容易产生一种压力:
既然是中文版,那么所有页面最好都有中文版 URL。
但实际翻译工作一定是逐步推进的。
特别是在:
- 内容持续更新;
- 新内容包不断加入;
- 审核要求较高;
- 需要独立复审;
- 生产上线还要经过多层验证;
这样的系统里,不可能要求所有内容永远同时完成。
所以比起“看起来完整”,我更希望站点做到:
已经完成的部分明确本地化。
还没有完成的部分明确链接官方来源。
而不是制造一种:
“所有内容好像都有本地版本。”
的假象。
这也能让后续新增内容包更简单
假设未来又加入一个新的 Go 内容包。
如果架构要求:
所有官方英文 URL 都必须立刻在本地存在。
那么每增加一个内容包,就要先扩展大量路由、跳转、canonical、sitemap 特例。
然后再慢慢填内容。
整个系统会先产生一大批“未来页面”。
但如果采用正式覆盖原则:
新内容进入正式翻译流程
→ 完成以后建立本地覆盖
→ 激活以后进入本地站点那么新内容包的扩展会自然很多。
站点只公开已经完成的部分。
没有完成的继续使用官方英文来源。
本地覆盖、canonical、sitemap 和 404 其实必须是一套规则
现在回头看,我觉得这些原本看起来彼此独立的问题:
- 本地内容覆盖;
- 路由;
- 导航;
- canonical;
- sitemap;
- 404;
- 官方英文回退;
实际上不能分别设计。
它们必须围绕同一个事实:
这个页面现在是否具备正式本地覆盖?
如果答案是“有”,那么:
使用本地 URL
→ 本地页面真实存在
→ canonical 使用正确的本地正式地址
→ sitemap 可以包含
→ 导航可以链接本地页面如果答案是“没有”,那么:
不伪造本地页面
→ 本地路径真实 404
→ sitemap 不包含
→ 不生成假的本地 canonical
→ 需要访问内容时直接链接 go.dev 官方页面这样整个系统才不会互相打架。
真实状态比“看起来完整”更重要
这一篇最后其实又回到了 v2 很多设计都会遇到的同一个问题:
我们到底是在优化表面完整度,还是在维护真实状态?
如果追求表面完整度,很容易得到:
所有 URL 都存在
所有链接都是本地域名
所有页面都返回 200看起来非常漂亮。
但其中可能有大量页面并没有真正完成本地化。
而 v2 更想追求的是:
完成了 → 明确存在
没完成 → 明确不存在如果没有本地覆盖,就老老实实使用官方英文来源。
如果本地 URL 不应该存在,就返回真实 404。
如果页面已经正式完成,就让导航、canonical、sitemap 和生产环境一起承认它。
这套规则可能没有“所有页面都有本地 URL”那么漂亮。
但它更加诚实。
也更加容易长期维护。
最终形成的原则
现在这部分设计可以总结成几条比较稳定的规则。
第一:
官方英文页面存在,不自动等于本地页面应该存在。
第二:
只有正式完成本地内容覆盖,才建立对应的本地 URL。
第三:
没有本地覆盖时,继续直接链接官方
https://go.dev/<canonical>。
第四:
不要使用假的本地页面,也不要用 302 跳转伪装本地覆盖。
第五:
缺少正式本地覆盖的本地 URL,应该返回真实 404。
第六:
canonical 和 sitemap 只承认已经完整建立的本地内容覆盖。
第七:
正式 canonical 必须来自当前内容权威,不能根据页面名称或历史路径猜测。
第八:
整合站点审核通过,不等于生产环境已经验证完成。
最终目标不是:
让本地站点看起来拥有和 go.dev 一模一样数量的页面。
而是:
让每一个公开存在的本地页面,都真正代表一份已经完成、已经审核、并且可以对用户负责的本地内容。
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 官方无隶属、授权或背书关系。

