Django - Translation Tutorial
How to create multilingual websites using Django.
Updated Jul 14, 2020

Quickstart

Add LOCALE_PATHS setting to the project settings file and change the default language:

LOCALE_PATHS = [os.path.join(BASE_DIR, 'locale')]
# LANGUAGE_CODE = 'en-us' 
LANGUAGE_CODE = 'fi'

Edit a template file, load the i18n tag and use the trans tag to translate a string:

{% load i18n %}
<p>{% trans "Life is life." %}</p>

Create a directory for the translation files and run the makemessages command:

mkdir locale
django-admin makemessages -l fi --ignore venv

Edit locale/fi/LC_MESSAGES/django.po and add the translation:

msgid "Life is life."
msgstr "Elämä on laiffii." # HERE

Compile the messages:

django-admin compilemessages --ignore venv

Result:

<p>Elämä on laiffii.</p>

Full Tutorial

Setup

Run these commands to setup a new project:

python3 -m venv venv && \
source venv/bin/activate && \
pip install django --upgrade pip && \
django-admin startproject config . && \
python manage.py startapp pages && \
python manage.py makemigrations && \
python manage.py migrate && \
python manage.py createsuperuser

Edit the project settings.py file and add these lines to it:

INSTALLED_APPS = [
    # HERE
    'pages.apps.PagesConfig',
    'django.contrib.staticfiles',
    # ...
]

# HERE
LOCALE_PATHS = [os.path.join(BASE_DIR, 'locale')]

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        # HERE
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        # ..
    },
]
  • The LOCALE_PATHS setting defines a list of directories where Django looks for translation files.

Home view

Edit the pages app views.py file and add these lines to it:

from django.views.generic import TemplateView


class HomePageView(TemplateView):
    template_name = 'pages/home.html'

URLs

Create a file called urls.py in the pages app directory and add these lines to it:

from django.urls import path
from .views import HomePageView

app_name = 'pages'

urlpatterns = [
    path('', 
         HomePageView.as_view(), 
         name='home')
]

Edit the main urls.py file and add these lines to it:

from django.contrib import admin
# HERE
from django.urls import include, path

urlpatterns = [
    # HERE
    path('', include('pages.urls')),
    path('admin/', admin.site.urls),
]

Homepage template

Create a directory called templates in the project root. Create a directory called pages in it and add a file called home.html in the pages directory. Add these lines in the home.html file:

{% load i18n %}
<h1>Home</h1>
<p>{% trans "Life is life." %}</p>

Load the i18n tag in every template that uses translation tags (even if the parent template already loads it). Visit the homepage and you should see this:

Translation

Run these commands:

mkdir locale
django-admin makemessages -l fi --ignore venv
  • The makemessages command creates a message file (or updates it).
  • -l option specifies the language we want to translate strings to.
  • --ignore venv ignores the virtual environment directory.

Edit locale/fi/LC_MESSAGES/django.po and add the translation:

#: templates/pages/home.html:3
msgid "Life is life."
msgstr "Elämä on laiffii."

Run this command:

django-admin compilemessages --ignore venv

The compilemessages command creates .mo files from .po files. .mo files are binary files optimized for the gettext function. The gettext function translates a message and returns it as a string.

Now if we change the LANGUAGE_CODE setting in the settings.py file to fi, we can see the translation in action:

# LANGUAGE_CODE = 'en-us' 
LANGUAGE_CODE = 'fi'

Language preference

Let's chance the LANGUAGE_CODE back to en-us:

LANGUAGE_CODE = 'en-us' 
# LANGUAGE_CODE = 'fi' 

You can also use Django's LocaleMiddleware to determine user’s language preference:

MIDDLEWARE = [ 
    'django.contrib.sessions.middleware.SessionMiddleware',
    # HERE
    'django.middleware.locale.LocaleMiddleware', 
    'django.middleware.common.CommonMiddleware',
] 

Add it in the settings.py file MIDDLEWARE setting (between SessionMiddleware and CommonMiddleware).

  • First the LocaleMiddleware tries to determine user's language using URL language prefix (mysite.com/fi/about/).
  • Then it looks for a cookie.
  • Then it looks at the Accept-Language HTTP header (sent by the browser).
  • Failing all the above, it uses the LANGUAGE_CODE setting.

URL Language Prefix

The first thing the LocaleMiddleware looks for is URL language prefix if you are using the i18n_patterns function in the main URL configuration file. Let's try that.

Edit the config/urls.py file, import i18n_patterns and use it like this:

from django.contrib import admin
from django.urls import include, path
from django.conf.urls.i18n import i18n_patterns 

urlpatterns = i18n_patterns(
        path('', include('pages.urls')),
        path('admin/', admin.site.urls),
        prefix_default_language=False
    )                    
  • The i18n_patterns function prepends the current active language code to all URL patterns defined inside the function.
  • The prefix_default_language=False argument hides the language prefix for the default language.

Now you can add /fi/ language prefix to the URL to see the Finnish translation:

# shows finnish translation
http://127.0.0.1:8000/fi/
# shows english translation
http://127.0.0.1:8000/

You can also see the admin login page translated to Finnish in:

http://127.0.0.1:8000/fi/admin/

Next place the LocaleMiddleware looks for the language preference is a cookie called django_language. You can change the language cookie name by changing the LANGUAGE_COOKIE_NAME setting in the settings.py file.

Let's print out the current language code in the template:

{% load i18n %}
<h1>Home</h1>
<p>{% trans "Life is life." %}</p>

{% get_current_language as LANGUAGE_CODE %}
LANGUAGE_CODE: {{ LANGUAGE_CODE }}
  • The get_current_language tag returns the current language.

Here is an example on how to set up the cookie manually. Edit the pages app views.py file and add a render_to_response method to it:

from django.shortcuts import render
from django.views.generic import TemplateView
# HERE
from django.conf import settings

class HomePageView(TemplateView):
    
    # HERE
    def render_to_response(self, context, **response_kwargs):
    
        from django.utils import translation
        user_language = 'fi' 
        translation.activate(user_language)
        response = super(HomePageView, self).render_to_response(context, **response_kwargs)
        response.set_cookie(settings.LANGUAGE_COOKIE_NAME, user_language)
        return response
    
    template_name = 'pages/home.html'
  • translation.activate() changes the language for the current thread.
  • response.set_cookie() makes the preference persistent.

Now the language for the home page will always be Finnish. Our custom code overrides all other behaviours.

You can do this with function-based views:

def home(request): 
    from django.utils import translation
    user_language = 'fi' 
    translation.activate(user_language)
    response = render(request, 'pages/home.html')
    response.set_cookie(settings.LANGUAGE_COOKIE_NAME, user_language)
    return response

Add it to pages app URLs:

from django.urls import path
# HERE
from .views import HomePageView, home

app_name = 'pages'

urlpatterns = [
    path('', 
         HomePageView.as_view(), 
         name='home'),
    # HERE
    path('home/', 
         home,
         name='home_function')
]

Browser detection

The Accept-Language header is sent by the browser. In Chrome you can change the preferred language in here: chrome://settings/?search=language. In this example I moved the Finnish language on top:

Comment out the language setting code and clear the cookie:

class HomePageView(TemplateView):
    
    def render_to_response(self, context, **response_kwargs):
    
        # from django.utils import translation
        # user_language = 'fi' 
        # translation.activate(user_language)
        response = super(HomePageView, self).render_to_response(context, **response_kwargs)
        # response.set_cookie(settings.LANGUAGE_COOKIE_NAME, user_language)
        response.delete_cookie(settings.LANGUAGE_COOKIE_NAME)
        return response
    
    template_name = 'pages/home.html'

Do this in a function-based view:

def home(request): 
    # from django.utils import translation
    # user_language = 'fi' 
    # translation.activate(user_language)
    response = render(request, 'pages/home.html')
    # response.set_cookie(settings.LANGUAGE_COOKIE_NAME, user_language)
    return response

Also remove the i18n_patterns() function from the pages app urls.py file:

from django.contrib import admin
from django.urls import include, path
# from django.conf.urls.i18n import i18n_patterns 

urlpatterns = [
        path('', include('pages.urls')),
        path('admin/', admin.site.urls),
]

# urlpatterns = i18n_patterns(
#         path('', include('pages.urls')),
#         path('admin/', admin.site.urls),
#         prefix_default_language=False
#     )                                    

Refresh the home page and it should be translated to the Finnish language. So, now we can't use language prefixes in the URL, or load the default language without the prefix, neither we have a django_languge cookie. Now your browser language preference determines the language.

If your browser doesn't send the Accept-Language header, the global LANGUAGE_CODE that we set in the settings.py is used.

URL translation

To translate URLs it's recommended to use language prefixes, so let's enable them again.

Edit the project urls.py file and make these changes:

from django.contrib import admin
from django.urls import include, path
from django.conf.urls.i18n import i18n_patterns 
# HERE
from django.utils.translation import gettext_lazy as _

# urlpatterns = [
#         path('', include('pages.urls')),
#         path('admin/', admin.site.urls),
# ]

# HERE
urlpatterns = i18n_patterns(
        path('', include('pages.urls')),
        path(_('admin/'), admin.site.urls),
        prefix_default_language=False
    )             
  • The gettext_lazy function translates the string when the value is accessed, not when we call the function.

Run the makemessages command:

django-admin makemessages -l fi --ignore venv

Edit the messages file:

#: config/urls.py:13
msgid "admin/"
msgstr "yllapito/"

Run the compilemessages command:

django-admin compilemessages --ignore venv

Edit the home.html template file and add these lines to it:

{% load i18n %}
<h1>{{ title }}</h1>
<p>{% trans "Life is life." %}</p>

{% get_current_language as LANGUAGE_CODE %}
LANGUAGE_CODE: {{ LANGUAGE_CODE }}

<!-- START -->
<br><br>

<a href="{% url 'admin:index' %}">
    {% trans 'Site administration' %}
</a>
<!-- END -->

The home page should now have a translated link to the admin. Both the text and the link should be translated. Visit /fi/yllapito/ to see the admin translated in Finnish:

Language switcher

Let's add a switcher form that redirects the client to the home page and changes the active language. If the default language is en-us the switcher will redirect to (/), not (/en/) or (/en-us/). For other languages it redirects to the home page but leaves the language prefix (/fi/).

Edit the project settings.py file and add the languages you want to show up in the switcher using the LANGUAGES setting:

LANGUAGE_CODE = 'en-us'
LANGUAGES = [
    ('en-us', 'English'), 
    ('fi', 'Finnish'), 
]

Edit the pages app home.html file and add this form to it:

<br><br>

<form action="{% url 'change_language' %}" method="post">
    {% csrf_token %}
    <select name="language">   
        {% get_available_languages as LANGUAGES %}
        {% for language in LANGUAGES %}       
        <option value="{{ language.0 }}" {% if language.0 == LANGUAGE_CODE %} selected{% endif %}>
            {{ language.0|language_name_local }} ({{ language.0 }})
        </option>
        {% endfor %}
    </select>
    <input type="submit" value="Change language">
</form>
  • get_available_languages returns a list of language tuples in the form of ('language code', 'language translated to currently active language')
  • The language_name_local filter returns the language name translated to its original language.

Edit the pages app views.py file and add this function to it:

from django.http import HttpResponseRedirect

def change_language(request):
    response = HttpResponseRedirect('/')
    if request.method == 'POST':
        language = request.POST.get('language')
        if language:
            if language != settings.LANGUAGE_CODE and [lang for lang in settings.LANGUAGES if lang[0] == language]:
                redirect_path = f'/{language}/'
            elif language == settings.LANGUAGE_CODE:
                redirect_path = '/'
            else:
                return response
            from django.utils import translation
            translation.activate(language)
            response = HttpResponseRedirect(redirect_path)
            response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language)
    return response
  • if request.method == 'POST': checks if the request is a POST method. If not, we redirect the client to the home page. Otherwise we examine the selected language option.
  • If language is not equal to the default LANGUAGE_CODE and it can be found in the LANGUAGES list, we set the redirect_path variable to the language value. It will be something like fi or es.
  • The [lang for lang in settings.LANGUAGES if lang[0] == language] list comprehension returns True if it finds the language in the LANGUAGES list. lang[0] gets the first element of the tuple. That's the language code (for example (en-us) in ('en-us', 'English')).
  • If language is equal to the default LANGUAGE_CODE, we set the redirect_path as /.
  • If the language is not the default language and we can't find it in the LANGUAGES list, we redirect the client to the home page. Otherwise we activate the language and redirect to the redirect_path.

Edit the main urls.py file:

from django.contrib import admin
from django.urls import include, path
from django.conf.urls.i18n import i18n_patterns 
from django.utils.translation import gettext_lazy as _
# HERE
from pages.views import change_language

# HERE
urlpatterns = [
    path('change_language/', 
         change_language, 
         name='change_language')
]

# HERE (add +)
urlpatterns += i18n_patterns(
        path('', include('pages.urls')),
        path(_('admin/'), admin.site.urls),
        prefix_default_language=False
    )

The official docs offer slightly different switcher implementation.

Translations in Python code

You can use gettext function and its variations to translate messages outside templates. Edit the pages app views.py file and add a title variable to the context:

from django.utils.translation import gettext as _

class HomePageView(TemplateView):
    
    def get_context_data(self, **kwargs):  
        context = super().get_context_data(**kwargs)
        context['title'] = _('Home')
        return context

    template_name = 'pages/home.html'
  • gettext returns a translated string. We import it as _ so we don't have to type so much.
  • Override the get_context_data method to change the context.
  • The context object is a dictionary that maps Python objects to template variables. What you put in the context dictionary will be available in the template.
  • context = super().get_context_data(self, **kwargs) gets the context.
  • context['title'] = _('Home') changes the context.

With function-based views you can pass the context like this:

def home(request): 
    return render(request, 
                  'pages/home.html', 
                  {'title': _('Home')})

Edit the pages app home.html template and print out the title:

{% load i18n %}
<!-- START -->
<h1>{{ title }}</h1>
<!-- END -->
<p>{% trans "Life is life." %}</p>

{% get_current_language as LANGUAGE_CODE %}
LANGUAGE_CODE: {{ LANGUAGE_CODE }}

Run the makemessages command:

django-admin makemessages -l fi --ignore venv

Edit the translation file:

#: pages/views.py:21 pages/views.py:29
msgid "Home"
msgstr "Etusivu"

Run the compilemessages command:

django-admin compilemessages --ignore venv

The home page should now display the translated title:

Model translation

Let's use the django-modeltranslation package to translate database items:

pip install django-modeltranslation

Edit the pages app models.py file and add these lines to it:

from django.db import models

class Post(models.Model):
    text = models.TextField()

Edit the pages app admin.py file and add these lines to it:

from django.contrib import admin
from .models import Post

admin.site.register(Post)

Create a file called translation.py in the pages app directory and add these lines to it:

from modeltranslation.translator import translator, TranslationOptions
from .models import Post

class PostTranslationOptions(TranslationOptions):
    fields = ('text', )

translator.register(Post, PostTranslationOptions)

Make sure to add the comma (,) in fields = ('text', ) if you have only one field.

Edit the project settings.py file and add modeltranslation to the INSTALLED_APPS list:

INSTALLED_APPS = [
    'pages.apps.PagesConfig',
    'modeltranslation',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.sites',
]

# Also mark the languages for translation like this:

from django.utils.translation import ugettext_lazy as _

LANGUAGES = (
    ('en-us', _('English')),
    ('fi', _('Finnish')),
)

Run migrations:

python manage.py makemigrations && \
python manage.py migrate

Edit the pages app views.py file and make these changes to the HomePageView:

# HERE
from .models import Post

class HomePageView(TemplateView):
    
    def get_context_data(self, **kwargs):
        # HERE
        posts = Post.objects.all()
        context = super().get_context_data(**kwargs)
        context['title'] = _('Home')
        # HERE
        context['posts'] = posts 
        return context

Create a post:

Edit the home.html template file and loop through the posts like this:

<ul>
    {% for post in posts %}
        <li>{{ post.text }}</li>
    {% endfor %}
</ul>