Skip to main content
You ship a site in several languages and want to check what visitors in each locale and timezone get: the language the server picks, date and number formats, and the rendered page. This walkthrough opens a page once per locale, saves a screenshot and prints what the page read.

What Apostate changes

The locale option sets the browser’s UI locale, navigator.languages and the Accept-Language header. The timezone option sets the timezone that Intl and Date use. The browser takes both at launch, so the request headers, the page and its workers agree. A Windows persona shows only Windows fonts, and only the ones installed on the host. With the Windows fonts installed, a screenshot uses the fonts a Windows visitor has. Locale and timezone covers both options.

Set up

On Linux and macOS hosts, install the Windows fonts before you compare screenshots. Fonts explains what the command installs. check_locales.py opens a page for each region, reads what the page sees and saves a full-page screenshot. With no argument, it serves a local test page that picks its language from Accept-Language and shows dates, numbers, a price, a relative time and a list formatted by Intl.

The script

Each region has a locale, a timezone and the name of an environment variable that may hold a proxy in that region:
One launch per region, with the same seed and persona each time:
The test page’s server chooses its language the way many sites do, from the first supported language in Accept-Language:

Run it

  • htmlLang is the lang attribute of the page the server returned. On your site, it shows which translation each locale received.
  • date is the same instant, noon UTC on 15 January 2026, in each region’s timezone and format.
  • ja-JP gives a language list that starts with ja, and ar-EG one that starts with ar, because Chrome’s UI locale for these languages carries no region. One tag or a list shows how to pass an exact list instead.
The screenshots are in out/, one per locale. Each is 1920 pixels wide, the width of the window seed 42 opens as a Windows persona.

Check your own site

Pass the page’s URL:
The output has the same fields. date and number come from the browser, so they are the same for any page. htmlLang and the screenshots show what your site served.

Regions and proxies

A site that chooses the language, currency or content from the visitor’s IP address needs a visit from that region. Put a proxy in the region’s variable, and that region’s launch goes through it:
The region’s line then reads de-DE Europe/Berlin through APOSTATE_PROXY_DE. The script still passes the region’s locale and timezone, so the package skips its GeoIP lookup. Use a proxy whose exit is in that region. Proxies covers proxy URLs and credentials.

Points for this job

  • Seed and persona. Every region uses seed 42 as Windows, so only the locale and timezone change between screenshots. To compare macOS rendering, change fingerprint_platform in check().
  • Fonts. The host’s font engine draws the glyphs, so glyph edges in a screenshot can differ slightly from a Windows machine’s (Known gaps). Compare screenshots taken on one host with each other.
  • Voices and keyboard. A Windows persona lists English speech voices, and every persona reports a US keyboard layout, whatever the locale. For a site that uses either, Windows voices are English and The keyboard layout is US say what to do.
  • Timezone names. Pass IANA names such as Asia/Tokyo. From 0.4.4 both packages refuse any other name before launch. Python raises ConfigurationError with timezone 'Asia/Tokio' is not an IANA zone name, such as Europe/Berlin, and Node throws RangeError with the same text. In 0.4.3 the package writes an unknown name into TZ, and a page’s Intl timeZone reads undefined. Locale and timezone covers what Intl shows.
  • Headless. The script takes the screenshots headless, the default. A persona presents the same screen and window headless as headed (Screen and window).