Fix
The page never says what language it is in
One attribute, on one tag, missing from every page of the shop — and it is the first thing a screen reader has to guess at.
Who this locks out
The page never declares what language it is in, so screen readers guess the pronunciation.
- Blind shoppers whose screen reader voice is set to another language
- Deafblind shoppers reading a braille display, where accents break
- Shoppers using read-aloud tools to get through checkout
- Anyone relying on page translation to understand your product pages
The transform
Before
<html class="no-js" dir="ltr"> After
<html class="no-js" dir="ltr" lang="en-GB"> Detection
How this is tested here
Every scan runs three independent engines and nine custom probes against your rendered pages, and records which engine flagged what. These are the real rules behind this fix family.
| Criterion | Level | Detected by | What we do |
|---|---|---|---|
| 3.1.1 Language of Page | A | axe-core (html-has-lang, html-lang-valid, html-xml-lang-mismatch) · IBM Equal Access (html_lang_exists, html_lang_valid) | auto |
This family is keyed to the axe rules html-has-lang</code>, <code>html-lang-valid</code>, <code>valid-lang in the
engine's own category map.
What Klarvo Access does
Klarvo reads the store's real locale, adds the matching lang attribute to the html tag in the HTML your server sends, and then re-validates the served page independently to prove it stuck.
Auto-applied
Generated, then applied server-side in the HTML your store actually serves — and independently re-validated in the served page before it counts as resolved.
An automated finding is evidence, not a certificate. Conformance is certified only by the professional audit — how that works.
Do it yourself
Fixing this by hand
What a missing language declaration does to your shop
A shopper in Lyon opens your product page with VoiceOver set to French. Nothing in the page says the words are English, so the software carries on in the voice it already has: your product name, your size guide and your delivery promise are read with French phonetics. It is not gibberish exactly — it is the sound of a badly tuned radio, and it takes real effort to decode. Most people leave.
The same failure runs the other way. A German store that never declares German is read to a blind customer by an English voice: Warenkorb becomes a noise, Zur Kasse becomes another one, and the postcode field’s error message is unintelligible at exactly the moment the sale is at risk.
It bites hardest in the places where money moves. The product grid is browsable by guesswork; the basket is not. Quantities, the subtotal, the shipping line, the promo-code field, the “Place order” button and every validation message WooCommerce returns are all read aloud in the wrong voice. Search suggestions suffer the same way, which means the shopper cannot even find the product to begin with.
Braille is worse than mispronunciation. Contracted braille tables are language-specific, so a display driven with the wrong table renders accented characters — ä, é, ø — as unrelated cell patterns. The reader is not slowed down; they are stopped. Read-aloud tools used by shoppers with dyslexia depend on the same declaration, as does the browser’s own offer to translate the page.
Because the attribute belongs on the root element, one omission affects every URL you have: home page, category pages, every product, the basket, checkout, the order confirmation and your policy pages. It is also, for the same reason, one of the cheapest failures in WCAG to put right.
How it is detected here
Every page is rendered in a real browser, then checked by two independent engines: axe-core 4.12 and IBM Equal Access. HTML_CodeSniffer, the third engine in the scan, also reports against this criterion, so a finding here arrives confirmed by more than one implementation.
Three axe rules own this family:
html-has-lang— the<html>element has nolangattribute at all.html-lang-valid— the attribute is there but the value is not a real language tag:lang="english",lang="uk", or the common WordPress sliplang="en_GB"with an underscore where BCP 47 wants a hyphen.valid-lang— any element further down the page carries alangvalue that is not a real language tag. This usually comes from pasted markup, a translation widget, or a hand-edited product description.
IBM Equal Access checks the same ground with html_lang_exists and
html_lang_valid. axe additionally flags html-xml-lang-mismatch, where a page
declares both lang and xml:lang and the two disagree about the base language.
The criterion is 3.1.1 Language of Page, Level A. No custom Klarvo probe is involved: this one is fully machine-checkable from the served markup, so the engine rules carry it and there is nothing here that needs a judgement call.
What Klarvo does about it
This family is applied automatically. The fix is generated, applied server-side in the HTML your server sends, and then independently re-validated. It is not a script that patches the page after it loads.
The value is your store’s own. The WordPress plugin reports the real locale —
get_locale() returns something like fr_FR, and get_bloginfo('language') returns
the BCP 47 form fr-FR. The engine canonicalises that (de_DE_formal becomes
de-DE) and writes a single instruction: ensure the html element carries
lang="de-DE". The plugin applies it by rewriting just the opening <html> tag in
the response, never by round-tripping the whole document. If you already have a
lang value, yours is left alone.
Then it is checked rather than assumed. The page is re-rendered and re-tested against
html-has-lang, html-lang-valid and valid-lang; a separate plain fetch, as an
ordinary visitor, confirms the served HTML carries that exact value rather than
merely some value; and the <html> tag before and after is kept as evidence.
One honest exception. If your store reports no usable locale, a small deterministic
check over the words on your own pages can identify the language — and only a clear
result is applied. If the result is not clear, nothing is applied: the fix goes to
your review queue with a proposed value for a person to confirm. Stamping en on a
shop that is not English would be a new failure wearing a pass, so the engine
declines to guess.
How to fix it yourself
Set the locale first. In WordPress, go to Settings → General → Site Language and pick the language your shop is actually written in. Everything below reads from this, so a wrong setting here quietly produces a wrong declaration everywhere.
Classic themes. Open your theme’s header.php. The opening tag must delegate to
WordPress:
<!doctype html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo( 'charset' ); ?>" />
language_attributes() prints lang="en-GB" for you, and adds dir="rtl" on
right-to-left locales. If instead you find a hard-coded <html lang="en"> — or a
bare <html> — that is your bug. Edit a child theme, not the parent, or the next
theme update will undo it.
Block themes. There is no header.php to edit; WordPress core generates the
opening tag itself and already calls language_attributes(). So if a block theme is
missing the attribute, the cause is almost always something rewriting the tag
afterwards: an HTML minifier, a page-speed or caching plugin, or a hybrid theme with
its own index.php. Find the culprit before adding markup on top of it.
If you cannot edit the template, filter the output instead. Put this in a small
site-specific plugin (preferable, because it survives theme changes) or in a child
theme’s functions.php:
add_filter(
'language_attributes',
function ( $output ) {
if ( false === strpos( $output, 'lang=' ) ) {
$output = trim( $output . ' lang="' . esc_attr( get_bloginfo( 'language' ) ) . '"' );
}
return $output;
}
);
Use a real language tag. en, en-GB, fr, fr-FR, de-DE. Keep it short
unless the region genuinely matters. And make it true: a French shop declaring en
still fails 3.1.1, because the declaration and the words disagree.
Multilingual shops. With WPML, Polylang or TranslatePress, each translated URL
should declare its own language — /fr/panier/ wants lang="fr-FR", not the
default. Test one URL per language.
Passages in another language get their own attribute on the element that wraps them:
<p>Our best seller is the <span lang="de">Handkäse mit Musik</span> gift set.</p>
That is criterion 3.1.2 Language of Parts, Level AA — a separate requirement. Klarvo detects and reports it; automation does not rewrite your product copy, so this one stays yours.
Then clear your caches. A page cache will happily keep serving the old opening tag for hours.
How you know it worked
Read the served HTML, not the editor. View source on the front end and check the
first two lines: the <html> tag should carry lang, and the value should match the
language of the words on the page. From a terminal, a plain request proves the same
thing more strictly:
curl -s YOUR_STORE_URL | head -3
If the attribute is present in the browser’s inspector but absent from that response, something client-side is adding it — which does not count. The declaration has to be in the HTML your server sends.
Check more than one URL. The home page, a product page, the basket and the checkout can come from different templates, and a page builder or a checkout block can emit its own wrapper. Add one URL per language if you are multilingual.
Then re-run whichever checker you use and confirm all three rules pass —
html-has-lang, html-lang-valid and valid-lang. Finally, spend thirty seconds
with a screen reader: turn on VoiceOver or NVDA, move to the “Add to basket” button
and listen. The voice should sound like your shop.
Sources: W3C WAI — Understanding SC 3.1.1: Language of Page · W3C WAI — Technique H57: Using the language attribute on the HTML element · W3C — Web Content Accessibility Guidelines (WCAG) 2.1 · Deque University — axe rule html-has-lang (axe-core 4.12) · WordPress — Settings General screen, including Site Language
FAQ
Questions this raises
My shop is in English and my customers are in the UK. Does this still matter?
Yes, because the declaration is for the visitor's software, not for you. A shopper's screen reader may be set to Polish, French or Urdu at home and still be used to buy from you; without a declaration it reads your English checkout with that voice. It is also a Level A failure, the lowest bar in WCAG, and one an automated check finds in seconds.
I use a multilingual plugin. Won't one lang attribute be wrong on my translated pages?
It would be, which is why the value comes from the locale WordPress reports for the page being served rather than a fixed string. A translated URL that WordPress serves in French reports French. The thing to verify is that each translated URL declares its own language — check one page per language, not one page per shop.
Klarvo reports this as fixed. Does that make my store compliant?
It makes this criterion pass, with the before and after markup and an independent re-check recorded as evidence. That is one criterion of fifty-five. An automated finding is evidence, not a certificate: conformance across a whole store is certified only by the professional audit, at a stated scope and date.
Find out whether your store has this one.
The free scan checks your real pages — home, product, populated basket, hydrated checkout — with all three engines and all nine probes, and tells you exactly what it found and where.