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

单边编辑导致 EN/ZH 章节、代码块、链接逐步分叉。按结构(不只 mtime)审计、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,但内容结构已经走散。

这和单纯的字数漂移(中文更紧凑)不同——这是结构漂移:章节数不同、代码块数不同、链接指向不同、小标题在翻译已不存在的概念。通过 hreflang 落地的读者会觉得被骗,Google 看到 alternate 不匹配会同时降两端的信任度。修法有三条腿:按结构审计(不只看时间戳)、PR 层强制 translate-as-you-edit、接受”标单语”是低价值页的合法选择。

常见原因

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 mtime 比 ZH 新 30 天以上的对。

3. 改了 translationKey 或移动文件——悄悄断对

EN 改了 slug,translationKey 指向缺失的 ZH 文件(或带旧 key 的过期文件)。hreflang 发出悬空对,build 不报错。

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

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

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

EN 加了一段带新 shell 脚本的代码块,ZH 还是旧版(或根本没代码块)。代码块本身语言无关,但前后散文不是——ZH 页现在引用一段不在页面上的脚本。

如何判断:按对统计三反引号数并 diff。

5. FAQ 段只加在一边

EN 加了 ## FAQ 带 3 个 ### Question? 条目,ZH 没收到。FAQ JSON-LD 只在 EN 发出——ZH 页失去一次 rich result 机会、看起来更薄。

最短修复路径

Step 1:跑一次全量结构 diff

写一个比结构(不只比 mtime)的脚本:

# 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}`);
  }
}

输出按漂移程度排序,先同步最差的几对。

Step 2:按对决定——同步 / 拆分 / 标单语

3 个合法选项:

- 同步:把落后那侧补到对齐结构
- 拆分:内容已合理分叉——去掉 translationKey 配对、当两篇独立文章对待
- 标单语:低流量 ZH——去掉 ZH 的 translationKey、从 EN 的 hreflang alternate 中移除

不要”机翻补全缺失章节”。烂 MT 比缺失更糟——要么认真翻,要么拆对。

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 不是 fail——作者要么同 PR 更新 ZH,要么明确开一张 i18n 工单承认漂移。

Step 4:按价值手工回填最差的

在 GSC 用 /zh/articles/* 过滤、按印象排序,挑 top 20 漂移对手工同步。长尾别管,等流量上来再说。

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

同步后重查 sitemap 中的 hreflang。每对都应该发:

<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/" />

如果某页标了单语——完全去掉 alternate。半发的 hreflang 比没有还糟。

预防

  • CI warning:EN/ZH 单独变更——作者必须回应
  • 每周 prebuild 跑结构审计(h2/h3/code-block 计数)
  • 新增 ## FAQ 只在一边需要先开 i18n 工单才能 merge
  • 改 slug 必须同 PR 改两端——lint 规则强制
  • 低流量页明确标单语而不是留着漂
  • 季度回顾:top 20 漂移对纳入排期同步

相关

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