Your Analytics Reports a Country. Your Visitor Reads a Language.
Every analytics dashboard can tell you a session came from Germany, or Texas, or São Paulo. Almost none can tell you what language the person behind that session actually reads — and country is a bad proxy. A Spanish-speaking household in Houston, a Portuguese-speaking engineer in Berlin, and a French-speaking buyer in Montreal all collapse into "US," "DE," and "CA" rows that look exactly like everyone else's.
Meanwhile the strongest language signal a visitor can send happens right in front of your site and never reaches your reports: they translate the page. Chrome and Edge ship built-in translation; the Google Translate widget persists across pages. When a visitor invokes it, they are telling you, unambiguously, "I want to read this in another language — badly enough to do it myself." Standard analytics tools don't watch for it, so the signal evaporates.
ClickStream treats that moment as first-class data. This post covers how translation detection works, how each visitor resolves to one primary language, what language-geo mismatch analysis surfaces, and what marketers should actually do with hidden-demand data.
Translation Is an Action, Not a Preference
The language signals most tools rely on — the Accept-Language header, navigator.language — are defaults. Somebody set up the operating system once, possibly years ago, possibly in an office, possibly not the person browsing now. They're useful, but they're passive.
Translating a page is different. The visitor hit a wall in your language and did work to get past it. That's why ClickStream's tracker source describes an active translation as the strongest native-language signal it can observe: the visitor acted to read that language, which is far stronger evidence than any header.
How Detection Works
When Chrome or Edge's built-in translate — or the Google Translate widget — translates a page, it rewrites the document's lang attribute to the target language. The Google widget additionally stamps translated-ltr or translated-rtl classes on the root element. ClickStream's SDK watches for exactly these changes with a single attribute-scoped MutationObserver on <html> that fires only when lang or class changes — near-zero overhead by design.
The detector distinguishes three cases:
- Translate engine confirmed — the Google Translate marker classes are present, so an actual translation engine did the work.
- Language switch — a bare
langchange with no engine markers. That's Chrome/Edge native translate or your own i18n switcher — and either way, the visitor chose to render the page in that language, so it still counts as a valid language signal (just attributed to a different method). - Reverted — the page returned to its original language.
Edge cases are handled deliberately. If the page was already translated when the SDK loaded — the Google widget persists translation across page loads — an initial class check catches it. And the event stream stays quiet: the detector emits exactly one translation_summary event per state change, so a page translated once yields one event, not a firehose. The current translation target also rides every subsequent event in the session, which is what lets downstream analytics segment everything else — pages, conversions, behavior — by the language the visitor was actually reading.
One Primary Language per Visitor
Raw locale signals are messy: a browser reports a preference list, a language tag, maybe a translation target. ClickStream resolves them into a single primary language per visitor through a fixed hierarchy in which the language the visitor acted on always outranks the language they merely prefer:
translatedTo— the language they actively translated the page into. If they translated, that is their language.languages[0]— the top entry of the browser's ordered preference list.language— the rawnavigator.languagetag.
Regional variants normalize to the base tag — es-MX and es-ES both count as es — so your Spanish-language demand doesn't fragment across regions.
Just as important is what ClickStream refuses to infer. Every locale field is nullable, and the system prefers silence over a guess: an unmapped timezone, a language-only tag, or an unknown country all resolve to null — never a fabricated verdict. When you see a mismatch in the dashboard, it's because the data supports one.
Language-Geo Mismatch: Demand That Doesn't Match the Map
The resolved primary language is cross-referenced against IP geography, producing a three-state verdict: true (the visitor's primary language is not one their IP-geo country expects — the strategic signal), false (consistent), or null (unknown). A Spanish-primary visitor browsing from a country where Spanish isn't expected is the canonical example — and directly actionable for targeting.
Three companion signals round out the locale picture:
- Timezone-geo mismatch — the device's IANA timezone disagrees with the IP-geo country, corroborating a traveler or a VPN. Same three-state discipline.
- Active hours — an hour-of-day histogram of when this audience is actually on your site (stored UTC, shifted for display).
- Locale formats — details like
hourCycle(12- vs 24-hour clock) carried on the locale struct.
Hidden Demand in the Dashboard
The dashboard aggregates these per-event locale signals into three group-level views:
Languages shows visitor share per primary language, using the same translated-first hierarchy per row. Rows without locale data count as unknown and are excluded from percentages — so the denominators stay honest instead of quietly inflating your dominant language.
Hidden demand is the view this post is named for: the languages visitors actively translated your pages into. Because each of those visitors took an action, this is the strongest signal available that a language market is underserved by your site. It isn't "people who might prefer German" — it's people who wanted your content in German enough to run the translation themselves.
Active hours shows when each audience is actually present, which matters the moment you start acting on the other two.
Both language charts click through: selecting a language opens the People view filtered to exactly those visitors, and each person's language shows as a chip in the list. Hidden demand isn't a summary statistic — it's a segment you can open, inspect, and act on.
A Town-Level Map That Never Calls a Tile CDN
Geography gets the same first-party treatment. ClickStream's primary geo view is a town-level map, and it is self-hosted end to end: the basemap is a Protomaps PMTiles archive served from ClickStream's own infrastructure via HTTP range requests, with the map fonts mirrored alongside it. No third-party tile CDN is in the loop.
That's a privacy property, not just an architecture choice. Most analytics geo maps are drawn on top of an external tile service, which means a mapping provider observes every pan and zoom your team makes while exploring visitor geography. Here, your dashboard's map traffic never leaves ClickStream's infrastructure.
The dots themselves are clustered visitor aggregates placed by IP-geolocation town and city centroids — never GPS coordinates. And like the language charts, the map clicks through: select a place and you get the People there.
Put the two together and hidden demand gains a where. The languages people translate your pages into tell you which market is underserved; the town map tells you whether that market clusters somewhere specific enough to act on.
Reading It in Page Code
The resolved locale also rides each visitor's Signals snapshot — the same snapshot that carries behavioral scores like visitor.scores.intent — as an optional, nullable locale object: primaryLanguage, translatedTo, languageGeoMismatch, timezoneGeoMismatch, timezone, country, hourCycle, and the raw language list. The @clickstreamhq/signals package is currently a developer preview (0.1.0-alpha), so expect the surface to evolve; the read pattern looks like this:
import { configure, getVisitorOrNull } from '@clickstreamhq/signals';
// configure() must run before any read — getVisitor() throws
// SignalsNotConfiguredError without it.
configure({ apiKey: 'cs_live_...' });
const visitor = await getVisitorOrNull();
if (visitor?.locale?.primaryLanguage === 'es' && visitor.locale.languageGeoMismatch) {
// Spanish-primary visitor somewhere your geography doesn't expect Spanish —
// surface the Spanish entry point.
showSpanishEntryPoint();
}
You can also gate on locale criteria with waitFor, which resolves when a matching snapshot arrives or times out:
import { configure, waitFor } from '@clickstreamhq/signals';
configure({ apiKey: 'cs_live_...' });
// Resolves with the visitor once both criteria match;
// rejects if they aren't met within 30 seconds.
const visitor = await waitFor(
{ primaryLanguage: 'es', languageGeoMismatch: true },
30_000
);
One honest caveat from the docs: locale is null until the first locale-carrying event refreshes the snapshot. Treat missing as unknown and keep your default experience — the same fail-quiet discipline the rest of the pipeline follows.
What Marketers Should Do with Hidden Demand
1. Rank Your Localization Backlog by Observed Behavior
Localization is expensive, and most teams prioritize it by guesswork — market-size reports, a founder's hunch, whichever regional office asks loudest. Hidden demand replaces the guess with observed behavior: translate the pages people are already translating. Machine translation got those visitors through this time, but it mangles nuance, navigation, and forms — and it tells you nothing about the visitors who bounced instead of translating. Suppose — hypothetically — your docs pages show steady translation into Portuguese while your pricing page draws translation into German. Those are two different localization projects, and now you have a data-backed order for them.
2. Treat Language-Geo Mismatch as a Market-Entry Signal
Mismatch analysis finds audiences your geographic reporting structurally hides: real demand inside geographies you thought you understood. Cross-reference with the town map to see whether that audience clusters — a language community concentrated in a handful of metro areas is a very different (and much cheaper) opportunity than one spread thin across a continent. Click through to the People behind the numbers before you commit budget.
3. Build Language-Aware Experiences Before You Localize
Full localization takes quarters. A language-specific entry point takes a sprint: a banner offering existing Spanish-language material, a localized signup flow, or routing to a native-speaking sales rep, gated on primaryLanguage and languageGeoMismatch exactly as in the code above. You can serve the demand you've discovered while the bigger localization effort catches up.
4. Time It with Active Hours
Each language segment carries its own hour-of-day activity histogram. When you launch that Portuguese landing path or a campaign aimed at a mismatch segment, schedule sends and budget around when that audience is actually on your site — not around your own office hours.
The Bottom Line
- Translation is the strongest language signal there is — the visitor acted, rather than inheriting a browser default.
- ClickStream detects it with a single attribute-scoped observer on the document's
langattribute and translate-engine markers, emitting one summary event per state change. - Each visitor resolves to one primary language — translated-to first, then browser preferences — with
null, never a guess, when the data is incomplete. - Language-geo mismatch finds demand your country reports hide, and the self-hosted, PMTiles-based town map — no third-party tile CDN — shows where it lives.
- Hidden demand converts to action: a localization backlog ranked by observed behavior, market-entry signals you can inspect person by person, and language-aware experiences you can ship in a sprint.
A visitor who translates your page just did the work your site should have done for them. Hidden demand is the report of everyone who hit that wall — and stayed anyway.