Internalization in Django

Photo by Kyle Glenn on Unsplash

Internalization in Django

ยท

8 min read

Internalization in Django

Django is one of the most popular web frameworks known for its battery-inclusion. One of the inbuilt features of Django is internationalization (also known as i18n). This article shows how to utilize i18n to reduce language barriers in web applications by offering their content in languages tailored to the audience.

What is internationalization and why it is needed

Internationalization, abbreviated as i18n, involves making a software application translatable to have a wider audience in the international market.

With the borderlessness of the internet, it binds different individuals speaking different languages to come across your web application.

Considering language being a major barrier in software adoptions, hence, the introduction of i18n in web frameworks to help translators make translations for varied languages available.

Why should developers care?
Well, when one intends to scale their applications beyond the immediate environment, the need for internationalization arises.

Django supports text translation. According to the Django official documentation:

Essentially, Django does two things:

  • It allows developers and template authors to specify which parts of their apps should be translated or formatted for local languages and cultures.
  • It uses these hooks to localize Web apps for particular users according to their preferences.

Internationalization is done by developers since it involves preparing software for localization -- target audience.

Translation strings and message files

I18n involves translating from one language to another. We need to know which information (dynamic and static data) will be translated. One may choose to translate a fraction of a software application whereas another can choose to translate the whole application.

The selected information to that is translated to the end user's language is referred to as "translation strings".

Message file is a file that contains these translation strings and their equivalent end-user target language. Translators fill message file with translations for the target language.

Language file creation

Let's create a Django project titled transy

>> django-admin startproject transy
>> cd transy

We create an app named simple

>> python manage.py startapp simple

Add it to INSTALLED_APPS in settings.py

...
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'simple', # new
]
...

Setup URLs in transy/urls.py and simple/urls.py

# in transy/urls.py
from django.contrib import admin
from django.urls import path, include

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

In simple/views.py:

from django.views.generic import TemplateView
from django.http import HttpResponse

def user_dashboard(request, count):
    statistics =  {
        'transactions_today': 4,
        'account_balance': 5439
    }
    return HttpResponse(statistics)

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

In simple/urls.py:

from django.urls import path

import users.views as views

urlpatterns = [
    path('', views.user_dashboard),
    path('home/', views.HomePageView.as_view(), name='home'),
    path('about/', views.about),

To specify texts in your web application that you will like to make translatable we use the gettext() function in views.py file, for example:

from django.http import HttpResponse
from django.utils.translation import gettext

def about(request, count):
    page = gettext('Welcome to Section.io blog, a home to so many wonderful articles.')
    return HttpResponse(page)

To use gettext() on Windows, you will need to download the executable file from here

Django comes with django-admin makemessages command which creates and updates the message files by pulling out all strings marked for translation. Run the command to create a message file for our simple app:

If you execute django-admin makemessages --all and run into CommandError like below, make sure you have gettext binary file installed. de and fr are the language codes for German and French respectively.

>> django-admin makemessages -l de    
CommandError: Can't find msguniq. Make sure you have GNU gettext tools 0.15 or newer installed

The command for creating message files is in this format django-admin makemessages -l <LANGUAGE_CODE>. If you would like to find out the available language codes in Django check out this list

>> django-admin makemessages -l fr

You will run into a CommandError like below:

>> django-admin makemessages -l fr  
CommandError: Unable to find a locale path to store translations for file simple/__init__.py

This is because Django is looking for a folder named " locale". By default, the script django-admin makemessages -l fr, is expected to run from one of these two places:

  1. The base folder of the Django project itself
  2. The base folder of one of the Django apps, in our case "simple".

To direct Django, we add a LANGUAGE_PATHS which is a list, similar to TEMPLATES.DIR, that gives several locations to search.

In settings.py:

LANGUAGE_PATHS = [
    os.path.join(BASE_DIR, 'locale'),  # base folder where manage.py resides
    os.path.join(BASE_DIR, 'simple/locale')  # app folder
]

We include the languages we want to make available for translation in settings.py file. This restricts the languages a user can have the site translated to. Of course, you can choose to remove LANGUAGES from your settings.py file.

from django.utils.translation import ugettext_lazy as _

# create a list of tuples ('LANGUAGE_CODE', 'LANGUAGE NAME')
LANGUAGES = [
   ('de', _('German')),
   ('en', _('English')),
   ('fr', _('French')),
   ('es', _('Spanish')),
   ('pt', _('Portuguese'))
]

We can include a default language for users visiting the website as below:

...
LANGUAGE_CODE = 'de'
LANGUAGES = [
   ('de', _('German')),
   ('en', _('English')),
   ('fr', _('French')),
   ('es', _('Spanish')),
   ('pt', _('Portuguese'))
]

We can include the Django's LocaleMiddleware which allows the user to specify the language preferred.

MIDDLEWARE_CLASSES = [
    'django.middleware.security.SecurityMiddleware',

'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.locale.LocaleMiddleware',  # <-- here
    'django.middleware.common.CommonMiddleware',
]

Because LocaleMiddleware makes use of session and cache (if set) data, it is advised to place LocaleMiddleware after the two ( or one, if the CacheMiddleware is not set).

MIDDLEWARE_CLASSES = [
    'django.middleware.security.SecurityMiddleware',

'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.cache.UpdateCacheMiddleware',
    'django.middleware.locale.LocaleMiddleware',  # <-- here
    'django.middleware.common.CommonMiddleware',
]

In our simple app, we create a folder named locale which shall store the message file for our application. Then rerun makemessages command in the app-level (simple):

>> cd simple
>> django-admin makemessages -l fr
processing locale fr
>> django-admin makemessages -l de
processing locale de
>> django-admin makemessages -l en
processing locale en

The folder structure of your app should be similar to this:

---transy
    |
    |---simple
        |
        |---locale
        |   |---de
        |       |---LC_MESSAGES
        |           |
        |           |-django.po
        |   |---fr
        |       |---LC_MESSAGES
        |           |
        |           |-django.po
        |   |---en
                |---LC_MESSAGES
                    |
                    |-django.po

The .po files will be used by translators where they provide the strings' equivalent in different languages based on the folder they exist in. Example, /locale/de/LC_MESSAGES/django.po will have translations in German.

Upon translations made available, we compile them by running the following command which makes these translations available on the website.

>> django-admin compilemessages

Note that the above command should run in the same directory django-admin makemessages was executed, that is, simple app. Each time changes are made to message (.po) files you are required to compile it.

django-admin compilemessages create .mo files, which according to the Django documentation, are binary files optimized for use by gettext.

Pluralization

Pluralization simply means providing a singular word with its plural form. The function responsible for pluralization in Django is ngettext() which takes 3 arguments; the singular string, the plural string and the number of objects expected to be represented.

For example:

from django.http import HttpResponse
from django.utils.translation import ngettext

def user_dashboard(request, count):
    statistics = ngettext(
        'there is %(count)d transaction today',
        'there are %(count)d transactions today',
        count,
    ) % {
        'count': count,
    }
    return HttpResponse(statistics)

Using Translations in Template

Often, one would like to apply translations directly in the Django template. To achieve this in our Django project titled trans, we need to load internationalization tag similar to how "static" is loaded in template files.

In our app template folder, create home.html file:

{% load static %}
{% load i18n %}

<title>Ifenna's Section.io blog</title>
<p>Good morning! How are you?</p>

To include translations in the template folder, Django provides {% translate '<STRING TO TRANSLATE>' %} and {% blocktranslate %} STRING with VARIABLES {% endblocktranslate %}.

{% load i18n %}
{% get_current_language as LANGUAGE_CODE %}
{% block content %}

<h1>Home Page</h1>
<p>{% translate 'Good morning! How are you?' %}</p>

{% translate "Good afternoon" as afternoon %}
{% translate "Good evening" as evening %}

<p>
    {% blocktranslate %} 
        This is the proper way to say "Good afternoon": {{afternoon}}. 
        This is "Good evening": {{evening}}
    {% endblocktranslate %}
</p>
{% endblock content%}

translate accepts either a string or a variable but never both. blocktranslate accepts a mix of strings, variables and template literals. For example:

{% for house in houses %}
<ul>
    <li>{% blocktranslate %}
            Name: {{ house }}
        {% endblocktranslate %}
    </li>
</ul>
{% endfor %}

We update messages file for our home.html file. Django detects changes and automatically updates the messages file

>> django-admin makemessages -l de
processing locale de

Take a look at the generated message (.po) file for German (de). You find something similar to this:

# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-12-17 09:14+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#: .\templates\home.html:7
msgid "Good morning! How are you?"
msgstr "Guten Morgen! Wie geht's?"         <------- ADD THIS

#: .\templates\home.html:9
msgid "Good afternoon"
msgstr "Guten Nachmittang"         <------- ADD THIS

#: .\templates\home.html:10
msgid "Good evening"
msgstr "Good Abend"         <------- ADD THIS

#: .\templates\home.html:13
msgid ""
" \n"
"        This is the proper way to say \"Good afternoon\": %(afternoon)s. \n"
"        This is \"Good evening\": %(evening)s\n"
"    "
msgstr ""
" \n"
"        Dies ist der richtige Weg zu sagen \"Good afternoon\": afternoon. \n"
"        Das ist \"Good evening\": \n"
"    "         <------- ADD THIS

You will notice that the message file has additional information like email, full name and language team. These are documented by the translator(s) working on the message file.

msgstr is where translators input translation strings based on the msgid it references.

Start your local server:

>> python manage.py runserver

Visit http://localhost:8000/home/, there are no obvious changes. Let's compile the messages file.

>> django-admin compilemessages

Now, visiting http://localhost:8000/home/, you should see the translated version since we previously set LANGUAGE_CODE='de', the browser detects this and uses the translation provided for German.

Summary

In this article, we discussed what internationalization is and how Django handles it. We discussed what developers role is in relation to internationalization and how translators can access messages file. We also showed how translations can be done in the Django templates.