> ## Documentation Index
> Fetch the complete documentation index at: https://docs.37audits.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Localization Auditor

> Finds localized versions of a page that still show the default-language text, do not resolve, or declare the wrong language

<Info>
  **i18n / SEO Auditor** — fetches the localized versions a page publishes (`<link rel="alternate" hreflang>`, or language-switcher links when there are none) and reports the ones that still show the audited page's language, the ones that do not resolve, and the ones whose `<html lang>` contradicts the language promised for them.
</Info>

<CardGroup cols={2}>
  <Card title="Translation is not localization" icon="language">
    The Liferay 2026 Website Localization Report found that 56% of multilingual consumers walked away from a purchase because a site felt poorly adapted, and 22% had met buttons, menus or messages left in English.
  </Card>

  <Card title="Fallback hides in plain sight" icon="eye-slash">
    A CMS that cannot find a translation shows the default language instead, with HTTP 200 and the right `lang` attribute. Nothing breaks, so nobody notices until a customer does.
  </Card>
</CardGroup>

Localization is standards-based, so the auditor is generic: it reads hreflang alternates, `<html lang>` and switcher links, and compares text. It needs no platform in the software stack and runs from Quick Audit.

## How it works

1. **Fetch the audited page** with 37AuditsBot's user agent, Web Bot Auth signature and the URL's Basic Auth. HTTP 403, HTTP 429 or a Cloudflare challenge is `ERROR` (`590`); any other failure is `ERROR` (`500`).
2. **Discover the localized versions** — `<link rel="alternate" hreflang="…" href="…">` first; when the page publishes none, `<a hreflang href>` then `<a lang href>` switcher links. The page itself, `x-default` and versions in the page's own language (`en-GB` for an `en-US` page) are dropped, and at most `maxAlternates` (default 10) are kept in document order. No version left is `INFORMATION` (`100`): the run is not charged.
3. **Fetch every version in parallel** (five at a time, 40 s budget). A version that answers 403, 429, a Cloudflare challenge or a bot wall is `WARNING` (`302`); any other non-2xx answer or network error is `FAIL` (`401`).
4. **Compare the text** — the body text nodes of each page outside `script`, `style`, `noscript`, `template` and `svg`, whitespace-collapsed, kept when they have at least 3 words containing a letter (or 6 letters in a script without word spaces, such as Japanese). A version's block counts as untranslated when it appears verbatim on the audited page, or when it has at least 8 words and a two-language detector (Lingua, loaded for the audited page's language and the version's only) attributes it to the audited page's language by a clear margin. Detection is skipped, and the reason recorded in `data.languageDetection`, when either language has no model, the audited page declares no language, or the two pages share a language.
   Text the markup **hides at load time** is left out of the count first, unless `countHiddenText` is on: an ancestor with the `hidden` or `inert` attribute or `aria-hidden="true"`, an inline style with `display:none`, `visibility:hidden` or `opacity:0`, an `<option>`, a closed `<dialog>` or `<details>`, or a class such as `hidden`, `hide`, `d-none`, `invisible` or anything ending in `-hidden` (closed mega-menus, inactive tab panels, validation messages waiting for an error). Untranslated blocks found there are listed on the version's summary check in `data.hiddenBlocks` with the signal that hid them, so the percentage describes what a visitor sees while nothing is lost. Visually hidden text (`sr-only`, `visually-hidden`, `screen-reader-text`) stays counted: screen readers read it, so an untranslated skip link is a real defect. Text hidden by a stylesheet rule alone cannot be told apart from the markup and stays counted.
   Two further kinds of block are then left out of the count and recorded apart on the version's summary check: blocks that match the **ignored terms** (`data.ignoredBlocks`: the `ignoredTerms` preference plus the site's own name from its host, so "Liferay OEM Partner" or "Liferay vs. Adobe" with both names listed) and blocks that **look like a name** (`data.nameLikeBlocks`: at most five words besides connectors such as "vs." or "&", every one capitalised or an acronym, none a function or action word such as "a", "to", "you" or "apply", and no sentence punctuation at the end). "Digital Asset Management", "Gartner Peer Insights™", "IM Project Manager" and "Diamond Bar, CA 91765" are names; "Create An Account", "Request a demo", "Skip to Main Content" and "All Rights Reserved." are copy and stay counted.
5. **Judge each version** — `untranslated / blocks` at or above `untranslatedFailPercent` (default 50) is `FAIL` (`400`); at or above `untranslatedWarningPercent` (default 5) is `WARNING` (`300`). Every version also gets one check per distinct untranslated block, in document order and capped at 50 per version, so the list of strings to translate is always in the result: `WARNING` (`303`) when the version reached the warning threshold, `INFORMATION` (`101`) when it stays under it. A version whose `<html lang>` primary subtag differs from the hreflang that points at it is `FAIL` (`402`); a page without `<html lang>` is `WARNING` (`301`).
6. **Aggregate** — ERROR, then FAIL (`400`, `401`, `402`), then WARNING, else `SUCCESS` (`200`).

Up to five untranslated blocks are kept per version in `data.samples`, sentence-like copy first (ending punctuation, or mostly lower-case words) and Title Case labels last, so "Please enter a valid email address." is quoted before a product name.

## What it audits

Every row maps to one `checkRule*` method and one `CheckCode`. CheckCode suffixes use status ranges: `100` information, `200` success, `300–399` warning, `400–499` fail, `500–599` error.

| CheckCode | Status | When it fires | Recommendation | Why it matters |
| - | - | - | - | - |
| <a id="37A-LocalizationAuditor-100" />`37A-LocalizationAuditor-100` | `INFORMATION` | The page publishes no hreflang alternate and no language-switcher link to another language; nothing is compared and the run is not charged (the check carries `notApplicable: true`). | If the site exists in other languages, add `<link rel="alternate" hreflang="…">` for each version to `"{url}"` so visitors and search engines find them and this audit can compare them. | — |
| <a id="37A-LocalizationAuditor-101" />`37A-LocalizationAuditor-101` | `INFORMATION` | One check per distinct text block still in the audited page's language on a localized version that stays under the warning threshold (at most 50 per version, in document order). The version passes; the check lists what is left to translate. | Translate `"{text}"` into `{hreflang}` on `"{variantUrl}"`. | — |
| <a id="37A-LocalizationAuditor-300" />`37A-LocalizationAuditor-300` | `WARNING` | A localized version keeps between `untranslatedWarningPercent` (inclusive) and `untranslatedFailPercent` (exclusive) of its text blocks in the audited page's language. | Translate the remaining `{baseLang}` strings on `"{variantUrl}"` (menus, buttons, footer, form messages), starting with `"{sample}", "{sample}", "{sample}"`; if the `{hreflang}` version is not ready, remove it from the published alternates until it is. | Visitors who pick their language and still meet untranslated menus, buttons or messages read the site as careless; the Liferay 2026 Website Localization Report found 56% of multilingual consumers walked away from a purchase over a poorly adapted site. |
| <a id="37A-LocalizationAuditor-301" />`37A-LocalizationAuditor-301` | `WARNING` | The audited page, or a fetched localized version, has no `lang` attribute on `<html>` (or an empty one). | Set `<html lang="{hreflang}">` on `"{pageUrl}"`: the hreflang that points at it promises `{hreflang}`. For the audited page: set `<html lang="…">` to the language the page is written in. | Without a declared language, screen readers mispronounce the page and search engines may file it under the wrong language, so the translation reaches fewer of the people it was made for. |
| <a id="37A-LocalizationAuditor-302" />`37A-LocalizationAuditor-302` | `WARNING` | A localized version answered HTTP 403, HTTP 429, a Cloudflare challenge or a bot wall, so it could not be compared. | Allow 37AuditsBot on `{host}` (allowlist its user agent or verify its Web Bot Auth signature, see [https://www.37audits.com/bot](https://www.37audits.com/bot)) so the `{hreflang}` version can be audited. | A locale the auditor cannot read is a locale nobody is checking; problems on it stay invisible until a customer finds them. |
| <a id="37A-LocalizationAuditor-303" />`37A-LocalizationAuditor-303` | `WARNING` | One check per distinct text block still in the audited page's language on a localized version that reached the warning threshold (at most 50 per version, in document order). | Translate `"{text}"` into `{hreflang}` on `"{variantUrl}"`. | Each untranslated menu item, button or message is a moment where a visitor who asked for their language is answered in another one; listing them one by one is what lets a translator fix them. |
| <a id="37A-LocalizationAuditor-400" />`37A-LocalizationAuditor-400` | `FAIL` | A localized version keeps at least `untranslatedFailPercent` of its text blocks in the audited page's language. | Translate `"{variantUrl}"` into `{hreflang}` before offering it as the `{hreflang}` version, or remove that alternate until the translation exists; today visitors who choose `{hreflang}` read `{baseLang}` content such as `"{sample}", "{sample}", "{sample}"`. | A language option that opens a page still written in the default language tells the visitor the brand did not bother; most leave, and the Liferay 2026 Website Localization Report found 32% lose trust in the brand itself. |
| <a id="37A-LocalizationAuditor-401" />`37A-LocalizationAuditor-401` | `FAIL` | A localized version could not be fetched: a network error, a timeout or an HTTP status other than 2xx that is not a block. | Fix or remove the `{source}` link to `"{variantUrl}"`: a visitor choosing `{hreflang}` currently gets `{HTTP 404 / error}` instead of the page. | A language link that leads to an error page loses the visitor at the moment they asked to be served in their language, and search engines drop broken alternates from results. |
| <a id="37A-LocalizationAuditor-402" />`37A-LocalizationAuditor-402` | `FAIL` | A localized version declares a primary language on `<html lang>` other than the one the hreflang (or switcher link) promised for it. | Make `"{variantUrl}"` declare `lang="{hreflang}"`, or point the `{hreflang}` alternate at the page that really serves that language instead of one declaring `{declaredLang}`. | When the promised and declared languages disagree, search engines show the wrong page to the wrong audience and screen readers read it in the wrong voice. |
| <a id="37A-LocalizationAuditor-500" />`37A-LocalizationAuditor-500` | `ERROR` | The audited page could not be fetched: network error, timeout or a non-2xx status that is not a block. | Make `"{url}"` reachable over HTTP(S) and re-run the audit. Last error was `"{error}"`. | — |
| <a id="37A-LocalizationAuditor-200" />`37A-LocalizationAuditor-200` | `SUCCESS` | All rules passed. Message is `"{url}" ({lang}) and its {n} localized versions ({hreflangs}) each keep text in {lang} under {warningPercent}% of their text blocks (highest: {hreflang} at {percent}%).` `data.variantSummary` lists every version with its `percent`, `untranslatedCount`, `nameLikeCount` and `ignoredCount`. | — | — |
| <a id="37A-LocalizationAuditor-590" />`37A-LocalizationAuditor-590` | `ERROR` | The audited page answered HTTP 403, HTTP 429 or a Cloudflare challenge, so 37AuditsBot was blocked before it could read the page. | Allow 37AuditsBot in your firewall or bot protection: allowlist its user agent, or verify its Web Bot Auth signature. See [https://www.37audits.com/bot](https://www.37audits.com/bot). | While 37AuditsBot is blocked this audit cannot look at the site, so problems on it go unreported. |

Example messages, measured on `https://www.liferay.com/` and `https://liferay.dev/` on 2026-10-18:

The result message (also the message of the `200` check) is a summary: the audited URL and its default language, how many localized versions were found, and the verdict in figures for the versions concerned. Check messages carry the detail.

* result, pass — `"https://www.liferay.com/" has its default language set to en-US. 6 localized versions (es-ES, it-IT, fr-FR, pt-BR, de-DE, ja-JP) were found; all of them keep untranslated text blocks under 5% (highest: de-DE at 4%).`

* result, untranslated — `"https://petrobras.com.br/" has its default language set to pt-BR. 1 localized version (en-US) was found; it shows untranslated text blocks at 36% (48 out of 135).`

* `300` — `"https://www.liferay.com/de/" (de-DE) still shows 36 of its 321 text blocks (11%) in en-US: 35 copied verbatim from "https://www.liferay.com/" and 1 recognised by language detection, e.g. "Request a demo", "by 233 verified users as of September 2026, Source:", "Please enter a valid email address.".`

* `400` — `"https://dev.liferay.com/es/" (es-ES) is mostly the en-US page: 37 of its 41 text blocks (90%) are still in en-US, 37 copied verbatim from "https://liferay.dev/" and 0 recognised by language detection, e.g. "Explore the Liferay Community Universe.", "The Liferay Community has been integral to our journey.", "Open Source. For Life.".`

* `303` — `"Please enter a valid email address." is still in en-US on "https://www.liferay.com/de/" (de-DE): it appears verbatim on "https://www.liferay.com/".`

* `101` — `"Create An Account" is still in en-US on "https://www.liferay.com/es/" (es-ES): it appears verbatim on "https://www.liferay.com/". The version stays under the 5% warning threshold.`

* `100` — `"https://www.37audits.com/" declares lang="en" and publishes no hreflang alternates or language-switcher links, so there is no localized version to compare.`

Findings on a version carry `variantUrl`, `hreflang`, `source` (`hreflang` or `switcher`), `statusCode`, `error`, `blocked`, `declaredLang`, `baseUrl`, `baseLang`, `totalBlocks`, `identicalCount`, `detectedCount`, `untranslatedCount`, `distinctUntranslated`, `percent`, `warningPercent`, `failPercent`, `samples`, `nameLikeCount`, `nameLikeBlocks`, `ignoredCount`, `ignoredBlocks`, `hiddenCount`, `hiddenBlocks` (text → hiding signal, such as `hidden`, `aria-hidden`, `style`, `<details>` or `class:atb-tab-panel-hidden`) and `languageDetection` in `data`. A `303` or `101` finding carries `variantUrl`, `hreflang`, `baseUrl`, `baseLang`, `text`, `reason` (`copied` or `detected`), `occurrences`, `xpath` (the block's first occurrence on the version, browser style: `/html/body/div[2]/footer/span[1]`, with `/text()[n]` when the element mixes several text nodes), `listed` and `distinctUntranslated`.

## Preferences

Set per monitored URL; the defaults apply to Quick Audit.

| Key | Type | Default | Range | Effect |
| - | - | - | - | - |
| `untranslatedWarningPercent` | integer | `5` | 1–100 | Share of a version's text blocks still in the audited page's language from which `300` fires and the per-block list switches from `INFORMATION` (`101`) to `WARNING` (`303`). Brand and product names that the name heuristic misses count too, so a site with many of them may raise this. |
| `untranslatedFailPercent` | integer | `50` | 1–100 | Share from which `400` fires instead. A value below the warning threshold makes both equal. |
| `maxAlternates` | integer | `10` | 1–20 | How many localized versions are fetched, in document order. Each one is a request against the audited site. |
| `ignoredTerms` | string | — | — | Product, company and people names allowed to stay untranslated, one per line or separated by semicolons. A block made only of these terms (plus connectors, numbers and punctuation) is left out of the count and listed in `data.ignoredBlocks`. The site's own name from its host is always ignored. |
| `countHiddenText` | boolean | `false` | — | Count text the markup hides at load time (closed menus, inactive tabs, validation messages) like visible text. Off, such blocks are left out of the percentage and listed in `data.hiddenBlocks` instead. Turn it on when menus and messages matter as much as the visible page. |

## Output documentation

| Status | Description | Test logic |
| - | - | - |
| **INFORMATION** | The page has no localized version to compare, or a passing version still has a few untranslated blocks | No hreflang alternate or switcher link to another language (`100`; the run is not charged), or one check per untranslated block on a version under the warning threshold (`101`; the response stays `SUCCESS`). |
| **SUCCESS** | Every localized version is translated and reachable | Each version answered 2xx, declares the promised language, and keeps untranslated text under `untranslatedWarningPercent` (`200`). |
| **WARNING** | Some text is untranslated, a page declares no language, or a version could not be verified | Untranslated share between the thresholds (`300`) with one check per untranslated block (`303`), missing `<html lang>` (`301`), or a version that blocked the bot (`302`). |
| **FAIL** | A version is mostly untranslated, broken, or in the wrong language | Untranslated share at or above `untranslatedFailPercent` (`400`), a version that does not resolve (`401`), or a declared language that contradicts the hreflang (`402`). |
| **ERROR** | The auditor could not compare anything | The audited page could not be fetched (`500`) or 37AuditsBot was blocked (`590`). |

## Risks and considerations

### What untranslated text costs

* A visitor who switched to their language and meets English buttons, validation messages or a whole English page reads it as a signal about the brand, not a technical glitch. The Liferay 2026 Website Localization Report puts the cost at lost purchases for 56% of multilingual consumers and lost brand trust for 32%.
* Search engines index each hreflang version for its audience. A Spanish URL full of English text ranks for neither language well.

### Names are set aside, not everything is a name

* A Title Case block without a function or action word is treated as a name and left out of the count, so a forgotten button written that way ("Download Brochure") is missed; one with such a word ("Create An Account", "Apply Thank You") is counted. Review `data.nameLikeBlocks` on the summary check when a locale passes unexpectedly, and add genuine names to `ignoredTerms` when they slip through (a job title containing "and" or "for", a tagline).
* On liferay.com the heuristic removes 29 of the 51 distinct texts reported on 2026-10-18, leaving the UI strings, the footer and the taglines, and every locale drops from 7–11% to about 3%.
* Language detection is deliberately conservative: it runs only on blocks of eight words or more and needs a clear margin, because short marketing copy mixing brand names ("Explore o Headless CMS") is otherwise attributed to English.

### What this auditor does not judge

* Tone, idiom, cultural fit, machine-translation quality, and date, currency or address formats: the article's remaining findings need a fluent reader.
* Only the audited URL and its own localized versions are compared; other pages of the site may differ.
* Versions in the audited page's own language (`en-GB` for an `en-US` page) are skipped, since their text is expected to match.
* The language detector covers 75 languages; a version in a language without a model is scored by the verbatim comparison alone, which `data.languageDetection` says.

### Cost and load

* One audit fetches the page plus up to `maxAlternates` versions, five at a time, so `cost` is 2. Lower `maxAlternates` on sites with many locales if the load matters.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.