Internationalization (i18n & l10n)
Reviewed & published by Brayan K
Internationalization in Django means preparing your application so its text and formats can be translated and adapted for different languages and regions — you mark strings for translation with gettext and template tags, configure the languages you support, and let Django serve each visitor content in their own language.
Part of the free Django course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll mark strings with the _() function, translate templates with {% trans %} and {% blocktrans %}, wire up LocaleMiddleware and the LANGUAGES setting, and run makemessages and compilemessages to build your .po and .mo files.
What You'll Learn in This Lesson
- The difference between i18n and l10n
- Marking strings with gettext, imported as _()
- Translating templates with {% trans %} and {% blocktrans %}
- Enabling USE_I18N, LANGUAGES, and LocaleMiddleware
- Generating .po files with makemessages
- Compiling .mo files with compilemessages
1 Marking Strings in Python with gettext
Internationalization (i18n) is the work of making your code translatable; localization (l10n) is supplying the actual translations and regional formats. The first step is always the same: tell Django which strings are user-facing so they can be collected and translated later.
In Python code you do this with gettext, which is conventionally imported as _. Wrapping a string in _() marks it for translation without changing what it returns today.
# blog/views.py
from django.http import HttpResponse
from django.utils.translation import gettext as _
def greeting(request):
# The marked string will be replaced by its translation at runtime
message = _("Welcome to our blog!")
return HttpResponse(message)For text that is evaluated at import time — model field labels or default values — use gettext_lazy instead, so translation is deferred until the active language is known.
# Django's _() looks up a translated string in the active language.
# We'll fake a translation catalog so you can see how it works.
catalogs = {
"es": {"Welcome to our blog!": "Bienvenido a nuestro blog!"},
"fr": {"Welcome to our blog!": "Bienvenue sur notre blog!"},
}
active_language = "es"
def _(text):
"""gettext: return the translation for the active language, or the original."""
return catalogs.get(active_language, {}).get(text, text)
# In a view you'd write _("Welcome to our blog!")
print(_("Welcome to our blog!")) # Spanish, because active_language = "es"
active_language = "fr"
print(_("Welcome to our blog!")) # now French
active_language = "de"
print(_("Welcome to our blog!")) # no German entry -> falls back to English
# ✅ Expected output:
# Bienvenido a nuestro blog!
# Bienvenue sur notre blog!
# Welcome to our blog!2 Translating Templates with trans and blocktrans
Templates have their own translation tags. First load the library with {% load i18n %} at the top of the file. Then use {% trans %} for a single literal string, and {% blocktrans %} when the text contains variables.
{% load i18n %}
<h1>{% trans "Welcome to our blog!" %}</h1>
{% blocktrans %}
Hello {{ username }}, you have {{ count }} new messages.
{% endblocktrans %}- {% load i18n %} — required before any translation tag.
- {% trans "..." %} — translates one literal string, no variables.
- {% blocktrans %}...{% endblocktrans %} — translates text containing placeholders.
# {% trans %} handles a plain string; {% blocktrans %} handles one
# with variables. Here we model both in plain Python.
es = {
"Welcome to our blog!": "Bienvenido a nuestro blog!",
# blocktrans keeps placeholders as named slots
"Hello %(username)s, you have %(count)s new messages.":
"Hola %(username)s, tienes %(count)s mensajes nuevos.",
}
def trans(text):
return es.get(text, text)
def blocktrans(template, **vars):
return es.get(template, template) % vars
# {% trans "Welcome to our blog!" %}
print(trans("Welcome to our blog!"))
# {% blocktrans %}Hello {{ username }}, ...{% endblocktrans %}
print(blocktrans(
"Hello %(username)s, you have %(count)s new messages.",
username="Alice", count=3,
))
# ✅ Expected output:
# Bienvenido a nuestro blog!
# Hola Alice, tienes 3 mensajes nuevos.3 Settings, Middleware, and the Message Workflow
Three settings turn the system on: USE_I18N = True enables translation, LANGUAGE_CODE names the default language, and LANGUAGES lists the languages you offer. LocaleMiddleware then picks the active language for each request from the URL, session, cookie, or browser header.
# settings.py
USE_I18N = True
USE_TZ = True
LANGUAGE_CODE = "en-us" # default / fallback language
LANGUAGES = [
("en", "English"),
("es", "Spanish"),
("fr", "French"),
]
MIDDLEWARE = [
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.locale.LocaleMiddleware", # after Session, before Common
"django.middleware.common.CommonMiddleware",
]With strings marked and settings in place, you run the two-step message workflow. makemessages collects strings into editable .po files; you translate them; then compilemessages builds the binary .mo files Django reads at runtime.
# 1. Collect all marked strings into locale/es/LC_MESSAGES/django.po
python manage.py makemessages -l es
# (now edit django.po and fill in each msgstr with the translation)
# locale/es/LC_MESSAGES/django.po
# msgid "Welcome to our blog!"
# msgstr "Bienvenido a nuestro blog!"
# 2. Compile the edited .po files into binary .mo files Django reads
python manage.py compilemessages# A .po file pairs each original string (msgid) with its translation
# (msgstr). compilemessages turns that into the lookup Django uses.
po_entries = [
{"msgid": "Welcome to our blog!", "msgstr": "Bienvenido a nuestro blog!"},
{"msgid": "Read more", "msgstr": "Leer mas"},
{"msgid": "Log out", "msgstr": "Cerrar sesion"},
]
# "compile" the .po entries into a fast lookup dict (the .mo at runtime)
catalog = {e["msgid"]: e["msgstr"] for e in po_entries}
def _(text):
return catalog.get(text, text)
for label in ["Welcome to our blog!", "Read more", "Log out", "Untranslated"]:
print(label, "->", _(label))
# ✅ Expected output:
# Welcome to our blog! -> Bienvenido a nuestro blog!
# Read more -> Leer mas
# Log out -> Cerrar sesion
# Untranslated -> Untranslated🎯 Your Turn: Mark a String for Translation
Fill in the blank with the translation function (imported as _) so the label is looked up in the active language.
# 🎯 YOUR TURN: replace ___ with the translation function (it's named _)
# so the button label is looked up in the catalog.
catalog = {"Sign up": "Registrarse"}
def _(text):
return catalog.get(text, text)
# Replace ___ below so the label gets translated
button_label = ___("Sign up")
print(button_label)
# EXPECTED OUTPUT:
# RegistrarseCommon Errors (And How to Fix Them)
❌ My translations don't show up
You edited the .po file but never compiled it, so Django is still reading the old (or empty) .mo.
✅ Fix: run python manage.py compilemessages after every change to a .po file.
❌ Invalid block tag: 'trans'
You used a translation tag without loading the library first.
✅ Fix: add {% load i18n %} at the top of the template.
❌ The site never switches language
LocaleMiddleware is missing or out of order, so the requested language is never activated.
✅ Fix: add LocaleMiddleware after SessionMiddleware and before CommonMiddleware, and confirm the language is listed in LANGUAGES.
📋 Quick Reference
| Tool | What It Does |
|---|---|
| _("text") | Mark a Python string for translation (gettext) |
| {% trans "text" %} | Translate a literal string in a template |
| {% blocktrans %} | Translate text that contains variables |
| makemessages | Collect marked strings into editable .po files |
| compilemessages | Compile .po files into binary .mo files |
🏆 Mini Challenge: A Two-Language Catalog
Build a tiny translation system with two languages and a _() helper that switches based on the active language — exactly what Django does under the hood.
# 🏆 Mini Challenge: build a two-language translation lookup.
catalogs = {
"es": {"Home": "Inicio", "About": "Acerca de", "Contact": "Contacto"},
"fr": {"Home": "Accueil", "About": "A propos", "Contact": "Contact"},
}
active_language = "es"
def _(text):
# Fall back to the original text if no translation exists
return catalogs.get(active_language, {}).get(text, text)
nav = ["Home", "About", "Contact"]
print("Spanish:", [_(item) for item in nav])
active_language = "fr"
print("French: ", [_(item) for item in nav])
# EXPECTED:
# Spanish: ['Inicio', 'Acerca de', 'Contacto']
# French: ['Accueil', 'A propos', 'Contact']❓ Frequently Asked Questions
Lesson complete — your app now speaks many languages!
You can mark strings with _(), translate templates with {% trans %} and {% blocktrans %}, configure USE_I18N, LANGUAGES, and LocaleMiddleware, and run makemessages and compilemessages to build your .po and .mo files.
Practice quiz
What is the difference between internationalization (i18n) and localization (l10n)?
- i18n prepares code so it can be translated; l10n provides the actual translations and regional formats
- i18n is server-side, l10n is client-side
- i18n is for dates, l10n is for currency
- They are two names for the same thing
Answer: i18n prepares code so it can be translated; l10n provides the actual translations and regional formats. Internationalization is the engineering work of making strings translatable; localization is supplying the translations and locale-specific formats.
Which function is the standard way to mark a string for translation in Python code?
- localize()
- i18n()
- gettext (often imported as _)
- translate()
Answer: gettext (often imported as _). django.utils.translation.gettext, conventionally aliased to _, marks strings so makemessages can collect them.
Which template tag must you load before using trans or blocktrans?
- {% load static %}
- {% load i18n %}
- {% load l10n %}
- {% load translation %}
Answer: {% load i18n %}. You must put {% load i18n %} at the top of a template before using the translation tags.
Which template tag translates a literal string with no variables?
- {% blocktrans %}
- {% localize %}
- {% gettext %}
- {% trans %}
Answer: {% trans %}. {% trans "text" %} marks and translates a single literal string in a template.
When do you need {% blocktrans %} instead of {% trans %}?
- When the text contains template variables or placeholders
- When the text is shorter than 10 characters
- When translating numbers
- When the page uses a base template
Answer: When the text contains template variables or placeholders. {% blocktrans %} handles strings that interpolate variables, which {% trans %} cannot do.
Which middleware selects the active language from the request?
- CommonMiddleware
- django.middleware.locale.LocaleMiddleware
- SecurityMiddleware
- SessionMiddleware
Answer: django.middleware.locale.LocaleMiddleware. LocaleMiddleware inspects the URL, session, cookie, and Accept-Language header to choose the language.
Which setting must be True for Django's translation system to be active?
- USE_TZ
- USE_TRANSLATION
- USE_L10N
- USE_I18N
Answer: USE_I18N. USE_I18N must be True to enable Django's translation machinery.
Which management command scans your code for marked strings and creates or updates .po files?
- collectstatic
- maketranslations
- makemessages
- compilemessages
Answer: makemessages. manage.py makemessages collects translatable strings into .po message files for each locale.
What does compilemessages produce from your .po files?
- HTML files
- Compiled binary .mo files Django reads at runtime
- JSON files
- Python modules
Answer: Compiled binary .mo files Django reads at runtime. compilemessages converts human-edited .po files into the binary .mo files Django loads at runtime.
Which setting defines the default language used when no translation is selected?
- LANGUAGE_CODE
- LOCALE_CODE
- DEFAULT_LANGUAGE
- LANGUAGES
Answer: LANGUAGE_CODE. LANGUAGE_CODE sets the default language, for example 'en-us', used as a fallback.
Continue this course
- Previous: Capstone: A Blog App
- Next: GraphQL APIs with Graphene — Expose a typed GraphQL API with graphene-django types, queries and mutations
- Quick reference: Django cheat sheet
Frequently asked questions
What is the difference between i18n and l10n in Django?
Internationalization (i18n) is the engineering work of preparing your application so it can be translated and adapted later: you wrap user-facing strings in translation functions like gettext or template tags like {% trans %}, and you let Django format dates and numbers according to a locale. Localization (l10n) is the step that follows: providing the actual translated text in .po files and the region-specific formats for a particular language and country. In short, i18n makes your code translatable, and l10n supplies the translations.
Why do I need both makemessages and compilemessages?
These two commands are the two halves of the translation workflow. makemessages scans your Python and template files for strings you marked with _() , {% trans %}, or {% blocktrans %} and writes them into editable .po files inside your locale directory, one folder per language. You (or a translator) then fill in the translated text. compilemessages takes those edited .po files and compiles them into binary .mo files, which is the format Django actually reads at runtime. If you edit a .po file but forget to run compilemessages, your new translations will not appear.
What does LocaleMiddleware do?
django.middleware.locale.LocaleMiddleware is what decides which language a given request should be served in. On each request it looks, in order, at any language prefix in the URL, a language stored in the session or a cookie, and finally the browser's Accept-Language header, and it activates the first match it finds among your configured LANGUAGES. It must sit after SessionMiddleware and before CommonMiddleware in your MIDDLEWARE list so that the session is available and the chosen language affects URL handling.
How does Django know which languages my site supports?
You declare them in the LANGUAGES setting, a list of (code, name) tuples such as [('en', 'English'), ('es', 'Spanish'), ('fr', 'French')]. Django will only activate a language that appears in this list. The LANGUAGE_CODE setting names the default language used as a fallback when nothing else is selected, and USE_I18N must be True for the whole translation system to be turned on. Together these settings define the set of languages your project offers and the one it falls back to.