How to change locale in chrome
Chrome has four locale overrides and each one moves a different surface. Emulation.setLocaleOverride changed only the formatting locale here. Start Chrome with --lang=de-DE and the header, navigator.language and Intl all move together. Pick the override that matches what the page under test reads.
Why check this
Switching a browser to another language is the first step of almost every localization test, and the usual advice, change the language in Chrome settings, is the one method a test runner cannot repeat. Worse, the override a tester reaches for first tends to move a surface the page under test never reads, so the page looks correctly localized when nothing changed.
Run this before writing localization tests, and any time a test that switches language passes while the screenshot stays in English. The matrix below tells you which override the page is actually responding to.
Prerequisites
- Chrome 152 and
scripts/browser/session.mjs, which starts Chrome with a fresh profile directory. Chrome's own language settings would change every tab on the machine, so nothing here touches them. - Node 22. Save the probe page as
probepage.mjsand runnode probepage.mjs. It prints the header the browser sent and what the page reports about itself.
import { createServer } from 'node:http';
createServer((req, res) => {
const sent = req.headers['accept-language'] ?? '(absent)';
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', Vary: 'Accept-Language' });
res.end(
'<!doctype html><meta charset="utf-8"><title>Locale probe</title>' +
'<p id="header">Accept-Language received: ' + sent + '</p><p id="nav"></p><script>' +
"const r = Intl.DateTimeFormat().resolvedOptions();" +
"document.getElementById('nav').textContent = JSON.stringify({" +
"language: navigator.language, languages: navigator.languages, intlLocale: r.locale," +
"sample: new Intl.NumberFormat().format(1234567.89)," +
"date: new Intl.DateTimeFormat(undefined, { dateStyle: 'long' }).format(new Date(Date.UTC(2026, 0, 5)))" +
'});</script>'
);
}).listen(8393, '127.0.0.1', () => console.log('listening on 8393'));
- The driver, saved as
override.mjs. Each step runs it with one mode name.
import { open } from '../../scripts/browser/session.mjs';
const flags = {
'lang-flag': ['--lang=de-DE'],
'accept-lang': ['--accept-lang=de-DE,de,en'],
'accept-lang-q': ['--accept-lang=de-DE,de;q=0.9'],
};
const mode = process.argv[2] ?? 'none';
const s = await open({ args: flags[mode] ?? [] });
try {
if (mode === 'locale' || mode === 'locale+header') {
await s.cdp.send('Emulation.setLocaleOverride', { locale: 'de-DE' });
}
if (mode === 'locale+header') {
await s.page.setExtraHTTPHeaders({ 'Accept-Language': 'de-DE,de;q=0.9' });
}
await s.goto('http://127.0.0.1:8393/');
console.log(await s.page.$eval('#header', (e) => e.textContent));
console.log(await s.page.$eval('#nav', (e) => e.textContent));
} finally {
await s.close();
}
- Every capture below is one run on one Windows machine on 2026-09-12. The machine locale is Ukrainian, which is what the baseline shows.
Steps
- Step 1.
Record the baseline with no override at all.
node override.mjs noneAccept-Language received: uk-UA,uk;q=0.9,en-US;q=0.8,en;q=0.7 {"language":"uk-UA","languages":["uk-UA","uk","en-US","en"],"intlLocale":"uk","sample":"1 234 567,89","date":"5 січня 2026 р."}Three surfaces to watch: the header on the wire, the
navigatorvalues, and the localeIntlformats with. - Step 2.
Apply the CDP locale override, the one the DevTools protocol offers for this.
node override.mjs localeAccept-Language received: uk-UA,uk;q=0.9,en-US;q=0.8,en;q=0.7 {"language":"uk-UA","languages":["uk-UA","uk","en-US","en"],"intlLocale":"de-DE","sample":"1.234.567,89","date":"5. Januar 2026"}Dates and numbers are German. The header and both
navigatorvalues did not move. A page that picks its translation from either of those renders exactly as before. - Step 3.
Add the request header, which the locale override does not touch.
node override.mjs locale+headerAccept-Language received: de-DE,de;q=0.9 {"language":"uk-UA","languages":["uk-UA","uk","en-US","en"],"intlLocale":"de-DE","sample":"1.234.567,89","date":"5. Januar 2026"}The server now sees German and formats in German.
navigator.languageis stilluk-UA, so a client-side switcher reading it still shows Ukrainian. - Step 4.
Start Chrome in another interface language instead.
node override.mjs lang-flagAccept-Language received: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7 {"language":"de-DE","languages":["de-DE","de","en-US","en"],"intlLocale":"de","sample":"1.234.567,89","date":"5. Januar 2026"}--lang=de-DEmoves all three surfaces at once, and Chrome wrote the quality values into the header itself. This is the closest thing to a German user, and it is the override to reach for first. - Step 5.
Set only the preference list, with the flag that exists for it.
node override.mjs accept-langAccept-Language received: de-DE,de;q=0.9,en;q=0.8 {"language":"de-DE","languages":["de-DE","de","en"],"intlLocale":"uk","sample":"1 234 567,89","date":"5 січня 2026 р."}The header and
navigatorare German whileIntlis still Ukrainian. That combination is a real user with a German preference on a Ukrainian machine, and it is worth testing on purpose. - Step 6.
Pass quality values to that flag, the way the header is written.
node override.mjs accept-lang-qAccept-Language received: de-DE,de;q=0.9,de;q=0.9;q=0.8 {"language":"de-DE","languages":["de-DE","de;q=0.9"],"intlLocale":"uk","sample":"1 234 567,89","date":"5 січня 2026 р."}The flag takes a plain list of tags. Given
de-DE,de;q=0.9it treatedde;q=0.9as a language tag, put it innavigator.languagesverbatim, and emitted a header with two quality parameters on one entry.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| Dates change and the translation does not | The formatting locale moved, the language preference did not | The page reads navigator or the header. Use --lang or set the header as well. |
| The header changed and nothing on screen did | The page is client-rendered and never reads the header | Override navigator too, with --lang or --accept-lang. |
| Nothing changed on any surface | The override was applied after the navigation | Apply it before goto, as the driver does, and reload. |
| A tag with ;q= shows up inside navigator.languages | Quality values were passed to --accept-lang | Pass tags only. Chrome generates the quality values itself. |
| Formatting stays on the machine locale under --accept-lang | That flag never touches the formatting locale | Add Emulation.setLocaleOverride, or use --lang. |
The four overrides side by side
| Override | Accept-Language header | navigator.language | Intl formatting |
| --- | --- | --- | --- |
| Emulation.setLocaleOverride | unchanged | unchanged | changed |
| setExtraHTTPHeaders | changed | unchanged | unchanged |
| --accept-lang=de-DE,de,en | changed | changed | unchanged |
| --lang=de-DE | changed | changed | changed |
Common mistakes
What to check next
- How to check the browser locale: read the three surfaces before you decide which one to override.
- How to check accept language header: what a server should do with the header these overrides produce.
- How to change timezone in chrome for testing: the same emulation domain, for the other half of a regional setting.
- How to check the thousand separator and decimal separator: the formatting that step 2 moved, checked properly.
FAQ
How do I test localization in a browser without changing my own settings?
Start a second Chrome with a throwaway profile directory and a language flag, as the driver does. Your everyday browser keeps its settings and the run is repeatable on any machine.
Which override should a Playwright or Puppeteer test use?
Whichever moves the surface the page reads. Assert it rather than trusting a context option: print navigator.language, the formatting locale and the header the server received, the way the probe page does, on the first run of the test.
Does the CDP locale override change the timezone?
No. The timezone has its own override in the same emulation domain, Emulation.setTimezoneOverride. Regional settings in Chrome move independently of each other.
Why does the page still show English after the override?
The page reads a surface the override did not move. Compare the three lines in your run against the matrix above and choose the override that covers what the page reads.
Can I override the locale for one tab only?
Yes, with the CDP override, which is attached to a page target. The command-line flags apply to the whole browser instance.
Verified
Verified by Maks VernyChrome 152.0.7977.76node 22.23.2
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
intermediate10 minpublished updated Maks Verny