Accept-Language Header: Locale in Headless Browsers

Accept-Language is the HTTP header that lists the languages a browser prefers. What it contains, what headless Chrome sends by default, and how to set it.

By Simon Chiu · Updated

Accept-Language is an HTTP request header in which the browser lists the human languages it prefers, in order, so the server can choose which translation of a page to send.

A typical value is fr-FR,fr;q=0.9,en;q=0.8. It is one of three signals sites use to localise a page, next to navigator.language in JavaScript and the visitor's IP location. This entry is part of the screenshot glossary.

What it means in plain terms

Every request your browser sends says, roughly, "I read these languages, best first". A multilingual site reads that list and returns the German page to a German reader at the same URL. When you capture localised pages with a headless browser, you are pretending to be that reader, so the header has to say what a real one would.

How it works technically

The value is a comma-separated list of BCP 47 language tags, each with an optional weight:

Accept-Language: fr-FR,fr;q=0.9,en;q=0.8
                 ^^^^^ ^^^^^^^^ ^^^^^^^^
                 q=1   q=0.9    q=0.8

The server compares the list with the languages it has and picks the best match. This is called content negotiation. A well-behaved server also sends Vary: Accept-Language, so caches keep one copy per language.

Two other inputs can override it. Sites often redirect by a cookie or a path such as /fr/ once you pick a language. Client-side apps often read navigator.language instead of the header, because the header is not visible to JavaScript.

What headless browsers send: a test

We pointed each setup at a local server that echoes the received header back into the page, then read navigator.language in the page:

Setup Accept-Language the server received navigator.language
Puppeteer, default headless mode en-US,en;q=0.9 en-US
Puppeteer, headless: 'shell' no header at all en-US
Puppeteer, --lang=fr-FR + setExtraHTTPHeaders fr-FR,fr;q=0.9,en;q=0.8 fr-FR
Playwright, locale: 'fr-FR' fr-FR fr-FR

Tested on 2026-10-15 with Node 22.18.0, [email protected] (Chrome 148) and [email protected] (Chromium 156) on macOS.

Two findings matter. First, the lightweight headless shell sent no Accept-Language header at all, and some servers treat a missing header as a sign of a bot or fall back to a default language. Second, Playwright sent a single tag with no fallback list, so a server without fr-FR gets no hint that fr would do.

How to set it

Puppeteer

Puppeteer needs two settings, because the header and navigator.language are separate:

import puppeteer from 'puppeteer';

// --lang sets navigator.language; the header override sets what servers see.
const browser = await puppeteer.launch({ args: ['--lang=fr-FR'] });
try {
  const page = await browser.newPage();
  await page.setExtraHTTPHeaders({ 'Accept-Language': 'fr-FR,fr;q=0.9,en;q=0.8' });
  await page.goto(process.argv[2] ?? 'https://example.com');

  console.log('server saw:', await page.$eval('body', (el) => el.innerText.trim()));
  console.log('navigator.language:', await page.evaluate(() => navigator.language));
} finally {
  await browser.close();
}

--lang applies to the whole browser, so use one browser per language or rely on the header alone if the site only reads the header.

Playwright

One context option sets the header, navigator.language and number and date formatting together:

import { chromium } from 'playwright';

const browser = await chromium.launch();
try {
  // One option sets the header, navigator.language and Intl formatting together.
  const context = await browser.newContext({ locale: 'fr-FR' });
  const page = await context.newPage();
  await page.goto(process.argv[2] ?? 'https://example.com');

  console.log('server saw:', await page.locator('body').innerText());
  console.log('navigator.language:', await page.evaluate(() => navigator.language));
  console.log('number:', await page.evaluate(() => (1234.5).toLocaleString()));
} finally {
  await browser.close();
}

It printed 1 234,5, the French number format. If you need a weighted list in the header, be careful: when we added extraHTTPHeaders: { 'Accept-Language': 'fr-FR,fr;q=0.9' } to a context that also set locale, the server still received only fr-FR. Without locale, the custom header went through, but navigator.language stayed en-US. In Playwright, you get one or the other.

RenderScreenshot

browser.locale sets the Accept-Language header, navigator.language and date and number formatting from one BCP 47 tag. Pair it with a timezone for dates that match:

curl -G https://api.renderscreenshot.com/v1/screenshot \
  -H "Authorization: Bearer $RS_API_KEY" \
  --data-urlencode "url=https://example.com/pricing" \
  -d locale=fr-FR -d timezone=Europe/Paris \
  --output pricing-fr.png

When you need an exact header value, such as a weighted list, send it in network.headers in a POST request:

{
  "url": "https://example.com/pricing",
  "browser": { "locale": "fr-FR" },
  "network": { "headers": { "Accept-Language": "fr-FR,fr;q=0.9,en;q=0.8" } }
}

In the Node.js SDK these are .locale('fr-FR') and .headers({ 'Accept-Language': '...' }). See browser options and the localized screenshots guide, which also covers several locales from one page. The API requests on this page were not run for it.

Common mistakes

  • Setting the header but not navigator.language (or the reverse). The site reads the one you did not set.
  • Expecting the header to beat a language cookie or a /en/ URL. Most sites honour an explicit choice first.
  • Expecting it to change the region. Prices in local currency often depend on IP location, which the header does not change.
  • Forgetting the timezone. A French page with New York dates looks wrong. See timezone emulation.
  • Fonts. A Japanese or Arabic page needs the right fonts on the rendering machine, or text shows as boxes. See font fallback and tofu.

To check your site in several languages, read localization QA with screenshots or get a free API key and change the locale value.

Frequently asked questions

What does q=0.9 mean in Accept-Language?
It is a quality value from 0 to 1 that ranks the preference. A language without q has weight 1. fr-FR,fr;q=0.9,en;q=0.8 means French (France) first, any French second, English third.
Is Accept-Language the same as navigator.language?
No. The header is what the server sees; navigator.language is what JavaScript in the page sees. Browsers normally derive both from the same setting, but automation tools can set them separately, and a site may read either one.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.