--- 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) 逐类过一遍,产出"翻 / 不翻"清单: 必翻:``、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 # 五道闸 ```