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.
Related terms
- Timezone emulation
- Geolocation emulation
- User agent
- prefers-color-scheme: the same idea for colour themes.
To check your site in several languages, read localization QA with screenshots or get a free API key and change the locale value.