skip to content

How does Django's built-in set_language view switch a visitor's language, and where does it store the choice?

level: middleimportance: should knowfreq 35%

answer

  1. a POST-only redirect view
  2. the language field plus next
  3. a cookie, not the session
  4. translate_url rewrites the prefix

basics

~20 s

set_language, included from django.conf.urls.i18n, takes a POST with a language field and a next URL, stores the code in the django_language cookie, and redirects to next translated into the new language. A GET changes nothing.

solid answer

~30 s

I include `django.conf.urls.i18n` outside `i18n_patterns`, which exposes `set_language` at `setlang/`. A form POSTs `language` and `next` with a CSRF token. The view validates the code with `check_for_language`, checks `next` is a same-host URL (otherwise it falls back to the `Referer`, then `/`), rewrites it with `translate_url` so `/fr/tours/` becomes `/de/tours/`, and sets the cookie named by `LANGUAGE_COOKIE_NAME`, `django_language` by default. `LANGUAGE_COOKIE_AGE` defaults to `None`, so the cookie lasts only for the browser session unless you set an age. Since Django 4.0 it no longer writes the session, and a GET only redirects without changing the language.

code

python · 9 lines
python
from django.conf.urls.i18n import i18n_patterns
from django.urls import include, path

urlpatterns = [
    path("i18n/", include("django.conf.urls.i18n")),  # /i18n/setlang/, name="set_language"
]
urlpatterns += i18n_patterns(
    path("tours/", include("tours.urls")),
)

go deeper

for a junior

Recall that set_language is included from django.conf.urls.i18n and expects a POST form with language and next fields.

for a middle

Explain the cookie it writes, the next and Referer fallback, translate_url's prefix rewrite and why GET changes nothing.

for a senior

Harden the switcher: persistent and secure cookie settings, safe next handling, and syncing a signed-in user's stored preference with the cookie.

for a principal

Decide where a language preference lives across anonymous visits, accounts and devices, and which source wins when they disagree.

## What set_language is `django.views.i18n.set_language` is Django's ready-made **language switcher endpoint**. It records the visitor's explicit choice so that `LocaleMiddleware` finds it on later requests, then sends the visitor back to a page. You wire it by including Django's i18n URLconf, which defines one route named `set_language`: ```python path("i18n/", include("django.conf.urls.i18n")), # -> /i18n/setlang/ ``` The docs ask for this include to stay **outside** `i18n_patterns()`, so the switcher itself has one language-independent URL. ## What the view does on a POST 1. Reads the `language` field from the POST body and validates it with `check_for_language()`, which requires a well-formed code with an installed translation catalog. 2. Reads `next` from the POST data, falling back to the query string. 3. Checks `next` with `url_has_allowed_host_and_scheme()` against the request's own host (and requires HTTPS when the request is secure). An unsafe or missing value falls back to the `Referer` header, then to `/`. 4. If `next` is set, runs it through `translate_url()`, which resolves the path and reverses it under the new language, so `/fr/tours/lyon/` becomes `/de/tours/lyon/` (translated route segments included). 5. Sets the **language cookie** and returns the redirect. When the request does not accept HTML and carries no `next`, the view answers `204 No Content` instead of redirecting, which suits a switcher driven by JavaScript. ## Where the choice is stored The choice lives in a cookie, configured entirely by settings: | Setting | Default | Effect | |---|---|---| | `LANGUAGE_COOKIE_NAME` | `"django_language"` | the cookie `LocaleMiddleware` reads | | `LANGUAGE_COOKIE_AGE` | `None` | a browser-session cookie, gone when the browser closes | | `LANGUAGE_COOKIE_PATH` | `"/"` | sent for the whole site | | `LANGUAGE_COOKIE_DOMAIN` | `None` | host-only cookie | | `LANGUAGE_COOKIE_SECURE` | `False` | sent over plain HTTP too | | `LANGUAGE_COOKIE_HTTPONLY` | `False` | readable by JavaScript | | `LANGUAGE_COOKIE_SAMESITE` | `None` | no SameSite attribute | The `None` default for the age surprises people: a tourist who picks German and comes back tomorrow in a fresh browser session is back to header negotiation. Set `LANGUAGE_COOKIE_AGE` (in seconds) when the choice should persist. The view does **not** write the session. It stopped doing so in Django 4.0, and `LocaleMiddleware` stopped reading the session in 3.0. ## Why POST only The docs are explicit: because the view changes how the visitor sees the rest of the site, it must be called with **POST**. A GET only performs the redirect to `next` and changes nothing, so crawlers and link prefetchers cannot flip a visitor's language. A POST form in a Django template also needs `{% csrf_token %}`; the CSRF mechanism itself is another topic. ## A switcher form ```django {% load i18n %} <form action="{% url 'set_language' %}" method="post">{% csrf_token %} <input name="next" type="hidden" value="{{ request.get_full_path }}"> <select name="language"> {% get_current_language as CURRENT %} {% get_available_languages as AVAILABLE %} {% for code, name in AVAILABLE %} <option value="{{ code }}"{% if code == CURRENT %} selected{% endif %}>{{ name }}</option> {% endfor %} </select> <button type="submit">{% translate "Go" %}</button> </form> ``` ## Mistakes that show up in review - **A GET link as the switcher.** `<a href="/i18n/setlang/?language=de">` looks like it works in a quick test because it redirects, but the cookie is never set and nothing changes. - **Forgetting `next`.** Without it the view relies on the `Referer` header, which privacy settings and some proxies strip, so visitors land on `/`. - **Building `next` by hand.** `translate_url()` already swaps the language prefix; prepending `/de/` yourself produces `/de/fr/tours/`-style paths that 404. - **Offering unlisted languages.** `check_for_language()` accepts any code with an installed catalog, and Django ships catalogs for many languages; build the `<select>` from `get_available_languages` so the switcher only offers what `LANGUAGES` allows. - **Expecting persistence.** Leaving `LANGUAGE_COOKIE_AGE` at `None` means the choice lasts one browser session. ## Storing a signed-in guest's preference Django has no built-in per-user language field. A common pattern: - add a `language` field to the custom user model; - after login, call `translation.activate()` and set the `LANGUAGE_COOKIE_NAME` cookie on the response from that field; - when the switcher is used by a signed-in user, also save the new value to the field. That keeps the cookie as the one source `LocaleMiddleware` reads while the profile makes the choice follow the user across devices.

  • Why does a visitor's language choice vanish after they restart the browser?
    `LANGUAGE_COOKIE_AGE` defaults to `None`, so `set_language` writes a browser-session cookie that expires when the browser closes. The next visit falls back to URL prefix, `Accept-Language` and `LANGUAGE_CODE`. Setting an age in seconds makes the cookie persistent.
  • What does set_language do with a next parameter pointing at another domain?
    It rejects it: `url_has_allowed_host_and_scheme()` only accepts URLs on the request's own host (and HTTPS when the request is secure). The view falls back to the `Referer` header if that is safe, otherwise to `/`, so the switcher cannot be used as an open redirect.

saying these in an interview costs you the question

  • set_language stores the language in the session
  • A GET link to /i18n/setlang/?language=de switches the language
  • The language cookie persists for a year by default
  • set_language redirects to any next URL it is given
  • The switcher URL should live inside i18n_patterns