You open the EN article, you open its ZH counterpart, and they barely look like translations of each other. EN has five ## sections and three fenced code blocks; ZH has three sections and one code block. EN added an FAQ block six weeks ago; ZH never got it. EN renamed a step from Step 1: Audit to Step 1: Inventory and the ZH version still says the old phrasing. The pair shares a translationKey but the content has structurally diverged.
Fastest fix: run a structural diff (count ## headings and ``` fences per pair, not just file timestamps), sort by the biggest gap, and for each drifted pair pick one of three outcomes — sync the laggard, split into two independent articles, or formally mark the page single-language and drop its hreflang alternate. Then add a CI check so the gap stops reopening every time someone edits one side. Do not paste raw machine translation into the missing sections: as of June 2026, Google’s scaled content abuse policy explicitly names automated translation as a transformation that can be treated as spam when it adds little value (the March 24, 2026 spam update sharpened that enforcement). Edited, high-quality AI translation is fine; unreviewed dump is the risk.
This is different from word-count drift (where ZH is just terser by language). It is structural drift: section count differs, code-block count differs, link targets differ, headings translate concepts that no longer exist in the other locale. Readers who land via an hreflang alternate feel cheated, and the SEO damage is concrete — hreflang annotations must be reciprocal, so a broken or half-emitted pair gets both sides ignored by Google.
Which bucket are you in
| Symptom | Likely cause | Fix section |
|---|---|---|
ZH missing whole ## sections EN has | Solo EN edit never round-tripped | Step 1 → Step 2 (sync) |
| ZH always lags EN by weeks, never catches up | One-directional pipeline | Step 3 (CI gate) |
| hreflang alternate 404s or points to wrong page | Renamed slug / moved file | Cause 3 + Step 5 |
| ZH prose references a snippet that is not on the page | Code block added to EN only | Cause 4 |
| ZH page looks visibly thinner, no FAQ | FAQ added one side only | Cause 5 |
| ZH is short but covers the same points | Normal language verbosity — not a bug | None |
The last row matters: Chinese routinely renders the same meaning in fewer characters and fewer lines. Do not “fix” a pair that is genuinely equivalent just because the line counts differ. The structural audit below keys on section and code-block counts, which survive verbosity differences, instead of byte length.
Common causes
1. Solo edits never trigger a translation ticket
You edit en/foo.mdx to add a new section. You commit. Nothing reminds you that zh/foo.mdx now lacks that section. Repeat for six months across 200 articles and the structural gap is huge.
How to spot it: count ## headings per file and diff across pairs.
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
Anything where en and zh disagree by 2+ is structural drift, not language verbosity.
2. Edits propagate one direction only — usually EN -> ZH stalls
Most content sites have a primary author who writes EN first. ZH gets translated weeks later, if at all. Subsequent EN edits never round-trip back to ZH. The pair starts mirrored and drifts every PR.
How to spot it: list pairs where the EN file’s last commit is more than 30 days newer than the ZH file’s.
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
Use commit time (git log), not filesystem mtime — a git checkout or git clone resets every file’s mtime to the checkout moment, so mtime comparisons are meaningless on a fresh clone or in CI.
3. Renamed translationKey or moved file breaks the pair silently
You renamed a slug in EN. The translationKey now points to a missing ZH file (or a stale one with the old key). The hreflang annotation emits a dangling pair, and nothing fails the build. This is the most expensive bucket, because broken hreflang is not “neutral”: Google requires reciprocal return tags, and every URL in the set must return HTTP 200. If page A points to a B that 404s or 301-redirects, Google ignores the annotation for the entire set — both directions lose their alternate signal.
How to spot it: dump translationKey values from both locales and diff.
diff \
<(grep -h "^translationKey:" src/content/articles/en/**/*.mdx | sort -u) \
<(grep -h "^translationKey:" src/content/articles/zh/**/*.mdx | sort -u)
Any key that shows up on only one side is an unpaired article — either its twin is missing or one side has the wrong key.
4. New code examples added in EN never copied to ZH
You added a fenced code block in EN with a fresh shell script. ZH still shows the old version (or has no code block at all). Code blocks are language-agnostic but the surrounding prose isn’t — so the ZH page now references a snippet that does not appear on the page.
How to spot it: count triple-backtick fences per pair and diff. A correct file has an even number of fence lines (open + close); an odd count means an unterminated fence, which on its own can break the MDX build.
5. FAQ block added on one side only
You added a ## FAQ section with three ### Question? entries on EN. ZH never got it. The page-level FAQPage JSON-LD only emits on EN, and the ZH page reads visibly thinner.
Be precise about what this costs in June 2026. Google deprecated FAQ rich results: they stopped appearing in Search on May 7, 2026, and the FAQ search-appearance filter, the rich-result report, and Rich Results Test support were removed through June 2026 (Search Console API support follows in August 2026). So the missing FAQ on ZH no longer costs you a rich result — that visual treatment is gone for almost everyone. What it still costs is real: a thinner, less helpful ZH page that answers fewer of the questions a reader actually has, plus structural drift from the EN twin. FAQPage remains a valid Schema.org type and Google says unused structured data does no harm, so the fix is to bring the ZH page’s content up to parity, not to chase a SERP feature that no longer exists.
Shortest path to fix
Step 1: Run a structural diff across all pairs
Build a script that compares structure, not just timestamps:
// 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}`);
}
}
Run it with node scripts/audit-pair-structure.mjs. The output ranks the most divergent pairs; sync those first. Tune the >= 2 thresholds to your tolerance — >= 1 is noisy because one-section differences are often legitimate.
Step 2: For each drifted pair, decide sync or split
Three legitimate outcomes:
- Sync: bring the laggard up to match the leader's structure
- Split: content has legitimately diverged; remove the translationKey pair and treat as two distinct articles
- Mark single-language: low-traffic page; remove translationKey on one side, drop hreflang alternate from the other
Do not auto-translate the missing sections. As of June 2026, Google’s scaled content abuse policy explicitly lists automated translation among the transformations that can be treated as spam when the result adds little value for users — and 2026 enforcement (the March 2026 spam update) sharpened that. Bad machine translation is worse than a missing section. Either commit to a real translation or split the pair.
Step 3: Enforce translate-as-you-edit at the PR layer
Add a CI step that flags any PR touching en/*.mdx without touching the matching zh/*.mdx:
# .github/workflows/translation-sync.yml fragment
- 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:: surfaces in the GitHub Actions log and the PR’s Files Changed annotations without failing the job. Keep it a warning, not a hard failure: the author either updates ZH in the same PR or opens an i18n ticket and acknowledges the drift explicitly. If you want a hard gate later, swap ::warning:: for ::error:: and add exit 1.
Step 4: Backfill the worst offenders deliberately
Pick the top 20 drifted pairs by traffic. In Google Search Console, open Performance, add a Page filter for /zh/articles/, and sort by Impressions — that is where a thin or mismatched ZH page costs you the most. Sync those manually. Ignore the long tail until it earns the work.
Step 5: Verify hreflang still pairs cleanly
After sync, recheck the hreflang annotations in your sitemap. Each URL entry should list every variant including itself (self-referencing is required), and the two pages must point at each other:
<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>
Two rules Google enforces strictly (confirmed in its localized-versions docs, June 2026):
- Reciprocity. If EN names ZH as its alternate, ZH must name EN back. If either return tag is missing, Google ignores both annotations.
- HTTP 200 for every URL in the set. A
301,404, or500on any alternate voids the annotation for the whole page set — this is exactly the failure that the renamed-slug bucket (cause 3) produces.
If a page got marked single-language, drop its alternate entirely. A half-emitted hreflang set is worse than none, because it can take down the working direction with it.
How to confirm it’s fixed
- Re-run
node scripts/audit-pair-structure.mjs— the pair you fixed should no longer print aDRIFTline. - Open both URLs side by side: same
##sections in the same order, same number of code blocks, same FAQ questions. - Validate the live alternate links return 200:
curl -I https://site.com/zh/articles/<slug>/should showHTTP/2 200, not a 301 or 404. - In Search Console, open Indexing → Pages (or the legacy International Targeting report where still available) and confirm no “no return tags” hreflang errors for the pair.
Prevention
- CI warning whenever EN/ZH change in isolation; the author must respond
- Structural audit (
h2/h3/code-block count) runs weekly, or in prebuild, againstorigin/main - New
## FAQblock on one side requires an i18n ticket before merge - Renaming a slug requires updating both locales in the same PR; a lint rule enforces it
- Low-traffic pages are explicitly marked single-language rather than left drifted
- Quarterly review: the top 20 drifted pairs by GSC impressions get scheduled sync work
FAQ
Is it bad SEO to have an EN page much longer than its ZH translation?
Not by itself. Google does not require equal length, and Chinese typically expresses the same content in fewer characters. What hurts is missing substance — sections, steps, or answers present on one side and absent on the other — and broken hreflang. Match the structure and the substance; ignore raw line counts.
Should I just machine-translate the missing ZH sections to close the gap fast?
Not as a raw dump. As of June 2026 Google’s scaled content abuse policy explicitly names automated translation among the transformations that can be treated as spam when it adds little value, and the March 24, 2026 spam update tightened enforcement around exactly this pattern. The nuance: AI translation of genuinely helpful content that you then edit for accuracy and idiom is fine — Google has said it still uses such pages. The risk is the unreviewed machine dump. So either translate the new sections properly (AI-assisted plus human edit) or split the pair into two independent articles.
Do I still need FAQ structured data if FAQ rich results are gone?
The rich result is gone — FAQ snippets stopped showing in Search on May 7, 2026, and the related reports and Rich Results Test support are being retired through June 2026. But FAQPage is still a valid Schema.org type and Google says leaving unused structured data in place does no harm. Keep the markup if your layout emits it automatically; just do not expect a SERP feature from it. The real reason to keep a strong FAQ is that it answers reader questions on the page.
Why does one broken hreflang link break both directions?
Google requires reciprocal annotations: page A must point to B and B must point back to A, and every URL in the set must return HTTP 200. If a renamed slug makes one alternate 404 or 301, Google discards the annotation for the entire set, so even the still-working side loses its alternate signal. That is why fixing the slug (cause 3) and re-verifying 200 responses (Step 5) is the highest-value repair.
What’s the difference between drift I should fix and normal verbosity?
Drift shows up in structure: different ## section counts, different code-block counts, headings that translate a concept the other side no longer has, or link targets that point to different pages. Verbosity shows up only in length while the sections, steps, and code blocks all line up. The audit script keys on counts, not bytes, precisely so it ignores verbosity and catches drift.
Related
- Bilingual Pages Drift Apart Over Time
- Content Site Hreflang Tags Misconfigured
- Content Site Canonical Points to Self Wrong
- Stale Articles Not Updated
- Article Count Looks Big But Real Coverage Is Weak
- Content Site Broken Internal Link Rot
Tags: #Content ops #Site quality #Site audit #Troubleshooting #Bilingual