skip to content

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%

answer

  1. no middleware outside a request
  2. active language is per thread
  3. save, switch, restore
  4. context manager that survives exceptions

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.

solid answer

~40 s

Django stores the active language per thread or async context, and only `LocaleMiddleware` sets it per request, so in a management command everything renders in `LANGUAGE_CODE` unless the code activates a language. Calling `translation.activate()` in a loop leaves the last guest's language active, so a following staff summary, or the next job on a reused worker thread, renders in the wrong language, and an exception skips any manual restore. `translation.override(code)` saves `get_language()`, activates the code, and restores the saved language in `__exit__` even when an exception propagates. Inside the block I render the subject, the template and any `reverse()` links, because lazy strings and prefixed URLs pick up the language at evaluation time.

code

python · 24 lines
python
from datetime import timedelta

from django.core.mail import send_mail
from django.core.management.base import BaseCommand
from django.template.loader import render_to_string
from django.utils import timezone, translation
from django.utils.translation import gettext

from bookings.models import Booking


class Command(BaseCommand):
    help = "Email tomorrow's booking confirmations in each guest's language"

    def handle(self, *args, **options):
        tomorrow = timezone.localdate() + timedelta(days=1)
        for booking in Booking.objects.filter(start_date=tomorrow).select_related("guest"):
            with translation.override(booking.guest.language):
                send_mail(
                    subject=gettext("Your tour starts tomorrow"),
                    message=render_to_string("bookings/reminder.txt", {"booking": booking}),
                    from_email=None,
                    recipient_list=[booking.guest.email],
                )

go deeper

for a junior

Recall that translation.override is a context manager that switches the active language for a block and switches it back afterwards.

for a middle

Explain that the active language is per thread, that only LocaleMiddleware sets it per request, and what override saves and restores.

for a senior

Diagnose wrong-language emails from leaked activate() calls, lazy strings evaluated outside the block, and state bleeding between jobs on reused worker threads.

for a principal

Define where a recipient's language is captured and validated so every out-of-request message, from email to push, renders consistently.

## Where Django keeps the active language Django's translation functions (`gettext`, lazy strings when they are rendered, template `{% translate %}`, `reverse()` under `i18n_patterns`) all read the **active language**. That value is stored in an `asgiref.local.Local`, so it is scoped to the current thread, or to the current task context under ASGI. Three calls change it: - `translation.activate(code)` sets it. - `translation.deactivate()` removes it, so lookups fall back to the default translation. - `translation.deactivate_all()` installs a null translation, so strings render as their original source text. When nothing is active, `translation.get_language()` returns `LANGUAGE_CODE`. ## Outside a request, nobody picks a language for you `LocaleMiddleware` activates a language per request. A **management command**, a scheduled job or a background worker never passes through middleware, so: - everything renders in `LANGUAGE_CODE` unless the code activates something itself; - `BaseCommand` leaves translations as they are by default; the `@no_translations` decorator on `handle()` is the opt-in that deactivates them for the command's duration. ## Why activate() leaks Consider a nightly command that sends booking confirmations: ```python for booking in bookings: translation.activate(booking.guest.language) send_confirmation(booking) send_staff_summary(bookings) # rendered in the LAST guest's language ``` The last guest's language is still active when the staff summary renders. The same leak happens across jobs in a long-lived worker that reuses its thread: job B inherits job A's language. An exception in `send_confirmation` also skips any restore code written after it. The bug is intermittent and data-dependent, which is why it survives review. ## translation.override: scoped activation `django.utils.translation.override(language, deactivate=False)` is a context manager and a decorator: 1. On enter it saves `get_language()` and activates `language`, or calls `deactivate_all()` if `language` is `None`. 2. On exit, **including when an exception propagates**, it re-activates the saved language, or calls `deactivate()` if `deactivate=True` was passed, or `deactivate_all()` if translations had been deactivated with `deactivate_all()` before the block. ```python from django.utils import translation for booking in bookings: with translation.override(booking.guest.language): send_confirmation(booking) send_staff_summary(bookings) # back to the language active before the loop ``` ## What must happen inside the block - **Lazy strings render at evaluation time.** A subject defined with `gettext_lazy` at module level is only turned into text when `str()` is called on it, so it must be rendered inside the `override` block to come out in the guest's language. - **Templates** rendered with `render_to_string()` inside the block use the overridden language, and a template can switch a section itself with the `{% language "de" %}` block tag. - **URLs** reversed inside the block get that language's prefix under `i18n_patterns`, so the email links to `/de/bookings/…`. - **Dates and numbers** formatted by the template engine follow the active language's locale formats as well. | Tool | Scope | Restores previous language | |---|---|---| | `translation.activate()` | until changed | no | | `translation.override()` | the `with` block or decorated call | yes, also on exceptions | | `{% language %}` tag | the enclosed template section | yes | | `@no_translations` | one management command's `handle()` | yes | ## Spotting and preventing the leak - **Grep for bare `activate(`** in commands, jobs and signal receivers; outside middleware, almost every call should be an `override` block. - **Assert the language after the job** in a test: run the command, then check that `translation.get_language()` still returns what it did before. - **Decorate helpers** that always render in a given language with `@translation.override("en")`, for example an internal report that must stay in English regardless of the caller. - **Mind async code.** Because the active language lives in an `asgiref` `Local`, each task context has its own value; the leak pattern is the same within one context, and `override` fixes it the same way. ## Choosing the language to override with Store the guest's language on the booking or the user, captured from `request.LANGUAGE_CODE` when they booked, and validate it against `LANGUAGES` before use. Time zone activation for the same email is a separate mechanism with its own API.

  • What does translation.override(None) do?
    It calls `deactivate_all()` for the block, installing a null translation so every string renders as its original source text. That is useful when you need the untranslated value, for example when writing a stable log line or a machine-readable identifier. On exit the previous language is restored.
  • Why can a subject defined with gettext_lazy at module level still come out in the right language inside override?
    A lazy string is only translated when it is converted to text, not when it is defined. If `str()` happens inside the `override` block, for example when `send_mail` builds the message, the active language at that moment, the guest's, is used. Converting it before the block freezes the wrong language.

Borrowing a colleague's keyboard layout for one document: override switches it for the document and switches it back when you leave the desk, while activate switches it and walks away, leaving the next person typing in the wrong layout.

saying these in an interview costs you the question

  • Management commands run in the visitor's browser language
  • activate() is automatically undone when the function returns
  • override() changes the language for the whole process
  • LocaleMiddleware also runs for management commands
  • A subject already converted to str is retranslated inside override