What 3.1.1 requires
The default human language of each web page can be programmatically determined.
With the right lang on the html element, screen readers load the right pronunciation rules, browsers display characters and scripts correctly, and media players show captions properly. With the wrong one, a screen reader reads a page in Spanish with English rules.
The default language is the one used most. Understanding 3.1.1 adds that when several languages are used equally, the first one used is the default. Passages in other languages are 3.1.2.
The value is a BCP 47 language tag, such as en, es or pt-BR. A common way to get it wrong is a page translated from a template that kept the template’s lang.
Common failures
Under each example: what axe-core and Rampa reported on the markup before the fix, in a run with Gemma 4 12B on 8 October 2026.
No lang at all
Without lang, a screen reader reads the page with whatever voice the device is set to, whatever language the text is in. Here rules decide: axe-core fails it, and Rampa reports that without asking a model.
Before, fails 3.1.1
<html>After, passes
<html lang="en">- axe-core
Fails:
html-has-lang.- Rampa
✗ html <html> element must have a lang attribute high · rule html-has-lang
A value that is not a language tag
lang takes a language tag such as en, not the name of a language. axe-core fails this too.
Before, fails 3.1.1
<html lang="english">After, passes
<html lang="en">- axe-core
Fails:
html-lang-valid.- Rampa
✗ html <html> element must have a valid value for the lang attribute high · rule html-lang-valid
A valid tag for the wrong language
The Spanish page was built from the English template and kept lang="en". The tag is valid, so every rule passes, and a screen reader reads Spanish with English pronunciation.
Before, fails 3.1.1
<html lang="en">
…
<h1>Horario de atención</h1>
<p>De lunes a viernes, de 7 a 18 h.
Los sábados, de 8 a 13 h.</p>After, passes
<html lang="es">- axe-core
Passes:
html-has-langandhtml-lang-validfind a valid tag.- Rampa
✗ html The page is marked lang="en", but most of its text is in Spanish (es). Evidence: "Horario de atención De lunes a viernes, de 7 a 18 h. Los sábados, de 8 a 13 h." Patch: - <html lang="en"> + <html lang="es"> confidence high · 1/1 runs · evidence verified · id 804b6c733e31
What axe-core checks
axe-core 4.14.0 maps three rules to 3.1.1: html-has-lang fails a page with no lang, html-lang-valid one whose lang is not a valid tag, and html-xml-lang-mismatch one whose lang and xml:lang disagree. Rampa reports their failures as they are, and a page that failed any of them never goes to the model.
| Rule | What it checks | Mapped to | In a Rampa run |
|---|---|---|---|
html-has-lang, rule page at Deque University | <html> element must have a lang attribute | 3.1.1 | runs |
html-lang-valid, rule page at Deque University | <html> element must have a valid value for the lang attribute | 3.1.1 | runs |
html-xml-lang-mismatch, rule page at Deque University | HTML elements with lang and xml:lang must have the same base language | 3.1.1 | runs |
None of them reads the text. lang="en" on a page written in Spanish passes all three.
Each rule links to its page at Deque University, which makes axe-core.
What Rampa judges, and how
What goes to the model
The page, once: when its root element declares a valid lang, none of the three rules failed, and at least 12 letters of text inherit that language.
What the model sees
- The declared language.
- The text that inherits it, in reading order, up to 1,500 characters. Passages that declare their own
langare left out, because they are 3.1.2.
What it must answer
A verdict, the language most of the text is in as a BCP 47 primary tag, a short excerpt copied from the text as evidence, and a confidence.
What drops a claim
- The excerpt is not in the page’s text, is shorter than two letters, or is longer than 400 characters.
- The detected language is not a valid BCP 47 tag.
- A fail detects the declared language, or a pass detects another one.
The patch
The root element’s lang becomes the detected primary tag, such as es.
For every criterion
- Page content reaches the model marked as data, and the prompt tells it to ignore any instruction inside.
- Answers are cached by a hash of the prompt, the image, the model and its settings, so the same input never calls a model twice.
--runs 3asks three times and keeps the majority; when runs disagree, confidence drops. Findings below--min-confidence(medium by default) are hidden, and--verboselists them.- Claims dropped by verification are counted in every report, never shown as findings.
How Rampa was measured on it
| Set | Tests | Cases | axe-core, precision / recall | Rampa, precision / recall / F1 |
|---|---|---|---|---|
| 3.1.1, page has a valid lang (ACT b5c3f8, bf051a) | syntax | 11 | 1.00 / 1.00 | 1.00 / 1.00 / 1.00 |
| 3.1.1, lang matches the page (ACT ucwvc8) | meaning | 14 | 0.00 / 0.00 | 0.50 / 1.00 / 0.67 |
Corrupted pairs: Rampa told 4 of 4 apart (page lang), axe-core 0. A pair is a passing test page and a copy broken on purpose; a checker that gives both the same verdict is not judging.
Run on 9 October 2026: axe-core 4.14.0, Gemma 4 12B on a local GPU, reasoning off, one run, ACT test cases a9a1483e. Small samples and one run: read the numbers as a working pipeline, not as a result.
Read with these caveats
- The judgment for 3.1.1 came with 2.4.2, 2.4.4 and 2.4.6 on 2026-10-08, and their prompts were tuned after reading their errors on these cases, so the numbers are optimistic until fresh pages confirm them.
- The low precision on ucwvc8 is scoring, not judgment. Every passed and failed example is right; the false positives are pages the rule calls inapplicable, four of which axe-core fails for a missing or invalid
lang.
Limitations
- Only a valid
langis judged. A missing or invalid one is axe-core’s finding. - The model reads up to 1,500 characters of the text that inherits the page language, so a page whose start differs from the rest can be misjudged.
- Pages with fewer than 12 letters of text are skipped.
- When two languages are used evenly, WCAG makes the first one used the default; the model may answer that it cannot tell instead.
- The patch proposes the primary tag only, such as
pt; add a region subtag yourself if you want one. - Only web pages: snapshots from apps are not judged for 3.1.1.
Questions about 3.1.1
Which language should lang name on a page with two languages?
The one used most. Understanding 3.1.1 says that when two are used equally, the first one used is the default. Passages in the other language get their own lang, which is 3.1.2.
Is lang="pt" enough, or does it need pt-BR?
3.1.1 asks that the language can be determined by software, and a primary tag such as pt does that. A region subtag such as pt-BR adds information. Rampa’s patch proposes only the primary tag.
Does axe-core check that lang matches the content?
No. html-has-lang checks that the attribute exists and html-lang-valid that its value is a valid tag. lang="en" on a page in Spanish passes both.
Check your pages
Rampa is not on npm yet. Clone it and run it from source with Node.js 22.12 or newer and Chrome or Edge. --criteria 3.1.1 judges only this criterion; leave it out to run the eight that run by default.
With Ollama running, Rampa picks a local model by itself; node dist/cli.mjs doctor says what is missing.
git clone https://github.com/guilhermebsantiago/rampa-cli.git
cd rampa-cli
pnpm install && pnpm build
node dist/cli.mjs check https://example.com --criteria 3.1.1Checked against rampa-cli (commit 0459809) and the W3C sources on 9 October 2026.