---
django: main at 4fab678a0739d54401ccee7eb587553657c9f76e (2026-09-26), on its way to 6.2
part: Infrastructure
contents: translating, catalogues, active-language, lazy, choosing, views, formats, writing-values, time-zones, commands
---

# Internationalization and time zones

Django serves each request in a language and a time zone of its own, from code and templates written once: a **message** marked for translation is looked up as it is used, in the translations of the **active language**; numbers and dates are written in that language's formats; and a datetime, stored as an instant, is shown in the **current time zone**. `django.utils.translation`, `django.utils.formats` and `django.utils.timezone` do the three, and `LocaleMiddleware` chooses each request's language.

A process serves many visitors at once, and each may want a different language. The strings in the code are written once, in the language of the source, and a module is imported, and a template ordinarily compiled, once for the whole process. So a translation cannot be made when a module is imported or a template compiled. Django makes it at the last moment: a message is looked up as it is used, in the translations of whichever language is active then, and one marked lazily at import stays in the source language until something writes it out.

The active language is neither a setting nor an argument. It is a value held for the thread, or for the task in asynchronous code, which `django.utils.translation.activate` sets and every lookup reads (`django/utils/translation/trans_real.py:activate`). The formats in which a number or a date is written follow the same value. The time zone in which a datetime is shown is a second value, held the same way and set by `django.utils.timezone.activate` (`django/utils/timezone.py:activate`). A view or a template never passes either one on: whatever reads them reads them where it stands.

Two settings switch the machinery off. With `USE_I18N` false, the functions of `django.utils.translation` that translate a message or choose a language are bound, the first time each is called, to those of a module that does no translating: its `gettext` gives back the message it was given, and its `get_language` gives `LANGUAGE_CODE` (`django/utils/translation/trans_null.py`). With `USE_TZ` false, Django's datetimes carry no time zone. Formatting has no switch of its own: a value is written in the active language's formats unless the caller asks otherwise.

## The objects

| object | what it is | when it acts |
|---|---|---|
| `DjangoTranslation` | one language's translations: Django's own **catalogue** (a compiled file of translations), each installed application's and those of `LOCALE_PATHS`, layered, with the translations of `LANGUAGE_CODE` behind them | made the first time the language is asked for, and ordinarily kept for the life of the process |
| the active language | the translations installed for the thread, a `DjangoTranslation` unless translation was switched off, by `django.utils.translation.activate` or `django.utils.translation.override`; `django.utils.translation.get_language` gives its code | read by every lookup and by the formats |
| `django.utils.translation.gettext`, `django.utils.translation.ngettext`, `django.utils.translation.pgettext` | look a message up in the active language's translations: as it is, with a number that chooses a plural form, or with a context that tells two uses of one string apart | when called |
| `django.utils.translation.gettext_lazy` and the other lazy functions | return an object that holds the message and looks it up each time it is used as a string | when the string is used |
| `LocaleMiddleware` | chooses a request's language, from its path, a cookie, its Accept-Language header or `LANGUAGE_CODE`, and activates it | on the way in; on the way out it adds Content-Language |
| the **format modules** | a locale's `formats.py`, under `django/conf/locale/` or a package that `FORMAT_MODULE_PATH` names: how dates and numbers are written in it | read by `get_format` for the active language, and cached |
| `django.utils.numberformat`, `django.utils.dateformat` | write a number with a locale's separators, and a date by a format string of letters after those of PHP's date function | when a number is localized or a date written |
| the current time zone | the `tzinfo` installed for the thread by `django.utils.timezone.activate` or `django.utils.timezone.override`, or else the **default time zone**, `TIME_ZONE` | read by `django.utils.timezone.localtime`, `make_aware`, the templates and the forms |
| makemessages, compilemessages | collect the messages of a project's files into `.po` files, and compile those into `.mo` catalogues, with the GNU gettext programs | when run |

*The objects of Django's internationalization. Translation is in `django/utils/translation/`, formats in `django/utils/formats.py` and the locale packages under `django/conf/locale/`, time zones in `django/utils/timezone.py`.*

## One request in three languages

The harbour office's site lists the ships expected in port, in English, Dutch and German. Its settings install the one middleware and an application, office:

```python
# harbour/settings.py, in part
INSTALLED_APPS = ["office"]
MIDDLEWARE = ["django.middleware.locale.LocaleMiddleware"]
LANGUAGE_CODE = "en"
LANGUAGES = [("en", "English"), ("nl", "Dutch"), ("de", "German")]
USE_TZ = True
TIME_ZONE = "Europe/Amsterdam"
```

The view's title is translated lazily, and its ships arrive at instants given in UTC:

```python
# harbour/office/views.py
from datetime import UTC, datetime

from django.shortcuts import render
from django.utils.translation import gettext_lazy as _

TITLE = _("Arrivals")
SHIPS = [
    {"name": "Endeavour", "arrives": datetime(2026, 3, 29, 9, 30, tzinfo=UTC), "tonnage": 1234.5},
    {"name": "Resolution", "arrives": datetime(2026, 3, 29, 14, 5, tzinfo=UTC), "tonnage": 980},
]


def arrivals(request):
    return render(request, "office/arrivals.html", {"title": TITLE, "ships": SHIPS})
```

Its template, `"office/arrivals.html"`, writes the title, a sentence whose form depends on a count, and each ship's time and tonnage:

```text
{% load i18n %}<h1>{{ title }}</h1>
<p>{% blocktranslate count counter=ships|length %}One ship expected.{% plural %}{{ counter }} ships expected.{% endblocktranslate %}</p>
<ul>
{% for ship in ships %}  <li>{{ ship.name }}: {{ ship.arrives }}, {{ ship.tonnage }} t</li>
{% endfor %}</ul>
```

The application has a Dutch catalogue, `"office/locale/nl/LC_MESSAGES/django.mo"`, compiled from a `.po` file that holds, among others, these two entries; it has no German one:

```text
# office/locale/nl/LC_MESSAGES/django.po
msgid "Arrivals"
msgstr "Aankomsten"

msgid "One ship expected."
msgid_plural "%(counter)s ships expected."
msgstr[0] "Eén schip verwacht."
msgstr[1] "%(counter)s schepen verwacht."
```

A run of the example, with Django's test client making three requests for the same page:

```text
GET /arrivals/, with no cookie and no Accept-Language
200 OK
Content-Type: text/html; charset=utf-8
Vary: Accept-Language
Content-Language: en
<h1>Arrivals</h1>
<p>2 ships expected.</p>
<ul>
  <li>Endeavour: March 29, 2026, 11:30 a.m., 1234.5 t</li>
  <li>Resolution: March 29, 2026, 4:05 p.m., 980 t</li>
</ul>

GET /arrivals/, with Accept-Language: nl-BE,nl;q=0.9,en;q=0.8
200 OK
Content-Type: text/html; charset=utf-8
Vary: Accept-Language
Content-Language: nl
<h1>Aankomsten</h1>
<p>2 schepen verwacht.</p>
<ul>
  <li>Endeavour: 29 maart 2026 11:30, 1234,5 t</li>
  <li>Resolution: 29 maart 2026 16:05, 980 t</li>
</ul>

GET /arrivals/, with the cookie django_language=de and Accept-Language: nl
200 OK
Content-Type: text/html; charset=utf-8
Vary: Accept-Language
Content-Language: de
<h1>Arrivals</h1>
<p>2 ships expected.</p>
<ul>
  <li>Endeavour: 29. März 2026 11:30, 1234,5 t</li>
  <li>Resolution: 29. März 2026 16:05, 980 t</li>
</ul>
```

The view, the template and its compiled form were the same for all three. What differed was the language that `LocaleMiddleware` activated before the view ran. For the first request it found nothing to go on and took `LANGUAGE_CODE`. For the second it read the header, found `"nl-be"` missing from `LANGUAGES`, and took the more general `"nl"`. For the third it found a language in the cookie that the view `set_language` sets, and did not read the header at all (`django/utils/translation/trans_real.py:get_language_from_request`).

Everything on the page that depends on the language was decided as it was written. The title is a lazy translation, made when the module was imported, before any language was active; it became "Aankomsten" when the template turned it into a string. `{% blocktranslate %}` asked the catalogue for the form that goes with two. The times and the tonnage were **localized**, written in the active language's formats, its order of day and month and its decimal comma, with its names of the months.

The hours are another matter. Each arrival is an instant, 09:30 and 14:05 in UTC, and the template showed it in the current time zone, Amsterdam's, two hours ahead of UTC at those hours, in all three languages. Nothing chose that zone for the request: Django has no middleware that activates one, so the current zone was the default, `TIME_ZONE`. One value of the page passed through all three services:

```text figure=one-value
{{ ship.arrives }} in a request whose language is nl

the value          datetime(2026, 3, 29, 9, 30, tzinfo=UTC)
template_localtime reads the current time zone, Europe/Amsterdam   ->  2026-03-29 11:30+02:00
localize           a datetime: date_format(value, "DATETIME_FORMAT")
  get_format       reads the active language, nl: django.conf.locale.nl.formats   ->  "j F Y H:i"
  dateformat       j    F       Y      H:i
                   29   maart   2026   11:30
                   F is the month's name: a lazy translation, read in the active language
written            "29 maart 2026 11:30"
```

*How the template writes one arrival for a Dutch request: the current time zone decides the hour, and the active language the format and the month's name.*

A template writes every value through `render_value_in_context`, which converts an aware datetime to the current time zone with `template_localtime` and then localizes it (`django/template/base.py:render_value_in_context`). German shows the layers of a language's translations. The office has no German catalogue, so its title and its sentence stayed English; "März" came from Django's own German catalogue, which the translations of a language always include where Django has one.

On the way out, `LocaleMiddleware.process_response` set Content-Language to the active language's code and added `"Accept-Language"` to `"Vary"` (`django/middleware/locale.py:LocaleMiddleware.process_response`). It did not deactivate the language. After the third response the thread still had German active, and code that runs later in that thread sees it, until something activates another ([The active language](i18n/active-language.md)).

## Translation

A message is looked up in a `DjangoTranslation`, of which a process has one for each language it has used. It is made the first time the language is asked for, from every catalogue Django can find for it: Django's own under `django/conf/locale/`, the `locale` directory of each installed application, and each directory of `LOCALE_PATHS`. They are layered so that `LOCALE_PATHS` come before the applications, an application listed earlier in `INSTALLED_APPS` before one listed later, and Django's own beneath them all. Behind them stand the translations of `LANGUAGE_CODE`, which answer what the language's own catalogues lack, except where the language is `LANGUAGE_CODE` itself or a code beginning `"en"`; a regional code such as `"pt-br"` has its language's catalogue, `"pt"`, before them ([Message catalogues: `DjangoTranslation`](i18n/catalogues.md)) (`django/utils/translation/trans_real.py:DjangoTranslation.__init__`).

`django.utils.translation.gettext` looks a message up in the active translation, or, with none active, in that of `LANGUAGE_CODE`, and gives the message back, its line endings made `"\n"`, when no catalogue has it (`django/utils/translation/trans_real.py:gettext`). `django.utils.translation.pgettext` adds a context, joined to the message by the character `"\x04"` as GNU gettext joins them, so that one English word can be translated two ways: in the office's Dutch catalogue a berth that is free is "Vrij" and a service that is free "Gratis". `django.utils.translation.ngettext` takes both forms of a message and a number, and the catalogue's own formula, from its Plural-Forms header, chooses which of the translation's forms the number takes ([Translating a message](i18n/translating.md)) (`django/utils/translation/trans_real.py:TranslationCatalog.plural`).

`django.utils.translation.activate` makes a language's translation the active one for the thread, and `django.utils.translation.override` does so for the length of a `with` block and then activates again the language that was active before, unless it was made with `deactivate=True`. Within Django, `LocaleMiddleware` activates the request's language, `{% language %}` overrides it for a part of a template, and a command can be marked to run with translation off ([The active language](i18n/active-language.md)).

Code that runs at import, as a model's or a form's class body does, has no request's language to translate into, and may run before the application registry has imported every application's configuration, which making a `DjangoTranslation` requires. `django.utils.translation.gettext_lazy` returns instead an object that keeps the message and looks it up each time it is made a string, in the language active then ([Lazy translations](i18n/lazy.md)) (`django/utils/translation/__init__.py:gettext_lazy`).

## Choosing the language

`LocaleMiddleware.process_request` ordinarily activates the language that `django.utils.translation.get_language_from_request` returns, and puts its code on the request as `LANGUAGE_CODE` (`django/middleware/locale.py:LocaleMiddleware.process_request`). The candidates, in order, are a language prefix in the path, where the root URLconf uses `i18n_patterns`; the cookie `LANGUAGE_COOKIE_NAME` names, `"django_language"` unless the setting says otherwise; the languages of the Accept-Language header, the most preferred first; and last `LANGUAGE_CODE`. A candidate is taken where `LANGUAGES` has it, or a more general code (`"nl"` for `"nl-be"`), and a catalogue exists for it; failing both, another regional variant of its language that `LANGUAGES` lists is taken in its place ([Choosing a language: `LocaleMiddleware`](i18n/choosing.md)) (`django/utils/translation/trans_real.py:get_supported_language_variant`). The prefix in the path, and the redirect `process_response` makes when a path lacks one, belong to [Language prefixes and translated routes](urls/i18n.md).

Two views serve the other end. `set_language` takes a posted language, sets the cookie, and redirects to the page `"next"` names, or else the one the visitor came from, translated into the new language where its address has one (`django/views/i18n.py:set_language`). `JavaScriptCatalog` sends the active language's catalogue of the messages in JavaScript, the `"djangojs"` domain, with its plural formula and its formats, as a script that gives a browser its own gettext ([The language views: `set_language` and `JavaScriptCatalog`](i18n/views.md)) (`django/views/i18n.py:JavaScriptCatalog.get`).

## Formats

A date or a number is written differently from one locale to the next, as much as a word is. `get_format` finds a format by its name, `"DATETIME_FORMAT"` or `"DECIMAL_SEPARATOR"`, in the format modules of the active language: those under `FORMAT_MODULE_PATH` first, then Django's own, `django.conf.locale.nl.formats` for Dutch, with a regional locale's module tried before its language's. A format that no module sets is the setting of the same name. Each answer is cached for the format and the language (`django/utils/formats.py:get_format`).

`localize` sends a value to the writer for its type: a number to `number_format`, which uses the locale's decimal separator, and its thousands separator only where `USE_THOUSAND_SEPARATOR` is true; a datetime, a date or a time to `date_format` or `time_format` with the locale's format of that name. A string comes back as it was, and a boolean as `str` writes it (`django/utils/formats.py:localize`). A template localizes every value it writes, unless `{% localize off %}` says not to; a form's date or time field reads what is typed in the locale's input formats as well as the ISO ones ([Locale formats: `get_format` and the format modules](i18n/formats.md)).

The writing itself is done by `django.utils.numberformat.format`, which groups the digits and places the separators, and by `django.utils.dateformat`, whose format strings are made of letters: `"j F Y H:i"` is the day, the month's name, the year and the time. The names of the months and the days are lazy translations, so the one format string writes "maart" for Dutch and "März" for German ([Writing numbers, dates and durations](i18n/writing-values.md)) (`django/utils/dateformat.py:DateFormat`).

## Time zones

With `USE_TZ` on, as it is by default, Django's datetimes are **aware**: each carries a `tzinfo` and names an instant. One without is **naive**. `django.utils.timezone.now` gives the present in UTC, and a database backend that keeps no time zone stores an aware value in UTC, or in the zone the database's settings name ([Values in and out: adapters, converters and time zones](backends/values.md)) (`django/utils/timezone.py:now`).

The zone a datetime is shown in, and read in, is the current time zone: the one `django.utils.timezone.activate` installed for the thread, or else the default, `TIME_ZONE`. A project that shows each visitor their own time activates the zone itself, in a middleware of its own. On the way out a template ordinarily converts each aware datetime it writes to the current zone; on the way in a form takes a datetime typed without a zone as one in the current zone, and refuses a time that the zone's clocks skip or repeat. In Python code, `django.utils.timezone.localtime` converts an aware value to the current zone, and `make_aware` attaches the current zone to a naive one, without the form's refusal ([Time zones](i18n/time-zones.md)) (`django/utils/timezone.py:localtime`).

## Making catalogues

The catalogues are files of the GNU gettext format. A `.po` file is text, a message and its translation to an entry, the translations written by people; a `.mo` file is the same compiled to a table that the standard library's module of the same name, gettext, reads. makemessages finds the messages a project's Python files and templates mark for translation, turning each template first into something the GNU program xgettext can read, and writes or updates a `.po` file for each language; compilemessages runs msgfmt to make each `.mo`. Both need the GNU gettext programs installed ([Making catalogues: makemessages and compilemessages](i18n/commands.md)) (`django/core/management/commands/makemessages.py:Command.handle`).

## Where internationalization meets the rest of Django

The language prefix of a path, translated routes and `translate_url` are in [Language prefixes and translated routes](urls/i18n.md). The template tags that translate, localize and convert time zones, `{% translate %}`, `{% blocktranslate %}`, `{% language %}`, `{% localize %}` and `{% timezone %}`, are in [The libraries to load: static, i18n, l10n, tz and cache](templates/loadable-libraries.md); the context processors that put the language and the time zone in a template's context are in [The context: `Context`, `RequestContext` and context processors](templates/context.md); and the order in which a written value is converted, localized and escaped is in [Rendering: nodes and the render context](templates/rendering.md). A form field's input formats and its use of the current time zone are in [The built-in form fields](forms/field-types.md), and a localized widget in [Widgets: `Widget` and its subclasses](forms/widgets.md). A model's `django.db.models.DateTimeField` given a naive value is in [The field classes](models/field-types.md), and how the backends store and read datetimes in [Values in and out: adapters, converters and time zones](backends/values.md). The date views and the current zone are in [The date views](views/dates.md). The settings object puts a settings module's `TIME_ZONE` into the process's environment where the platform allows ([The settings object](startup/settings.md)), and the caches of translations and formats emptied when a test overrides a setting are in [Overriding settings](startup/overrides.md). A command that must not translate is marked `no_translations` ([Running a command: `BaseCommand`](commands/running.md)), and the autoreloader that watches `.mo` files is in [The autoreloader](commands/autoreload.md). Lazy values themselves, `lazy` and `Promise`, belong to [Lazy values: `lazy`, `Promise` and `keep_lazy`](utils/lazy.md); the admin's own JavaScript catalogue to [The site's own pages: the index, the login and `AdminSite.each_context`](admin/site-views.md).
