What 3.1.2 requires

The human language of each passage or phrase in the content can be programmatically determined except for proper names, technical terms, words of indeterminate language, and words or phrases that have become part of the vernacular of the immediately surrounding text.

Success criterion 3.1.2 Language of Parts (Level AA), quoted from Web Content Accessibility Guidelines (WCAG) 2.1, W3C Recommendation 06 May 2025. Copyright © 2020-2025 World Wide Web Consortium. Used under the W3C Document License.

A quote, a review or a name of a language in another language needs its own lang, so speech synthesizers pronounce it with the right rules and braille software translates it correctly. A single word can count when a change of language is intended.

The exceptions keep this practical: proper names, technical terms, words of indeterminate language, and words that became part of the surrounding language. Understanding 3.1.2 gives “rendezvous” in English and “podcast” in French as examples.

A wrong lang is a problem of its own: the screen reader switches to a voice that reads the passage with another language’s rules.

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.

A passage marked with the wrong language

A review in German was marked as French. A screen reader switches to French and reads German words with French rules. The tag is valid, so the rules pass.

Before, fails 3.1.2

<blockquote lang="fr">
  <p>Die Lieferung kam einen Tag früher
     als angekündigt.</p>
</blockquote>

After, passes

<blockquote lang="de">
  …
</blockquote>
axe-core

Passes: valid-lang finds a valid tag.

Rampa
✗ html > body > main > blockquote
  Marked as lang="fr", but the text is in German (de).
  Evidence: "Die Lieferung kam einen Tag früher als angekündigt."
  Patch:
    - <blockquote lang="fr">
    + <blockquote lang="de">
  confidence high · 1/1 runs · evidence verified · id d0eb6daacd91

A phrase in another language with no lang

The motto is Portuguese on an English page, with no lang on it, so it is read with English rules. Neither tool notices: axe-core has no tag to check, and Rampa judges only elements that declare a language.

Before, fails 3.1.2

<p>Our founder’s motto:
  <q>Feito à mão, devagar e com cuidado.</q></p>

After, passes

<p>Our founder’s motto:
  <q lang="pt">Feito à mão, devagar e com cuidado.</q></p>
axe-core

Passes: there is no lang to check.

Rampa

Rampa reported nothing here. Finding passages that lack a lang is not built yet.

A tag with the wrong separator

Language tags use a hyphen: pt-BR, not pt_BR. axe-core fails the invalid tag, and Rampa reports that without asking a model.

Before, fails 3.1.2

<p lang="pt_BR">Entregamos em todo o Brasil.</p>

After, passes

<p lang="pt-BR">Entregamos em todo o Brasil.</p>
axe-core

Fails: valid-lang.

Rampa
✗ html > body > main > p:nth-of-type(2)
  lang attribute must have a valid value
  high · rule valid-lang

What axe-core checks

axe-core 4.14.0 maps one rule to 3.1.2: valid-lang, which fails a lang value that is not a valid language tag. Rampa reports its failures as they are, and an element it failed never goes to the model.

axe-core 4.14.0 rules for 3.1.2
RuleWhat it checksMapped toIn a Rampa run
valid-lang, rule page at Deque Universitylang attribute must have a valid value3.1.2runs

The rule does not read the text. A German review marked lang="fr" passes, and so does a passage with no lang at all.

Each rule links to its page at Deque University, which makes axe-core.

What Rampa judges, and how

What goes to the model

Every element other than the root that declares a valid lang, is not hidden, was not failed by valid-lang, and has at least four letters of text that inherit that language.

What the model sees

  • The declared language, and the language inherited from around the element.
  • The element’s markup, up to 800 characters.
  • The text that inherits the declared language, in reading order, up to 1,200 characters, with the names of images from alt and aria-labelledby.

What it must answer

A verdict, the language of the text as a BCP 47 primary tag, a short excerpt copied as evidence, which 3.1.2 exception applies (a proper name, a technical term, indeterminate words, the surrounding vernacular, or none), and a confidence.

What drops a claim

  • The excerpt is not in the element’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 claims that an exception applies.
  • A pass detects another language without claiming an exception.

The patch

The element’s lang becomes the detected primary tag, such as de.

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 3 asks three times and keeps the majority; when runs disagree, confidence drops. Findings below --min-confidence (medium by default) are hidden, and --verbose lists them.
  • Claims dropped by verification are counted in every report, never shown as findings.

How Rampa was measured on it

W3C ACT test cases for 3.1.2: precision, recall and F1, Gemma 4 12B on a local GPU
SetTestsCasesaxe-core, precision / recallRampa, precision / recall / F1
3.1.2, valid lang on parts (ACT de46e4)syntax191.00 / 1.001.00 / 1.00 / 1.00
3.1.2, lang matches the text (ACT off6ek)meaning13not defined, nothing flagged / 0.000.80 / 1.00 / 0.89

Corrupted pairs: Rampa told 4 of 5 apart (lang on parts), 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 first run, on 2026-10-07, had recall 0.75 on off6ek. The misses never reached a model, because Rampa did not count image names from aria-labelledby as text; with that fixed, recall is 1.00.
  • The one pair Rampa did not tell apart is an intact page it also fails: “Paul put dire comment on tape”, a sentence that is English and French at once. On 1.1.1 and 3.1.2, the model errors of both Gemma and openai:gpt-6-luna fell on that sentence.
  • On 1.1.1 and 3.1.2, openai:gpt-6-luna got the same numbers as Gemma in two runs with fresh calls, at about US$ 0.005 per run.

Method and error analysis in the README

Limitations

  • Only elements that declare a lang are judged. A passage in another language with no lang at all is not found, by Rampa or by axe-core.
  • Text shorter than four letters is skipped, and so are hidden elements.
  • A sentence that mixes two languages is hard for a model, as the caveats above show.
  • The patch proposes the primary tag only, such as de.
  • Only web pages: snapshots from apps are not judged for 3.1.2.

Questions about 3.1.2

Do proper names and loanwords need lang?

No. 3.1.2 exempts proper names, technical terms, words of indeterminate language and words that became part of the surrounding language, such as “rendezvous” in English. Rampa asks the model which exception applies, and a pass that rests on an exception is allowed.

Does Rampa find foreign passages that have no lang?

Not yet. It judges only elements that declare a language, so a quote in another language without a lang attribute goes unnoticed, as it does in axe-core.

Does axe-core check language of parts?

It checks that each lang value is a valid tag (valid-lang). A valid tag on text in another language passes.

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.2 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.2

Checked against rampa-cli (commit 0459809) and the W3C sources on 9 October 2026.