Django’s internationalization and localization features, often abbreviated as i18n and l10n respectively, are surprisingly powerful and can be enabled with minimal code changes, but their true magic for production lies in understanding how Django chooses the correct language and locale for each request.

Let’s see this in action with a simple Django project.

First, we need to tell Django which languages our application will support. In your settings.py:

# settings.py

LANGUAGE_CODE = 'en-us'  # The default language

TIME_ZONE = 'UTC'

USE_I18N = True
USE_L10N = True

LANGUAGES = [
    ('en', 'English'),
    ('fr', 'French'),
    ('es', 'Spanish'),
]

LANGUAGE_CODE is the fallback language if no translation is found. USE_I18N enables internationalization (handling different languages), and USE_L10N enables localization (handling different regional formats for dates, numbers, etc.). LANGUAGES is a list of tuples where the first element is the language code (e.g., en) and the second is the human-readable name.

Now, let’s create a simple view and template that uses translatable strings.

views.py:

# views.py
from django.shortcuts import render
from django.utils.translation import gettext as _

def hello_world(request):
    message = _("Hello, world!")
    return render(request, 'hello.html', {'message': message})

templates/hello.html:

<!-- templates/hello.html -->
<!DOCTYPE html>
<html>
<head>

    <title>{% trans "Greeting" %}</title>

</head>
<body>

    <h1>{{ message }}</h1>

</body>
</html>

Notice the use of {% trans "Greeting" %} in the template and gettext as _("Hello, world!") in the view. These are the standard ways Django marks strings for translation.

To generate the translation files, run:

python manage.py makemessages -l fr

This creates a locale/fr/LC_MESSAGES/django.po file. Inside, you’ll find entries like:

#: views.py:4
msgid "Hello, world!"
msgstr ""

#: templates/hello.html:4
msgid "Greeting"
msgstr ""

You’ll manually fill in the msgstr (message string) for each language. For French:

#: views.py:4
msgid "Hello, world!"
msgstr "Bonjour, le monde !"

#: templates/hello.html:4
msgid "Greeting"
msgstr "Salutation"

After editing, compile the translations:

python manage.py compilemessages

This creates .mo files, which are optimized for runtime lookup.

Now, how does Django actually pick the language? This is where it gets interesting. Django checks several places, in order of precedence, to determine the user’s preferred language for a request:

  1. request.GET parameter: If you have ?lang=fr in your URL, Django will use French.
  2. User Profile: If the user is logged in and has a language attribute set on their User object (e.g., request.user.language = 'fr'), that language is used.
  3. request.session: If you manually set request.session['django_language'] = 'fr', that will be used. This is common for language switchers.
  4. Accept-Language HTTP header: Browsers send this header indicating the user’s preferred languages, ordered by preference (e.g., fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7). Django parses this and picks the best match from your LANGUAGES setting.
  5. LANGUAGE_CODE setting: If none of the above yield a result, Django falls back to the default language specified in settings.py.

A common pattern for building a language switcher involves middleware. Django provides django.middleware.locale.LocaleMiddleware. Ensure it’s in your MIDDLEWARE setting, after SessionMiddleware and AuthenticationMiddleware (if you use them):

# settings.py

MIDDLEWARE = [
    # ... other middleware
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.locale.LocaleMiddleware', # Important for language switching
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    # ... other middleware
]

This middleware automatically handles the request.session['django_language'] part. You can then create a view that sets this session variable:

# views.py
from django.shortcuts import render, redirect, reverse
from django.utils.translation import activate, get_language

def set_language(request, language):
    from django.utils import translation
    translation.activate(language)
    request.session[translation.get_language_cookie_name()] = language
    return redirect(request.META.get('HTTP_REFERER', '/')) # Redirect back to where they came from

And a URL pattern for it:

# urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('hello/', views.hello_world, name='hello_world'),
    path('set_language/<str:language>/', views.set_language, name='set_language'),
]

In your templates, you can link to this view:


<a href="{% url 'set_language' 'fr' %}">Français</a>


<a href="{% url 'set_language' 'en' %}">English</a>

When a user visits /set_language/fr/, their session is updated, and subsequent requests will be rendered in French.

One crucial detail for production is that the makemessages and compilemessages commands should ideally be run as part of your deployment process, not manually on the live server. This ensures that your translation files are always up-to-date and compiled.

Finally, to truly internationalize for production, you’ll want to ensure your templates are correctly set up to handle dynamic content that might also need translation or localization. For example, dates and numbers should be formatted according to the user’s locale. Django’s template tags like {% localize %} and {% date %} or {% timesince %} are useful here, and USE_L10N = True is essential.

The most counterintuitive aspect of Django’s i18n/l10n for many is how the Accept-Language header is parsed. Django doesn’t just pick the first language in the header if it’s available; it performs a sophisticated matching algorithm. If the header lists es-ES,es;q=0.9,en;q=0.8 and your LANGUAGES are ('es', 'Spanish') and ('en', 'English'), Django will prefer es. If you only had ('en', 'English'), it would fall back to English. If you had ('fr', 'French') and ('en', 'English'), it would still pick English because es isn’t supported, and en has a higher quality value (q=0.8) than if no match were found (which would have an implicit q=0.0). This allows for graceful degradation and broad language support.

Once you have your translations set up and your language switcher working, the next hurdle is often managing pluralization correctly across different languages, as grammatical rules for plurals vary wildly.

Want structured learning?

Take the full Django course →