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

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'));
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();
}

Steps

  1. Step 1.

    Record the baseline with no override at all.

    node override.mjs none
    
    Accept-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 navigator values, and the locale Intl formats with.

  2. Step 2.

    Apply the CDP locale override, the one the DevTools protocol offers for this.

    node override.mjs locale
    
    Accept-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 navigator values did not move. A page that picks its translation from either of those renders exactly as before.

  3. Step 3.

    Add the request header, which the locale override does not touch.

    node override.mjs locale+header
    
    Accept-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.language is still uk-UA, so a client-side switcher reading it still shows Ukrainian.

  4. Step 4.

    Start Chrome in another interface language instead.

    node override.mjs lang-flag
    
    Accept-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-DE moves 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.

  5. Step 5.

    Set only the preference list, with the flag that exists for it.

    node override.mjs accept-lang
    
    Accept-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 navigator are German while Intl is still Ukrainian. That combination is a real user with a German preference on a Ukrainian machine, and it is worth testing on purpose.

  6. Step 6.

    Pass quality values to that flag, the way the header is written.

    node override.mjs accept-lang-q
    
    Accept-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.9 it treated de;q=0.9 as a language tag, put it in navigator.languages verbatim, 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

Sign: A localization test switches locale, passes, and the screenshot is still in the original language.Cause: Emulation.setLocaleOverride moves the formatting locale only. In step 2 the dates came back German while navigator.language and the request header stayed Ukrainian, so every string chosen from either of those was unchanged. The assertion on a formatted date passed and the page was never translated.
Sign: navigator.languages contains an entry like de;q=0.9.Cause: --accept-lang expects a comma-separated list of tags with no quality values. Passing a header-shaped string makes Chrome treat the whole segment as a tag, and the header it then sends carries two q parameters on one entry, as step 6 shows. Any strict parser on the server rejects it.
Sign: Changing the language in Chrome settings fixes the test on one machine and nowhere else.Cause: That setting lives in the profile, so it cannot be committed with the test and it changes every other tab on the machine. Launching a throwaway profile with a language flag is repeatable and leaves the tester's own browser alone.

What to check next

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.

intermediate10 minpublished updated Maks Verny