Internationalization

Every piece of text in TaskFlow so far has been hardcoded in English — "Mark as completed", "You've been assigned", and so on. That's fine until TaskFlow needs to serve users who read Spanish, or French, or any language other than the one it was written in. Django's internationalization framework is what makes that possible without rewriting a single view — the trick is marking translatable text as you write it, not retrofitting it later.

Internationalization vs. Localization

These two terms get used almost interchangeably, but they describe different work:

TermMeans
Internationalization (i18n)Designing and writing your app so it can be translated — wrapping every user-facing string in a translation marker, once, regardless of whether a translation exists yet
Localization (l10n)Actually supplying the translated text (and locale-specific formatting — dates, numbers, currency) for one particular language or region

In short: i18n is the groundwork, l10n is filling it in. A project can be internationalized long before a single translation exists.

Marking a String Translatable in Python

Django's translation function is conventionally imported under the short alias _ — wrapping a string in it doesn't translate anything by itself, it just marks that string as eligible for translation, and returns the translated version if one exists for the current active language (otherwise, the original string, unchanged):

# taskflow/views.py
from django.utils.translation import gettext as _
from django.contrib import messages

def create_task(request):
    # ... form handling and task.save() ...
    messages.success(request, _('Task created successfully'))
    return redirect('task-list')

The string itself, 'Task created successfully', stays exactly where it always was — wrapping it in _() is a small, almost invisible change to the code, but it's what makes this particular message discoverable by Django's translation tooling later.

Marking Text Translatable in a Template

Templates get their own tags instead of a function call. {% trans %} handles a plain, fixed string:

{% load i18n %}
<button>{% trans "Mark as completed" %}</button>

For text that needs to embed a variable, {% trans %} isn't enough — that's what {% blocktrans %} is for, letting a variable sit inside the translatable text itself:

{% load i18n %}
{% blocktrans %}You have {{ count }} tasks remaining{% endblocktrans %}

Every language's translation of that sentence can move {{ count }} to wherever that language's grammar naturally puts it — a translator isn't stuck matching English word order, only the presence of the variable itself.

The LANGUAGE_CODE and USE_I18N Settings

Two settings control whether any of this is even active:

# taskflow_project/settings.py
LANGUAGE_CODE = 'en-us'
USE_I18N = True

LANGUAGE_CODE sets the project's default language — the one used when nothing else (a browser header, a session preference) says otherwise. USE_I18N is the master switch for the whole translation system; it defaults to True in a new Django project, and every _() call and {% trans %} tag above only actually translates anything while it stays that way.

Generating and Compiling Translation Files

Marking strings is only half the job — someone still has to supply the actual translated text. Django's makemessages management command scans your entire codebase for every _() call and {% trans %} / {% blocktrans %} tag, and collects them into one file per language, ready for a translator to fill in:

django-admin makemessages -l es

That command creates locale/es/LC_MESSAGES/django.po — a plain-text file listing every marked string found in the project, each with a blank spot next to it for the Spanish translation. Once a translator has filled those in, compilemessages turns that editable .po file into the compact binary .mo format Django actually loads at runtime:

django-admin compilemessages

Both commands are run again any time new translatable strings get added to the codebase — a fresh makemessages picks up anything new (leaving existing translations untouched), and a fresh compilemessages rebuilds the .mo file the running project reads from.

Detecting a Visitor's Language: LocaleMiddleware

None of the above decides which language an individual visitor actually sees — that's the job of LocaleMiddleware, one more entry in the MIDDLEWARE list covered in Middleware. On every request, it inspects the visitor's browser Accept-Language header (or a language already stored in their session), and activates the best matching language Django has translations for, automatically, before any view code runs:

# taskflow_project/settings.py
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.locale.LocaleMiddleware',
    'django.middleware.common.CommonMiddleware',
    # ...
]

LocaleMiddleware has to sit after SessionMiddleware (it may read a saved language preference from the session) and before CommonMiddleware — one more concrete case of the ordering rules covered in that lesson applying directly here.

Why Set This Up Early

Being honest about the cost here: marking a string translatable while writing it is a two-second change — wrap it in _() or {% trans %} and move on. Going back through an entire existing codebase months later, hunting down every hardcoded string across every view and every template, is a genuinely tedious, easy-to-miss-things retrofit. If there's any real chance TaskFlow will need more than one language eventually, wrapping strings as you write them costs almost nothing now and saves a lot of nothing-but-grunt-work later.

Note: This is correct, real i18n code and commands — but this website's own project doesn't run a Django server, so makemessages doesn't actually scan anything on this page. Run it against your own TaskFlow project to see a real .po file generated.
Next up: Translated text still needs to be tracked and debugged like everything else in TaskFlow. Logging covers recording what your application is actually doing, in any language.