skip to content

In Django settings, what is the difference between LANGUAGE_CODE and LANGUAGES, and what does each default to?

level: juniorimportance: should knowfreq 42%

answer

  1. one default versus a whitelist
  2. the fallback of last resort
  3. default list is every shipped translation
  4. en-us out of the box

basics

~20 s

LANGUAGE_CODE is the single fallback language, "en-us" by default, used when no request data selects another. LANGUAGES is the whitelist request-based selection may choose from; it defaults to every language Django ships, so real projects narrow it.

solid answer

~40 s

`LANGUAGE_CODE` is one language code, `"en-us"` by default. Without `LocaleMiddleware` the whole site runs in it; with the middleware it is the last fallback after the URL prefix, the `django_language` cookie and the `Accept-Language` header. `LANGUAGES` is a list of `(code, name)` tuples that defaults to every language Django has translations for. `LocaleMiddleware` only activates a language that matches an entry in it, so I narrow it to the languages the project actually translated; otherwise a Portuguese browser gets Django's admin and form errors in Portuguese around untranslated project text. The `translation.E004` system check fails when `LANGUAGE_CODE` has no match in `LANGUAGES`.

code

python · 8 lines
python
from django.utils.translation import gettext_lazy as _

LANGUAGE_CODE = "en"
LANGUAGES = [
    ("en", _("English")),
    ("fr", _("French")),
    ("de", _("German")),
]

go deeper

for a junior

Recall that LANGUAGE_CODE is one fallback string defaulting to en-us, while LANGUAGES is a list of code and name pairs that limits which languages can be picked.

for a middle

Explain how LocaleMiddleware uses LANGUAGES as its filter and LANGUAGE_CODE as its last resort, including variant matching such as de-at to de.

for a senior

Show why the default LANGUAGES list causes mixed-language pages and phantom URL prefixes, and why the fallback should be the source-string language.

for a principal

Frame the languages list as a product commitment: every listed code is a promise of complete translations, reviewed copy and SEO-visible URLs the team must maintain.

## Two settings, two different jobs Django's translation machinery reads two settings that sound alike but answer different questions. `LANGUAGE_CODE` answers *"which language does this installation speak when nothing else decides?"*. `LANGUAGES` answers *"which languages is a visitor allowed to be switched to?"*. | Setting | Type | Default in `global_settings.py` | Answers | |---|---|---|---| | `LANGUAGE_CODE` | one language code string | `"en-us"` | the fallback, and the site language when no per-request selection runs | | `LANGUAGES` | list of `(code, name)` tuples | every language Django ships a translation for | the whitelist that request-based selection may choose from | A **language code** here is the lowercase, hyphenated form Django uses throughout: `en-us`, `pt-br`, `de`, `zh-hans`. ## LANGUAGE_CODE: the last resort and the static language - Without `LocaleMiddleware` in `MIDDLEWARE`, Django does **static translation**: every request is served in `LANGUAGE_CODE` and there is no per-visitor choice at all. - With `LocaleMiddleware`, `LANGUAGE_CODE` becomes the **final fallback** after the URL prefix, the language cookie and the `Accept-Language` header have all failed to produce a supported language. - With `i18n_patterns(..., prefix_default_language=False)`, it is also the language served on URLs that carry no language prefix. - The system checks validate it: `translation.E001` fires for a malformed value and `translation.E004` fires when neither `LANGUAGE_CODE` nor a generic variant of it (for example `en` for `en-us`) appears in `LANGUAGES`. ## LANGUAGES: the whitelist - `LocaleMiddleware` only ever activates a language that matches an entry in `LANGUAGES`. A cookie, header or URL prefix naming anything else is skipped and the next source is tried. - Matching tolerates regional variants: a request for `de-at` is served in `de` when only `de` is listed. - The default list is long because Django's own strings (admin, form validation messages, the auth views) are translated into many languages. That default is convenient for a quick start and wrong for most real projects. - The same list feeds the `{% get_available_languages %}` template tag, which is what a language switcher usually iterates over. - The language names in the tuples are normally wrapped in `gettext_lazy` so the switcher shows them translated; how that marking works is its own topic. ## Why a real project narrows LANGUAGES Leaving the default in place on a site that translated only English, French and German causes three visible problems: 1. **Mixed-language pages.** A visitor whose browser prefers Portuguese gets Django's built-in strings, such as form errors, in Portuguese, while every project string with no Portuguese translation falls back to its source text. 2. **Phantom URL prefixes.** Under `i18n_patterns`, any code in `LANGUAGES` is a valid prefix, so `/pt-br/tours/` resolves and serves a half-translated duplicate of your content. 3. **An unusable switcher.** A dropdown built from `get_available_languages` lists dozens of languages the site does not actually support. ## A configuration for a three-language tourism site ```python from django.utils.translation import gettext_lazy as _ LANGUAGE_CODE = "en" LANGUAGES = [ ("en", _("English")), ("fr", _("French")), ("de", _("German")), ] USE_I18N = True # already the default ``` Here `LANGUAGE_CODE` appears in `LANGUAGES`, so `translation.E004` stays quiet, and only three languages can ever be selected. Keep `LANGUAGE_CODE` the language your source strings are written in unless you have a reason not to; that way the fallback always renders complete text. ## How the two settings meet at request time With the three-language settings above and `LocaleMiddleware` enabled, the same two settings produce these outcomes: | Request carries | Language activated | Why | |---|---|---| | `Accept-Language: fr-CH` | `fr` | `fr-ch` is unlisted, its generic `fr` is listed | | `Accept-Language: pt-BR` | `en` | Portuguese is not in the narrowed `LANGUAGES`, so the fallback wins | | cookie `django_language=de` | `de` | a listed code from the stored choice | | nothing at all | `en` | `LANGUAGE_CODE` | The second row is the point of narrowing: the Portuguese visitor now sees a complete English page instead of a patchwork. Note that `LANGUAGE_CODE` is never *rejected* the way request data is; at most it is mapped to a listed variant, and it is the answer when filtering leaves nothing. ## Related settings worth recognising - `USE_I18N` (default `True`) switches the translation machinery on; with it off, `i18n_patterns` returns the plain URL list with no prefixes. - `LANGUAGES_BIDI` lists the right-to-left codes (Hebrew, Arabic and others), which `get_language_bidi()` and the `LANGUAGE_BIDI` template variable read. - `LANGUAGE_COOKIE_NAME` (default `django_language`) names the cookie in which a visitor's explicit choice is stored.

  • What happens when LANGUAGE_CODE is "en-us" but LANGUAGES lists only "en" and "fr"?
    Nothing breaks. Django matches `en-us` to its generic variant `en`, which is listed, so the `translation.E004` check passes and the fallback resolves to `en`. The check only fails when neither the code nor a generic form of it is in `LANGUAGES`.
  • Where does a Django project without LocaleMiddleware get its language from?
    Only from `LANGUAGE_CODE`. That is static translation: every request renders in the one configured language, and the cookie, the `Accept-Language` header and URL prefixes are never consulted because nothing reads them.

saying these in an interview costs you the question

  • LANGUAGES defaults to an empty list, so nothing is selectable
  • LANGUAGE_CODE alone makes the site switch languages per visitor
  • Any language in a browser header is served even if unlisted
  • Default LANGUAGES is harmless because untranslated strings stay English
  • LANGUAGE_CODE must match a LANGUAGES entry exactly, variants included