You launched bilingual two years ago. Today: English articles got updated 5 times each since launch; Chinese versions are mostly untouched. ZH pages reference outdated screenshots, miss sections that exist only in EN, and link to pages that were renamed on the EN side. The bilingual site you proudly launched is really one live site plus a fading shadow.
Fastest fix: don’t translate everything now. Run a one-time audit script that pairs every translationKey across en/ and zh/, sort the drift list by traffic, and only sync the pages that get clicks. For everything else, either declare it single-language (drop its hreflang pair) or leave it. Then add a CI warning so future drift surfaces at the pull-request stage instead of a year later.
Bilingual content is a commitment, not a launch event. Drift compounds: six months of one-side updates produces a year of catch-up debt. The fix is audit + decide per pair + automate sync, and accept that some articles should be marked single-language rather than maintained badly.
One thing to clear up first: Search Console no longer reports this
If you came here because you expected hreflang warnings in Google Search Console, note that the International Targeting report (the Language / hreflang error report) was deprecated by Google after September 2022 and never replaced. Google’s own help page confirms the report is gone and offers no migration path. As of June 2026, GSC does not surface a dedicated, site-wide hreflang validation report at all. So:
- “No hreflang warnings in GSC” does not mean your pairs are healthy. It means GSC stopped telling you.
- You detect drift yourself, with the audit script below or a crawler such as Screaming Frog (which still has a dedicated hreflang report) or Ahrefs / Sitebulb.
- What Google still does: it reads
hreflangannotations and quietly ignores any link that is not reciprocal. Per Google’s own docs, “If two pages don’t both point to each other, the tags will be ignored.” A broken pair fails silently; you never get a console alert.
Common causes
Ordered by hit rate, highest first.
1. Translation happened once at launch; updates only touch the primary language
You translated 200 EN articles to ZH at launch. Since then, every update was English-only. ZH froze in time while EN evolved.
How to spot it: compare git log dates on the en/ vs zh/ file of the same translationKey. If en is newer for more than half of the pairs, you have systematic drift.
# newest commit touching each side of one pair
git log -1 --format=%ci -- src/content/articles/en/troubleshooting/chatgpt-tips.mdx
git log -1 --format=%ci -- src/content/articles/zh/troubleshooting/chatgpt-tips.mdx
2. The translationKey link broke when one side was renamed
You renamed gpt-tips.mdx to chatgpt-tips.mdx in EN. ZH still has gpt-tips.mdx. They no longer pair, so the hreflang link is non-reciprocal and Google ignores it for both pages.
How to spot it: find translationKey values present in one language only.
diff \
<(grep -h "translationKey:" src/content/articles/en/**/*.mdx | sort -u) \
<(grep -h "translationKey:" src/content/articles/zh/**/*.mdx | sort -u)
3. New articles published in one language never got translated
You add 5 EN articles a month but translate only 1-2 to ZH. The backlog grows monthly. After a year, 50+ articles are EN-only.
How to spot it: articles with a translationKey in en/ but no matching zh/ file. Count these; that is your translation debt.
4. Auto-translation got applied without review
To “fix drift,” you machine-translated the missing pieces. Now the non-English locale reads fluently but is contextually wrong: it references the English button name when the UI shows the localized one, or English nouns stick out mid-paragraph. You have bilingual presence with worse quality than monolingual.
Note the nuance, current as of June 2026: Google’s spam policy does not treat machine or AI translation as spam by itself. On June 11, 2025, after the Reddit AI-translation episode, Google removed its old guidance about blocking auto-translated pages with robots.txt (a docs-only change that aligned the docs with the existing scaled-content policy). The bar is value, not method: helpful, accurate translations are fine; thin or nonsensical ones fall under the scaled content abuse policy. AdSense applies the same logic, and “Low value content” remains the most-cited rejection reason for AI-heavy sites. So the rule is not “MT is banned” but “unreviewed MT that adds no value gets you nothing and risks a penalty.”
How to spot it: read 5 random recent ZH translations. AI-translation tells (literal idioms, English remnants, mismatched UI terms) mean unreviewed auto-translation is the culprit.
5. Cross-links point at the wrong language
An EN article links to /zh/articles/old-name/ because the EN was written first and the author copy-pasted a link without updating the locale. A ZH reader on that EN page clicks through and the locale flips, or hits a 404 if the slug also changed.
How to spot it: grep for /zh/articles/ inside en/ files (and the reverse). Those are usually wrong unless the reference is deliberately cross-locale.
grep -rn "/zh/articles/" src/content/articles/en/ | wc -l
grep -rn "/en/articles/" src/content/articles/zh/ | wc -l
6. Translations diverge in content, not just freshness
EN got expanded with new examples; ZH got pruned for brevity. They are no longer translations of each other; they are related articles. hreflang implies they are the same, and readers find they are not.
How to spot it: word-count ratio. If en > 1.5x zh (or the reverse) and the gap is more than language verbosity, the content actually diverged.
| Drift type | Tell-tale signal | Detection command / check |
|---|---|---|
| Stale ZH (cause 1) | en commit newer for >50% of pairs | git log -1 --format=%ci per side |
| Broken pairing (cause 2) | translationKey in one lang only | diff of sorted translationKey lists |
| Translation backlog (cause 3) | EN-only translationKey count | audit script EN only output |
| Unreviewed MT (cause 4) | literal idioms, English remnants | manual read of 5 random ZH pages |
| Wrong-locale links (cause 5) | /zh/... links inside en/ files | grep -rn "/zh/articles/" src/content/articles/en/ |
| Content divergence (cause 6) | word-count ratio > 1.5x | wc -w per side |
Shortest path to fix
Ordered by ROI. Step 1 audits; the rest decide what to do per drift type.
Step 1: Build a drift audit
Script the pairing check across the whole tree (this version walks subdirectories, not just one folder):
// scripts/audit-bilingual.mjs
import fs from "node:fs";
import path from "node:path";
import matter from "gray-matter";
const enKeys = collectKeys("src/content/articles/en");
const zhKeys = collectKeys("src/content/articles/zh");
console.log("EN only:", [...enKeys.keys()].filter(k => !zhKeys.has(k)));
console.log("ZH only:", [...zhKeys.keys()].filter(k => !enKeys.has(k)));
console.log(
"Both, EN newer:",
[...enKeys.keys()].filter(
k => zhKeys.has(k) && enKeys.get(k).mtime > zhKeys.get(k).mtime
)
);
function collectKeys(dir) {
const map = new Map();
for (const f of walk(dir)) {
const { data } = matter(fs.readFileSync(f, "utf8"));
if (data.translationKey) {
map.set(data.translationKey, { path: f, mtime: fs.statSync(f).mtime });
}
}
return map;
}
function walk(dir) {
return fs.readdirSync(dir, { withFileTypes: true }).flatMap(e => {
const p = path.join(dir, e.name);
return e.isDirectory() ? walk(p) : p.endsWith(".mdx") ? [p] : [];
});
}
One caveat: fs.statSync().mtime is filesystem modified time, which a fresh git clone resets to checkout time. For a reliable “which side is newer” signal, prefer git commit dates (git log -1 --format=%ct) over mtime. The mtime version is fine for a quick local pass on a working tree you have been editing.
Output: lists of EN-only, ZH-only, and EN-newer pairs. That is your drift inventory.
Step 2: For each pair, decide translate, sync, or single-language
| Pair type | Action |
|---|---|
| EN-only | (a) translate to ZH, or (b) mark single-language and drop its hreflang pair |
| ZH-only | same, in reverse |
| Both, EN newer | sync ZH from current EN |
| Both, ZH newer | sync EN from current ZH |
| Both, diverged content | pick a canonical, sync the other, or split into two distinct articles |
Not every article needs to be bilingual. “Mark single-language” is a legitimate choice and better than maintaining badly. A single-language page with no hreflang annotation is valid; Google indexes it normally.
Step 3: Sync the high-value pairs first
Don’t try to sync 200 pairs in one weekend. Prioritize by traffic. Since GSC dropped the International Targeting report, pull traffic from the standard Performance report instead, filtered by URL.
# GSC -> Performance -> Search results
# Add filter: Page -> contains -> /zh/articles/
# Sort by Impressions (or Clicks), export the top 20
# Those 20 ZH pages get sync priority
A ZH page with 100 monthly impressions deserves a sync; a ZH page with 0 impressions does not justify the work yet.
Step 4: For new updates, automate the translation queue
Add a CI check or pre-commit hook so drift surfaces at the pull-request stage:
# scripts/check-translation-sync.sh
# When an en/*.mdx is modified, flag the matching zh/*.mdx for review.
CHANGED=$(git diff --name-only origin/main -- 'src/content/articles/en/' | grep '\.mdx$')
for f in $CHANGED; do
zh=$(echo "$f" | sed 's/\/en\//\/zh\//')
if [ -f "$zh" ]; then
echo "::warning::EN updated: $f - ZH may need sync: $zh"
else
echo "::warning::EN updated but no ZH twin exists: $f"
fi
done
This does not auto-translate; it makes drift visible while the change is still in review, so you decide before it ships. The ::warning:: syntax is a GitHub Actions workflow command and shows up inline on the pull request.
Step 5: Fix hreflang explicitly and reciprocally
Reciprocity is the whole game: per Google’s docs, if A links to B but B does not link back to A, both annotations are ignored. Every page must also list itself. In your sitemap or page <head>:
<url>
<loc>https://site.com/en/articles/topic/</loc>
<xhtml:link rel="alternate" hreflang="en" href="https://site.com/en/articles/topic/" />
<xhtml:link rel="alternate" hreflang="zh" href="https://site.com/zh/articles/topic/" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://site.com/en/articles/topic/" />
</url>
The matching /zh/articles/topic/ entry must carry the identical set of links. Use x-default for the version shown to any language not otherwise listed. An article with no counterpart gets no hreflang entry at all; that is the single-language declaration. Don’t half-do it: a one-directional annotation is the same as none.
Step 6: Reject blind machine translation
If you must auto-translate to catch up, run it through review rather than publishing raw:
1. Auto-translate into a draft branch (do not publish from it).
2. A human reviews each page for UI terms, idioms, and tone.
3. Verify cross-links are localized (no /en/... inside zh/ and vice versa).
4. Only then merge and publish.
Per the June 2026 policy picture above, the risk is not “MT exists” but “MT that adds no value.” Reviewed MT that reads naturally and keeps UI strings correct is acceptable to both Google and AdSense; raw MT that nobody checked is the kind of low-value content AdSense rejects.
How to confirm it’s fixed
- Re-run
scripts/audit-bilingual.mjs. The “EN only” / “ZH only” lists should contain only pages you deliberately chose to keep single-language; “Both, EN newer” should be empty (or only freshly synced pairs). - Crawl the live site with Screaming Frog (or Ahrefs / Sitebulb) and open its hreflang report. It should show zero “missing return link” / non-reciprocal errors. This is the practical replacement for the retired GSC report.
- Spot-check the rendered
<head>of one EN page and its ZH twin in browser dev tools. Each should listen,zh, andx-default, and the URLs must match exactly between the two pages. - Re-run the wrong-locale grep from cause 5; both counts should be 0 (or only intentional cross-locale references).
FAQ
Does Google Search Console still report hreflang errors in 2026?
No. Google deprecated the International Targeting report (which held the hreflang / Language tab) after September 2022 and did not replace it with a dedicated hreflang report. Google still reads and uses hreflang, but it no longer surfaces site-wide hreflang validation in GSC. Use a crawler such as Screaming Frog, Ahrefs, or Sitebulb for that; each still has a hreflang / “missing return link” report.
Will machine-translated ZH pages get my site penalized or rejected from AdSense? Not automatically. As of June 2026, Google judges translated content by value, not by how it was made, and removed its old advice to block auto-translated pages. Thin or nonsensical translations still fall under scaled content abuse, and AdSense’s most common rejection reason remains “Low value content.” Reviewed, accurate translation is fine; unreviewed raw MT is the risk.
If I make some articles single-language, will that hurt SEO?
No. A page with no hreflang annotation is indexed normally as a standalone page. What hurts is a broken or one-directional hreflang pair, because Google ignores non-reciprocal annotations. A clean single-language declaration is better than a half-maintained pair.
My EN and ZH versions are now genuinely different articles. What should I do?
Decide whether they should be one piece or two. If they cover the same topic, pick a canonical version, sync the other to it, and keep the hreflang pair. If they have diverged into distinct articles, split them: give them different slugs and translationKey values, and remove the hreflang pairing so each is its own page.
Should I fix every drifted pair? No. Sort the drift list by traffic (Step 3) and sync only pages that get impressions or clicks. Zero-traffic pages can stay single-language until they earn the work. Translation debt is a metric to manage down deliberately, not a backlog to clear in one weekend.
Prevention
- Automate a CI drift check that fires whenever an EN file is updated without a matching ZH change (or vice versa).
- Treat translation debt as a tracked metric: review it monthly and reduce it deliberately.
- Articles without bilingual maintenance get marked single-language, not left half-maintained.
- For high-traffic pages, sync is mandatory; for zero-traffic pages, single-language is fine.
- Don’t rely on GSC to catch hreflang breakage; run your own audit or a crawler on a schedule.
translationKeyis the contract: renaming a file requires updating its counterpart too.
Related
- Hreflang warning Search Console
- Hreflang missing return tag
- Hreflang x-default confusion
- Canonical mismatch bilingual
- Article Count Looks Big But Real Coverage Is Weak
Tags: #Content ops #Site quality #Site audit #Troubleshooting #Bilingual