Previously, we explored the basics of how the Django framework supports internationalization. Now, it’s time to take a deeper dive into advanced Django internationalization. In this guide, we’ll cover practical techniques such as pluralization, context-aware translations, dynamic language switching, localizing dates and times, and handling RTL languages like Hebrew.
By mastering these features, you can build highly adaptable multilingual applications that deliver seamless experiences to users across different cultures and regions.
The source code for this Django i18n tutorial is available on GitHub.
Prerequisites for Django i18n
Before diving into advanced Django internationalization, ensure you have the following set up:
- Python and Django installed: You’ll need Python 3.8 or higher and Django installed on your system.
- gettext installed: Django relies on
gettext
for managing translation files. - A code editor: Use any editor you’re comfortable with, such as VS Code or PyCharm.
- A basic app prepared for i18n: You should already have an app configured for basic internationalization, including translatable strings and language settings.
If you don’t have an app set up yet, follow our Django i18n basics guide or use the sample app from GitHub.
Dynamically set the lang attribute
First of all, let’s check the base.html
template we’ll be using for this tutorial. In my sample app it’s stored under blogs/templates/base.html
:
{% load i18n %} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>My Django App</title> </head> <body> <header> <nav> <h1>My Django App</h1> {% block language_switcher %} <form method="post" action="{% url 'set_language' %}"> {% csrf_token %} <label for="language-select">{% trans "Choose your language" %}:</label> {% get_current_language as current_language %} {% load language_tags %} <select id="language-select" name="language"> {% get_current_language as current_language %} {% get_languages as languages %} {% for code, name in languages %} <option value="{{ code }}" {% if current_language == code %}selected{% endif %}>{{ name }}</option> {% endfor %} </select> <button type="submit">{% trans "Switch" %}</button> </form> {% endblock %} </nav> </header> <main> {% block content %} {% endblock %} </main> </body> </html>
As you can see, it displays a language switcher and all in all it looks nice. However, there’s one issue I don’t really like: the <html lang="en">
attribute is currently hardcoded. Ideally, it should reflect the active language of the application. To make it dynamic, we can use Django’s {% get_current_language %}
template tag.
Update your base.html
file like this:
<!DOCTYPE html> <html lang="{% get_current_language as current_language %}{{ current_language }}"> <!-- ... -->
This ensures that the lang
attribute dynamically updates to the current language code, such as en
or lv
, depending on the active locale.
By making this change, your app’s HTML structure will accurately reflect the current locale, improving accessibility and SEO.
Handling pluralization in Django
Next, let’s see how to introduce pluralization in our app. Django makes this easy with the {% blocktrans %}
template tag, which allows you to handle pluralization dynamically in templates.
Let’s open blogs/templates/blogs/home.html
template that currently has the following content:
{% extends 'base.html' %} {% load i18n %} {% block title %}Home Page{% endblock %} {% block content %} <h2>{% trans "Hello, world!" %}</h2> {% endblock %}
Also open the blogs/views.py
file:
from django.shortcuts import render from django.utils.translation import gettext as _ def home(request): return render(request, 'blogs/home.html') # Render the specific home template
Suppose we would like to pluralize the phrase “You have X messages”.
Step 1: Add a message count to the view
First, pass the number of messages to the template from your view. Update your home
view like this:
def home(request): message_count = 3 return render(request, 'blogs/home.html', {"message_count": message_count})
Here, message_count
represents the number of messages, which will be used for pluralization in the template.
Step 2: Use {% blocktrans %}
in the template
Update your home.html
template to display a pluralized message based on the message_count
:
{% block content %} <h2>{% trans "Hello, world!" %}</h2> <p> {% blocktrans count message_count as count %}You have {{ count }} message.{% plural %}You have {{ count }} messages.{% endblocktrans %} </p> {% endblock %}
{% blocktrans %}
: Marks the block of text as translatable and handles pluralization.count message_count as count
:message_count
is the variable passed from the view.count
is an alias used within the block to display the number.
{% plural %}
: Separates the singular and plural forms of the message.{{ count }}
: Dynamically inserts the number into the message.
Django uses this structure to decide which version of the message to display based on the value of message_count
.
Step 3: Provide pluralized translations
Use the makemessages
command to extract the translatable strings into .po
files for both English and Latvian:
python manage.py makemessages -l en -l lv
Now, open blogs/locale/en/LC_MESSAGES/django.po
message file. You’ll see the following entry for the pluralized translations:
#: .\blogs\templates\blogs\home.html:10 #, python-format msgid "You have %(count)s message." msgid_plural "You have %(count)s messages." msgstr[0] "" msgstr[1] ""
English has only two plural forms: singular and plural. Since the source strings are already in English, there’s no need to change the translations here. The header of the .po
file includes the pluralization rule for English:
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
This defines that English has two forms:
- Plural (
msgstr[1]
) for all other cases (count != 1
). - Singular (
msgstr[0]
) for whencount == 1
.
Next, open blogs/locale/lv/LC_MESSAGES/django.po
. The header will include this line:
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : " "2);\n"
Latvian has three plural forms, so the translations for msgstr
will differ:
#: .\blogs\templates\blogs\home.html:10 #, python-format msgid "You have %(count)s message." msgid_plural "You have %(count)s messages." msgstr[0] "" msgstr[1] "" msgstr[2] ""
So, under reach msgstr
you’ll need to provide an individual form, like this:
msgstr[0] "Jums ir %(count)s ziņa." # Singular (1) msgstr[1] "Jums ir %(count)s ziņas." # Few (2, 3, 4) msgstr[2] "Jums ir %(count)s ziņu." # Many or zero (0, 5+)
Ensure each msgstr
corresponds to the proper plural form based on the pluralization rules for Latvian. Without these, translations might break for specific numeric values.
After adding translations, compile the .po
files into .mo
files:
python manage.py compilemessages
Restart the server and test to confirm everything is working as expected.
Step 4: Handle pluralization in Python files
For pluralization in Python code, Django provides the ngettext
function. This allows you to programmatically handle singular and plural forms of strings in your logic.
Here’s how to use ngettext
to handle pluralized translations:
from django.shortcuts import render from django.utils.translation import ngettext, gettext as _ def home(request): message_count = 1 message = ngettext( "You have %(count)s message.", # Singular form "You have %(count)s messages.", # Plural form message_count ) % {'count': message_count} print(message) return render(request, 'blogs/home.html', {"message_count": message_count})
ngettext(singular, plural, count)
:- Takes the singular and plural forms of the string, and the numeric value (
count
) to decide which form to use. - Uses the same pluralization rules defined in the
.po
files.
- Takes the singular and plural forms of the string, and the numeric value (
%(count)s
substitution:- Dynamically inserts the value of
count
into the translated string.
- Dynamically inserts the value of
Now you can start your development server and see this feature in action!
Localizing date and time in Django
Localizing dates and times ensures that they are formatted correctly based on the user’s locale. Django makes it easy to display dates and times in a way that respects the user’s language and regional preferences. Here’s how to do it.
Step 1: Enable localization settings
First, ensure that localization is enabled in your settings.py
file:
USE_I18N = True # Enable internationalization USE_L10N = True # Enable localization of formats (dates, numbers, etc.) USE_TZ = True # Enable timezone support
USE_I18N
: Enables Django’s internationalization system.USE_L10N
: Ensures that dates and numbers are formatted according to the active locale.USE_TZ
: Enables timezone support for working with date and time data.
Here in the same file you can also choose the default timezone:
TIME_ZONE = 'UTC'
Step 2: Format dates and times in Python code
Use Django’s utilities to format dates and times according to the user’s locale. Here’s an example using the dateformat
and timeformat
filters:
# ... from django.utils.timezone import localtime, now from django.utils.formats import date_format def home(request): # ... pluralization ... current_time = localtime(now()) formatted_date = date_format(current_time, use_l10n=True) print(formatted_date) return render(request, 'blogs/home.html', {"message_count": message_count})
Please note that in this case we use now()
to return a time-zone aware object. We cannot apply localtime()
to a so-called “naive datetime format” that does not provide timezone information.
Step 3: Format dates and times in templates
In templates, you can use the date
and time
filters to format dates and times dynamically. For example:
<p>{% trans "The current date and time is:" %} {{ current_time|date:"DATETIME_FORMAT" }}</p>
Make sure to provide the current_time
in the view:
return render(request, 'blogs/home.html', {"message_count": message_count, "current_time": now()})
Step 4: Customize date and time formats
You can override the default formats for specific locales by creating a formats.py
file in your app. For example, create a format file in your app directory:
blogs/ ├── formats/ │ └── lv/ │ └── formats.py
Add custom formats for Latvian (or any locale):
DATE_FORMAT = "d.m.Y" # Example: 24.12.2024 TIME_FORMAT = "H:i" # Example: 15:45
Django will use these formats when the active locale matches lv
. For example, you can do this:
<p>{% trans "The current date and time is:" %} {{ current_time|date:"TIME_FORMAT" }}</p>
Step 5: Handle time zones
When working with time zones, Django uses the USE_TZ
setting to store all times in UTC by default and converts them to the user’s local time for display.
To activate a specific time zone for a user dynamically, use the activate
function from django.utils.timezone
:
# ... from django.utils.timezone import activate def home(request): # ... other code ... # Choose and activate the timezone: user_timezone = 'Europe/Riga' activate(user_timezone) return render(request, 'blogs/home.html', {"message_count": message_count, "current_time": now()})
Now the localized time will be displayed according to the currently activated timezone.
Adding i18n_patterns for language-prefixed URLs
Using i18n_patterns
, we can add target language prefixes to URLs, such as /en/
for English or /lv/
for Latvian. This approach enhances user experience by clearly indicating the current language in the URL.
Step 1: Modify urlpatterns
with i18n_patterns
Wrap your existing URL patterns in i18n_patterns
to enable language prefixes inside the urls.py
file:
from django.contrib import admin from django.urls import path, include from django.conf.urls.i18n import i18n_patterns from blogs.views import home urlpatterns = [ path('i18n/', include('django.conf.urls.i18n')), # Language switcher ] urlpatterns += i18n_patterns( path('admin/', admin.site.urls), path('', home, name='home'), # Home view with language prefix )
- The
i18n_patterns
function automatically adds the active language code (e.g.,en
,lv
) as a prefix to all URLs within it. - For example:
/en/
for English./lv/
for Latvian.
Django dynamically resolves the language from the URL and activates it for the current request.
Step 2: Update templates with language-prefixed URLs
When using language-prefixed URLs, Django provides the {% url %}
tag to generate correct links automatically. Update your templates to use language-aware URLs:
<a href="{% url 'home' %}">{% trans "Home" %}</a>
Django will automatically prepend the language code to the URL based on the user’s preferred language.
Context-aware translations in Django
Some words or phrases can have different meanings depending on the context. For example, the word “File” could be a noun (“File this document”) or a verb (“File a report”). Django provides tools like pgettext
and npgettext
to handle such context-aware translations effectively.
Step 1: Use pgettext
in Python code
The pgettext
function allows you to provide a context for your translation strings. Here’s how to use it:
from django.utils.translation import pgettext def home(request): # ... file_noun = pgettext("noun", "File") print(file_noun) # Context for "File" as a verb file_verb = pgettext("verb", "File") print(file_verb)
In this example:
"noun"
and"verb"
are contexts to differentiate the meaning of “File”.- These contexts will appear in the
.po
file during extraction.
Step 2: Use pgettext
in templates
For context-aware translations in templates, use the {% trans %}
tag with the context
parameter:
<p>{% trans "File" context "noun" %}</p> <p>{% trans "File" context "verb" %}</p>
This works just like pgettext
in Python but is used directly in the template.
Step 3: Extract context-aware strings
When you run makemessages
, context-aware translations will be included in the .po
file with a msgctxt
field:
#: .\blogs\views.py:31 msgctxt "noun" msgid "File" msgstr "File (noun)" #: .\blogs\views.py:35 msgctxt "verb" msgid "File" msgstr "File (verb)"
Step 4: Use npgettext
for context-aware pluralization
For pluralizable strings with context, use npgettext
. Here’s an example:
from django.utils.translation import npgettext def plural_context_example(request): count = 3 # Example count message = npgettext( "email context", "You have %(count)d email.", "You have %(count)d emails.", count ) % {'count': count} print(message)
Use context-aware translations when:
- A word or phrase has multiple meanings depending on usage.
- You want to avoid confusion for translators by clearly defining the intended meaning.
Supporting RTL languages in Django
Right-to-left (RTL) languages like Hebrew and Arabic require special handling to ensure that your application layout and text direction are correctly displayed. So, let’s expand our advanced Django internationalization app with support for RTL!
Step 1: Enable RTL support
Open the settings.py
file and add a new locale name to the list. Also make sure to set LANGUAGE_BIDI
to True
:
LANGUAGES = [ ('en', 'English'), ('lv', 'Latvian'), ('he', 'Hebrew'), # <=== ] LANGUAGE_BIDI = True # <===
Django already keeps tracks of common RTL languages therefore we don’t need to make any further changes here.
Step 2: Dynamically set the dir
attribute in HTML
The dir
attribute of your <html>
tag should reflect the text direction of the current language.
Make the following changes in your base.html
template:
<html lang="{% get_current_language as current_language %}{{ current_language }}" dir="{% get_current_language_bidi as bidi_direction %}{% if bidi_direction %}rtl{% else %}ltr{% endif %}">
The dir
attribute dynamically switches between rtl
(right-to-left) and ltr
(left-to-right) based on the active language.
Step 3: Adjust styles for RTL
Some elements, like margins, padding, or text alignment, may require special CSS rules for RTL languages. Use the :dir()
pseudo-class:
<style> body:dir(rtl) { direction: rtl; text-align: right; } body:dir(ltr) { direction: ltr; text-align: left; } </style>
Great job!
Lokalise: Your sidekick for Django i18n
Managing PO files in Django can get overwhelming, especially as your app grows. Enter Lokalise—a powerful translation management system that simplifies the entire internationalization process.
With Lokalise, you get:
- Easy integration with other services and platforms.
- Collaborative tools for translators and developers.
- Quality assurance to maintain consistent translations.
- A centralized dashboard for managing all your locales.
It’s designed to save you time and help you scale your Django app to reach users worldwide.
Follow these simple steps to get up and running:
- Sign up for a free trial—no credit card required.
- Log in and create a project with any name. Use “Web and mobile” project type.
- Upload your
.po
files, and edit translations via the dashboard. - Download your translations back to the Django project.
- Use Lokalise’s API, CLI, or Python SDK for seamless integration with your Django app.
Conclusion
In this guide, we explored advanced Django internationalization techniques to help you create highly adaptable and multilingual applications. From handling pluralization and context-aware translations to implementing language-prefixed URLs with i18n_patterns
, we’ve covered practical methods to refine your app’s localization.
We also delved into localizing dates, times, and RTL languages, ensuring your app accommodates diverse cultural and linguistic preferences. With tools like Lokalise for translation management and Django’s built-in utilities, you now have the knowledge to handle complex internationalization scenarios effortlessly.
Mastering these advanced Django internationalization features will not only make your app more user-friendly but also enhance its appeal to global audiences. Keep experimenting, and let Django handle the heavy lifting as you focus on building great experiences for your users worldwide.