翻译页结构对不上:EN 5 节 ZH 3 节——按结构审计 + PR 强制同步

单边编辑导致 EN/ZH 章节、代码块、链接逐步分叉。按结构(不只时间戳)审计、diff 双语内容、PR 层强制 translate-as-you-edit。

你打开 EN 文章,再打开对应的 ZH,两边几乎不像同一篇的翻译。EN 有 5 个 ## 章节、3 个代码块;ZH 只有 3 个章节、1 个代码块。EN 六周前加了 FAQ 段,ZH 根本没收到。EN 把一步从 Step 1: Audit 改名成 Step 1: Inventory,ZH 还是旧措辞。这一对共享同一个 translationKey,但内容已经从结构上走散了。

最快修复: 跑一次结构 diff(按对统计 ## 标题数和 ``` 代码围栏数,而不是只看文件时间戳),按差距大小排序,给每一对漂移的页面选三条出路之一——补齐落后那侧、拆成两篇独立文章、或正式标为单语页并去掉它的 hreflang alternate。然后加一个 CI 检查,让这个差距不会每次有人改一侧就重新裂开。不要把生机翻直接贴进缺失的章节:截至 2026 年 6 月,Google 的「大规模内容滥用」(scaled content abuse)政策明确把自动翻译列为「在价值很低时可被当作垃圾内容处理」的一类转换手法(2026 年 3 月 24 日的 spam update 进一步收紧了这方面的执行)。经过人工编辑、质量过关的 AI 翻译是可以的;未经审阅的直接倾倒才是风险。

这和单纯的字数漂移(中文本来就更紧凑)不是一回事。这是结构漂移:章节数不同、代码块数不同、链接指向不同、小标题在翻译一个对面已经不存在的概念。通过 hreflang alternate 落地的读者会觉得被骗,而 SEO 损失是实打实的——hreflang 标注必须互相呼应(reciprocal),所以一对断裂或只发了一半的 alternate 会让 Google 把两端都忽略掉。

你属于哪一类

症状可能原因对应修法
ZH 整段缺了 EN 有的 ## 章节EN 单边改完没回流Step 1 → Step 2(同步)
ZH 永远落后 EN 几周,从来追不上单向流水线Step 3(CI 闸门)
hreflang alternate 报 404 或指向错误页改名 slug / 移动文件原因 3 + Step 5
ZH 散文引用了页面上没有的片段代码块只加到 EN原因 4
ZH 明显更薄、没有 FAQFAQ 只加在一侧原因 5
ZH 短,但覆盖了相同要点正常的语言精简,不是 bug无需处理

最后一行很关键:中文经常用更少的字、更少的行表达同样的意思。不要因为行数不同就去「修」一对其实已经对等的页面。下面的结构审计以章节数和代码块数为锚,这两个指标不受啰嗦程度影响,正好绕开字节长度。

常见原因

1. 单边编辑从不触发翻译工单

你编辑 en/foo.mdx 加了一个新章节,提交。没有任何东西提醒你 zh/foo.mdx 现在缺了这段。在 200 篇文章上重复半年,结构鸿沟就拉得很大。

如何判断:按文件统计 ## 标题数,再按对 diff。

for f in src/content/articles/en/troubleshooting/*.mdx; do
  key=$(basename "$f")
  zh="src/content/articles/zh/troubleshooting/$key"
  [ -f "$zh" ] || continue
  en_sec=$(grep -c '^## ' "$f")
  zh_sec=$(grep -c '^## ' "$zh")
  if [ "$en_sec" != "$zh_sec" ]; then
    echo "$key: en=$en_sec zh=$zh_sec"
  fi
done

凡是 en 和 zh 相差 2 个以上的对,就是结构漂移,而不是语言精简。

2. 编辑只往一个方向走——通常是 EN -> ZH 卡住

多数内容站有一个主作者先写 EN,ZH 几周后才翻(如果翻的话)。之后的 EN 修订基本不会反向回到 ZH。这一对一开始是镜像的,然后每个 PR 都漂一点。

如何判断:列出 EN 文件最后一次提交比 ZH 文件新 30 天以上的对。

for f in src/content/articles/en/troubleshooting/*.mdx; do
  zh="${f/\/en\//\/zh\/}"
  [ -f "$zh" ] || continue
  en_t=$(git log -1 --format=%ct -- "$f")
  zh_t=$(git log -1 --format=%ct -- "$zh")
  diff_days=$(( (en_t - zh_t) / 86400 ))
  [ "$diff_days" -gt 30 ] && echo "$(basename "$f"): EN newer by ${diff_days}d"
done

用提交时间(git log),不要用文件系统的 mtime——git checkout 或 git clone 会把每个文件的 mtime 重置到检出那一刻,所以在新克隆的仓库或 CI 里,mtime 比较是毫无意义的。

3. 改名 translationKey 或移动文件,悄悄断对

你在 EN 里改了一个 slug。translationKey 现在指向一个缺失的 ZH 文件(或者一个还带旧 key 的过期文件)。hreflang 标注发出一对悬空的配对,而 build 不会报错。这是最贵的一类,因为断裂的 hreflang 不是「中性」的:Google 要求互相呼应的 return tag,且集合里每个 URL 都必须返回 HTTP 200。如果页面 A 指向一个 404 或 301 跳转的 B,Google 会对整个集合忽略这条标注——两个方向都丢掉 alternate 信号。

如何判断:从两端导出 translationKey 值后 diff。

diff \
  <(grep -h "^translationKey:" src/content/articles/en/**/*.mdx | sort -u) \
  <(grep -h "^translationKey:" src/content/articles/zh/**/*.mdx | sort -u)

任何只在一侧出现的 key,都是一篇未配对的文章——要么它的孪生缺失,要么有一侧用了错误的 key。

4. EN 新加的代码示例从没复制到 ZH

你在 EN 里加了一个代码块,里面是一段全新的 shell 脚本。ZH 还是旧版(或者根本没有代码块)。代码块本身与语言无关,但它前后的散文不是——所以 ZH 页现在引用了一段不在页面上的片段。

如何判断:按对统计三反引号围栏的行数并 diff。一个正确的文件围栏行数应当是偶数(开 + 闭);奇数意味着有一个未闭合的围栏,单这一点就能让 MDX build 挂掉。

5. FAQ 段只加在一侧

你在 EN 加了一个 ## FAQ 章节,带 3 个 ### Question? 条目。ZH 没收到。页面级的 FAQPage JSON-LD 只在 EN 发出,而 ZH 页读起来明显更薄。

把 2026 年 6 月这件事说清楚。Google 弃用了 FAQ rich results:它们从 2026 年 5 月 7 日起不再出现在搜索结果里;FAQ 的搜索展示筛选器、rich-result 报告、以及 Rich Results Test 的支持在 2026 年 6 月陆续移除(Search Console API 的支持延续到 2026 年 8 月)。所以 ZH 缺这个 FAQ,已经不再让你损失一次 rich result——那个视觉展示对几乎所有人来说都没了。它仍然让你损失的,是实打实的东西:一个更薄、更不顶用、回答了更少读者真实疑问的 ZH 页,再加上与 EN 孪生之间的结构漂移。FAQPage 依然是有效的 Schema.org 类型,而 Google 说留着没用上的结构化数据并无害处,所以正确的修法是把 ZH 页的内容补到对等,而不是去追一个已经不存在的 SERP 功能。

最短修复路径

Step 1:跑一次全量结构 diff

写一个比结构(不只比时间戳)的脚本:

// scripts/audit-pair-structure.mjs
import fs from "node:fs";
import path from "node:path";

const EN_DIR = "src/content/articles/en/troubleshooting";
const ZH_DIR = "src/content/articles/zh/troubleshooting";

function metrics(file) {
  const txt = fs.readFileSync(file, "utf8");
  return {
    h2: (txt.match(/^## /gm) || []).length,
    h3: (txt.match(/^### /gm) || []).length,
    code: (txt.match(/^```/gm) || []).length / 2,
    lines: txt.split("\n").length,
  };
}

for (const f of fs.readdirSync(EN_DIR)) {
  const en = path.join(EN_DIR, f);
  const zh = path.join(ZH_DIR, f);
  if (!fs.existsSync(zh)) continue;
  const a = metrics(en), b = metrics(zh);
  if (Math.abs(a.h2 - b.h2) >= 2 || Math.abs(a.code - b.code) >= 2) {
    console.log(`DRIFT ${f}: h2 en=${a.h2} zh=${b.h2}, code en=${a.code} zh=${b.code}`);
  }
}

用 node scripts/audit-pair-structure.mjs 运行。输出会把最分叉的对排在前面,先同步这些。把 >= 2 的阈值按你的容忍度调整——>= 1 会很吵,因为差一个章节往往是合理的。

Step 2:每一对决定同步还是拆分

三个合法的出路:

- 同步:把落后那侧补到与领先侧对齐的结构
- 拆分:内容已经合理分叉,去掉 translationKey 配对,当成两篇独立文章对待
- 标单语:低流量页,去掉一侧的 translationKey,从另一侧删掉 hreflang alternate

不要把缺失的章节直接机翻补上。截至 2026 年 6 月,Google 的「大规模内容滥用」政策明确把自动翻译列入「结果对用户价值很低时可被当作垃圾内容处理」的转换手法之一,而 2026 年的执行(2026 年 3 月的 spam update)正好收紧了这一点。烂机翻比缺一段还糟。要么认真做一份真翻译,要么把这一对拆开。

Step 3:在 PR 层强制 translate-as-you-edit

加一个 CI 步骤,标记出任何动了 en/*.mdx 却没动对应 zh/*.mdx 的 PR:

# .github/workflows/translation-sync.yml 片段
- name: Check translation parity
  run: |
    CHANGED_EN=$(git diff --name-only origin/main -- 'src/content/articles/en/' | grep '\.mdx$' || true)
    for f in $CHANGED_EN; do
      zh=$(echo "$f" | sed 's|/en/|/zh/|')
      if [ -f "$zh" ] && ! git diff --name-only origin/main | grep -q "$zh"; then
        echo "::warning::EN changed: $f -- but ZH not updated: $zh"
      fi
    done

::warning:: 会在 GitHub Actions 日志和 PR 的 Files Changed 标注里显示出来,但不会让任务失败。把它保持为 warning 而不是硬失败:作者要么在同一个 PR 里更新 ZH,要么开一张 i18n 工单、明确承认这次漂移。如果以后想要硬闸门,把 ::warning:: 换成 ::error:: 并加上 exit 1。

Step 4:按价值刻意回填最糟的几对

按流量挑出漂移最严重的 top 20 对。在 Google Search Console 里打开 Performance,给 Page 加一个 /zh/articles/ 的过滤器,按 Impressions 排序——薄或对不上的 ZH 页正是在这里让你损失最大。手工同步这几对。长尾先放着,等它挣到这份工作量再说。

Step 5:验证 hreflang 仍然干净配对

同步后重查 sitemap 里的 hreflang 标注。每个 URL 条目都应当列出每一个变体(包括它自己,自引用是必需的),且两个页面必须互指:

<url>
  <loc>https://site.com/en/articles/slug/</loc>
  <xhtml:link rel="alternate" hreflang="en" href="https://site.com/en/articles/slug/" />
  <xhtml:link rel="alternate" hreflang="zh" href="https://site.com/zh/articles/slug/" />
</url>

Google 严格执行的两条规则(其 localized-versions 文档确认,2026 年 6 月):

  1. 互相呼应(reciprocity)。 如果 EN 把 ZH 列为它的 alternate,ZH 必须反过来列上 EN。任何一侧的 return tag 缺失,Google 就会把两条标注都忽略。
  2. 集合里每个 URL 都返回 HTTP 200。 任意一个 alternate 返回 301、404 或 500,都会让整个页面集合的标注作废——这正是改名 slug 那一类(原因 3)造成的故障。

如果某页被标成了单语,就把它的 alternate 完全去掉。半发的 hreflang 集合比完全没有还糟,因为它可能把还在工作的那个方向也一起拖垮。

如何确认已修好

  1. 重新跑 node scripts/audit-pair-structure.mjs——你修过的那一对不应再打印 DRIFT 行。
  2. 并排打开两个 URL:相同的 ## 章节、相同顺序、相同数量的代码块、相同的 FAQ 问题。
  3. 验证线上 alternate 链接返回 200:curl -I https://site.com/zh/articles/<slug>/ 应当显示 HTTP/2 200,而不是 301 或 404。
  4. 在 Search Console 里打开 Indexing → Pages(或还能用的旧版 International Targeting 报告),确认这一对没有「no return tags」之类的 hreflang 错误。

预防

  • 只要 EN/ZH 单独变更就发 CI warning,作者必须回应
  • 结构审计(h2/h3/代码块计数)每周跑一次,或在 prebuild 时对着 origin/main 跑
  • 一侧新增 ## FAQ 段,merge 前需要先开一张 i18n 工单
  • 改 slug 必须在同一个 PR 里改两端,由一条 lint 规则强制
  • 低流量页明确标为单语,而不是留着任它漂移
  • 季度回顾:按 GSC impressions 排出的 top 20 漂移对纳入排期同步

FAQ

EN 页比它的 ZH 翻译长很多,是不是 SEO 问题?

单凭这一点不是。Google 不要求等长,而且中文通常用更少的字表达同样的内容。真正伤的是实质内容缺失——一侧有、另一侧没有的章节、步骤或答案——以及断裂的 hreflang。对齐结构和实质,忽略原始行数。

为了快点补上差距,直接把缺的 ZH 章节机翻行不行?

别当成原始倾倒。截至 2026 年 6 月,Google 的「大规模内容滥用」政策明确把自动翻译列为「在价值很低时可被当作垃圾内容处理」的转换手法之一,而 2026 年 3 月 24 日的 spam update 正好收紧了对这一模式的执行。微妙之处在于:对真正有用的内容做 AI 翻译、再人工编辑校准准确度和地道表达,是可以的——Google 说过它仍然会用这类页面。风险来自未经审阅的机翻倾倒。所以要么把新章节认真翻好(AI 辅助 + 人工编辑),要么把这一对拆成两篇独立文章。

FAQ rich results 都没了,我还需要 FAQ 结构化数据吗?

rich result 是没了——FAQ 摘要从 2026 年 5 月 7 日起不再出现在搜索里,相关报告和 Rich Results Test 支持在 2026 年 6 月陆续退役。但 FAQPage 仍是有效的 Schema.org 类型,Google 说留着没用上的结构化数据并无害处。如果你的布局会自动发出这段 markup,就留着,只是别再指望它带来 SERP 功能。保留一份强 FAQ 的真正理由,是它能在页面上回答读者的疑问。

为什么一条断裂的 hreflang 会把两个方向都搞坏?

Google 要求互相呼应的标注:页面 A 必须指向 B,B 也必须反指 A,且集合里每个 URL 都返回 HTTP 200。如果改名 slug 让某个 alternate 变成 404 或 301,Google 会丢掉整个集合的标注,于是连还在工作的那一侧也丢掉 alternate 信号。这就是为什么修 slug(原因 3)、再重新验证 200 响应(Step 5)是价值最高的修复。

该修的漂移和正常的啰嗦差别在哪?

漂移体现在结构上:## 章节数不同、代码块数不同、小标题翻译了一个对面已经没有的概念、或链接指向不同的页面。啰嗦只体现在长度上,而章节、步骤、代码块全都对得上。审计脚本以计数(而非字节)为锚,正是为了忽略啰嗦、抓住漂移。

相关

标签: #内容运营 #站点质量 #站点审计 #排查 #双语