Samuli Natri - Software Developer
menu

Django - How To Translate A Website

Tutorial on how to create multilingual websites.

Basic Translation Example

Load the i18n tag in every template that uses translation tags:

{% load i18n %}

Now you can do this to translate a text:

<h1>{% trans "Let's translate this" %}</h1>

The translation system is enabled by default but you can turn it off in the settings.py file for performance if you don't need it:

USE_I18N = True

We need a place to store the translations. I'm going go use the site root, so let's add this to the settings file:

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

Create a folder locale in the root of your site and run the makemessages command:

django-admin makemessages -l fi

This will generate a message file for the finnish language.

Open that file in /locale/fi/LC_MESSAGES/django.po and you should see something like this:

#: base/templates/base/home.html:7
msgid "Let's translate this"
msgstr ""

Add the translation:

#: base/templates/base/home.html:7
msgid "Let's translate this"
msgstr "Käännetäänpä tää"

Run compilemessages to compile the .po files:

django-admin compilemessages

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

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

So if you just want one language for the site, define it in the LANGUAGE_CODE string and make sure you have the compiled version (.mo) of the message file available.

Visit the admin area and you will probably see most of it translated to your chosen language.

Let's chance this back to en-us:

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

Language Preference

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

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

URL Language Prefix

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

Import i18n_patterns and use it like this:

from django.conf.urls.i18n import i18n_patterns # < here

urlpatterns = [
    path('admin/', admin.site.urls),
]
urlpatterns += i18n_patterns( # < here
    path('', include('base.urls'))
) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

So now you can add /fi/ language prefix to the url to see the Finnish translation and /en/ to see the original string.

You can hide the language prefix for the default language by setting the prefix_default_language as False for the i18n_patterns function:

urlpatterns += i18n_patterns(
    path('', include('base.urls')),
    prefix_default_language=False, # < here
) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Now we will get the default language in the root / and Finnish with the prefix /fi/.

LANGUAGE_SESSION_KEY and LANGUAGE_COOKIE_NAME

Next place the LocaleMiddleware looks for the language preference is the LANGUAGE_SESSION_KEY and LANGUAGE_COOKIE_NAME.

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

<h1>{% trans "Let's translate this" %}</h1>
    
{% get_current_language as LANGUAGE_CODE %} # < this
    
{{ LANGUAGE_CODE }} # < this

Here is an example on how to set up LANGUAGE_SESSION_KEY manually:

def home(request):

    from django.utils import translation # < this
    user_language = 'fi' # < this
    translation.activate(user_language) # < this
    request.session[translation.LANGUAGE_SESSION_KEY] = user_language # < this

    posts = Post.objects.all().order_by('order')

    return render(request, 'base/home.html', {'posts': posts})

Also let's not use i18n_patterns to prepend the current language code:

# urlpatterns = [
#     path('admin/', admin.site.urls),
# ]
#
# urlpatterns += i18n_patterns(
#     path('', include('base.urls')),
#     prefix_default_language=False,
# ) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('base.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Visit the homepage and you can see that the language code is fi even as we have the default language code set to en in the settings file.

Let's remove the session code and refresh the page:

def home(request):

    # < here
    
    posts = Post.objects.all().order_by('order')

    return render(request, 'base/home.html', {'posts': posts})

The finnish language is still activated because we made it persistent with the session key.

If we delete the language session key and visit the homepage, we are now seeing the language preference sent by the browser.

    from django.utils import translation  # < this
    # user_language = 'fi'
    # translation.activate(user_language)
    # request.session[translation.LANGUAGE_SESSION_KEY] = user_language
    if translation.LANGUAGE_SESSION_KEY in request.session: # < this
        del request.session[translation.LANGUAGE_SESSION_KEY]

Browser Detection

So the last place to look for the language preference is the Accept-Language header that is sent by the user browser. In Chrome you can change the preferred language in here: chrome://settings/?search=language.

Recap The Language Determination

So if you are using i18n_patterns, then Django first looks the language prefix in the URL. Then it checks the language session key and cookie. After that it checks the http header sent by the browser and if none of those are available, it uses the LANGUAGE_CODE from the settings file.

Language Switcher

There is a helper view that you can use to set the user language preference. Add this to the main urls.py:

urlpatterns = [
    path('i18n/', include('django.conf.urls.i18n')), # < here
    path('admin/', admin.site.urls),
    path('', include('base.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

There is an example in the Django documentation on how to create a form for the language switcher, let's use that:

<form action="{% url 'set_language' %}" method="post">{% csrf_token %}
    <input name="next" type="hidden" value="{{ redirect_to }}" >
    <select name="language">
        {% get_current_language as LANGUAGE_CODE %}
        {% get_available_languages as LANGUAGES %}
        {% get_language_info_list for LANGUAGES as languages %}
        {% for language in languages %}
            <option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected{% endif %}>
                {{ language.name_local }} ({{ language.code }})
            </option>
        {% endfor %}
    </select>
    <input type="submit" value="Go" />
</form>

You can restrict the language list by defining the languages in the settings file:

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

Translations In Python Code

You can use gettext and its variations to translate messages in the Python code:

from django.utils.translation import gettext # < here

    posts = Post.objects.all().order_by('order')

    title = gettext('Homepage') # < here

    return render(request, 'base/home.html', {'posts': posts,
                                              'title': title, # < here
                                              })

Print out the title in the template:

<h1>{{ title }}</h1>

Run makemessages:

django-admin makemessages -l fi

Edit the translation file:

#: base/views.py:18
msgid "Homepage"
msgstr "Kotisivu" # < this

Run compilemessages:

django-admin compilemessages

And the title variable will print out the right translation.

To save typing, you could use an alias _:

from django.utils.translation import gettext as _

...

    title = _('Homepage')

Translate Urls

To translate urls it's recommended to use language prefixes, so let's enable them again. This time let's prefix the admin pages as well and translate the /admin/ path:

# urlpatterns = [
#     path('i18n/', include('django.conf.urls.i18n')),
#     path('admin/', admin.site.urls),
#     path('', include('base.urls')),
# ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

urlpatterns = [
    path('i18n/', include('django.conf.urls.i18n')),
]

urlpatterns += i18n_patterns(
    path('admin/', admin.site.urls),
    path('', include('base.urls')),
    prefix_default_language=False,
) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Import gettext_lazy and use it like this:

from django.utils.translation import gettext_lazy as _ # < here

urlpatterns += i18n_patterns(
    path(_('admin/'), admin.site.urls), # < here
    path('', include('base.urls')),
    prefix_default_language=False,
) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Run makemessages:

django-admin makemessages -l fi

Edit the messages file:

#: main/urls.py:36
msgid "admin/"
msgstr "yllapito/" # < here

Run compilemessages:

django-admin compilemessages

Now in /fi/yllapito/ the admin pages are translated in Finnish, but /admin/ and /en/admin are in the default languge (en).

How To Get Translated Path To The Admin Page

<h1><a href="{%  url 'admin:index' %}">{% trans '> Admin pages' %}</a></h1>
<h1>{%  url 'admin:index' %}</h1>

Localized Formatting

Localized formatting for numbers and dates is on by default:

USE_L10N = True

This means that if you for example add a date field and print the date in a template, it will be formatted using localized conventions:

Add this in you model:

date = models.DateField(blank=True, null=True)

And migrate:

python manage.py makemigrations
python manage.py migrate

Use this in the template:

{{ post.date }}

Now in English we will see something like this:

March 15, 2018

and this in Finnish:

15. maaliskuuta 2018