# Content-Language Header

> Learn how the Content-Language header specifies the natural language(s) of response content. Understand language tags and internationalization best practices.

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

> **TL;DR:** `Content-Language` is a response header that says who the content is for, such as `Content-Language: en-US`. It names the intended audience, not every language that appears in the text. To mark the language of the text itself, use the HTML `lang` attribute.

## What is Content-Language?

Picture a German course written for English speakers. It's full of German examples, but its audience is English readers, so `Content-Language: en` is right. Audience and text language are different ideas. [RFC 9110 §8.5](https://www.rfc-editor.org/rfc/rfc9110#section-8.5) defines the audience meaning.

## How Content-Language Works

Response headers with the body omitted (headers on this page are trimmed examples):

```http
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Language: en
```

When the field is missing, the content is meant for every language audience. A missing header doesn't mean "English", and the header has nothing to do with character encoding either way.

## Syntax

```http
Content-Language: en-US
Content-Language: en, fr
```

Use plain language tags. `q` weights and `*` belong in requests, not here. List several tags when the content serves several audiences. [MDN gives the syntax and audience examples](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Language).

## Common Examples

`Content-Language: en, fr` fits content written for both English and French readers. An English article that quotes one French sentence is still for English readers, so `en` is enough.

Take a bilingual instruction sheet with the complete instructions in both English and French. Either group can use it, so `en, fr` describes its audience. A vocabulary worksheet that teaches French through English explanations is for English speakers, so it declares `en`.

In that worksheet, mark the French text where it appears:

```html
<p lang="en">The French word for bread is <span lang="fr">pain</span>.</p>
```

The header can't do this job for you. Screen readers and speech software need to know exactly where pronunciation changes, not just who the server thinks will read the page.

## Real-World Scenarios

Say `/welcome` negotiates language and picks French. It returns `Content-Language: fr` with `Vary: Accept-Language`. A fixed `/fr/welcome` route doesn't need that `Vary`, because `Accept-Language` doesn't change its response.

APIs can localize too. A French response keeps the key `status` and translates the human-readable value. `Content-Language: fr` covers the messages; protocol tokens and JSON property names stay as they are.

For a response negotiated between two translations:

```http
Content-Type: application/json
Content-Language: fr
Vary: Accept-Language
```

If the resource always contains both translations, its audience might be `en, fr`, and it has no reason to vary by the request's language preference. Set the header from what you actually send, rather than copying the request field.

## Server Implementation

This nginx route goes inside a `server` block:

```nginx
location = /welcome.fr.txt {
    default_type text/plain;
    charset utf-8;
    add_header Content-Language fr;
    return 200 "Bienvenue\n";
}
```

[`add_header`](https://nginx.org/en/docs/http/ngx_http_headers_module.html#add_header) sets the audience and the [charset module](https://nginx.org/en/docs/http/ngx_http_charset_module.html) sets UTF-8. This route always serves French; nothing is negotiated.

An Express route can pick from the translations the app has:

```javascript
const greetings = { en: 'Welcome', fr: 'Bienvenue' }
app.get('/welcome', (req, res) => {
  res.vary('Accept-Language')
  const language = req.acceptsLanguages(['en', 'fr']) || 'en'
  res.set('Content-Language', language)
  res.json({ message: greetings[language] })
})
```

When nothing matches, this route falls back to English. Note that the header carries the language the route chose, not the client's whole preference list. [Express's request API](https://expressjs.com/en/5x/api/request/#req.acceptsLanguages) documents the matching helper.

A fixed French Django view needs no negotiation at all:

```python
from django.http import JsonResponse

def welcome_fr(request):
    response = JsonResponse({'message': 'Bienvenue'})
    response['Content-Language'] = 'fr'
    return response
```

Set the header on the response object the view returns. [Django response objects](https://docs.djangoproject.com/en/6.0/ref/request-response/#setting-header-fields) support dictionary-style header assignment.

For Apache serving a directory of French documents, with `mod_headers` loaded:

```apache
<Directory "/srv/www/fr">
    Header set Content-Language "fr"
</Directory>
```

[`Header set`](https://httpd.apache.org/docs/2.4/mod/mod_headers.html#header) replaces any existing value. Keep it scoped to the French documents. Set it site-wide and every other translation gets labeled French too.

## Getting Content-Language right

In HTML, mark the document language and any language changes in the markup:

```html
<html lang="en">
  <head><title>French lesson</title></head>
  <body><p>The French greeting is <span lang="fr">bonjour</span>.</p></body>
</html>
```

The [HTML Standard](https://html.spec.whatwg.org/multipage/semantics.html#the-html-element) recommends a root language declaration to help speech synthesis and translation tools. The audience header doesn't have to match every `lang` attribute on the page.

Language and writing direction are separate settings. For an Arabic document, declare both the language and the layout direction:

```html
<html lang="ar" dir="rtl">
  <head><meta charset="utf-8"><title>مرحبا</title></head>
  <body><p>مرحبا</p></body>
</html>
```

`Content-Language: ar` won't flip the layout or pick UTF-8 for you. The HTML [`dir` attribute](https://html.spec.whatwg.org/multipage/dom.html#the-dir-attribute) controls direction and the charset declaration controls decoding. A language tag does neither.

## Language Tags

Tags follow [BCP 47](https://www.rfc-editor.org/rfc/rfc5646), which allows language, script, region and other subtags. You're not limited to two ISO codes.

| Header value | Intended audience |
| --- | --- |
| `en-US` | US English readers |
| `pt-BR` | Brazilian Portuguese readers |
| `zh-Hant` | Readers of Chinese in Traditional script |
| `en, fr` | English and French readers |

Two common mistakes: `en_US` uses the locale-style underscore instead of a hyphen, and `en;q=0.8` is request-negotiation syntax. This response field takes plain tags, never a weighted preference list.

Tags are case-insensitive. By convention regions are uppercase and scripts are title-case, but `EN-us` and `en-US` mean the same language. Treat them as one translation, not two.

## Content-Language vs Accept-Language

`Accept-Language` is what the client would like. `Content-Language` describes what the server actually sent. Sending one doesn't mean the server looked at the other, so add `Vary: Accept-Language` only on routes that really negotiate.

A client can send `Accept-Language: fr` to an English-only resource and get `Content-Language: en` back. That's not a contradiction; the server is accurately describing what it sent. Look at the route's selection logic before calling mismatched values a bug. On negotiated endpoints, also check that a cached English response isn't being served to French requests.

## SEO Considerations

This header describes the current response. It doesn't tell search engines where the other translations live, so it can't stand in for links between translated pages.

For an English/French pair, put these annotations in the HTML head of **both** pages, using their real published URLs:

```html
<link rel="alternate" hreflang="en" href="https://example.com/en/welcome">
<link rel="alternate" hreflang="fr" href="https://example.com/fr/welcome">
```

[Google's localized-page documentation](https://developers.google.com/search/docs/specialty/international/localized-versions) requires each set to include the page itself plus its alternatives, linked in both directions. `Content-Language: fr` on its own can't express that pairing. The annotations point to alternatives. Google still decides which URL to show each searcher, and nothing gets translated for you.

## Testing Content-Language

```bash
curl -sS -D - -o /dev/null https://example.com/welcome.fr.txt
```

For a negotiated route, repeat with different `Accept-Language` values and check both `Content-Language` and `Vary`:

```bash
curl -sS -D - -o /dev/null -H 'Accept-Language: en' https://example.com/welcome
curl -sS -D - -o /dev/null -H 'Accept-Language: fr' https://example.com/welcome
```

When chasing a language mismatch, compare the body as well as the header. A response labeled `fr` that still contains the English message means the app picked the wrong translation. If the origin gets it right but the public URL doesn't, look at `Vary` and your cache configuration. Check the HTML `lang` declaration on its own, too: a correct header won't fix missing or wrong markup.

Same-origin Fetch can read the field:

```javascript
const response = await fetch('/welcome.fr.txt')
if (!response.ok) throw new Error(`HTTP ${response.status}`)
console.log(response.headers.get('Content-Language'))
console.log(await response.text())
```

A missing field comes back as `null`. Keep it that way in any diagnostic tool you write. Turning `null` into a guessed `en` swaps HTTP's all-audiences default for an assumption of your own.

`Content-Language` is a [CORS-safelisted response field](https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name), so a permitted cross-origin Fetch can read it without listing it in `Access-Control-Expose-Headers`. The server still has to allow the cross-origin request in the first place.

## Related Headers

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