Accept-Language header: format, q values, real examples and a parser

Accept-Language is the browser's ranked list of preferred languages. Here is what Chromium really sent, and a small parser tested on it.

1. Real values

Browser setupAccept-Language sent
Chromium 153, English (UK) systemen-GB,en-US;q=0.9,en;q=0.8
Chromium with --accept-lang=de-DE,frde-DE,de;q=0.9,en-US;q=0.8,en;q=0.7
Playwright default headless shellno header at all

Format: comma-separated language tags, each optionally followed by ;q= (0 to 1, default 1). Chromium wrote the q values itself, stepping down 0.1 per entry, and added the base language de after de-DE. In our run it also left out fr and added English, so don't assume the header mirrors a setting one-to-one. --lang=de-DE did not change the header on this Linux build; only --accept-lang did. A missing header (as from the headless shell) is common for scripts and bots: always have a default.

2. Parsing it (Python, tested)

def parse_accept_language(header):
    # language tags sorted by q (highest first); q=0 means 'not acceptable'
    langs = []
    for i, part in enumerate((header or "").split(",")):
        tag, _, params = part.strip().partition(";")
        tag = tag.strip()
        if not tag:
            continue
        q = 1.0
        for p in params.split(";"):
            k, _, v = p.strip().partition("=")
            if k.strip().lower() == "q":
                try:
                    q = float(v)
                except ValueError:
                    q = 0.0
        if q > 0:
            langs.append((-q, i, tag))
    return [tag for _, _, tag in sorted(langs)]

def pick(header, supported, default="en"):
    sup = {s.lower(): s for s in supported}
    for tag in parse_accept_language(header):
        t = tag.lower()
        if t in sup: return sup[t]
        if t.split("-")[0] in sup: return sup[t.split("-")[0]]
        if t == "*": return default
    return default

print(pick("de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7", ["en", "de", "fr"]))  # de
HeaderRankedpick(en, de, fr)
fr;q=0.5, en;q=0.8 ,dede, en, frde
fr-CA;q=0, fr;q=0.9, *;q=0.1fr, * (fr-CA dropped: q=0 = "not acceptable")fr
en;q=abc,de;q=0.4de (invalid q ignored)de
(empty or missing)[]en (default)

Order in the header is not the ranking: de came first in the third row because it has the implicit q=1. Ties keep header order.

3. Using it well

Frequently asked questions

What does q mean in Accept-Language?

Quality, 0 to 1: how much the user wants that language. No q means 1. q=0 means not acceptable, so our parser drops it.

What does Chrome send in Accept-Language?

In our test, full Chromium on an English (UK) system sent en-GB,en-US;q=0.9,en;q=0.8. Playwright's default headless shell sent no Accept-Language header at all.

Is the first language in the header always the preferred one?

No, q decides. In 'fr;q=0.5, en;q=0.8, de' the preferred language is de (implicit q=1).

How do I test my site with a different Accept-Language?

Start Chromium with --accept-lang=de-DE (that changed the header in our test; --lang did not), change the browser's language settings, or set the header with an extension such as PlainHeaders.

Written with AI assistance. Measured on 2026-10-01: a local server logged the Accept-Language header from Chromium 153 (full build, new headless, and Playwright's chrome-headless-shell) with default settings, --lang and --accept-lang. The Python parser below was run on those real headers plus edge cases. PlainHeaders is our extension; the other methods are free.