In Django, how do you mark a user-facing string for translation in Python code and in a template?
answer
- one import, one underscore
- load the tag library first
- constant string vs sentence with variables
- named placeholders, never f-strings
basics
~20 sIn 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 sIn 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 linesfrom 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
Recall gettext imported as _, {% load i18n %}, and when to pick translate versus blocktranslate.
Explain why placeholders must survive into the msgid, why named ones matter, and blocktranslate's with-binding rules.
Spot sentence fragmentation and f-string marking in review, and set conventions that keep whole sentences translatable.
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.