( Understandable / WCAG 3.1.1 )
lang attribute has an invalid value
What is this issue?
The <html> element has a lang attribute set, so at first glance the page looks correctly declared, but the attribute’s value isn’t a valid language tag under BCP 47, the internet standard that defines the structure of language tags like en, fr-CA, or zh-Hant, maintained by the IETF and referenced directly by the HTML specification and WCAG. A value like "eng", "english", or "en_US" with an underscore instead of a hyphen looks language-related to a human reader but doesn’t parse as a recognized tag to a browser or screen reader.
This is distinct from a completely missing lang attribute, which is a separate rule. Here, something is present and syntactically looks like an attempt at a language code; it’s simply not one the browser or assistive technology can resolve.
Why does this matter?
A screen reader that encounters lang="eng" doesn’t recognize it as “close enough” to "en"; the code either matches a real language tag or it doesn’t, with no fuzzy fallback in between. When it doesn’t match, the browser and screen reader treat the page the same way they’d treat a page with no lang attribute at all: falling back to a default pronunciation engine, usually the user’s own system language, regardless of whether that matches the page’s real language.
The practical effect is identical to a missing lang attribute, which makes this a particularly deceptive defect for anyone reviewing a codebase: the attribute is right there in the markup, so it’s easy to assume the page is correctly set up and move on, when in fact every screen reader user hits the exact same mispronunciation problem a missing attribute would cause.
Who is affected?
- Screen reader users: hear the entire page read with the wrong pronunciation engine, since the invalid code fails to resolve to any real language and the fallback default is applied instead, exactly as if no
langattribute existed at all.
What users experience
Aisha uses NVDA on Windows to read a job posting on a recruiting site. The page has <html lang="englsh"> (a typo introduced when the value was hand-typed instead of selected from a validated list). NVDA can’t resolve "englsh" to any known language, so it falls back to its configured default voice, which happens to be set for Spanish pronunciation on her machine. NVDA reads the English job posting with Spanish pronunciation rules applied to every word, turning the listing into a stream of mispronounced syllables she can’t follow, even though the page’s markup technically “has” a lang attribute.
How do I fix it?
Replace the invalid value with a real BCP 47 language tag: for most sites, a two-letter ISO 639-1 code such as en, fr, es, or de, optionally followed by a hyphen and a region subtag like en-US. This works because it’s the exact format every browser’s language-resolution logic and every screen reader’s pronunciation engine expect, so a correctly formatted tag resolves immediately instead of falling through to a guess.
If the invalid value was hardcoded by hand, the most durable fix is to generate it from a validated source instead: a locale configuration object, a CMS field constrained to a dropdown of real language codes, or a small allowlist checked at build time, so a future typo can’t silently reintroduce this defect.
Code Examples
<html lang="english"><html lang="en_US"><!-- Two-letter ISO 639-1 code -->
<html lang="en">
<!-- With a region subtag, hyphen-separated -->
<html lang="en-US">"english" and "en_US" both look like reasonable attempts at declaring English, but neither is a tag any browser resolves: the first spells out the language name instead of using its code, and the second uses an underscore where BCP 47 requires a hyphen. The corrected versions use the exact two-letter code and hyphen-separated region format the specification defines.
Common Mistakes
Mistake: “The value is clearly meant to be English, so most tools will figure it out.” Language-tag resolution in browsers and screen readers is exact-match, not fuzzy: "eng", "english", and "en_US" all fail to resolve even though a human reader can tell what was intended. There’s no partial credit for a value that’s close; it either parses as a valid BCP 47 tag or it doesn’t.
Mistake: “As long as the lang attribute has something in it, this rule doesn’t apply.” This rule exists specifically because a non-empty value can still be invalid: html-has-lang catches a missing attribute, but a page can pass that check and still fail this one if the value present doesn’t parse as a real language tag. Presence and validity are two different conditions.
Mistake: “We can validate this later since it’s just an attribute value.” An invalid lang value produces the exact same mispronunciation failure as a missing one for every screen reader user visiting the page in the meantime; there’s no reduced severity for “the attribute is at least there.” Treat it with the same urgency as a missing attribute.
How RedFlag Detects This
Automated: axe-core rule, runs on every scan. RedFlag calls axe-core’s html-lang-valid rule as part of every scan, restricted to the WCAG 2.0/2.1/2.2 A and AA rule set. The rule reads the <html> element’s lang (and xml:lang) attribute value and checks it against axe-core’s list of valid BCP 47 primary language subtags, flagging any value that isn’t a recognized code.
False negative: axe-core confirms the value is a syntactically valid language subtag; it cannot judge whether that language is actually correct for the page’s content. A German-language page with lang="fr" set is a valid tag, so it passes this specific check even though it names the wrong language entirely; that mismatch needs a human reviewer to catch. False positive: none typical for this check, since matching against a fixed list of valid subtags is a binary condition axe-core evaluates reliably. Manual step: confirm the declared language code is not just syntactically valid, but genuinely matches the page’s actual written language.
Manual Testing
- Open the page in Chrome or Firefox and check the
<html>element’slangattribute value in the browser’s developer tools. - Compare the value against the IANA Language Subtag Registry or a known list of BCP 47 codes to confirm it’s a real, recognized tag.
- Load the page with NVDA or VoiceOver running and listen to the pronunciation and accent used for the first paragraph of visible text.
- If the announced speech uses an unexpected accent, mispronounces common words, or the attribute value doesn’t match any real language code, the check fails.
Related WCAG Success Criteria
3.1.1 Language of Page: The default human language of each web page must be programmatically determinable. An invalid lang value fails this criterion just as thoroughly as a missing one, since a code that doesn’t parse to a real language can’t be programmatically determined any more reliably than no code at all.
Related Issues
Page language is not set is the closely related failure one step earlier: no lang attribute at all, rather than one present with an invalid value. Both produce the identical mispronunciation outcome for screen reader users.
lang and xml:lang do not match covers a page with two conflicting language declarations instead of one invalid one; a different way the same page-level metadata can go wrong.
lang attribute on an element has an invalid value applies this identical validity check to every element other than <html>, catching an invalid language code on a single passage rather than the whole page’s default.
HTML lang attribute is not a valid BCP 47 tag documents the same conceptual defect this rule detects, but is not wired to any automated detector of its own; treat this page, not that one, as the actual automated check for an invalid page-level lang value.
References
- W3C Understanding 3.1.1: Language of Page
- W3C Technique H57: Using the language attribute on the html element
- MDN: lang global attribute
- IANA Language Subtag Registry
Frequently asked questions
What is BCP 47, and why does RedFlag check against it?
BCP 47 is the internet standard that defines how language tags like "en", "fr-CA", or "zh-Hant" are structured, maintained by the IETF and referenced directly by both HTML and WCAG. RedFlag checks lang values against it because it is the same standard every browser and screen reader relies on to resolve a language tag to an actual pronunciation engine.
Is "en_US" with an underscore a valid lang value?
No. BCP 47 requires a hyphen between the language and region subtags, as in "en-US", not an underscore. An underscore is a common mistake carried over from other locale formats like Java or POSIX locale codes, and most browsers will not recognize it as a valid language tag.
Does capitalization matter in a lang value, like "EN" versus "en"?
Not for validity: BCP 47 language tags are case-insensitive, so "EN", "en", and "En" all resolve to the same language. The convention is lowercase for the language subtag and uppercase for the region subtag, as in "en-US", but a browser will still parse a differently-cased tag correctly.
Does this rule apply to invalid values on the lang attribute of elements other than html?
No, this specific rule checks only the html element's lang attribute. An invalid lang value on any other element, such as a span wrapping a foreign-language phrase, is checked separately by the valid-lang rule, which applies the identical validity test to every other element on the page.
Will an invalid lang value cause the whole page to fail, or just mispronounce some words?
It affects the whole page, not just isolated words. Because the html element sets the default language every other element inherits, an invalid value there forces the screen reader to fall back to a default voice for all of the page's text, the same outcome as a completely missing lang attribute.