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. Additionally, we’ll explore how Django supports translation management system to streamline the localization process and ensure consistency across languages.
By leveraging software internationalization techniques within Django, developers can create more flexible applications that easily adapt to different languages and cultural contexts, ultimately improving user satisfaction and market reach.
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:
- Open up the
django_i18n_advanced
project in your IDE. - Navigate to the project settings file situated at:
django_i18n_advanced\settings.py
. - Find the list named
INSTALLED_APPS
and addi18n_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
- Import
gettext
utility with an underscore used as an alias. - 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.
- The
I am localized inside the view.
value is marked as a translation string using gettext. - Call the
render
function which renders the dynamic content inside theindex.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>
- Load i18n-related tags provided by Django.
- DTL reference to
localize_key
passed over from the view. - The
I am localized inside the template.
value is marked as a translation string usingtranslate
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:
- Create a new directory inside the
django_i18n_advanced\i18n_app
and name itlocale
. This folder will be used to hold our message files. - 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.
- 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."
- 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
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.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:- Open up the
settings.py
file inside ourdjango_i18n_advanced
project. - Find the
MIDDLEWARE
list and addLocaleMiddleware
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.
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.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
, theLANGUAGE_CODE
acts as the default language to serve all clients.
Handle pluralization
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.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
- An additional
count
parameter is passed over to the view. - The
ngettext
function chooses the singular or plural form string based on thecount
we provide. Then, the value of thecount
parameter is added to the named placeholder (%(count)d
) of the string. Finally, thepluralized
variable is initialized with the value returned fromngettext
. - 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.
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
- The
now
class method inside the Python datetime library is invoked. This constructs adatetime
object holding the current date and time. date_time_dict
dictionary is created with a context value holding thedate_time_now
variable created at step 1.date_time_dict
passed over to adatetime.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>
- The
get_current_language_bidi
function is added to the template with aLANGUAGE_BIDI
alias. - 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:
- Run a
django-admin makemessages -l ar
command on the terminal to add the Arabic language to the project. - Make sure to compile it with a
django-admin compilemessages
command. - Add a simple
bidi
view insideviews.py
to call ourbidi.html
template:
def bidi(request): return render(request, 'bidi.html')
- Don’t forget to add a
path('bidi/', views.bidi),
line inside theurlpatterns
list inside theurls.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
Enable time zones on Django
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
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
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 otherdatetime
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
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
.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
- Construct a naive
datetime
object holding the current date and time. Note, thisdt_now_naive
object has no sense of time zones at this level. - Receive a
datetime.tzinfo
implementation for ‘Asia/Colombo’ time zone frompytz
. Note: You can usepytz.all_timezones
to retrieve the time zone list supported bypytz
. - Inject the naive
dt_now_naive
object with thetzinfo
data retrieved in step 2; this transforms the once naivedt_now_naive
datetime
object into an awaredatetime
. - 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#}
- Firstly, add a
{% load i18n %}
line at the top to load Django i18n-related tags. - 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. - Put in a CSRF tag to set the Django-provided Cross Site Request Forgery protection to this form.
- Open a select form with a name specifically set to “language”, as expected by the
set_language
view. - get_current_language is used to set a
LANGUAGE_CODE
variable that holds the user’s currently selected language. - Currently active languages within the project are extracted with the help of get_available_languages.
- 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. - The
languages
list is iterated to find and mark theoption
with a language code matching theLANGUAGE_CODE
. - A
Switch
submit button is added to submit the form with the user’s language preference data. - 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!