Request header
> Accept-Language: en-US,en;q=0.9Accept-Language Header
Learn how the Accept-Language header tells servers which languages your client prefers for localized content. Understand language tags and quality values.
- Direction
- Request
- Category
- Content negotiation
- Spec
- RFC 9110 §12.5.4
- JS can set it
- Yes
- CORS-safelisted
- Yes
On this page
TL;DR:
Accept-Languageis the browser telling the server which languages the user reads, in order of preference, such asen-US, en;q=0.9. Use it to pick a default language, let an explicit choice (URL, setting, selector) override it, and sendVary: Accept-Language. It tells you nothing about the user’s country or timezone.
What is Accept-Language?
The header lists language ranges, each with an optional quality value (q) for ranking. The server picks the best match from the translations it has and labels the response with Content-Language.
Accept-Language: en, fr;q=0.5 means “English, or French if you must.” It’s a preference, not a demand: an English-only site just serves English. And if the header is missing, that means no preference, not “English”.
How Accept-Language Works
Here are trimmed request and response headers, bodies omitted:
GET /welcome HTTP/1.1
Host: example.com
Accept-Language: fr-CA, fr;q=0.9, en;q=0.5
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Language: fr
Vary: Accept-Language
This server has French but no Canadian French, so it serves fr. HTTP doesn’t prescribe one fallback algorithm; each server picks its own matching policy. RFC 9110 §12.5.4 points to the matching schemes in RFC 4647.
Language Tag Format
en means English; en-US narrows it to US English. Subtags are joined with hyphens, not underscores, so it’s en-US, never en_US. * matches any language.
A missing q means 1, and q=0 means “not this one”. When two entries share the same q, some servers treat list order as a tiebreaker and some don’t, so give entries distinct weights if the ranking matters.
Real-World Examples
Send a specific preference and see which language comes back:
curl -i -H 'Accept-Language: fr-CA, fr;q=0.9, en;q=0.5' https://example.com/welcome
This only changes the response if that URL actually negotiates language.
Common Language Codes
Tags come from BCP 47. Besides regions, they can name a script: zh-Hans is Chinese written in Simplified characters. A region alone doesn’t tell you the script, so don’t infer one from it.
| Tag | Meaning |
|---|---|
en | English without a region refinement |
en-GB | English as used in the United Kingdom |
fr-CA | French as used in Canada |
zh-Hans | Chinese in Simplified script |
sr-Latn | Serbian in Latin script |
These identify languages. They aren’t currency or date-format settings, and en-GB doesn’t mean the user is in Britain.
Server Response Strategies
When the header is missing, the client has no preference, so serve your default. When nothing matches, you can either ignore the header and serve the default or return 406. Either way, match against the translations you actually have. Splitting on commas and grabbing the first token ignores both weights and exclusions.
Take fr;q=0, en;q=0.8. The client is saying “no French”, but a first-token parser would pick French, the one language it rejected. Pass the whole header and your list of supported languages to a real matcher.
Getting Accept-Language right
Add this route to an Express app with English and French translations:
app.get('/welcome', (req, res) => {
res.vary('Accept-Language')
const language = req.acceptsLanguages(['en', 'fr'])
if (!language) return res.status(406).end()
res.set('Content-Language', language)
res.json({ message: language === 'fr' ? 'Bienvenue' : 'Welcome' })
})
req.acceptsLanguages() does the weighted matching against the languages you offer. This route returns 406 when nothing matches; a product can just as well serve a documented default instead.
Browser Behavior
Browsers build the header from the user’s language settings, but privacy features can trim the list. navigator.languages shows you the browser’s preferences, which won’t always match the header byte for byte, so compare it with Network → Request Headers. MDN documents fallback tags and privacy restrictions.
To see what the browser exposes, run this in the console:
console.log(navigator.language)
console.log(navigator.languages)
Then look at the request for the page itself, not a later image or API call. The console values help you reproduce a bug report, but the Network panel shows what the server actually received.
API Localization
When the user has picked a language explicitly, through the URL, an account setting, or a language selector, that choice wins over the header. If one URL serves different languages based on the header, send Vary: Accept-Language so caches keep them apart. If a cookie or account setting also affects the language, Vary: Accept-Language won’t cover that; handle caching for those separately.
In API errors, translate the human message but keep the machine-readable code the same in every language. Add this route to the same Express app:
const messages = {
en: 'The report is not ready',
fr: "Le rapport n’est pas prêt"
}
app.get('/report-status', (req, res) => {
res.vary('Accept-Language')
const language = req.acceptsLanguages(['en', 'fr']) || 'en'
res.set('Content-Language', language)
res.status(409).json({ code: 'REPORT_NOT_READY', message: messages[language] })
})
Here a mismatch falls back to English. Clients branch on REPORT_NOT_READY and never have to parse a sentence that changes with the language.
Testing Language Negotiation
Test a missing header and an explicitly rejected language:
curl -i -H 'Accept-Language:' https://example.com/welcome
curl -i -H 'Accept-Language: en;q=0, fr;q=1' https://example.com/welcome
Common Patterns
A language in the URL, like /fr/welcome, makes the choice explicit and independent of browser settings. Keep negotiation to routes whose job is choosing between translations.
So /fr/welcome returns French even when the request says Accept-Language: en. Point the language selector at the translated route, and keep the user on their chosen language on the next request instead of quietly switching back. Test that separately from negotiation at /welcome.
Related Headers
Frequently asked questions
What is Accept-Language?
Accept-Language tells the server which languages the client prefers. Servers use this for content negotiation to return localized content. Example: Accept-Language: en-US, en;q=0.9, fr;q=0.8.
How does language preference work?
Languages are listed with optional quality values (q). Higher q means higher preference. en-US,en;q=0.9 prefers US English, then any English. Default q is 1.0.
Should I use Accept-Language for localization?
Use it as a default, but let users override. Store user preference in cookies or profiles. Accept-Language reflects browser settings, not necessarily user preference.
What is the difference between en and en-US?
en is generic English. en-US is US English specifically. en-GB is British English. Servers may match en-US to en if no specific variant exists.
Sources
Related
Accept Header
Learn how the Accept header tells servers which content types (JSON, HTML, XML) your client can handle. Master content negotiation and quality values.
Expect Header: 100-continue, curl Delays and 417
Expect: 100-continue asks the server to approve a request before the body is sent. Why curl adds it past 1 MB, the 1s delay, -H "Expect:", and 417.
Forwarded
Learn how the Forwarded header preserves original client information (IP, protocol, host) that would otherwise be lost when requests pass through proxies.
Host Header
Learn how the Host header specifies the target server domain name and port for HTTP requests. Essential for virtual hosting and routing on shared servers.