AI GrowthAI 内容创作工具集

多语言站点改造实战:从 1 个语种到 10 个语种

以 songbing.com.cn 为案例 — 134 个页面长出 9 个外语版本,全部由一条本地管线生成
🌐 多语言 · hreflang · 工程化翻译 · 静态站改造
本站自己就是案例:中文 134 个页面,扩成 10 个语种共 1340 个 URL。翻译不是逐页粘进翻译器,而是「源站只维护一份中文,其余全是生成物」。这套管线跑完,质检脚本给出 0 error / 0 warning。下面把这套流程完整拆开,包括每一步为什么这么做、以及真正踩过的坑。

一、先看结果:改造前后对比

先给结论,省得你以为是「找了个翻译公司翻了 1000 页」。

维度改造前改造后
语种1(zh-CN)10(zh-CN + en / ja / ko / zh-TW / es / de / fr / vi / th)
页面134 个中文页1340 个 URL(134 × 10),与源站严格同构
hreflang无每页 11 条 alternate(10 语种 + x-default)
sitemap1 份中文 sitemapsitemap.xml(中文)+ sitemap-i18n.xml(1340 条)+ sitemap_index.xml
站内搜索1 份中文索引10 份(按 <html lang> 自动切换,取不到回退中文)
质检人工翻着看脚本 5 道闸,逐页逐语种跑,0 error / 0 warning
日常维护—只改中文源页,跑一条命令,9 个语种同步更新

需要说明的是:这套东西不是运行时 i18n,是构建期生成本地文件。服务器上就是一堆静态 HTML,没有 i18n 框架、没有语言中间件、没有数据库切语种。好处是部署简单(rsync 即可)、页面快、缓存友好;代价是必须有条管线,否则 1340 个文件迟早失控。

六阶段改造流程总览
① 定架构         ② 盘点可译面        ③ 抽取器            ④ 翻译 + 缓存
/<lang>/ 子目录    翻 / 不翻 清单       文本指纹 sha1        术语表 + 三级补丁
      │                  │                    │                    │
      └──────────────────┴────────────────────┴────────────────────┘
                                    │
                          ⑤ 生成器(head / 导航 / 页脚 / 正文)
                                    │
                          ⑥ 质检 5 道闸 → 分批上线 → 回滚预案

二、第一步 · 定架构(最不该拍脑袋的一步)

很多人一上来就翻译,翻完了才发现 URL 结构错了——这时候要重做的是 hreflang、sitemap、内链、甚至已收录的地址。所以先定四件事。

决策 1:URL 策略选哪一个

方案样子优点代价
子目录(本站选的)/en/…同一份部署、同一套证书、权重集中在主域、日志与 CDN 规则通用没有地域信号,只能靠 hreflang 声明
子域名en.site.com可独立部署/独立团队,地域信号略强每个子域要单独证书、Search Console、分析、缓存策略
独立域名site.de本地品牌感最强,合规隔离好成本最高,权重从零开始,运营是 N 个站
参数?lang=en改动最小收录最差、分享易丢参数、缓存不友好,不建议

选子目录的核心理由很朴素:一套静态文件、一条部署链路、一个证书。对一个内容型站点来说,权重集中比地域信号更值钱。

决策 2:路径要不要翻译 slug

本站的决定是不翻:/100wen/what-is-geo/ 对应 /en/100wen/what-is-geo/,路径完全镜像。

决策 3:要不要自动跳转

不要按 IP 或浏览器语言自动 301。 主流的搜索质量指南都不推荐自动重定向:爬虫只有一个 IP 和一个 Accept-Language,你一跳转它就只能抓到默认语种;回访用户粘了书签也会被弹回去。正确做法是:页面顶部给一个语言选择弹窗(本站是点击地球图标打开),推荐而不强制。

决策 4:源语言页要不要动

不动。中文页保持在根目录原路径,这样既有的收录、外链、分享链接全部不受影响。新增的 9 个语种是"增量目录",出问题删掉目录就回滚,中文站毫发无损。

最终目录结构目录
/                          ← 中文源站(路径不变)
├── index.html
├── 100wen/…                 ← 134 个源页面
├── en/  ja/  ko/  zh-TW/    ← 生成物:与源站同构
├── es/  de/  fr/  vi/  th/
├── assets/js/i18n/          ← 工具页 JS 文案字典(en.js 等)
├── search-index-en.json     ← 每语种一份站内搜索索引
├── sitemap.xml              ← 只收中文页
├── sitemap-i18n.xml         ← 收全部 1340 条
└── sitemap_index.xml        ← 只提交这一个给搜索引擎
动手顺序建议:先手写一个 /en/index.html 占位页,确认相对路径、静态资源、导航都正常,再开始批量生成。架构错了改一次是灾难,架构对了后面全是流水线。

三、第二步 · 盘点可翻译面

「翻译页面」这句话太粗。一个页面里能被爬虫和用户看到的中文有十几个来源,逐类过一遍,产出清单:

位置翻不翻说明
<title> / meta description必翻搜索结果里的门面
og:* / twitter:*必翻分享卡片,含 og:site_name 这种容易漏的
H1–H4 / 正文 / 列表 / 表格必翻主体
图片 alt、title、aria-label、placeholder必翻属性和正文一样会被读出来
<pre> / <code> 里的中文散文必翻提示词示例、检查清单、ASCII 结构图——读者要照抄的正是这些
导航菜单 / 页脚 / 免责声明必翻由生成器统一注入,不靠人写
JSON-LD 里的 headline / description / inLanguage必翻否则富媒体卡片露中文
工具页的 JS 文案(按钮、提示)必翻在 inline <script> 里,要单独处理
品牌名与平台名(百度 / 抖音 / 微信 / 小红书…)不翻进术语表白名单,永不送进模型
ICP 备案号不翻法规要求保持中文原文
纯 ASCII 代码、URL、slug不翻翻译只会搞坏它们
最容易搞反的一条:很多人把 <pre> 当成"代码块"跳过。但本站的 <pre> 里装的是「AI 提示词示例」「上线检查清单」「目录结构图」——这些是给人看的散文。所以抽取器的跳过名单最终收窄成 script / style / textarea 三个,其余只要含中文就抽。判据不是标签,是这段文字有没有人读。

四、第三步 · 搭管线:抽取 → 翻译 → 生成 → 质检

为什么不用现成的 i18n 框架

因为本站是手写 HTML,不是模板渲染。任何"解析成 DOM 再序列化输出"的方案,都会把内联样式、注释、未加引号的属性、缩进风格打散——生成页和源页长得不一样,后面就再也无法用 diff 判断改动是否安全。

所以整套管线的第一原则是:只改字节区间,不重排结构。

① 抽取器:给每一段文字发一个身份证

  1. 用 quote-aware 的 tokenizer 扫出四类 token:tag / text / comment / raw(script、style)。
  2. 只抽"含中文字符"的文本节点,以及白名单属性(alt / title / aria-label / placeholder / meta 的 content)。
  3. 每段算一个文本指纹 key = sha1(text)[:16],作为翻译缓存的键。
  4. 回填时按 (start, end) 从后往前替换,光标不会错位。
这个指纹设计的回报:改一个中文段落,只有这一段的 key 变了,其余 1.1 万条缓存原样复用。新增一页时,待译段数等于"新页面的段数",而不是"全站的段数"。这就是成本控制的核心。

② 翻译层:术语表 + 三级补丁

调 OpenAI 兼容接口(本站用 DeepSeek),一次请求批量返回 JSON 数组。真正决定质量的是三件事:

机制解决的现实问题
术语表 keep品牌名不一定翻。百度 在英文页就该是 Baidu,小红书 保留原文;替换时必须按长度倒序,否则「大黄蜂」会先匹配并吃掉「大黄蜂跑车」
术语表 terms同一概念全站统一:非付费营销 = organic marketing、投流 = paid media buying、私域 = private domain
zh-TW 用词偏好繁中不是简体转码:优化→優化、视频→影片、软件→軟體、设置→設定

模型偶尔会把中文原样吐回来(尤其是三个字以内的短标签)。所以回填前要跑三级补丁:

  1. strict 重问:明确要求"每个汉字都要翻译",再来一轮;
  2. 按行拆:FAQ、JSON-LD 这类超长单段按换行拆成多行,一次批量翻完再拼回;
  3. 纯文本重问:混合引号会让 JSON 协议崩掉,就退化成"只回译文,不要 JSON"逐段问。

③ 生成器:把公共部分交给代码,不要手写

每个生成页由四块拼成,全部用可识别的注释围栏包住,方便幂等重跑:

生成页里的固定围栏HTML
<!-- i18n:assets:start -->   assets/i18n.css 的引用
<!-- i18n:meta:start -->     canonical / og:url / og:locale(每语种指向自己)
<!-- i18n:hreflang:start --> 10 条 alternate + x-default
<!-- i18n:header:start -->   导航 + 语言选择弹窗(按语种生成)
<div class="footer-legal">  ICP 备案号(中文)+ 该语种免责声明

重跑时第一步永远是先删旧的围栏块,再从源页重新生成。不删就会叠加——这个坑踩过一次,页面里会出现两套 hreflang。

页脚合规块必须由生成器注入。 一开始我们把它写在中文源页里,结果每次重跑生成器都用源页覆盖,外语页的备案号就被冲掉了。现在的做法是:备案号恒定输出中文(法规要求),免责声明与「关于站长」按语种查表输出,位置固定在版权行之前。

④ 质检:五道闸,缺一不可

闸门判据拦住的典型问题
残留文字非中文页的可见文本里不该有中文字符某段漏译、按钮回退中文
死链每个 href / src 都能解析到真实文件导航相对路径算错、语种目录缺页
hreflang 互指每个 alternate 都能回指自己,缺语种即报错新增语种后老页面没补 alternate
标签结构一致生成页的标签序列与源页逐项相等少一个 </div>、属性被引号截断
字典完整工具页 JS 字典键全齐、无中文残留值新增按钮没配 9 个语种的文案

第 4 道闸是最容易被忽略、也最值钱的一道。翻译错了你一眼能看出来,标签结构崩了往往只有某个设备上某个宽度才看得出来——而用"标签名序列比对"这种笨办法,能 100% 拦住它。

五、第四步 · 翻译质量:从 2140 条残留到 0

第一次全量跑完,质检脚本报了 2140 条残留。这不是"翻译得不好",是纯粹的漏网——每一条都对应一个具体的机制缺陷。收敛过程大致是:

现象根因处置
部分中文原样留在页面上那一段是超长段落,模型整段回吐超过 600 字的段落按句拆开,每 8 句一批重译再拼回
术语误替换(把长词拆坏)keep 按数组顺序匹配,短词先命中匹配前按长度倒序排序
结构化数据里仍是中文JSON-LD 的 articleBody 是超长单字段用该页面已本地化的 description 兜底
残余碎片(半句、单个词)模型输出被截断碎片补丁:只对剩余的中文片段再问一次
日文页大面积误报用"含汉字"判断日文未翻译,日语本来就有汉字改用简体字特征集合(这个们说时发…)判定
人工校对结果下次被覆盖直接改生成页统一写进 manual.json(按中文源文精确匹配),渲染时覆盖缓存

最后一步是把脚本清零,而不是把报错注释掉:2140 → 0 error / 0 warning。这之后每次改内容,跑一遍闸门就知道有没有回归。

机器能测"有没有中文",测不了"像不像人话"。 所以终审还是人:每个语种抽 3–5 页,重点看标题、按钮、表格表头、以及首屏第一段。这三处最容易出现"语法正确但读起来别扭"的译文。

六、第五步 · SEO 收尾:hreflang、sitemap、站内搜索

hreflang:每页 11 条,互相指认

每个生成页的 head 里都有这一组HTML
<link rel="alternate" hreflang="zh-CN" href="https://www.songbing.com.cn/100wen/what-is-geo/">
<link rel="alternate" hreflang="en"    href="https://www.songbing.com.cn/en/100wen/what-is-geo/">
<link rel="alternate" hreflang="ja"    href="https://www.songbing.com.cn/ja/100wen/what-is-geo/">
… 共 10 个语种 …
<link rel="alternate" hreflang="x-default" href="https://www.songbing.com.cn/100wen/what-is-geo/">
<link rel="canonical" href="https://www.songbing.com.cn/en/100wen/what-is-geo/">

三条硬规则:

sitemap:三层,只提交最外层

三层结构XML
sitemap_index.xml   ← 只把这一个提交到 Search Console / 百度站长平台
├── sitemap.xml         中文源站(不含多语言目录)
└── sitemap-i18n.xml    全部 1340 条(10 语种 × 134 页)

站内搜索:按 <html lang> 选索引

本站的《100 问》知识库有全文搜索。多语言后如果还只喂中文索引,英文读者搜 "geo" 会搜不到东西。做法是每语种生成一份 search-index-<lang>.json(正文取页面的 .content 容器,截前 4000 字),前端按 document.documentElement.lang 选文件,取不到就回退中文。

静态资源不要随手加版本参数。 我们曾给 search-index-en.json?v=20261021 这样的地址加版本号,结果因为属性没加引号,抽取器的属性正则把 = 当成了新的属性边界,把标签拆坏。要么统一给属性加引号,要么别在这种地址上加参数。

七、第六步 · 分批上线与回滚

1340 个文件一次性推上去是赌博。本站的做法是按语种目录分批:

批次内容放量理由
第 1 批/en/灰度:英文是最大的目标市场,也最容易人工校验
第 2 批/ja/ /ko/ /zh-TW/东亚语种,结构与英文同批验证过
第 3 批/es/ /de/ /fr/ /vi/ /th/其余语种
第 4 批导航入口 + sitemap-i18n.xml + robots最后开门,前面都验证过才让爬虫进来
按语种分批提交(静态站 + git 部署)Shell
git add en/ && git commit -m "i18n: 英文版上线(134 页)" && git push
# 观察 24h:日志无 404、抽查 5 页无残留中文、hreflang 互指通过
git add ja/ ko/ zh-TW/ && git commit -m "i18n: 日/韩/繁中上线"
git add es/ de/ fr/ vi/ th/ && git commit -m "i18n: 其余五语种上线"
git add sitemap-i18n.xml sitemap_index.xml robots.txt
git commit -m "seo: 多语言 sitemap 与入口开门" && git push

回滚就是一个 rm -rf:删掉出问题的语种目录,中文站完全不受影响。这是"增量目录"架构最大的好处——风险被物理隔离了。

孤儿页不要翻译

站里总有几个零入链的历史页面(本站是 3 个:article.html、douyin_estimator2.html、tianjin-transfer.html)。它们的处理方式是:不翻译、保留可访问、从 sitemap 与 llms.txt 里摘掉。省下来的不是翻译费,是 3 × 10 = 30 个"有内容但没人需要"的 URL 带来的抓取预算浪费。

八、避坑清单:13 条真实踩过的坑

  1. inline <script> 里的文案抽不到。 教程页有「复制 / 已复制」按钮,文案写在脚本里,页面一加载就变回中文。解法:单独一张 JS UI 文案表,按语种替换脚本里的字符串。
  2. <script> 里的 JSON 数据也是中文。 目录页的「热门文章」标题存在 var POP_TITLES={…} 里,抽取器按约定跳过 script。解法:用各语种文章自己的 H1 回填。
  3. og:site_name 这类 meta 容易漏。 默认只抽 name="description",property 形式的要显式加进白名单。
  4. 引号冲突会把页面拆坏。 模型返回的直引号落进单引号属性里,标签当场截断。回填前必须按属性引号种类转义。
  5. 页脚合规块不能只写在源页。 重跑生成器就冲掉,必须由生成器注入;ICP 号永久保留中文。
  6. 术语替换要按长度倒序。 否则「大黄蜂」先命中,「大黄蜂跑车」就被拆成两半。
  7. 未加引号的属性会被正则拆坏。 src=foo.json?v=1 里的 = 是隐患,要么加引号,要么别加参数。
  8. 生成器必须幂等。 先删旧围栏块再生成,否则重跑一次就多一套 hreflang。
  9. 标签结构漂移是最隐蔽的错。 文字全对、页面在某些设备上错位——只有标签序列比对能拦住。
  10. 日文不能按"含汉字"判未翻译。 用简体字特征集合,否则满屏误报,最后你会把闸门关掉。
  11. hreflang 不要指向不存在的页面。 生成前先验证文件存在,缺语种就不输出那一条。
  12. 语言变长要留白,不要缩短文案。 同一句德语常比中文长 1.5–2 倍。按钮用 CSS 弹性布局,别靠删字。
  13. "万 / 亿"不要直译。 用 Intl.NumberFormat(lang, {notation:'compact'}):中文显示 8.5万,英文 85K,韩文 만。

九、成本、维护与日常命令

成本结构

项量级说明
一次性全量翻译每语种约 1.1 万条文本片段按 token 计费,用便宜的中文强模型即可;质量靠术语表 + 人工抽检兜底
日常增量新增 1 页 ≈ 该页段数 × 9缓存命中,不改动的段落零成本
人力1 人 + 一套脚本主要花在清单盘点与终审抽检
服务器0 增量纯静态文件,nginx 直接吐

日常命令

日常只需要这几条Shell
# 1. 看看还有多少段要翻(不写文件、不调模型)
python3 tools/i18n/build.py --dry-run

# 2. 只跑某个语种 / 只跑某一页(增量)
python3 tools/i18n/build.py --locales en
python3 tools/i18n/build.py --pages index.html

# 3. 不调模型,只按缓存重新渲染(改了模板/导航时用)
python3 tools/i18n/build.py --render-only

# 4. 五道闸质检:目标是 0 error / 0 warning
python3 tools/i18n/check.py

# 5. 重建多语言站内搜索索引
python3 tools/i18n/build_search_index.py

维护心法三条

  1. 只改中文源页,外语页永远由生成器覆盖。手改生成页 = 下次构建就丢。
  2. 人工校对的成果写进 manual.json,按中文源文精确匹配,渲染时覆盖缓存——这是唯一允许手工干预的入口。
  3. 新增页面 = 加一行配置。把页面加进管线清单,跑一次 build,9 个语种的对应文件、hreflang、sitemap 全部自动就位。
新增页面的真实耗时:写中文页(大头)+ 跑一条命令(几分钟)+ 抽检 3 个语种(10 分钟)。没有"再翻译一遍"这个环节。

十、附:改造流程 SKILL.md

上面这套流程已经固化成一份可直接复用的技能文档。把下面这段 markdown 存成 SKILL.md 放进你的技能目录(或直接丢给 AI 当作业指导书),它就能按同样的六个阶段带你把一个单语站扩成多语站,包含决策表、五道闸判据和踩坑清单。

⬇ 下载 SKILL.md📘 怎么创建 SKILL

下载后把文件名改回 SKILL.md 即可(站点的源码防护规则会拦截 .md 直链,因此提供 .txt 副本,内容与仓库里的 SKILL.md 完全一致)。

multilingual-site-revamp / SKILL.mdMarkdown
---
name: multilingual-site-revamp
display_name: 多语言站点改造
description: 把已有的单语种站点扩成多语种(子目录镜像 + hreflang + 本地生成),覆盖架构选型、字符串抽取、术语表、增量翻译缓存、质检与分批上线。适合静态站、半静态站与手写 HTML 站点。
icon: "🌐"
trigger: 多语言 多语种 国际化 i18n 本地化 hreflang 站点改造 出海建站
inputs:
  - name: site_root
    description: 站点源码根目录(静态站即 HTML 所在目录)
    required: true
  - name: target_locales
    description: 目标语种代码,如 en,ja,ko,zh-TW,es,de,fr,vi,th
    required: true
  - name: url_strategy
    description: subdir(默认)/ subdomain / ccTLD,决定目录结构与 hreflang 写法
    required: false
scripts: [extract.py, llm.py, build.py, check.py]
---

# 多语言站点改造

## Overview

把一个已经在运营的单语种站点,扩成 N 个语种、且机器可读层面(hreflang / sitemap / 结构化数据)全部正确的站点。核心不是"翻译文字",而是**建立一条可重复执行的生成管线**:源语言只维护一份,其余语种全部是生成物。

参考实现:songbing.com.cn(134 页 × 10 语种 = 1340 URL,全静态,check 脚本 0 error / 0 warning)。

## 什么时候用 / 不用

用:
- 站点已有稳定内容与收录,不想为 i18n 重构框架
- 页面是手写 HTML / 模板生成,但结构相对统一(每页都有标题、正文、页脚)
- 目标语种 ≥ 3,且预期会持续更新内容(增量翻译价值才体现出来)

不用:
- 站点还没定型、页面结构天天变 → 先定型再翻译,否则每次改结构都要重译
- 只有 1-2 个页面要翻 → 手工翻更快
- 需要按用户语言自动 301 跳转 → 这是另一件事,且多数搜索引擎不推荐

## Workflow

### 阶段 1 · 定架构(deterministic)

先定三件事,写进决策表再动手:

| 决策 | 选项 | 判据 |
| --- | --- | --- |
| URL 策略 | 子目录 `/en/` | 默认选它:同一份部署、权重集中、证书与日志通用 |
| | 子域名 `en.site.com` | 只有需要独立 CDN/独立团队/独立合规时才选;每个子域要单独配证书、GSC、分析 |
| | 独立域名 `site.de` | 有本地化品牌与本地合规需求时才选;成本最高 |
| 路径是否翻译 slug | 不翻译,镜像原路径 | 默认不翻:别名映射是长期维护负担,hreflang 已足够表达对应关系 |
| 是否自动跳转 | 不自动跳 | 语言选择器给建议,不强制 301。自动跳转会干扰爬虫与回访用户 |

验证:手写一个 `/en/index.html` 占位页,确认目录结构、相对路径、静态资源都能正常访问,再开始批量。

### 阶段 2 · 盘点可翻译面(deterministic)

逐类过一遍,产出"翻 / 不翻"清单:

必翻:`<title>`、meta description、`og:*` 与 `twitter:*`、H1-H4、正文、列表、表格、图片 `alt`、`title`、`aria-label`、`placeholder`、JSON-LD 里的 `headline` / `description` / `inLanguage`、导航菜单、页脚免责声明、**`<pre>` / `<code>` 里的中文散文**(提示词、检查清单、ASCII 结构图——读者要照抄,必须翻)。

不翻:品牌名与平台名(百度、抖音、微信、小红书…)、ICP 备案号(法规要求保留中文)、纯 ASCII 代码与 URL、URL slug、统计数据里的数字格式(用 `Intl.NumberFormat` 处理,别直译"万/亿")。

验证:把清单写成术语表 `glossary.json` 的 `keep` 数组,后面所有语种共用。

### 阶段 3 · 写抽取器(deterministic,难度最高的 20%)

不要用 DOM 解析再序列化——内联样式、注释、未加引号的属性都会被打散。正确做法是**只改字节区间**:

1. 用 quote-aware 的 tokenizer 扫 `tag / text / comment / raw` 四类 token
2. 只抽取"含目标语言字符"的文本节点与指定属性
3. 每段生成一个**文本指纹** `key = sha1(text)[:16]`,作为翻译缓存的键
4. 回填时按 `(start, end)` 从后往前替换,源页与生成页除文字外逐字节一致

验证:对同一页面连续跑两次,第二次应无 diff(幂等)。回填后标签序列必须与源页完全一致。

### 阶段 4 · 接翻译(agentic 判断 + deterministic 调用)

- 用 OpenAI 兼容接口,按"语种 + 术语表 + 语境"拼 system prompt,一次请求批量返回 JSON 数组
- 缓存按 `key` 存 `cache/<lang>.json`,改一段只重译那一段;全量重翻用 `--force`
- 术语表 `keep` 按**长度倒序**替换,否则短词先吃掉长词
- 失败三级补丁:strict 重问 → 含换行的长段按行拆开重问 → 纯文本逐段重问
- 人工校对结果写 `manual.json`(按源文精确匹配),渲染时覆盖缓存——这是唯一允许手改的地方

验证:`--dry-run` 打印每语种待译段数,新增页面时该数字应等于新页面段数而不是全站段数。

### 阶段 5 · 生成器(deterministic)

每页按顺序拼装,全部用可识别的注释块包裹,便于幂等重跑:

1. `strip` 掉旧的生成块(否则重跑会叠加)
2. head 注入:canonical(指向自己)、`og:url`、`og:locale`、hreflang 集群 + `x-default`
3. 导航头按语种生成(菜单文案走字典,链接用相对路径计算,不要硬编码)
4. 页脚合规块按语种注入(备案号保留中文,免责声明翻译)
5. 回填正文,写出 `/<lang>/<page>`

hreflang 只输出**目标页真实存在**的语种——指向 404 的 hreflang 比不写更糟。

### 阶段 6 · 质检与上线(deterministic)

五道闸全部为 0 才算完成:

| 闸门 | 判据 |
| --- | --- |
| 残留文字 | 非中文页的可见文本里不应有源语言字符(日文用简体字特征表判定,不能用"含汉字") |
| 死链 | 每页每语种的 `href` / `src` 都能解析到真实文件 |
| hreflang 互指 | 每个 alternate 都能回指自己,缺语种即报错 |
| 标签结构 | 生成页的标签序列与源页逐项相等(最隐蔽的错就是这里) |
| 字典完整 | 工具页 JS 字典的键全齐,且没有回吐源语言的值 |

上线:按语种目录**分批** commit + push,先灰度 1 个语种看日志与收录,再放其余;`sitemap-i18n.xml` 与导航入口放在最后一批。回滚 = 删除该语种目录。

## Lessons Learned

### Do
- 先定架构与清单,再写代码。80% 的返工来自架构反复。
- 用文本指纹做缓存键:内容不变就不重译,这是成本控制的全部。
- 生成物全部加注释围栏,保证幂等可重跑。
- 把"校验"做成脚本而不是人工抽查:1330 页人工是看不完的。
- 品牌名、法规文本(ICP)走白名单,永不进模型。

### Don't
- 不要重排 DOM 再输出(内联样式/注释/引号风格会被毁)。
- 不要让 hreflang 指向不存在的页面。
- 不要把所有语种的 canonical 都指向源语言页——那是自废收录。
- 不要用"整站重翻"解决问题:先看是缓存键错、术语表顺序错,还是抽取器漏了区域。
- 不要在生成页上手改文字:下次生成就没了,改源语言页或 `manual.json`。

### Common Failures
- **按钮文案回退成源语言**:文案在 inline `<script>` 里(复制/已复制),抽取器按约定跳过 script → 需要单独的 JS UI 文案表按语种替换。
- **"热门文章"列表仍是中文**:标题在 `<script>` 里的 JSON 对象中 → 用各语种文章自己的 H1 回填。
- **分享卡片标题露中文**:`og:site_name` 这类 `meta property` 不在默认白名单里 → 要显式加入抽取属性集。
- **属性被截断、页面布局崩**:模型返回的直引号与属性引号冲突 → 回填前按属性引号种类转义。
- **术语替换错位**:短词先匹配吃掉长词("大黄蜂" 吃掉 "大黄蜂跑车")→ `keep` 按长度倒序。
- **未加引号的属性被拆坏**:`src=foo.js?v=1` 里的 `=` 会被属性正则误判 → 静态资源不加版本参数,或统一加引号。
- **日文页大面积误报**:用"含汉字"判定日文未翻译必然失败 → 改用简体字特征集合。
- **重跑后备案号消失**:页脚合规块必须由生成器注入,不能只写在源页。

### When to Ask the User
- URL 策略要不要动老 URL(涉及 301 与收录损失,必须人决策)
- 哪些品牌词必须保留原文(不同公司口径不同)
- 某个语种的译文质量是否达标(机器能测"有没有中文",测不了"像不像人话")
- 上线节奏(先灰度哪几个语种)

## Output

交付物固定为四样:

1. `<lang>/…` 全套生成页(与源站同构)
2. `sitemap-i18n.xml` + `sitemap_index.xml` + 更新后的 `robots.txt`
3. 质检报告:五道闸的 error / warning 计数(目标 0 / 0)
4. 术语表 `glossary.json` 与人工覆盖 `manual.json`(下一轮维护的唯一入口)

## 参考实现

songbing.com.cn 的管线(本地保留,不入公开仓库):

```
tools/i18n/
├── config.json        站点、语种、页面清单、资源路径
├── extract.py         字节级抽取/回填 + 未翻译检测
├── llm.py             OpenAI 兼容客户端 + 占位符保护
├── build.py           翻译调度 + 页面生成 + 页脚/hreflang 注入
├── check.py           五道闸
├── fix_residual.py    残留清理(长段拆句 → 三级补丁)
├── glossary.json      keep / terms / zh-TW 用词偏好
├── manual.json        人工校对覆盖
└── cache/<lang>.json  文本指纹 → 译文
```

日常命令:

```bash
python3 tools/i18n/build.py --dry-run              # 还要翻多少段
python3 tools/i18n/build.py --locales en           # 只跑一个语种
python3 tools/i18n/build.py --pages index.html     # 只跑一页(增量)
python3 tools/i18n/build.py --render-only          # 不调模型,只重渲染
python3 tools/i18n/check.py                        # 五道闸
```
这份 markdown 是工具的输入源,保持中文原文,脚本里的路径与命令按你的项目改。

结语

多语言站点的难点从来不在"翻译",而在把翻译变成一条可重复执行、可验证、可回滚的流水线。一旦这条线搭好,第 11 个语种的成本接近于零;搭不好,第 2 个语种就会把你拖进"每次改内容都要重新翻 9 个语种"的泥潭。

先用真实页面跑通一个语种,再谈规模化。这是这套流程里最值钱的一句话。

↑
关注公众号
扫码关注