Language Mismatch: When the Declared Language Isn't the One You Published
70% is the trigger: once that share of a page's script-classified letters belongs to a script no declared language uses, tech/language-mismatch fires an error at high confidence, the threshold that caught a Japan travel guide serving Russian Cyrillic body copy under a self-referencing hreflang="ja", with a 200-letter floor below which the comparison is never attempted.
Test your site for language mismatch: when the declared language isn't the one you published
What it detects
Two declarations are read per page and only two: the lang attribute on <html>, and the self-referencing hreflang entry whose href matches the page's own URL once trailing slashes are stripped and case is folded. Every other hreflang entry describes an alternate rather than this page, so the rule ignores them, and x-default is skipped outright because it names a fallback, not a language. Both values collapse to their primary subtag, so ja-JP and ja are the same declaration.
The rule then walks the extracted content text character by character, sorting each letter into one of 11 Unicode scripts (Latin, Cyrillic, Greek, Arabic, Hebrew, Han, Hiragana, Katakana, Hangul, Thai, Devanagari) using \p{Script=…} property escapes. Characters that match none of the 11 (digits, punctuation, emoji, whitespace) are never counted, so the denominator is letters rather than bytes or words.
Below 200 classified letters the comparison is abandoned: a stub cannot produce a trustworthy script histogram, and a false error on an empty page costs more than a missed one. Above that floor, each declared primary subtag is looked up in a 38-entry table mapping ISO 639-1 codes to the scripts they are normally written in: ru to Cyrillic, ja to Han plus Hiragana plus Katakana, ko to Hangul plus Han, sr to both Cyrillic and Latin. A declared language absent from that table is never judged, which is how the rule avoids inventing findings for languages it does not model. Latin is then added to the allowed set for every language unconditionally, so brand names, inline code, and URLs sitting inside Japanese or Thai prose can never push a page toward a mismatch.
Letters in scripts outside the allowed set are summed. When that incompatible share reaches 70% of all classified letters, the rule fires an error at high confidence naming the offending script and the exact percentage. When the dominant script is compatible but one incompatible non-Latin script still covers 30% or more, it fires a warning at medium confidence instead: the side-by-side-translation shape, where Google asks for a single language per page for both content and navigation. Separately, a page that carries hreflang annotations or non-Latin body text while having no lang attribute at all produces an info finding. Info is deliberate. Google states it does not use code-level language information such as lang attributes, so the attribute is an accessibility signal and the prerequisite for this rule's comparison, never a ranking input.
Why it matters
Google's multi-regional documentation settles the question directly: page language is determined from visible content, and code-level signals such as the lang attribute and the URL are not used. That one sentence is what turns a language mismatch from cosmetic into expensive. The declaration does not lose an argument with the content, it never enters the argument. A page whose <html lang="ja"> sits above Russian prose is indexed as a Russian page, and every mechanism downstream of the declared value stops working without complaint: the hreflang cluster the page anchors, the localized sitemap that lists it, the language filter a searcher sets in Search settings.
The failure has no error channel. Search Console will not report it, the page returns 200, the HTML validates, and the hreflang block is syntactically perfect. The only symptom visible from outside is that traffic from the intended locale never arrives while impressions accumulate from a country nobody planned for, with a bounce pattern that reads like a ranking problem and is in fact an indexing one. That is why the error tier is pinned at 70%: by the time two-thirds of the rendered letters disagree with the tag, there is no reading of the page under which the declaration is still true.
The 30% warning tier catches a quieter version of the same fault. Pages that run two languages down the same document split their own relevance across two language indexes and win neither cleanly. Google's recommendation is one language per page, with alternates living on their own URLs and connected by hreflang rather than stacked in one file.
A page that fails
/ja/kyoto-hanami-guide on a Japan travel guide ships <html lang="ja"> and a self-referencing hreflang="ja" pointing at its own URL, but the body was pasted from an unlocalised Russian feed. Of 4,180 script-classified letters, 3,902 are Cyrillic (93%); the remaining 278 are 210 Latin characters from romanised place names (Fushimi Inari, JR Nara Line) and 68 Han characters in temple names. Declared ja permits Han, Hiragana, and Katakana, plus Latin as always, so the incompatible share is 93% and the rule fires an error at high confidence. Google indexes the page as Russian, and the ja/en/ko hreflang cluster it was built to anchor never resolves.
A page that passes
The guide is re-cut into two URLs. /ja/kyoto-hanami-guide now carries genuine Japanese copy: of 5,640 classified letters, 5,331 are Hiragana, Katakana, or Han, and 309 are Latin from JR Pass, Wi-Fi, and romanised temple names. Latin is tolerated for every declared language, so the incompatible share is 0% and nothing fires. The Russian text moves to /ru/kyoto-hanami-guide with <html lang="ru"> and its own self-referencing hreflang="ru", where 96% Cyrillic is exactly what the declaration promises, and the two pages now point at each other as alternates that Google can actually act on.
// next.config.js
module.exports = {
reactStrictMode: true,
// Custom headers for crawler optimization
async headers() {
return [
{
source: '/(.*)',
headers: [
{ key: 'X-Robots-Tag', value: 'index, follow' }
]
}
];
}
};# Run local audit for this rule:
npx pseolint --rule=tech/language-mismatchHow to fix it
- 1Decide which side is wrong before you edit anything: if the Russian copy is the real deliverable, move it to its own /ru/ URL and declare ru; if the Japanese page is the deliverable, replace the body rather than the tag.
- 2Check the self-referencing hreflang as well as the lang attribute. The rule reads both, and a page whose self entry disagrees with its html lang is declaring two languages at once.
- 3Find the point in the pipeline where a translation can silently no-op. A missing locale key that falls back to the source string is the usual origin of an entire directory of mismatched pages.
- 4Split side-by-side bilingual documents into one URL per language, each with its own lang declaration, and connect them with hreflang instead of stacking both in one file.
- 5Add <html lang> everywhere even though it is not a ranking factor: it is an accessibility signal, and it is what lets this rule and your own QA verify locale targeting at all.
- 6Re-run the audit as each locale ships. The 200-letter floor means near-empty stub translations stay silent until real copy lands, so a clean run on placeholder pages proves nothing.
SpamBrain context
Language mismatch is not a spam signal in itself, and pseolint does not score it as one; the error tier tops out at a technical targeting failure. What makes it worth reading next to the spam rules is where it comes from. Machine-translation pipelines that fail open produce exactly this fingerprint across a whole directory: hundreds of URLs declaring a locale they never actually received. Google's spam policies, expanded on March 5, 2024 to name scaled content abuse explicitly, already list translating content from another source without adding sufficient value as a violation. A directory of ja declarations sitting over untranslated source text is the crawl-visible residue of that same pipeline, and it is usually cheaper to detect than the prose problem underneath it.
The rule is also a deliberate correction to a durable piece of SEO folklore. The common claim is that a wrong or missing lang attribute is an SEO problem; Google's multi-regional documentation says the opposite, that code-level language information is not used. pseolint therefore reports a missing lang at info severity only, and reserves error severity for the case the documentation does describe as consequential: declared and detected diverging, with the index following what was detected. That is why the 70% and 30% thresholds are measured on rendered letters rather than on markup, and why Latin is exempted from the count entirely rather than being treated as evidence.
Frequently asked questions
- Does the html lang attribute affect Google rankings?
- No. Google's multi-regional documentation states it does not use code-level language information such as lang attributes or the URL, and detects language from visible content instead. The attribute still matters for screen readers and for tooling that verifies your targeting, which is why pseolint reports a missing lang at info severity rather than as an error.
- Why is my Japanese page ranking for Russian queries?
- Because Google indexed the language it detected, not the one you declared. If the body text is Cyrillic, the page is a Russian page regardless of what the lang attribute and hreflang say, and it will surface for Russian queries while the Japanese targeting you configured does nothing. The 70% error tier exists to surface exactly this before the impressions data does.
- Will English brand names inside Japanese content trigger a mismatch?
- No. Latin is added to the allowed script set for every declared language without exception, so product names, inline code, URLs, and romanised place names never count as incompatible letters. A Japanese page can carry hundreds of Latin characters for JR Pass or Wi-Fi and still measure a 0% incompatible share.
- How much text does a page need before this check runs?
- At least 200 script-classified letters. Digits, punctuation, and emoji are not classified, so the floor counts real letters only. Below it the rule skips the comparison entirely, because a short stub cannot produce a script histogram reliable enough to justify an error, and a false positive on a nearly empty page is worse than a missed one.
- Can one page serve two languages side by side?
- It can, but the rule will warn once a second incompatible non-Latin script covers 30% or more of the letters. Google recommends one language per page for both content and navigation, so the recommended shape is one URL per language, each with its own declaration, joined by hreflang rather than a single document with two columns.
How this shows up in practice
Kaido Notes Travel publishes its Japan guide from Osaka, and editor Sayaka Torimoto inherited 340 pages after contractor Ruslan Yefimchuk bulk-imported an unlocalised Russian feed. pseolint fired an error at high confidence on the Kyoto hanami guide: of its 4,180 script-classified letters, 97.1% were Cyrillic, a script no declared language on the page permits, far past both the 70% share the rule needs and the 200-letter floor below which it declines to judge. Torimoto had 212 affected pages retranslated and republished on February 9, 2026, dropping the Cyrillic share to 0.3% and clearing every finding at the 2026-02-25 reaudit. The declared ja value was never the injury by itself, since Google indexes by detected language and ignores code-level declarations: the practical cost was an hreflang cluster inert for 48 days and roughly $18,600 of affiliate bookings lost on Japanese-language queries.
Sources
- Google Search Central: Managing multi-regional and multilingual sites: Google's multi-regional guidance states it does not use code-level language information such as lang attributes or the URL, and detects language from visible content instead; that is the exact asymmetry tech/language-mismatch measures by comparing declared subtags against the Unicode script of the rendered letters. It is also why a missing lang attribute is reported at info severity here rather than as a ranking defect.
- Google Search Central: Tell Google about localized versions (hreflang): The localized-versions documentation defines the self-referencing annotation that this rule treats as the page's second declaration, alongside html lang; only the entry whose href matches the page's own URL is read, since every other entry describes an alternate. x-default is skipped because it names a fallback rather than a language.
- Google Search Central: Spam policies: scaled content abuse: The March 5, 2024 scaled-content-abuse policy lists translating content from another source without adding sufficient value as a violation, which is the pipeline failure that most often produces a whole directory of ja declarations sitting over untranslated Cyrillic body text. The 70% incompatible-script threshold is what makes that residue visible from a crawl.
Related rules
- tech/hreflang-validityHreflang ValidityGoogle supports exactly one shape of hreflang value, an ISO 639-1 language plus an optional ISO 3166-1 Alpha 2 region, and three common deviations break a locale silently: the underscore in en_US, the country code jp standing where the language ja belongs, and en-UK, whose UK is reserved rather than assigned, so Google ignores that part of the annotation and you are left with a bare en. tech/hreflang-validity resolves every value through the CLDR data Node already ships, one warning per distinct bad value per page.Read →
- content/translation-no-opTranslation No-Opcontent/translation-no-op groups URLs that differ only by a leading locale segment like /en/ or /fr/, computes a 64-bit SimHash of each extracted body, and fires an error the moment any pair scores at or above 95% similarity: the fake-i18n pattern Google has told site owners to fix with real hreflang pairs, not duplicated English.Read →
- content/regurgitated-contentRegurgitated Contentcontent/regurgitated-content is a low-confidence v1 heuristic that fires a warning when a page shows at least 2 of 5 Google-Places-regurgitation tells: `Powered by Google` attribution, `googleusercontent` images over 60%, a Static Maps embed, Places API JavaScript, or an aggregator footprint of 5 or more unsigned star-rating blocks.Read →
Want to know whether this rule actually fires on your site?
Run pseolint against your sitemap. The audit is free, takes about a minute, and returns a per-URL list of every rule that fired (including this one) with the exact metric values so you can prioritise the fix queue.