Page language must be declared and match the content

Screen readers use the declared language to choose the pronunciation. A German shop that declares English, which is common when every translation keeps the default language, is read out with the wrong accent, so product names, prices and instructions become hard to understand. Browser translation and spell-checking depend on it too.

Rule id
vs-lang
Source
Validiscan check
Impact
Moderate
Fix tier
AUTO
on Shopify, Shopware, WooCommerce and Magento
Status
runs

What the check does

Flags a missing or invalid lang attribute on the <html> element and a mismatch between the declared language and the language detected in the page content (3.1.1). Supersedes the axe rules html-has-lang and html-lang-valid.

The check reads only the lang attribute of the <html> element. A value is valid when it is a well-formed BCP 47 tag whose primary subtag is a known two-letter ISO 639-1 code or has three letters. Private-use tags such as x-shop pass; en_US, english and xx do not.

For the comparison, the check takes the text of main or role="main" when it holds at least 300 characters, otherwise the whole body, and counts common function words for English, German, Danish, French, Spanish, Dutch, Italian, Swedish, Norwegian, Polish and Portuguese.

A mismatch is a finding only when the text has at least 300 characters and the leading language has at least 10 matches, at least 8% of all words, twice the declared language’s matches and at least 8 more. Weaker evidence does not produce a finding, and neither does a mismatch on a page declared in a language outside these eleven. lang on other elements is not checked.

How to fix it

Output the lang attribute on <html> from the current storefront locale, for example <html lang="de">, so every language version declares its own valid code (de, da, fr-BE and so on) instead of a hardcoded default.

WCAG success criteria

EN 301 549 clauses

The EN 301 549 clause of each criterion, by version of the standard
WCAG criterion Version 3.2.1 Version 4.1.1
3.1.1 9.3.1.1 9.3.1.1

Where a recipe fixes it

Shopify: recipe html-lang

Show the before and after (1)
Before and after Example data

layout/checkout.liquid Changes to layout/checkout.liquid: 1 added, 1 removed

Changes to layout/checkout.liquid: 1 added, 1 removed
Line before Line after Change Code
3 3 Checkout layout (the Shopify Plus `checkout.liquid` era). Only the first step
4 4 (contact, delivery, billing) is modelled; nothing is ever submitted.
5 5 {% endcomment %}
6 Removed <html class="no-js">
6 Added <html class="no-js" lang="{{ request.locale.iso_code }}">
7 7 <head>
8 8 <meta charset="utf-8">
9 9 <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">

layout/theme.liquid Changes to layout/theme.liquid: 3 added, 0 removed

Changes to layout/theme.liquid: 3 added, 0 removed
Line before Line after Change Code
6 6 {% endcomment %}
7 7 {%- liquid
8 8 assign page_lang = request.locale.iso_code
9 Added if page.metafields.custom.content_language != blank
10 Added assign page_lang = page.metafields.custom.content_language
11 Added endif
9 12 -%}
10 13 <html class="no-js" lang="{{ page_lang }}">
11 14 <head>

Shopware: recipe html-lang

Show the before and after (1)
normalises the locale code of the base layout; German content pages are left to the merchant Example data

Resources/views/storefront/base.html.twig Changes to Resources/views/storefront/base.html.twig: 1 added, 1 removed

Changes to Resources/views/storefront/base.html.twig: 1 added, 1 removed
Line before Line after Change Code
9 9 {% block base_doctype %}<!DOCTYPE html>{% endblock %}
10 10
11 11 {% block base_html %}
12 Removed <html lang="{{ page.lang }}" itemscope="itemscope" itemtype="https://schema.org/WebPage">
12 Added <html lang="{{ page.lang|replace({'_': '-'}) }}" itemscope="itemscope" itemtype="https://schema.org/WebPage">
13 13 {% endblock %}
14 14
15 15 {% block base_head %}

WooCommerce: recipe html-lang

Show the before and after (1)
adds language_attributes() to the checkout header of the fixture theme Example data

header-checkout.php Changes to header-checkout.php: 1 added, 1 removed

Changes to header-checkout.php: 1 added, 1 removed
Line before Line after Change Code
8 8
9 9 ?><!doctype html>
10 10 <?php // VALIDISCAN-SEED vs-lang: copied from a landing-page plugin, language_attributes() is missing, so <html> has no lang. ?>
11 Removed <html>
11 Added <html <?php language_attributes(); ?>>
12 12 <head>
13 13 <meta charset="<?php bloginfo( 'charset' ); ?>">
14 14 <?php // VALIDISCAN-SEED meta-viewport: zooming is disabled. ?>

Magento: recipe html-lang

Show the before and after (1)
fixes the vs-lang finding in root.phtml and validates Example data

app/design/frontend/Validiscan/fixture/Magento_Theme/templates/root.phtml Changes to app/design/frontend/Validiscan/fixture/Magento_Theme/templates/root.phtml: 1 added, 1 removed

Changes to app/design/frontend/Validiscan/fixture/Magento_Theme/templates/root.phtml: 1 added, 1 removed
Line before Line after Change Code
15 15 ?>
16 16 <!doctype html>
17 17 <!-- VALIDISCAN-SEEDED vs-lang: root.phtml prints the Magento locale code (en_US) instead of a BCP 47 language tag -->
18 Removed <html lang="<?= $block->escapeHtmlAttr($htmlLocale) ?>">
18 Added <html <?= /* @noEscape */ $htmlAttributes ?>>
19 19 <head <?= /* @noEscape */ $headAttributes ?>>
20 20 <?= /* @noEscape */ $requireJs ?>
21 21 <?= /* @noEscape */ $headContent ?>

Start with a free check of your shop.

Send your shop’s address. Validiscan scans it and replies with every failure the scan can prove, with the page and the code.

Request a free check

Or write to info@validiscan.com.

What the free check covers, and what it leaves out
You send
your shop’s address
Pages scanned
up to 15
You get
every failure the scan can prove, with the page and the code
On your shop
public pages only; no login, nothing bought, no forms sent
What it is not
a full audit

Write to Validiscan

For a free check, give the address of your shop and its platform.

Validiscan uses what you send to answer you. Privacy notice