Advanced Django Internationalization

A while back we looked into how the vastly popular Django framework for Python supports internationalization. But, if you are here, you surely must have needed a deeper dive into Django’s i18n capabilities. Looking for more of an advanced look at Django’s internationalization features?

In this article, we’ll take a look at some of the features developed by Django to address the most desired functions, and dreaded issues, encountered by its users when localizing their web applications.

We will be covering the following topics in this tutorial:

  • Django i18n/l10n explored further.
  • Delegating language selection to the client using LocaleMiddleware.
  • Language fallback behavior using LANGUAGE_CODE.
  • Handling pluralization with the help of ngettext.
  • Date-time localization using Django’s formatting system.
  • Changing language direction with the help of get_current_language_bidi.
  • Time zone and daylight saving time management.
  • Easily switching languages using set_language.

Assumptions

Basic knowledge of Python language & Django framework.

Prerequisites

Local environment set up with:

  • Python 3.8.x
  • Django 3.x
  • gettext
  • Python-supported IDE with the Django plugin installed.

Note: All explanations will be platform independent.

Environment

I will be using the following environment for my development purposes:

  • Python 3.8.5
  • Django Framework 3.1.2
  • gettext0.21-iconv1.16-shared-64
  • PyCharm Professional 2019.3

The source code is available on GitHub.

Create a simple Django project

First off, let us build a simple Django project where we can implement the upcoming advanced internationalization facilities.

We’ll start by opening up a command prompt and entering the following:

django-admin startproject django_i18n_advanced

As is evident from the startproject keyword, this will create a Django project named django_i18n_advanced in the current directory.

Secondly, let’s cd into this django_i18n_advanced directory and start a new app named i18n_app within our project:

python manage.py startapp i18n_app

Thirdly, we have to register this newly created i18n_app in our django_i18n_advanced project. Follow these steps to perform this task:

  1. Open up the django_i18n_advanced project in your IDE.
  2. Navigate to the project settings file situated at: django_i18n_advanced\settings.py.
  3. Find the list named INSTALLED_APPS and add i18n_app to the end of it:
INSTALLED_APPS = [
     .
    'i18n_app',
]

Fill Django project with data

Alright, we have created an empty Django project and added an app to it. Now as our next step, let’s fill it with some data that we can expose to various Django internationalization features in the upcoming topics.

Create a view

Firstly, let’s code a simple view with some l10n functionalities in the i18n_app inside our django_i18n_advanced project.

Head on over to the views.py file within the i18n_app directory. Let’s add an index view to it with a dictionary holding a random value in the English locale:

from django.utils.translation import gettext as _  # 1


def index(request):
    context_dict = {  # 2
        'localize_key': _('I am localized inside the view.')  # 3
    }
    return render(request, 'index.html', context_dict)  # 4
  1. Import gettext utility with an underscore used as an alias.
  2. The dictionary holding values to be passed on to the template. We’ll pass a record in the English locale which will be localized to other languages later on.
  3. The I am localized inside the view. value is marked as a translation string using gettext.
  4. Call the render function which renders the dynamic content inside the index.html template using Django’s template engine.

Create a template

Secondly, let’s create a basic template in our i18n_app to display its localization capabilities.

Make sure to create a templates directory inside the i18n_app. Then, make a new HTML file named index.html inside the templates directory, and fill this template as follows:

{% load i18n %}  {#1#}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Index</title>
</head>
<body>
    {{ localize_key }}  {#2#}
    {% translate "I am localized inside the template." %}  {#3#}
</body>
</html>
  1. Load i18n-related tags provided by Django.
  2. DTL reference to localize_key passed over from the view.
  3. The I am localized inside the template. value is marked as a translation string using translate i18n tag.

Extract translation strings into message file

After this, since we have put some translation strings in our i18n_app, we’ll thirdly Django to pull these out and place them inside the message files:

  1. Create a new directory inside the django_i18n_advanced\i18n_app and name it locale. This folder will be used to hold our message files.
  2. Open up a terminal inside the django_i18n_advanced directory and run this command:
django-admin makemessages -l it

This command will create a message file representing the Italian language, which will be situated at the: locale\it\LC_MESSAGES\django.po path inside our i18n_app directory.

  1. Open the django.po message file and add the correct localization values to it as follows:
#: .\i18n_app\templates\index.html:9
msgid "I am localized inside the template."
msgstr "Sono localizzato all'interno del modello."

#: .\i18n_app\views.py:7
msgid "I am localized inside the view."
msgstr "Sono localizzato all'interno della vista."
  1. Compile the message file using the following command:
django-admin compilemessages

So, this will create a django.po file containing the compiled message file to be used by the gettext utility.

Make changes to settings.py

We’re almost there! Now, as our fourth step let’s add the LOCALE_PATHS list to the settings.py file in our django_i18n_advanced project. Open up the settings.py file on the project root and add these changes to it:

from os.path import join

LOCALE_PATHS = [
    join(BASE_DIR, 'i18n_app', 'locale'),
]

Consequently, this will make sure our Django project knows where to look for the localization message files.

Add a URL

Finally, let’s go ahead and add a URL endpoint that will fire our index view. Open up the urls.py file within the project root (where settings.py lies) and edit it as shown below:

from i18n_app import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', views.index)
]

And that’s it! we have created a basic Django project with basic i18n functionalities coded within it.

Test basic Django project

Before we subject our django_i18n_advanced project to advanced internationalization features, let’s run its server once just to make sure it works, shall we?

Open up a terminal within the base directory (where the manage.py lies) and enter the following:

python manage.py runserver

In short, if all went well, our Django server should start, and the terminal will mention a link to where this server resides. Plus, clicking this URL should open up an HTML page on the browser displaying our index.html template holding the context value we passed on from the index view.

Django i18n Unchained

Django developers have implanted a set of advanced internationalization-related facilities in their framework just to make life for us developers a bit smoother along this localization journey. So, it would only be fair to get a proper idea of what these options are, and how we can work with them, right?

Let’s go ahead and see how we can introduce some of these i18n-related features into our django_i18n_advanced project.

Let the user choose the language

As we learned in the beginner’s guide, we can set a default language to be used by setting the LANGUAGE_CODE. But, instead of determining the language on our own, why don’t we hand over the remote to our user? This is achieved with the help of the Middleware plugin baked into the Django framework.
Django readily ships with a set of these Middleware components. And from that lot, we can specifically choose the middleware named LocaleMiddleware for our requirements.
LocaleMiddleware chooses the localized language based on some specific data sent in the request. Here’s how to set it up:
  1. Open up the settings.py file inside our django_i18n_advanced project.
  2. Find the MIDDLEWARE list and add LocaleMiddleware inside it:
.
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
.

Important Note: Make sure to place LocaleMiddleware after SessionMiddleware and before CommonMiddleware. This requirement boils down to how middleware components are loaded in Django. Since middleware can depend on other middleware, middleware loaded later on in the order might require functionalities of the middleware loaded before it.

Basically, that’s all it needs! Now, we can go ahead and change the language of our browser to test this out. When we switch it to the Italian language and send a request to our django_i18n_advanced server, it’ll return a page containing the localized values we set earlier. And, if we switch the browser language back to its default and reload the page, we’ll once again be presented with a page containing the default values.
Note: Once we set the middleware, no further server-level changes will be required for this language switch to occur when requested by the client.

Language fallback behavior

So, what if a user visits our Django application from a country still awaiting localization? Or worse, what if our application was meant to serve a population that doesn’t speak English, but instead of defaulting to that specific language, it keeps defaulting to English?

These concerns can easily be sorted out with a simple LANGUAGE_CODE tag which we can set in a Django project’s settings file.

Let us open up the settings.py file in the project root and set its LANGUAGE_CODE value to Italian:

LANGUAGE_CODE = 'it'

Important Note: The language code put here must conform to the standard language ID format, e.g., ‘it’, ‘en-us’, etc. The separator is a dash, not an underscore.

Evidently, Django uses the LANGUAGE_CODE of a project in two ways:

  • If the project is set up to use LocaleMiddleware, but for some reason it fails to determine the user’s preferred language, the LANGUAGE_CODE serves as the fallback mechanism by providing the language to which it will fall back.
  • If the project doesn’t use LocaleMiddleware, the LANGUAGE_CODE acts as the default language to serve all clients.

Handle pluralization

Let’s assume for a minute that our django_i18n_advanced project is for a pet adoption service. When customers add multiple pets to their carts, is it fair for our advanced, internationalization-enabled Django website to show them a grammatically invalid notification such as “3 cat added” or “5 dog added to cart”? No, it is not. Hence, this is where the need for proper pluralization comes in.
Django’s django.utils.translation module provides us with an ngettext function to handle pluralization. So, let’s see how it works! First off, let’s add a new view to the views.py of our i18n_app:
def add_cat(request, count):  # 1
    pluralized = ngettext(  # 2
        'added %(count)d cat',
        'added %(count)d cats', count
    ) % {'count': count}

    return HttpResponse(pluralized)  # 3
  1. An additional count parameter is passed over to the view.
  2. The ngettext function chooses the singular or plural form string based on the count we provide. Then, the value of the count parameter is added to the named placeholder (%(count)d) of the string. Finally, the pluralized variable is initialized with the value returned from ngettext.
  3. The pluralized variable is returned as an HTTP response.

Secondly, let’s also add a URL pattern on the urls.py file to fire our add_cat function:

urlpatterns = [
    .
    path('add/<int:count>/', views.add_cat),
]

This URL pattern will accept an integer parameter and send it over to the add_cat view as a count parameter.

Now, we can call this add/ URL appending singular and plural counts to watch ngettext work its magic and give out the pluralized string in the correct plural form.

Date and time formatting

When developing our Django application, at one time or another, we may come across a few dates and times. But the way you’d expect them to be displayed might be quite different from the way I’d expect them to be, right? So, let’s find out how to avoid our client mistaking a day for a month because we displayed the date on our app in a format that isn’t familiar to the user.

Django formatting system

Django ships with quite an advanced formatting system that takes care of formatting various internationalization-related units like dates, times, and numbers, to their localized formats before displaying them on templates. This feature is enabled by a simple boolean set in the project’s settings.py file:

USE_L10N = True

Note: If we visit the settings.py file in our django_i18n_advanced project, we’ll be able to notice that this setting has already been set. This is due to the behavior of Django’s project creation command. When we create a Django project using the django-admin startproject command, by default Django makes sure to enable localization on the project it creates. Ergo, the settings.py file it generates will always have a USE_L10N = True setting added to it.

Create a view

Let’s test whether the Django formatting system works as expected. How about we add a view that displays the current date and time to our django_i18n_advanced project?

Firstly, head on over to the views.py file in the project, and add a view:

def get_current_date_time(request):
    date_time_now = datetime.datetime.now()  # 1
    date_time_dict = {  # 2
        'date_time_key': date_time_now,
    }

    return render(request, 'datetime.html', date_time_dict)  # 3
  1. The now class method inside the Python datetime library is invoked. This constructs a datetime object holding the current date and time.
  2. date_time_dict dictionary is created with a context value holding the date_time_now variable created at step 1.
  3. date_time_dict passed over to a datetime.html template.

Add a template

Now, let’s create a simple datetime.html file inside the templates directory with the following content:

{{ date_time_key }}

To clarify, this holds a DTL reference to the context value passed over from the get_current_date_time view.

Test it out

Et voilà! Let’s go ahead and open up the URL: http://127.0.0.1:8000/datetime/ for each language into which we localized our django_i18n_advanced project. Now, we should be able to notice a localized date and time value for each of these languages.

e.g. Oct. 16, 2020, 8:18 p.m.
e.g. Venerdì 16 Ottobre 2020 20:18

Change language direction

Although most languages are written and read from left to right, some languages work in the opposite direction, e.g. Arabic, Hebrew, and Urdu. Let’s see how we can address this when we localize our Django applications.

Django provides a get_current_language_bidi function on its i18n module to help us in this case. Let’s go ahead and create a new template that utilizes this function. Create a new bidi.html file inside our django_i18n_advanced project’s i18n_app/template directory and fill it as follows:

{% load i18n %}
{% get_current_language_bidi as LANGUAGE_BIDI %}  <!-- 1 -->

<html dir="{% if LANGUAGE_BIDI %}rtl{% else %}ltr{% endif %}">  <!-- 2 -->
I am displayed in the right direction.
</html>
  1. The get_current_language_bidi function is added to the template with a LANGUAGE_BIDI alias.
  2. Direction of the HTML content is determined by a DTL if-else block.

So, there we go! Language direction functionality on the template has already been taken care of.

Just to make sure, let’s add a new right-to-left language to our django_i18n_advanced project and see:

  1. Run a django-admin makemessages -l ar command on the terminal to add the Arabic language to the project.
  2. Make sure to compile it with a django-admin compilemessages command.
  3. Add a simple bidi view inside views.py to call our bidi.html template:
def bidi(request):
    return render(request, 'bidi.html')
  1. Don’t forget to add a path('bidi/', views.bidi), line inside the urlpatterns list inside the urls.py file.

Now we can click the: http://127.0.0.1:8000/bidi/ URL on the browser and switch its language between the us and ar languages to see the content change direction.

Working with time zones

Earlier in the Date and time formatting section, we discussed the importance of localizing our project’s dates and times to match that of our users. But imagine this scenario:
Assume our application is scheduled to bill our users daily at 12:00 AM, their local time. However, due to a daylight saving time change on their end, their local time has changed by an hour. Hence, their local time reaches another 12:00 AM time just one hour after the previous one. And alas, our advanced-internationalized Django application bills them again!
Clearly this needs to be avoided, right? This is where the requirement of being ‘timezone-aware’ comes in.

Enable time zones on Django

Django has a handy USE_TZ setting in its advanced internationalization arsenal to address this. Let’s set this flag to true in our django_i18n_advanced project’s settings.py file:
USE_TZ = True
Note: As in the case with the USE_L10n setting, each time it creates a project, Django’s project creation command automatically sets the USE_TZ value to True on its settings.py file.

How datetime handles time zones

Python datetime objects follow a ‘naive or aware‘ concept to keep a record of the time zones they operate in:
  • A naive datetime object – as the name suggests – is sort of a dumb object that’s unaware of the time zone it operates in. It is up to the developer to determine its time zone.
  • In contrast, an aware datetime object knows how its allocated time zone behaves in real time. Hence, it can locate itself between other datetime objects, and is aware of any DST changes that occur.

Note: tzinfo property inside datetime objects is used to hold timezone-related data used by aware datetime objects. Python ships with a timezone concrete class implementation of this tzinfo abstract class.

Create timezone-aware datetime

We can call upon the assistance of an advanced and mature internationalization-related Django library named pytz for our requirements. We can use this library shipped with Django to easily create aware datetime objects in our internationalized Django projects. Let’s see how we can convert a usual naive datetime object into a timezone-aware datetime using pytz.
Head on over to the views.py file in the project, and add a view:
def get_aware_current_date_time(request):
    dt_now_naive = datetime.datetime.now()  # 1
    localized_tz = pytz.timezone('Asia/Colombo')  # 2
    dt_now_aware = localized_tz.localize(dt_now_naive)  # 3

    return HttpResponse(dt_now_aware)  # 4
  1. Construct a naive datetime object holding the current date and time. Note, this dt_now_naive object has no sense of time zones at this level.
  2. Receive a datetime.tzinfo implementation for ‘Asia/Colombo’ time zone from pytz. Note: You can use pytz.all_timezones to retrieve the time zone list supported by pytz.
  3. Inject the naive dt_now_naive object with the tzinfo data retrieved in step 2; this transforms the once naive dt_now_naive datetime object into an aware datetime.
  4. Return dt_now_aware object value as an HTTP response.

Let’s also add a path inside urls.py as an endpoint to reach our get_aware_current_date_time view:

urlpatterns = [
    . 
    path('awaredatetime/', views.get_aware_current_date_time),
]

Let’s call the http://127.0.0.1:8000/awaredatetime/ URL and see:

2020-10-19 13:40:44.979676+05:30

Now, we’ll be able to observe the date and time displayed with timezone-related information added to it (+05:30). This proves we have successfully converted the naive datetime object into a timezone-aware datetime object.

Note: The localize method assumes that the passed in datetime object already holds the correct date and time for the expected time zone. The method will make no changes to the date and time; its responsibility is to make the naive-to-aware conversion by adding a Pytz tzinfo to the datetime.

Add a language switcher

Before we wrap this up, let’s take a quick look at the set_language view provided by Django for our advanced-level internationalization purposes. This built-in view helps us easily switch our Django application between its localized languages. Let’s see how to set this up, shall we?

Create a language switcher template

First off, let’s create a new langswitch.html template inside the i18n_app of our django_i18n_advanced project. Now, let’s go ahead and fill this template as follows:

{% load i18n %} {#1#}

<form action="{% url 'set_language' %}" method="post">  {#2#}
    {% csrf_token %}  {#3#}
    <select name="language">  {#4#}
        {% get_current_language as LANGUAGE_CODE %}  {#5#}
        {% get_available_languages as LANGUAGES %}  {#6#}
        {% get_language_info_list for LANGUAGES as languages %}  {#7#}
        {% for language in languages %}
            <option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected{% endif %}>  {#8#}
                {{ language.name_local }} ({{ language.code }})
            </option>
        {% endfor %}
    </select>
    <input type="submit" value="Switch">  {#9#}
</form>

{% translate "I am localized inside the template." %}  {#10#}
  1. Firstly, add a {% load i18n %} line at the top to load Django i18n-related tags.
  2. Open up an HTML form with its action set to send a POST request to set_language view. Note: As mentioned in the Django documentation, this view specifically expects a POST request to be sent.
  3. Put in a CSRF tag to set the Django-provided Cross Site Request Forgery protection to this form.
  4. Open a select form with a name specifically set to “language”, as expected by the set_language view.
  5. get_current_language is used to set a LANGUAGE_CODE variable that holds the user’s currently selected language.
  6. Currently active languages within the project are extracted with the help of get_available_languages.
  7. The active language list received in step 6 is passed over to get_language_info_list. It finds further information about those languages and creates a new languages list containing this information.
  8. The languages list is iterated to find and mark the option with a language code matching the LANGUAGE_CODE.
  9. A Switch submit button is added to submit the form with the user’s language preference data.
  10. Finally, add a translation string to check if the language switches properly.

A few final steps

Next, let’s add a view in our django_i18n_advanced project’s views.py to call this langswitch.html template:

def switch_lang(request):
    return render(request, 'langswitch.html')

Thirdly, open up the urls.py file and add these lines:

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

As shown on the highlighted line, the django.conf.urls.i18n module is added to activate the set_language view within the project.

As the last step, let’s add a LANGUAGES list inside our django_i18n_advanced project’s settings.py file:

LANGUAGES = [
    ('en-us', 'English'),
    ('it', 'Italian'),
]

This will help us restrict our language selection list shown on our template form to a relevant subset of Django-provided languages.

Test it out

Now, the form we developed will eventually list the available languages. Once the user selects a language and submits, the application will be localized to the chosen language and the page will reload.

Just Lokalise it, Monsieur

Thus, you’ve taken the deep descent to the bottomless pits of Django.

But, it wouldn’t hurt to have a helping hand in this whole internationalization scenario, am I right?

Meet Lokalise, the translation management system that takes care of all your Django application’s—basic and advanced—internationalization needs. With features like:

  • Easy integration with various other services.
  • Collaborative translations.
  • Quality assurance tools for translations.
  • Easy management of your translations through a central dashboard.

Plus, loads of others. Lokalise will make your life a whole lot easier by letting you expand your Django app to all the locales you’ll ever plan to reach.

Get started with Lokalise in just a few steps:

  • Sign up for a free trial (no credit card information required).
  • Log in to your account.
  • Create a new project under any name you like.
  • Upload your translation files and edit them as required.

That’s it! You have already completed the baby steps toward Lokalise-ing your web application. See the Getting Started section for a collection of articles that will provide all the help you’ll need to kick-start the Lokalise journey. Also, refer to Lokalise API Documentation for a complete list of REST commands you can call on your Lokalise translation project. Lokalise also provides a Python API client that you can set up with your Django app in a flash.

Conclusion

In this tutorial, we explored a few advanced, internationalization-related features in Django. We created a basic Django internationalization-enabled application and filled it with advanced features such as setting user-preferred localization languages and configuring language fallback behavior. We also looked into how we can set the language direction, handle pluralization, date-time localization, and time zones.

Plus, we briefly examined how we can create a language switcher that helps us easily switch between the localized languages.

And with that, I’ll be signing off. Drop me a line if you have any questions, and don’t hesitate to leave a comment.

Till we meet again, don’t forget to wear your masks and keep a safe distance from your fellow coders!

Related posts

Sign up to our newsletter

Get the latest articles on all things localization and translation management delivered straight to your inbox.

Read also
Localization made easy. Why wait?
The preferred localization tool of 1500+ leading global companies