Back to HTTP Headers

Accept-Language content request

Lists the client's preferred natural languages for the response — the client's half of HTTP language negotiation.

What it does

Accept-Language tells the server the user's preferred language(s) for the response, in priority order. The server uses this to select the appropriate localisation of the content and declares the language used in Content-Language.

Browsers set this automatically from the user's OS language preferences. It's a key input for server-side internationalisation (i18n).

Syntax

Accept-Language: <language-tag>
Accept-Language: <language-tag>;q=<quality>
Accept-Language: <language-tag>, <language-tag>;q=<quality>
Accept-Language: *

Examples:

Accept-Language: en-US
Accept-Language: fr
Accept-Language: en-US,en;q=0.9,fr;q=0.8
Accept-Language: zh-Hans,zh;q=0.9,en;q=0.8,en-GB;q=0.7
Accept-Language: *

Language tags (BCP 47)

Language tags follow BCP 47: language[-region][-script]:

Tag Meaning
en English (any region)
en-US English, United States
en-GB English, United Kingdom
fr French
fr-CA French, Canada
zh-Hans Chinese, Simplified script
zh-Hant Chinese, Traditional script
pt-BR Portuguese, Brazil
ar Arabic
* Any language

Quality values in practice

A typical browser Accept-Language for a US English user who also speaks French:

Accept-Language: en-US,en;q=0.9,fr;q=0.8

Reading this:

  • en-US → q=1.0 — most preferred
  • en → q=0.9 — any English, second choice
  • fr → q=0.8 — French, third choice

The server iterates from highest to lowest q-value and returns the first language it can serve.

How browsers set Accept-Language

The browser derives Accept-Language from the OS/browser language settings — the user never sets it manually in normal usage. The primary OS language becomes q=1.0, with fallback languages added at decreasing q-values. This makes Accept-Language a genuine expression of user preference, not a developer-set header.

This also means Accept-Language can be used for browser fingerprinting — it reveals language preferences that, combined with other signals, can uniquely identify a user.

Server-side i18n implementation

The server reads Accept-Language, parses the q-values, and selects the best matching language from its available translations:

// Laravel example
public function detectLocale(Request $request): string
{
    $available = ['en', 'fr', 'de', 'es', 'ja'];
    $preferred = $request->getPreferredLanguage($available);
    return $preferred ?? 'en';
}

Then respond with the chosen language declared:

Content-Language: fr
Vary: Accept-Language

The Vary: Accept-Language requirement

Critical for multilingual sites using content negotiation at the same URL: always include Vary: Accept-Language when returning language-specific content.

Without it, a CDN caches the first response (say, French) and serves it to all users regardless of their language preference. English speakers get French pages. Hard to debug, very annoying.

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Language: fr
Vary: Accept-Language
Cache-Control: public, max-age=3600

Language negotiation vs URL-based i18n

Most modern sites use URL-based language selection (/en/, /fr/, ?lang=fr) rather than relying on Accept-Language negotiation. Reasons:

  • Shareability — a URL is shareable; header negotiation means the same URL shows different content to different people
  • SEO — search engines can index language-specific URLs separately
  • User control — users can explicitly switch language by changing the URL
  • Caching simplicity — no Vary: Accept-Language complications

Accept-Language is still useful as the initial detection: detect the user's language from the header on their first visit, then redirect to the appropriate language URL or set a language cookie.

Common mistakes and gotchas

Trusting Accept-Language without fallback. Always have a fallback language. If the user requests zh-Hant and you only have zh-Hans, serving zh-Hans is better than a 406 or showing untranslated content.

Not normalising language tags. Users might send en_US (underscore) or EN-us (wrong case). Normalise to BCP 47 format before comparing.

Forgetting Vary: Accept-Language on cached multilingual responses. Repeat: a CDN without this will serve cached content in the wrong language to a percentage of your users. Always set it.

Real-world examples

Browser from a multilingual user:

GET /homepage HTTP/1.1
Host: example.com
Accept-Language: ja,en-US;q=0.9,en;q=0.8

API requesting English content specifically:

GET /api/help-texts HTTP/1.1
Accept-Language: en-US
Authorization: Bearer token

Server responding with matched language:

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Language: ja
Vary: Accept-Language

FAQ

Should I use Accept-Language or a cookie/URL for language selection?

Both, in combination. Use Accept-Language to detect the preferred language on first visit. Store the selection in a cookie or URL. Give users a manual language switcher. Accept-Language alone is fragile — a shared computer or a user browsing from a different device will show wrong language.

Can I set Accept-Language programmatically in JavaScript?

No — Accept-Language is a "forbidden header" in the Fetch/XHR spec. Browsers automatically set it from OS preferences and don't allow JavaScript to override it. For API calls where you want to specify language, use a query param (?lang=fr) or a custom header.

What if the server doesn't support any of the listed languages?

Return the default language with a Content-Language declaring what was sent. Don't return 406 — language mismatch isn't a reason to refuse a request. Most users would rather get English content than a "Not Acceptable" error.

Fun fact

Accept-Language is one of the most accurate signals a browser sends about a user's background — more accurate in some ways than IP geolocation. A user accessing from a German IP address might have Accept-Language: tr,en;q=0.9 (Turkish speaker living in Germany). Accept-Language reflects where the user is from; IP geolocation reflects where they are. Ad targeting systems have used this distinction for decades — the header was never designed for this purpose, but it became invaluable for it.