How HTTP Works

Request header

> Accept-Language: en-US,en;q=0.9

Accept-Language Header

Learn how the Accept-Language header tells servers which languages your client prefers for localized content. Understand language tags and quality values.

Reviewed 4 min readbeginner2 sourcesTry itMarkdown
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-Language is the browser telling the server which languages the user reads, in order of preference, such as en-US, en;q=0.9. Use it to pick a default language, let an explicit choice (URL, setting, selector) override it, and send Vary: 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.

TagMeaning
enEnglish without a region refinement
en-GBEnglish as used in the United Kingdom
fr-CAFrench as used in Canada
zh-HansChinese in Simplified script
sr-LatnSerbian 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.

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

  1. MDN Web Docs: Accept-Languagedeveloper.mozilla.org
  2. RFC 9110: Accept-Languagerfc-editor.org

Keep going

Browse /search