the host voice roster represents the language the browser claims
Check id locale-vs-voice-coherence
What an ordinary browser yields
The primary language subtag of navigator.languages[0] appears among the lang values of speechSynthesis.getVoices(). The roster carrying many additional locales is normal and is not asserted against.
What a detector infers
The voice roster is installed at the operating-system level and the language preference is set in the browser, so the two are genuinely independent sources for one claim — which is what makes a persona announcing de-DE while the host speaks only English a contradiction it did not have to make. Getting the direction right is the whole check, and measuring is what settled it. On the stock Windows 11 machine this was written against, navigator.languages was ['en-GB','en-NL'] while the host roster held six voices spanning exactly en-GB and en-US. Two obvious versions of this check die on that single reading. Comparing full locale tags would have failed an entirely ordinary browser, because en-NL is a real declared preference with no voice behind it and never will have one. And requiring the roster to be LIMITED to the claimed locale is wrong in the other direction, since a macOS install ships dozens of locales nobody in front of it speaks. What survives is one claim: the primary language subtag the browser leads with should be represented SOMEWHERE in the roster. Three conditions make it decline rather than guess. An empty roster is nothing to compare against — the same condition the delivery-path check declines on, so the two go quiet together by design. An unreadable primary tag is no claim to test. And when navigator.languages and Intl disagree about the primary language, the claim is not settled between the browser's own two accounts of it, which languages-coherence and intl-locale-coherence already adjudicate; holding a roster against an unsettled claim would convict one browser twice for one fault. The row is a warning rather than a contradiction for an honest reason worth stating: someone whose browser language differs from their operating system's installed language packs is a real person, and this check is deliberately weak enough to let them through.
How to resolve it
Present a locale together with the host that goes with it: install the matching OS speech voice, or present the locale the machine actually has. Changing navigator.languages alone leaves the operating system speaking a language the browser says nobody there uses — and the voice list is not reachable from the page, so it cannot be brought into line from JavaScript. This row is CONTEXTUAL and never counts against the score, and that calibration was measured rather than assumed: an unmodified Chrome 150 launched with --lang=de-DE on an English Windows install lands here exactly, claiming a primary tag of "de" against a roster spanning only "en". That is an ordinary configuration, because Windows installs text-to-speech packs separately from display languages and most machines carry only the one they shipped with — so a German speaker running German Chrome on English Windows produces the identical observable to a persona claiming a locale it cannot back. Nothing available inside the page separates those two, which is exactly the condition under which a check should report rather than convict.
Read in the wild by
CloakBrowser Pro releases
The vendor's Chromium 150 stable release names speech AND language profiles together, in one clause, as the thing brought into line with a real Chrome install — the pairing is the point, and it is the vendor's own wording rather than our inference.
Release "CloakBrowser Pro — Chromium 150.0.7871.114.3" (tag chromium-v150.0.7871.114.3-pro, published 18 Jul 2026): "Closer alignment with a real Chrome install across speech and language profiles, font handling, storage reporting, Windows system colors and selection rendering, and graphics parameters."
CloakHQ/CloakBrowser — chromium-v150.0.7871.114.3-proPerimeterX / HUMAN
A flag-gated routine cross-checks the voice roster against what the browser CLAIMS in its UA-CH hints — the same shape of comparison this check makes, on a different pair of fields. When the brand list says Google Chrome and the form factor says desktop but no voice name matches /google/i, it fires a hidden iframe at a second PerimeterX endpoint. That is a deployed collector treating a roster that disagrees with a claimed identity as worth escalating.
PerimeterX collector main.min.js (PX_RE corpus, stample/ifood/source/main.min.js), `Gw()` invoked flag-gated as `nf(mU[lh])&&Gw()`: `hU[hQ(191)]&&hU.userAgentData[hQ(1066)]&&hS[hQ(195)]&&hS[hQ(195)].getVoices&&hU.userAgentData[hQ(1066)]([hQ(1067),"formFactor",hQ(1068)]).then(...)` then `n[-34]=function(){...t(hS.speechSynthesis.getVoices().some((function(t){return new RegExp(hQ(1070),"i")[hQ(236)](t.name)})))...}` and `if(n[4]&&n[61]&&!t){var e=jb("aHR0cHM6Ly9jbGllbnQud3JhLWFwaS5uZXQ=")...i[hQ(1073)]=h,i.style.display="none"` — hQ(191)='userAgentData', hQ(1066)='getHighEntropyValues', hQ(1067)='brands', hQ(1069)='google chrome', hQ(1070)='google'.
github.com/warterbili/PerimeterX_REEvery attribution traces to a published artifact. See the sources and their limits.
Nearby checks in Navigator identity
- UA-CH brand version matches the UA Chrome version
uach-ua-versionThe check extracts the major from Chrome/(\d+) in the UA string, finds the Chrome or Chromium entry in navigator.userAgentData.brands, and… - UA-CH high-entropy hints cohere with the UA
uach-highentropyThis awaits navigator.userAgentData.getHighEntropyValues and compares the richer hints against the UA string on three axes, collecting every… - every brand in a UA-CH list uses the same version format
uach-brand-version-arityUA-CH publishes the same brand set twice at two precisions. navigator.userAgentData.brands carries bare majors like "148", and the… - navigator.language === languages[0]
languages-coherenceIn a real browser navigator.language is defined as the first element of navigator.languages — one preference list, two views onto it. - eval.toString().length matches the engine (V8 = 33)
etslThe string a JS engine returns for a native function's source is an implementation detail, and its length differs per engine… - navigator.productSub === 20030107 (Chromium)
product-subnavigator.productSub is a frozen legacy constant, not a live value: Chrome, Safari and Opera return "20030107" while Firefox returns… - a Firefox user-agent reports the Gecko productSub constant 20100101
gecko-productsub-hardcodenavigator.productSub is a constant compiled into the engine, and the two families disagree: every Gecko build hardcodes it to "20100101"… - navigator.deviceMemory is a power-of-two spec value
device-memory-boundsnavigator.deviceMemory does not report actual RAM — the spec requires the UA to quantise it to a coarse power-of-two rung before exposing…
Clearcote is a browser built for fingerprint coherence
It is a Chromium fork, maintained by the same people who wrote this reference. It ships as a compiled browser rather than as a stealth script injected into someone else's — which is a description of how it is built, and is not an argument about how it behaves on this check.
This audit takes no position on how Clearcote scores on Navigator identity checks, on this one, or anywhere else. It has no baseline corpus of other people's fingerprints to rank you against and no vendor scoreboard — nearly every check is self-referential, asking one browser the same question through two independent APIs and reporting whether both answers can be true at once. It runs identically on any browser, including ours. Run it on yours and read the result yourself.
See the other checks in Navigator identity — the family locale-vs-voice-coherence belongs to.