skip to content

Translation & Locales

Django's gettext-based translation: marking strings lazily or eagerly, building .po catalogs, picking the active language and handling time zones. Interviewers probe lazy strings and USE_TZ.

part ofDjangooverview, primer and where to startread it →
on this pageshow

explore

questions

20

In Django, how do you mark a user-facing string for translation in Python code and in a template?

level: juniorimportance: must knowfreq 58%

answer

  1. one import, one underscore
  2. load the tag library first
  3. constant string vs sentence with variables
  4. named placeholders, never f-strings

basics

~20 s

In Python, wrap the string in gettext(), usually imported as _. In a template, add {% load i18n %}, then use {% translate %} for a constant string or {% blocktranslate %} for a sentence with variables. Use named placeholders, never f-strings.

solid answer

~40 s

In Python you call `django.utils.translation.gettext`, conventionally imported as `_`, and `_("Add to cart")` returns the text in the currently active language. Variables go in with named interpolation after translation, `_("Hello %(name)s") % {"name": name}`. An f-string would be substituted before `gettext()` sees it, so there is nothing to look up in the catalog. In templates you first `{% load i18n %}` in every template that translates, even one that extends a parent that already loaded it. `{% translate "Checkout" %}` handles a constant string. Any sentence with a variable needs `{% blocktranslate with name=product.name %}Buy {{ name }}{% endblocktranslate %}`, because `translate` cannot mix variables into its string. Marking only tags the text: extraction and compiled catalogs are a separate step.

code

python · 10 lines
python
from django.shortcuts import render
from django.utils.translation import gettext as _


def order_confirmation(request, order):
    message = _("Thank you, %(name)s! Order %(number)s is confirmed.") % {
        "name": request.user.first_name,
        "number": order.number,
    }
    return render(request, "shop/confirmation.html", {"message": message, "order": order})

go deeper

for a junior

Recall gettext imported as _, {% load i18n %}, and when to pick translate versus blocktranslate.

for a middle

Explain why placeholders must survive into the msgid, why named ones matter, and blocktranslate's with-binding rules.

for a senior

Spot sentence fragmentation and f-string marking in review, and set conventions that keep whole sentences translatable.

for a principal

Make translatability part of the definition of done, with lint checks for f-strings inside gettext calls.

## Marking strings in Python Django's translation functions live in `django.utils.translation`. The basic one is **`gettext(message)`**, which returns `message` translated into the **currently active language**. If the active language has no translation for it, Django falls back to the `LANGUAGE_CODE` language's translation, and then to the original text. By convention you import it as `_`: ```python from django.utils.translation import gettext as _ def cart_view(request): title = _("Your shopping cart") ``` Django deliberately does **not** install `_` as a builtin (the standard library's `gettext.install()` does). You import it explicitly in each module, which makes you choose between `gettext` and `gettext_lazy` file by file. Only single-argument functions may be aliased to `_`, which in practice means `gettext` and `gettext_lazy`. The extraction tool (`xgettext`, driven by `makemessages`) treats `_` as a one-argument keyword. ## Placeholders: why f-strings break translation The catalog is keyed by the **exact source string**. The string must therefore reach `gettext()` with its placeholders still in it: - **Good:** `_("Hello %(name)s, your order has shipped") % {"name": customer.first_name}` - **Good:** `_("Hello {name}").format(name=customer.first_name)` - **Bad:** `_(f"Hello {customer.first_name}")`. The f-string is evaluated first, so `gettext()` receives `"Hello Anna"`, which no catalog contains, and the text stays in the source language. Use **named** placeholders whenever there is more than one. A German or French translator may need to reorder them, and positional `%s` placeholders cannot be reordered. Strings built at runtime from variables (`_(some_variable)`) are also invisible to extraction. They only translate if the same literal was marked somewhere else. ## Marking strings in templates Templates use the `i18n` tag library, loaded with `{% load i18n %}` in **every** template that uses it. Template inheritance does not carry loaded libraries into child templates. | Tag | Use it for | Example | |---|---|---| | `{% translate "…" %}` | a constant string, or a variable whose value is a msgid | `{% translate "Checkout" %}` | | `{% translate "…" as var %}` | reusing the translation in several places | `{% translate "Free shipping" as promo %}` | | `{% translate "…" context "…" %}` | disambiguating the same word | `{% translate "Order" context "verb" %}` | | `{% blocktranslate %}…{% endblocktranslate %}` | sentences containing variables | `{% blocktranslate with name=product.name %}Buy {{ name }} today{% endblocktranslate %}` | Rules for `blocktranslate` that interviewers like to probe: 1. Only plain names can be substituted. Attribute lookups and filters must be bound first with `with name=product.name` or `with price=item.price|floatformat:2`. 2. Other block tags such as `{% if %}` or `{% for %}` are not allowed inside it. Neither is `{% url %}`: resolve the URL beforehand with `{% url 'cart' as cart_url %}`. 3. `asvar name` stores the result instead of printing it. The older `{% trans %}` and `{% blocktrans %}` names still work as aliases, but `translate` and `blocktranslate` have been the documented names since Django 3.1. The `u`-prefixed Python functions (`ugettext`, `ugettext_lazy`) were removed in 4.0. ## Marking without translating: `gettext_noop()` Sometimes a string must be **stored or exchanged in the source language** and translated only when it is shown. Examples are an order status code written to the database, or a message passed between systems. `gettext_noop("Shipped")` marks the literal so extraction picks it up, but returns it **untranslated**. Later, at display time, `gettext(order.status_label)` looks up that same msgid in the shopper's language. Without the no-op marker, `makemessages` would never see the literal, because the display-time call only receives a variable. The template equivalent is `{% translate "..." noop %}`, which resolves the value but skips translation. ## What marking does and does not do - Marking is only a **flag** for extraction plus a **lookup** at runtime. Nothing is translated until the strings are extracted into `.po` files, translated and compiled. That workflow is a separate topic. - `USE_I18N` defaults to `True`. With it off, the translation machinery is replaced by no-ops and marked strings come back unchanged. - The language used is whatever is **active** when `gettext()` runs. Deciding which language that is (URL prefix, cookie, `Accept-Language`) is also a separate topic. - Translated text in templates is **not** HTML-escaped when rendered, so a translator can add emphasis markup. Variables substituted inside `blocktranslate` are still auto-escaped like any other template variable. ## Common mistakes - Marking `_(f"...")` or concatenating fragments: `_("Total") + ": " + price` splits one sentence into pieces a translator cannot reorder. - Forgetting `{% load i18n %}` in a child template, which gives an "invalid block tag" error. - Putting `{{ product.name }}` straight into a `blocktranslate` body without binding it first. - Calling `gettext()` at module or class level, which is where `gettext_lazy()` belongs.

  • Why does Django make you import _ explicitly instead of installing it as a builtin?
    Because each module should consciously choose between `gettext` and `gettext_lazy`: a models or forms module usually needs the lazy one, a view the eager one. A global `_` would hide that choice. It would also clash with the interactive shell and doctests, where `_` means the previous result.
  • A template shows {% translate "Welcome {{ user.first_name }}" %} literally with the braces. Why, and what is the fix?
    `{% translate %}` only translates a constant string or the value of one variable. It never substitutes variables inside its string, so the braces are output as text. Use `{% blocktranslate with name=user.first_name %}Welcome {{ name }}{% endblocktranslate %}`, which turns the body into a msgid with a `%(name)s` placeholder and substitutes after translating.

saying these in an interview costs you the question

  • _(f"Hello {name}") is translated correctly as long as the msgid exists.
  • Django installs _ as a builtin, so no import is needed.
  • {% load i18n %} in a base template covers every child template.
  • {% translate %} can interpolate {{ variables }} inside its string.
  • Marking a string with _() is enough for it to appear in French.
open as a page

In a Django project with USE_TZ enabled, why use django.utils.timezone.now() instead of datetime.datetime.now()?

level: juniorimportance: must knowfreq 60%

basics

~20 s

With USE_TZ on, timezone.now() returns an aware datetime in UTC, matching how Django stores datetimes. datetime.now() is naive local time: saving it makes Django warn and assume TIME_ZONE, and comparing it with aware values raises TypeError.

open as a page

In Django, what do makemessages and compilemessages each do, and why does a project need both .po and .mo files?

level: middleimportance: must knowfreq 52%

basics

~20 s

makemessages scans source files for marked strings and creates or updates a human-editable .po file per language. compilemessages turns each .po into the binary .mo file that Django actually loads at runtime. Translators edit .po; Django reads only .mo.

open as a page

In Django, in what order does LocaleMiddleware look for a request's language, and what happens when nothing matches?

level: middleimportance: must knowfreq 55%

basics

~10 s

LocaleMiddleware tries the URL language prefix (only under i18n_patterns), then the django_language cookie, then the Accept-Language header by q-value, and finally LANGUAGE_CODE. Each candidate must match a language in LANGUAGES.

open as a page

In Django, why must a model field's verbose_name or a form field's label use gettext_lazy() instead of gettext()?

level: middleimportance: must knowfreq 66%

basics

~20 s

Model and form fields are class attributes, evaluated once when the module is imported, before any request activates a language. gettext() would translate at that moment. gettext_lazy() returns a proxy that translates whenever it is rendered, in the language active for that request.

open as a page

With USE_TZ on, what is the difference between Django's TIME_ZONE setting and the current time zone, and what uses each?

level: middleimportance: must knowfreq 50%

basics

~20 s

Django stores datetimes in UTC. TIME_ZONE is the default zone: the fallback and the assumption for naive values. The current time zone, set with timezone.activate() per request, is what templates render in and forms parse input in.

open as a page

In a Django-generated .po file, what do the msgid, msgstr, msgctxt, #., #: and #, lines of an entry mean?

level: juniorimportance: should knowfreq 30%

basics

~20 s

msgid is the source string and msgstr its translation; msgctxt, from pgettext, separates identical msgids. #. lines carry Translators: comments from the code, #: lines give source locations, and #, lines hold flags such as fuzzy or python-format.

open as a page

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

level: juniorimportance: should knowfreq 42%

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.

open as a page

In a Django project, how do LOCALE_PATHS and each app's locale/ directory differ, and which wins when both translate the same string?

level: middleimportance: should knowfreq 34%

basics

~10 s

An app's locale/ directory ships translations with that app, while LOCALE_PATHS lists project-level directories. At runtime LOCALE_PATHS wins, earlier entries first, then apps' locale/ directories in INSTALLED_APPS order, then Django's own catalogs.

open as a page

On a Django tourism site using i18n_patterns, what does prefix_default_language=False change, and what trade-off comes with it?

level: middleimportance: should knowfreq 45%

basics

~10 s

i18n_patterns() prefixes wrapped URLs with the active language, as in /fr/tours/. With prefix_default_language=False the LANGUAGE_CODE language is served unprefixed at /tours/, but unprefixed URLs then always render LANGUAGE_CODE, ignoring cookie and Accept-Language.

open as a page

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%

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.

open as a page

In Django, how do you translate a count-dependent message, such as the number of cart items, in Python and in templates?

level: middleimportance: should knowfreq 42%

basics

~20 s

Use ngettext(singular, plural, count) in Python, then interpolate the count, and {% blocktranslate count counter=... %}…{% plural %}…{% endblocktranslate %} in templates. Never branch on count == 1 yourself: the catalog's plural rule picks the right form per language.

open as a page

How do you make a Django appointments site show and accept times in each signed-in user's own time zone?

level: middleimportance: should knowfreq 42%

basics

~10 s

Store each user's zone name, then in a middleware call timezone.activate(ZoneInfo(name)), or deactivate() when unknown. Django then renders aware datetimes and parses form input in that zone, while storage stays UTC.

open as a page

A Django museum-guide site deploys an updated German .po file, yet several reworded exhibit texts still appear in English — what do you check, and in what order?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Check that compilemessages produced a fresh .mo and that it was deployed, that the changed entries are not marked fuzzy, that each msgid still matches the current source string, that the catalog sits in a searched locale directory, and that workers were restarted.

open as a page

A Django management command emails booking confirmations in each guest's language; why use translation.override() rather than translation.activate() there?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Outside a request no LocaleMiddleware runs, so the active language is LANGUAGE_CODE or whatever was last activated. translation.override(code) activates a language for one block and restores the previous one on exit, even on exceptions; bare activate() leaks into later work.

open as a page

On a Django storefront, French shoppers still see some German checkout labels although the code uses gettext_lazy() — where is the lazy string being evaluated too early?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Somewhere a lazy proxy is forced into a str at import time: concatenation, %, an f-string, .format(), str() or a string method in module or class scope. The frozen text is in the import-time language. Keep composition lazy with format_lazy() or lazy(), or move it into function scope.

open as a page

A Django clinic dashboard filters today's appointments with naive midnight bounds and misses early bookings for Singapore users; what went wrong and how do you fix it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Naive bounds in a DateTimeField lookup trigger a RuntimeWarning and are interpreted in TIME_ZONE, not the user's activated zone, and date.today() is the server's date. Build aware bounds from timezone.localdate(), or filter with __date, which uses the current zone.

open as a page

In Django, how does the JavaScriptCatalog view make translations available to front-end JavaScript, and what should you watch out for?

level: middleimportance: nice to knowfreq 22%

basics

~20 s

django.views.i18n.JavaScriptCatalog is a view that returns a script defining gettext(), ngettext(), pgettext(), interpolate() and the active language's djangojs catalog. Watch that it is regenerated on every request, so cache it, and that it must sit inside i18n_patterns when your URLs use them.

open as a page

In Django, what problem does pgettext() solve, and how do you supply the same context marker in a template?

level: middleimportance: nice to knowfreq 24%

basics

~20 s

pgettext(context, message) separates identical source strings that need different translations, such as "Order" the noun and "Order" the button. Each context becomes its own catalog entry. Templates pass it with the context keyword on {% translate %} or {% blocktranslate %}.

open as a page

Why does setting DATE_FORMAT in a Django project's settings often change nothing, and how does FORMAT_MODULE_PATH help?

level: middleimportance: nice to knowfreq 22%

basics

~20 s

Django always formats dates through the active locale's formats module, which defines DATE_FORMAT, so the setting only applies when the locale has none. FORMAT_MODULE_PATH points to your own per-locale formats modules, which take precedence over Django's.

open as a page