Which voice will read your page aloud?
Enter your domain. We read the HTML your server sends, check the lang attribute on the page — whether it is there, whether it is a real language code, and whether it matches the words the page is actually written in — under the criterion each problem sits beneath.
Free, instant, no signup and no card. Nothing is stored.
Free, instant, no signup and no card. We read the HTML your server sends for that one page, check its language declaration, and compare it with the words on the page. Nothing is stored.
The one failure a page cannot show you
Every page renders identically with any lang value or with none. There is no visual symptom, no console error, no layout shift. The only place the defect exists is the voice a screen reader chooses — and nobody on a web team listens to their own site in one.
When the attribute is missing, the reader uses its owner's default language. A Spanish page on an English-configured reader is pronounced with English rules: every word is spoken, and very few of them are understood. When the attribute is wrong it is worse, because a wrong value overrides the listener's settings on purpose — a starter theme's lang="en" on a German storefront makes every screen reader, everywhere, read German in an English accent.
The WebAIM Million finds a missing document language on 15.8% of the top million home pages. It is one of the six failures that make up 96% of everything automated testing finds, and it is by far the cheapest of the six to fix: one attribute, on one element, in one template.
What this checker refuses to call a failure
A free checker a stranger runs on their own site has one way to be useless, and it is to cry wolf. So five things are failures here, each naming its criterion: no language declared, an empty one, a value that is not a language code, an xml:lang that contradicts lang, and an invalid code on an inner element.
Case does not matter. en-us, EN-US and en-US are the same tag. Script and three-letter codes are real: zh-Hant-TW, sr-Latn, fil, es-419 all pass.
A declared language that disagrees with the text is review, not failure. The comparison is strong evidence — writing system and function-word frequency, over at least 80 words, with a two-to-one margin — and not proof. Close relatives it cannot separate, Danish and Swedish or Catalan and Spanish, are never compared at all.
lang="" inside the page is correct. On an inner element it means “language unknown”, which is a legitimate thing to say about a code sample or a username. Only an empty value on the root element is a failure.
What the checker looks for
Five failures, then the review and verify items — real problems that WCAG does not forbid at level A or AA, or that cannot be proved from served markup, reported separately and never counted as violations.
- No language declared on the pageFailureSC 3.1.1 Language of Page (Level A)
- The <html> element carries no lang attribute, so nothing tells a screen reader which language the page is in. It falls back to the listener's own default — which means a Spanish page read on an English-configured reader is spoken with English phonetics: every word audible, very few of them intelligible. Browser translation prompts, braille translation, hyphenation and spell-check all key off the same missing attribute. The WebAIM Million finds it on 15.8% of home pages.
- The fix: One attribute on one element: `<html lang="en">`, or whichever code matches the page. It lives in the root layout or theme header, so fixing it once fixes every page the template renders.
- Language attribute present but emptyFailureSC 3.1.1 Language of Page (Level A)
- `lang=""` on the root element is not a missing value — it is an explicit statement that the page's language is unknown, which is worse. A screen reader is told to stop guessing. This is almost always a template variable that rendered nothing: a CMS locale setting that was never filled in, or a framework's `lang={locale}` with an undefined locale.
- The fix: Find the variable that renders into the root element's lang attribute and give it a default. In Next.js that is the `<html lang>` in the root layout; in WordPress it is `language_attributes()` driven by Settings → General → Site Language.
- Language attribute is not a language codeFailureSC 3.1.1 Language of Page (Level A)
- The value is not a well-formed BCP 47 language tag, so assistive technology cannot map it to a voice and treats the page as if no language were declared. The usual causes: a country code where the language code goes (`us`, `jp`, `cn` — Japanese is `ja`, Chinese is `zh`), a spelled-out name (`english`), or a locale string pasted from server code (`en_US`, with an underscore; the tag uses a hyphen).
- The fix: Use the two-letter ISO 639-1 language code, optionally followed by a hyphen and a region: `en`, `en-US`, `pt-BR`, `zh-Hans`. The suggestion next to each finding is the value that almost certainly was meant.
- xml:lang contradicts langFailureSC 3.1.1 Language of Page (Level A)
- The root element declares one language in `lang` and a different one in `xml:lang`. HTML requires the two to agree when both are present, and different assistive technologies read different attributes — so the same page is announced in two languages depending on who is listening. This is usually a leftover XHTML doctype whose `xml:lang` was never updated when the site was translated or re-themed.
- The fix: Delete `xml:lang` — in a page served as text/html it does nothing that `lang` does not — or set it to exactly the same value.
- An inner element's language is not a language codeFailureSC 3.1.2 Language of Parts (Level AA)
- Somewhere inside the page an element switches language with a lang attribute whose value is not a language tag. The switch is the right idea — a quotation, a language picker or a translated heading should be marked — but an invalid code means the switch never happens and the passage is read in the page's voice. The most common instance is a language menu that writes the country (`lang="us"`, `lang="jp"`) rather than the language.
- The fix: Replace each value with its ISO 639-1 code. On a language picker, each option should carry the language it is written in: `<a lang="de" hreflang="de">Deutsch</a>`.
- Declared language does not match the textReview
- The page declares one language and its visible text reads as another. Unlike a missing attribute, a wrong one is acted on deliberately: every screen reader, whatever its user's settings, switches to the declared language's voice and reads the page with its rules. This is how a German storefront ends up read aloud in an English accent — a theme shipped with `lang="en-US"` and nobody changed it, because it is invisible on screen. It is a review item rather than a failure because the comparison runs on word frequency and writing system, which is strong evidence and not proof, and a genuinely bilingual page can trip it.
- The fix: Set the root element's lang to the language the page is actually written in. If the site is multilingual, each locale's pages need their own value — driven from the route or the CMS locale, not hard-coded in a shared layout.
- Right-to-left language with no dir attributeReview
- The page is declared in a right-to-left language but neither <html> nor <body> carries `dir="rtl"`. The attribute sets the base direction for text, punctuation, tables, form fields and the order a screen reader moves through inline content; without it, mixed-direction text — a price, a product code, an English brand name inside Arabic — can be reordered wrongly. It is a review item because a stylesheet can set `direction: rtl`, which this reader does not evaluate.
- The fix: `<html lang="ar" dir="rtl">`. Set direction in markup rather than CSS: the attribute is semantic and survives reader modes, translation and copy-paste; the style does not.
- A real language code that is also a country codeVerify
- The value is a valid language code, so no automated rule will fail it — but it is also a country code, and the language it names is probably not the one the page is in. `uk` is Ukrainian, not the United Kingdom; `se` is Northern Sami, not Swedish; `ca` is Catalan, not Canadian English. If the page really is in that language, ignore this. If not, every screen reader is reading it with the wrong voice.
- The fix: Check which one you meant. The usual intended values are in the suggestion beside the finding: `en-GB`, `sv`, `en-CA`.
- Region subtag is not a real regionVerify
- The primary language is fine, so screen readers pronounce the page correctly — they fall back to the part before the hyphen. But the region is not in the registry: `en-UK` is the classic (the United Kingdom's code is GB), along with `en-EN` and `en-EU`. Spell-checkers and translation tools that do use the region cannot, and strict validators will flag the tag.
- The fix: Use the ISO 3166 country code — `en-GB`, not `en-UK` — or drop the region entirely: `lang="en"` is complete and correct.
The codes people get wrong
Most invalid values are a country where the language should be, or a server locale pasted into markup. The left column is what we find; the right is what was meant.
- lang="en_US"lang="en-US"
- lang="us"lang="en"
- lang="english"lang="en"
- lang="jp"lang="ja"
- lang="cn"lang="zh"
- lang="kr"lang="ko"
- lang="dk"lang="da"
- lang="gr"lang="el"
- lang="cz"lang="cs"
- lang="uk"(Ukrainian)lang="en-GB"
- lang="se"(Northern Sami)lang="sv"
- lang="en-UK"lang="en-GB"
Common questions
- What does the lang attribute actually do?
- It tells software which language the page is written in, and the most important consumer is a screen reader. Screen readers pick their speech voice and pronunciation rules from it. With no language declared they fall back to the listener's own default, so a French page on an English-configured reader is spoken with English phonetics. Browser translation prompts, braille translation tables, automatic hyphenation, spell-checkers and search engines all read the same attribute. It is WCAG success criterion 3.1.1 Language of Page, at level A — the lowest bar in the standard.
- Is lang="en_US" valid?
- No. The value has to be a BCP 47 language tag, and those use a hyphen: en-US. The underscore form is a locale string from server code — Java, PHP, WordPress internals, gettext — and it is a common way for a correct-looking value to reach the page broken. Automated testing tools, including axe, fail it under the html-lang-valid rule, and assistive technology cannot map it to a voice. This checker flags it and tells you the value that was meant.
- Can I use a country code, like lang="us" or lang="jp"?
- No — and this is the most common invalid value we see. The attribute takes a language code from ISO 639, not a country code from ISO 3166. English is en, Japanese is ja, Chinese is zh, Korean is ko, Danish is da, Greek is el, Czech is cs. A country can follow the language after a hyphen, which is how you say which variety: en-US, pt-BR, zh-TW. Watch out for the codes that are valid but mean something else: uk is Ukrainian, not the United Kingdom (that is en-GB), se is Northern Sami rather than Swedish (sv), and ca is Catalan rather than Canadian (en-CA).
- My site is in German but the lang attribute says en. Does that matter?
- It matters more than a missing attribute does. A missing language leaves the choice to the listener's device, which is often right. A wrong language overrides it: every screen reader, whatever its user's settings, switches to the English voice and reads the German text with English pronunciation rules. It happens constantly — a theme or starter template ships with lang="en" hard-coded in the layout, the site is written in another language, and nothing on screen ever shows the mistake. This checker compares the declared language with the visible text and flags the disagreement as a review item.
- How does the checker know what language my text is in?
- Two signals. For pages in a non-Latin writing system — Cyrillic, Arabic, Hebrew, Greek, Thai, Japanese, Chinese, Korean and others — the script itself narrows the language down, and a Japanese page declared as zh is caught because Japanese text carries kana. For Latin-script pages it counts function words — the, und, que, het, och, się — across English, Spanish, French, German, Portuguese, Italian, Dutch, Swedish and Polish, and only names a language when it beats the runner-up by two to one over at least 80 words. Close relatives it cannot separate, such as Danish and Swedish or Catalan and Spanish, are never compared. That is why a mismatch is reported as review, not failure: it is strong evidence, not proof.
- What about pages with more than one language?
- Declare the main language on the html element and mark each passage in another language with its own lang attribute — a quotation, a product name, a language switcher. That is WCAG 3.1.2 Language of Parts at level AA. This checker lists every element that switches language, flags any whose code is invalid, and leaves those passages out of the declared-versus-actual comparison, so a German quotation on an English page does not make the page look German.
- Does the Content-Language header or a meta tag count?
- No. Screen readers read the lang attribute on the html element. A Content-Language response header and a <meta http-equiv="content-language"> tag are both ignored for this purpose, and so is lang on the body element or xml:lang on its own in a page served as HTML. If your server sends the header but the page has no lang attribute, this checker says so.
- Where do I fix it?
- In one place, almost always. In Next.js it is the <html lang> in app/layout.tsx; in a Pages Router site, _document.tsx. In WordPress it comes from language_attributes() in the theme header, driven by Settings → General → Site Language. In Shopify it is {{ request.locale.iso_code }} in theme.liquid. In Webflow and Squarespace it is a site-level language setting. On a multilingual site the value must come from the route or the CMS locale, not be typed into a shared layout.
- Does this store my domain?
- No. The check is a single HTTP GET of the page you name, run inside the request and discarded when it answers. There is no account, no email field and no row written anywhere. If you go on to run the free scan, that one does keep a report so you can come back to it.