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:
| Term | Means |
|---|---|
| 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.
makemessages doesn't actually scan
anything on this page. Run it against your own TaskFlow project to see a real .po
file generated.