How the Django framework for Python supports internationalization

Advanced Django internationalization

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 %}
    1. {% blocktrans %}: Marks the block of text as translatable and handles pluralization.
    2. 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.
    3. {% plural %}: Separates the singular and plural forms of the message.
    4. {{ 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 when count == 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})
    1. 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.
    2. %(count)s substitution:
      • Dynamically inserts the value of count into the translated string.

    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:

    1. A word or phrase has multiple meanings depending on usage.
    2. 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:

    1. Sign up for a free trial—no credit card required.
    2. Log in and create a project with any name. Use “Web and mobile” project type.
    3. Upload your .po files, and edit translations via the dashboard.
    4. Download your translations back to the Django project.
    5. 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.

    Further reading

    Related articles
    Stop wasting time with manual localization tasks. 

    Launch global products days from now.