Skip to main content
A persona’s languages and timezone are not drawn from the seed, because a drawn timezone would not match the proxy’s exit. The packages set them in this order:
  1. The locale and timezone options, when you pass them.
  2. A GeoIP lookup of the exit, when geoip is on, which is the default.
  3. en-US and the host’s timezone.
The packages pass the first two to the browser as --fingerprint-locale and --fingerprint-timezone. For the defaults they pass neither switch.

The GeoIP lookup

Before the browser starts, the package asks where the exit is. With a proxy, the exit is the proxy’s. Without one, it is the host’s own connection.
  • It asks ip-api.com, ipinfo.io, ipwho.is and ifconfig.co, in that order, over plain HTTP, through the proxy when there is one.
  • It tries each service twice, 5 seconds per try, and stops at the first answer that has both a country and a timezone.
  • The whole lookup may take 20 seconds. Change the limit with geoip_timeout in Python or geoipTimeoutMs in Node.
The timezone is the IANA name the service returns. The locale comes from the country code, through config/country-locales.json. The table covers 257 territories. It gives each the tag of its main official language in a form Chrome lists, and English where desktop installs are English, as in India and Pakistan. Some entries: GeoIP always gives one tag, never a list.

Set them yourself

Pass both options to make a launch independent of the lookup. The Python package then skips it. Behind a proxy, the Node package still runs it to learn the exit IP for WebRTC, unless args has --fingerprint-webrtc-ip (WebRTC).
With a proxy, pass values that match the exit. examples/python/locale_timezone.py and examples/node/locale-timezone.mjs run the same check.

One tag or a list

The first tag becomes the browser’s UI locale. What happens to the language list depends on the form you pass:
  • One tag, such as de-DE, sets only the UI locale. navigator.languages and the Accept-Language header are then Chrome’s own default list for that UI locale, as on a real machine set to that language.
  • A comma list, such as de-DE,de, sets navigator.languages and Accept-Language exactly.
The same launch as above, with three values of locale: Chrome’s UI locale for Japanese is ja, so the tag ja-JP gives the same result as ja. Chrome’s default lists include English after the local language. A real user who removed English from Chrome’s language settings sends a list without it. Pass a list to match that.

What Intl shows

The UI locale is Intl’s default locale. It decides date, time and number formats, collation, and the default calendar and hour cycle. Intl.DateTimeFormat().resolvedOptions().locale reports the UI locale as Chrome names it, such as de for de-DE and for de-CH, and ja for ja-JP. With ja-JP and Asia/Tokyo, the same noon UTC prints as 2026/9/27 21:00:00 and the number as 1,234,567.891. The timezone sets Intl.DateTimeFormat().resolvedOptions().timeZone, Date offsets and every local time a page formats. Pass an IANA name such as Europe/Berlin. From 0.4.4 the packages refuse any other name before the browser starts. Python raises ConfigurationError:
Node raises RangeError with the same wording. The 0.4.3 packages do not check the name. They put an unknown name in TZ, and a page’s Intl.DateTimeFormat().resolvedOptions().timeZone then reads undefined.

Environment variables

For a persona, the packages also set the browser’s locale environment, so that the C library and every child process agree with the UI locale. They set: The values come from the first tag, and TZ from the timezone when there is one. Without a locale, the four locale variables get en-US and en_US.UTF-8, so your shell’s language never reaches a persona. Host mode sets none of them. An env you pass to the package is applied last, so your entries win.

Voices

speechSynthesis.getVoices() lists the voices of the persona’s platform, chosen by the first tag. The voice tables distinguish en-US and en-GB. Any other tag gets the en-US set.
A Windows persona set to German lists English voices. Known gaps tracks this.

—lang

Chromium’s --lang switch has no effect on a persona. The launch below still presents German:
Use the locale option, or --fingerprint-locale when you run the browser without the packages.

Without GeoIP

geoip=False in Python or geoip: false in Node skips the lookup. A launch with no locale then presents en-US, and one with no timezone presents the host’s timezone.
Behind a proxy, the host’s timezone is not the exit’s. Pass timezone whenever you turn GeoIP off.

When the lookup fails

A lookup that fails or times out does not stop the launch. The package sends no locale or timezone switch, so the persona presents en-US and the host’s timezone. It reports a warning:
  • The Python package prints it to stderr, prefixed apostate: . From 0.4.4 it also adds it to browser.apostate_diagnostics["warnings"] (Python).
  • The Node package prints it with console.warn, prefixed [Apostate], adds it to browser.apostateDiagnostics.warnings and sets apostateDiagnostics.geoip to unresolved (Node).
From 0.4.4 the warning names the lookup’s error and ends:
A lookup that answers without a timezone, or without a country, sets the field it has and warns about the other.

Your own lookup

To use your own GeoIP service, pass a function. It receives the proxy URL and returns a two-letter country code and an IANA timezone. The package maps the country to a locale as above.
The result may also carry locale or languages to set the tag directly.

Without the packages

The browser reads --fingerprint-locale and --fingerprint-timezone directly. Without them, a persona presents en-US and the host’s timezone, never the host’s language. The browser logs an unknown timezone passed as a switch and keeps the host’s. Switches lists both, and Raw binary covers running the browser yourself.