# 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.

Source: https://howhttpworks.com/headers/accept-language
Last reviewed: 2026-10-05

> **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](https://howhttpworks.com/headers/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:

```http
GET /welcome HTTP/1.1
Host: example.com
Accept-Language: fr-CA, fr;q=0.9, en;q=0.5
```

```http
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](https://www.rfc-editor.org/rfc/rfc9110#section-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:

```bash
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](https://www.rfc-editor.org/rfc/rfc5646). 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:

```javascript
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()`](https://expressjs.com/en/5x/api/request/#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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Language).

To see what the browser exposes, run this in the console:

```javascript
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:

```javascript
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:

```bash
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

- [Content-Language](https://howhttpworks.com/headers/content-language)
- [Vary](https://howhttpworks.com/headers/vary)
- [Accept](https://howhttpworks.com/headers/accept)
