翻译页结构对不上: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 checkoutgit 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 返回 301404500,都会让整个页面集合的标注作废——这正是改名 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)是价值最高的修复。

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

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

相关

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