Invalid lang value on content (valid-lang)

The valid-lang rule flags a lang attribute on an element inside the page – a quote, a language switcher link, a paragraph – whose value is not a valid language tag, such as fr_FR or french. Screen readers cannot switch to the right pronunciation for that passage. The fix is a well-formed tag: lang="fr".

What the rule means

WCAG 3.1.2 Language of Parts requires that software can determine the language of each passage or phrase that differs from the page language. In HTML you mark such a passage with lang on the element that wraps it.

valid-lang checks every element other than <html> that carries lang (or xml:lang). It takes the part before the first hyphen and compares it with axe-core's list of known language codes. It fails on:

  • locale identifiers with an underscore: fr_FR, de_DE;
  • language names instead of codes: french, deutsch;
  • codes that do not exist, such as dk for Danish (correct: da).

It does not report:

  • an empty lang="" – that is allowed and means "language unknown";
  • elements with no text of their own, such as an empty <span lang="xx">;
  • a passage in another language with no lang at all. This is the most common failure of 3.1.2, and no automated rule can find it, because it needs someone to recognise the language.

WCAG 3.1.2 has exceptions: proper names, technical terms, words of indeterminate language, and words that have become part of the surrounding language, such as "kindergarten" in English, need no lang.

Who is affected

Mainly blind and partially sighted people using a screen reader. Screen readers such as NVDA, JAWS and VoiceOver can switch voice when the language changes – but only when the markup says so with a tag they can read. With an invalid tag, a French quote on an English page is read with English pronunciation and can become hard to understand. The same applies to translation tools and to people who use text-to-speech to support their reading.

Why the check fails

  • Language switchers that print the CMS locale on each link: lang="de_DE", lang="fr_FR".
  • Translation plugins that wrap translated snippets with their internal locale identifiers.
  • Language names from an editor field, such as lang="english", entered in a rich-text editor's "language" option.
  • Typos and country codes: lang="jp" instead of ja, lang="dk" instead of da.
  • Template placeholders that were never replaced, for example lang="{{ item.locale }}".

How to fix it

  1. Find where the value comes from – usually the language switcher template or the plugin that marks up translations.
  2. Write BCP 47 tags: the language code, optionally a hyphen and a region (fr, fr-CA). Replace underscores with hyphens.
  3. While you are there, mark up passages in other languages that have no lang yet – the rule will never point you to them.
<!-- Before: locale identifiers on the language switcher -->
<nav class="lang-switch" aria-label="Language">
  <a href="/de/" hreflang="de" lang="de_DE">Deutsch</a>
  <a href="/fr/" hreflang="fr" lang="fr_FR">Français</a>
</nav>
<!-- After: valid language tags, so each name is read in its own language -->
<nav class="lang-switch" aria-label="Language">
  <a href="/de/" hreflang="de" lang="de">Deutsch</a>
  <a href="/fr/" hreflang="fr" lang="fr">Français</a>
</nav>

A quote in another language is marked up the same way – on the element that contains exactly that passage:

<p>The motto of the house reads <q lang="fr">Liberté, égalité, fraternité</q>.</p>

How to test it manually

  1. In the browser console, list all language changes: document.querySelectorAll('[lang]'). Check each value against the IANA language subtag registry.
  2. Read the page and look for words and passages in another language that have no lang – quotes, product names that are full phrases, song or film titles, the names of other language versions in the switcher.
  3. Listen with a screen reader (NVDA on Windows, VoiceOver on macOS) with automatic language switching turned on: does the voice change where the language changes?
  4. Remember the exceptions: proper names and established loan words need no markup.

3.1.2 Language of Parts, Level AA. Related: 3.1.1 Language of Page, which covers the page language on <html> and uses the same tag format.

How Reviseberg reports it

Reviseberg runs valid-lang on every crawled page at 1280px. Language switchers sit in the header, so an invalid tag there shows up as one element per language link on every page. The issue list shows the rule with its severity, WCAG criterion 3.1.2 and level, the number of elements and pages, and the points you get back by fixing it. The detail view shows the selector, the HTML snippet with the invalid value and every affected page. You can mark a finding as "ignore", "can't fix" or "false positive" with a reason; every decision is logged.

Because passages without any lang cannot be found automatically, a crawl with no findings does not make 3.1.2 a pass: the criterion shows as partly checked until a person records a manual result.

  • undefined – the same check for the page language on <html>.
  • undefined – the page has no language at all.
  • undefined – links, such as those in a language switcher, need a readable name.

Check the language markup across your whole site – get a free scan

Frequently asked questions

Do I have to mark every foreign word with lang?

No. Proper names, technical terms and words that have become part of the surrounding language are exempt from WCAG 3.1.2. Mark up passages and phrases that a reader would pronounce differently.

Is lang="" an error?

No. An empty value explicitly means the language is unknown, and valid-lang does not report it.

Should the names in a language switcher carry lang?

Yes, when each name is written in its own language ("Deutsch", "Français"). Then a screen reader pronounces each one correctly.

Why did the scan find nothing, although my page has an unmarked German quote?

Because the rule only checks lang attributes that exist. A passage without one looks like the page language to any automated check.

Sources

  1. W3C, Understanding SC 3.1.2 Language of Parts – https://www.w3.org/WAI/WCAG22/Understanding/language-of-parts
  2. Deque University, axe-core 4.13 rule valid-lang – https://dequeuniversity.com/rules/axe/4.13/valid-lang
  3. W3C, Technique H58: Using language attributes to identify changes in the human language – https://www.w3.org/WAI/WCAG22/Techniques/html/H58
  4. W3C Internationalization, Language tags in HTML and XML – https://www.w3.org/International/articles/language-tags/
  5. IANA, Language Subtag Registry – https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry

See what a scan finds on your site

One page in about thirty seconds, no email needed. The full report covers up to 100 pages and a keyboard journey.