给 ffmpegtool.com 加 /en 英文站,翻译本身只占工作量的一小半。大头在三条线上:路由怎么镜像、预渲染产物怎么不被 headless 环境污染、水合怎么保持零错误。这篇记已经上线的工程决策,每条都有当时的验证数据。
先上线,再提交搜索引擎
背景:英文站翻译完成后想立刻提交 Bing IndexNow。核对生产发现 /en 还是 404——代码在本地,没推没部署。
当时的判断是拒绝先提交:Bing 爬到一堆 404 之后恢复收录更慢,等于自己给自己埋死链。顺序必须是先部署上线、验证 /en/* 全 200、再全量提交 sitemap。这个先后关系对小站很重要,新域名在搜索引擎面前没有信誉储备,头几次抓取全是 404 的代价尤其大。
决策一:locale 从 pathname 派生,不做 store
/en 前缀进 URL,locale 从 pathname 解析(useLocale/stripLocalePrefix),不做 Zustand locale store、不做服务端协商。理由:
- 每个 URL 语言自包含——/en/video-trim 和 /video-trim 是两个独立可索引页面,canonical/hreflang 各自成立;
- 静态预渲染友好,每条路由产出一个独立 HTML;
- 没有"当前语言"这个全局状态,就没有跨导航丢状态的问题。
代价是所有文案要走 COPY: Record<Locale, ...> 的数据层(seoContentEn/toolsEn/postsEn),组件里不能散落字符串字面量。这个约束一开始嫌烦,后来它逼出了完整的数据层,加第三种语言时几乎零成本。
决策二:扁平镜像,不嵌套路由
结构上把 en 站做成平铺镜像(/en/video-trim 与 /video-trim 平级),而不是嵌套 <Route path="/en">。原因是水合预载:routeLoaders 按精确 pathname 匹配预加载 chunk,嵌套路由下 en 页面的 pathname 键对不上 loader 表,水合时会整树等 chunk。
三处同步(App 的 enToolRoutes、routeLoaders 的 en 键、prerender 的 EN_ROUTES)不用人工对齐——都从基础路由表派生,加新页面只动基础键。能派生就不要复制,这是整站路由纪律,对单语言的站内导航同样适用。
决策三:自动语言协商必须有 webdriver 守卫
英文浏览器用户访问根路径应该自动进 /en。实现是:根路径 + navigator.language 主语言是 en + 无 localStorage 偏好 → 无痕 assign('/en') 一次,并写 ft_lang_chosen 钉住,之后直达。
这个逻辑在上线前必须加 navigator.webdriver 守卫,原因很具体:Puppeteer 预渲染和水合回归都跑在 headless Chrome 里,默认语言恰好是 en-US。不守卫的话,构建期预渲染执行这段代码,根路径的 HTML 就被渲染成了 /en 版本——首页水合回归直接全灭,而且根路径预渲染产物必须恒中文(它的 canonical 和 hreflang 都声明自己是 zh)。
所以最终的形态是:根路径预渲染 HTML 恒中文,真实英文用户由客户端硬导航去 /en 独立 HTML,hreflang 声明 alternate。守卫条件写进 main.tsx 注释,改这段代码的触发条件是"重跑 build + 全路由水合回归"。
配套纪律
- 双语相邻文本节点用 HTML 注释分隔符隔开(
{'文本'}{/* */}{'text'})。React 对相邻文本节点的合并规则在 hydration 时可能产生 #418/#423 类警告,分隔符是官方推荐的规避手段; - ToolShell 用 hasEn 判定是否注入 en hreflang:parse 页没有英文版,不声明 alternate,sitemap 对 parse 页只出 zh 单条;
- sitemap 根路径统一尾斜杠,与 canonical 自洽;
- 美区本地化单独一档:证件照 en 预设是美国护照/签证/社保/驾照(600×600),水印 en 模板是 CONFIDENTIAL/DRAFT/PROOF,Footer 在 en 下隐藏 ICP 备案号。
验证数据
上线时的门禁:预渲染 sitemap 46 URL(23 页 × 2 语言),水合回归 28 路由(20 zh + 8 en)0 错误,dist/en 产物无中文残留,每页 title 显示宽 ≤60、desc ≤160,/parse/douyin 无 en alternate。rebuild 后这套校验固化为 e2e 的 i18n-en 用例组,后来每次动路由都会跑。
复盘
给静态站加第二语言,最难的不是翻译,是三件事的键一致:路由镜像的键、水合预载的键、预渲染产物的键。而 headless 环境的两个特性——webdriver 标志和 en-US 默认语言——会反向污染构建产物,守卫必须先于产物存在。能派生的同步点就派生,人工对齐的三处表总会漏一处。