Want to make your Django app accessible to users around the globe? This beginner’s guide to Django i18n (internationalization) will walk you through the basics of adapting your application for different languages and cultures. Whether you’re starting fresh or updating an existing project, this guide has you covered.
We’ll begin by enabling Django i18n in your project, then explore how to integrate it with tools like Lokalise. By the end, you’ll have a solid grasp of the foundational techniques for software internationalization in Django.
The source code for this tutorial can be found on GitHub.
Prerequisites
Before diving into Django i18n, make sure you have the following set up:
- Python installed: Django runs on Python, so you’ll need Python 3.8 or higher.
- A code editor: Any editor you’re comfortable with, like VS Code or PyCharm.
- Basic Django knowledge: Familiarity with setting up a Django project and working with apps is helpful.
- A terminal or command-line tool: To run commands and manage your project.
Step 1: Install Django
Before starting with i18n, you need Django installed. If you haven’t already, install it using pip:
pip install django
You can check if Django is installed correctly by running:
django-admin --version
Once you’re all set, let’s create a new web application.
Step 2: Create a new Django project
To get started, we’ll create a new Django project. Run the following command to create a project called blog
:
django-admin startproject blog
After running the command, you’ll see the following directory structure:
blog ├── blog │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── db.sqlite3 └── manage.py
Here’s a quick overview of the key files:
- manage.py: A command-line utility for managing your Django project.
- settings.py: The configuration file where you’ll enable i18n features.
- urls.py: Defines the URL mappings for your app.
- db.sqlite3: A lightweight database created by default.
With your Django project ready, we can now focus on enabling i18n.
Step 3: Enable Django i18n
Before diving into the techniques, let’s check if i18n (internationalization) is enabled in Django. The good news? It’s enabled by default!
3.1 Adjust settings
In your project’s settings.py
, look for the following variable:
USE_I18N = True
This setting ensures that Django supports i18n out of the box. Even if you’re not actively using i18n, keeping this enabled has no performance overhead.
3.2 Set the default language
You can specify the default language code for your project with the LANGUAGE_CODE
setting:
LANGUAGE_CODE = 'en'
This will be the fallback language for your app if no translations are available.
3.3 Define supported languages
To support multiple languages, add the LANGUAGES
setting to settings.py
. This tells Django which languages your app will support:
LANGUAGES = [ ('en', 'English'), ('lv', 'Latvian'), ]
3.4 Adjust text direction
For languages like Arabic or Hebrew that read right-to-left (RTL), you can configure the LANGUAGE_BIDI
variable:
LANGUAGE_BIDI = True # Set to True for RTL languages, False otherwise
That’s it! With these settings, your Django project is ready to handle internationalization and localization.
Step 4: Create a Django app
Now that i18n is enabled, let’s create an app called blogs
to implement and test i18n features. Apps are the modular components of a Django project where the actual functionality lives.
Run this command in your project directory (make sure to cd blog
before that):
python manage.py startapp blogs
This will generate the following structure:
blog ├── blog │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── db.sqlite3 ├── manage.py └── blogs ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ └── __init__.py ├── models.py ├── templates ├── tests.py ├── urls.py └── views.py
Next, register your app in INSTALLED_APPS
in settings.py
to make sure Django recognizes it:
INSTALLED_APPS = [ # Other apps... 'blogs', ]
With the app set up, we’re now ready to implement basic i18n techniques.
Step 5: Add translatable strings with gettext
Before creating translation files, we need to add some text in our app that Django can process for translation. Django uses gettext
(or gettext_lazy
for lazy evaluation) to mark strings for translation in Python code.
Let’s add some translatable strings to the views.py
file in our blogs
app. Open blogs/views.py
and update it like this:
from django.utils.translation import gettext as _ def home(request): message = _("Hello, world!") print(message)
Here’s what’s happening:
gettext
(imported as_
): This marks"Hello, world!"
as a translatable string. Django will detect it when we generate the translation files. By convention, the_
function is used as a shorthand forgettext
. It keeps the code cleaner and easier to read.
Step 6: Create translation files in Django
To enable translations, Django uses .po
(Portable Object) files to store translation strings. Let’s generate these files so you can start managing translations for your app.
Check out how our translation management system can help you translate your apps faster.
6.1 Run the makemessages
command
Django includes a handy command to scan your project for translatable strings and create .po
files automatically. From the root of your project, run:
python manage.py makemessages -l en
Here’s what’s happening in this command:
makemessages
scans your codebase for strings wrapped in Django’s translation functions, such asgettext()
or{% trans %}
in templates.- The
-l en
argument specifies the language for the translation file (e.g., English in this case).
6.2 (Optional) Ensure gettext is installed
If you encounter an error saying xgettext
is not found, it means gettext is not installed on your system. Install it using your package manager:
- On Ubuntu/Debian:
sudo apt install gettext
- On macOS (via Homebrew):
brew install gettext
- On Windows: Download and install gettext from the GNU gettext page.
6.3 Project structure after running makemessages
After running the command, Django will create a locale
directory inside your app. It will look like this:
blog ├── blog │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── db.sqlite3 ├── manage.py └── blogs ├── __init__.py ├── admin.py ├── apps.py ├── locale │ └── en │ └── LC_MESSAGES │ └── django.po ├── migrations │ └── __init__.py ├── models.py ├── templates ├── tests.py ├── urls.py └── views.py
What’s inside a PO file?
When you run the makemessages
command, Django creates a .po
file for the specified language. For example, if you used -l en
, the message file is located at blogs/locale/en/LC_MESSAGES/django.po
. Here’s what it might look like:
# blogs/locale/en/LC_MESSAGES/django.po #: .\blogs\views.py:7 msgid "Hello, world!" msgstr ""
msgid
: The original string in your code (in English by default).msgstr
: The translation for the string, which is currently empty.
Since the en
file already contains the default English text, you don’t need to add translations there. Instead, run makemessages
again for other languages (like Latvian, French, or Arabic) to create .po
files for those locales. We’ll see how to achieve it a bit later.
Step 7: Set a locale in Django
Switching the language of your Django app is easy with the built-in set_language
view. Let’s walk through how to set it up and ensure everything works smoothly.
7.1 Use Django’s built-in set_language
view
Django provides a built-in view (django.conf.urls.i18n
) to handle language switching. This view allows users to switch locales and saves their language preference in the session.
First, include the i18n
URL pattern in your urls.py
:
from django.conf.urls.i18n import i18n_patterns from django.urls import path, include urlpatterns = [ path('i18n/', include('django.conf.urls.i18n')), # Language switching # Other URLs... ]
This URL pattern connects the set_language
view to your app, so it can handle requests to change the language.
7.2 Create a language switcher form
To allow users to change the language, add a form that interacts with the set_language
view. A good place to add this is in a base HTML file (like base.html
) so it’s accessible across the app.
In your blogs
app (or whichever app you’re working in), create a folder named templates
. This is where Django looks for HTML files.
Next, inside the templates
directory, create a base.html
file. This file will serve as a foundation for all your other templates. Here’s a sample markup with a language switcher:
<!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">Choose your language:</label> <select id="language-select" name="language"> <option value="en">English</option> <option value="lv">Latvian</option> </select> <button type="submit">Switch</button> </form> {% endblock %} </nav> </header> <main> {% block content %} {% endblock %} </main> </body> </html>
Now users can choose their preferred language.
7.3 Adding middleware
Open the settings.py
file and find the MIDDLEWARE
part. Make sure to add LocaleMiddleware
to the list:
MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.locale.LocaleMiddleware', # <=== add this
This middleware automatically detects the user’s language preference (from the session, cookie, or Accept-Language
header) and activates it for the current request.
7.4 Testing it out
Add a simple view to render the template. Update views.py
in your blogs
app:
from django.shortcuts import render from django.utils.translation import gettext as _, get_language def home(request): user_language = get_language() # Fetch the active language print(user_language) # Display current language in the terminal # Translatable message message = _("Hello, world!") print(message) return render(request, 'base.html')
In this view we fetch the currently chosen locale name for the user, print it, and then display translated message in the terminal.
In your blogs/urls.py
(or your main urls.py
if you don’t have one), add this route:
from django.urls import path from .views import home urlpatterns = [ path('', home, name='home'), ]
If you’re adding this code to the main urls.py
file, make sure to adjust the import: from blogs.views import home
.
Run migrations and start the development server:
python manage.py migrate python manage.py runserver
Visit http://127.0.0.1:8000/
to see your app. You should see the language switcher form. Select a language and click Switch—it should save your preference and apply the selected locale.
Step 8: Extract and translate strings
Now that we have a translatable string in the home
view, let’s extract it and create a translation for Latvian (lv
).
8.1 Extract translatable strings
To extract translatable strings into .po
files, use the makemessages
command. Run this command from your project’s root directory:
python manage.py makemessages -l lv
After running this command, your project structure will look like this:
blogs ├── locale │ └── lv │ └── LC_MESSAGES │ ├── django.po
8.2 Edit the .po
file
Open the blogs/locale/lv/LC_MESSAGES/django.po
file in a text editor. You’ll see the extracted string, Hello, world!
, with an empty msgstr
field:
#: blogs/views.py:5 msgid "Hello, world!" msgstr ""
To add the Latvian translation, update the msgstr
field:
msgid "Hello, world!" msgstr "Sveika, pasaule!"
8.3 Compile the translations
Once you’ve added the translation, compile the .po
file into a .mo
(Machine Object) file that Django can use. Run the following command:
python manage.py compilemessages
This creates the corresponding django.mo
file in the same directory:
blogs ├── locale │ └── lv │ └── LC_MESSAGES │ ├── django.mo
The .mo
file is what Django uses to display translated content. Please note that you should never edit MO files directly: instead, change the PO files and then re-run the compilemessages
command.
8.4 Note on lazy translations in Django i18n
By default, Django translates strings immediately when you use gettext()
(or its alias _
). This works perfectly for most scenarios, but there are times when you need the translation to happen later, closer to when the string is actually used.
Django provides gettext_lazy()
for this purpose, allowing you to defer translation until the string’s value is accessed. Let’s unpack why this is useful.
Why use lazy loading?
Here’s why lazy translation is needed:
- Translations in class attributes:
In Django models, forms, or any place where you’re defining class-level attributes,gettext()
would evaluate the translation immediately—possibly before the correct language is even set. Usinggettext_lazy()
ensures the string is translated when it’s actually used (e.g., rendered in a template). - Translation timing:
Lazy translations defer the evaluation of the string until it’s absolutely necessary. This avoids problems with translations being evaluated too early in a multi-language context.
Here’s how the two differ:
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _lazy # Immediate translation translated_now = _("Hello, world!") print(translated_now) # Translates the string immediately based on the current active language # Lazy translation translated_later = _lazy("Hello, world!") print(translated_later) # Does NOT translate the string immediately
gettext
(_
): The string is translated as soon as the line of code runs, using the active language.gettext_lazy
(_lazy
): The string is stored as a lazy object and only translated when its value is accessed (e.g., during rendering).
Step 9: Perform translations in templates
9.1 Create template for the home page
Now, let’s create a dedicated template for the home
view. Place this file in blogs/templates/blogs/home.html
:
{% extends 'base.html' %} {% load i18n %} {% block title %}Home Page{% endblock %} {% block content %} <h2>{% trans "Hello, world!" %}</h2> {% endblock %}
Here’s what’s happening:
{% extends 'base.html' %}
: Inherits the structure frombase.html
.{% load i18n %}
registers thetrans
andblocktrans
tags for use in your template.{% block title %}
: Overrides the<title>
tag for this specific page.{% block content %}
: Embeds the page-specific content into the{% block content %}
placeholder inbase.html
.
9.2 Update the home
view
Make sure the home
view renders the home.html
template:
def home(request): user_language = get_language() # Fetch the active language print(f"Current language: {user_language}") return render(request, 'blogs/home.html') # Render the specific home template
Now you can reload your server and make sure everything is working nice and the welcoming message is properly translated!
Step 10: Make the language switcher dynamic
To adjust the selected value in the language switcher based on the current locale, we can leverage Django’s get_language
function or the LANGUAGE_CODE
context variable (automatically added by LocaleMiddleware
).
10.1 Displaying the currently set locale
Open up the base.html
template and make the following changes:
{% load i18n %} <!DOCTYPE html> <html lang="en"> <!-- 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">Choose your language:</label> {% get_current_language as current_language %} <select id="language-select" name="language"> <option value="en" {% if current_language == "en" %}selected{% endif %}>English</option> <option value="lv" {% if current_language == "lv" %}selected{% endif %}>Latvian</option> </select> <button type="submit">Switch</button> </form> {% endblock %} </nav> </header> <main> {% block content %} {% endblock %} </main> </body> </html>
Key points to note:
- Make sure to add the
{% load i18n %}
line - We read the current locale using
get_current_language
- Then we simply mark one of the elements as selected
10.2 Listing all supported locales using template tag
Hardcoding all supported locales in the template is not the best approach. Let’s make it more dynamic! To achieve that, first create a templatetags
directory under blogs
. Inside create an empty __init__.py
file and a language_tags.py
file with the following content:
from django import template from django.conf import settings register = template.Library() @register.simple_tag def get_languages(): return settings.LANGUAGES
This way we’re reading the LANGUAGES
set in the settings.py
.
10.3 Iterating over the list of all supported locales
Now return to the base.html
file and adjust the language switcher:
{% 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 %}
Key points:
{% trans "Choose your language" %}
performs string translation{% load language_tags %}
prepares our language tags{% for code, name in languages %}
iterates over all supported locales
Great!
10.4 Adding missing translations
Let’s also add missing translations for the “Choose your language” phrase and the “Switch” button. Run the following command:
python manage.py makemessages -l en -l lv
This should update both translation files for English and Latvian in one go.
Here are Latvian translations:
#: .\blogs\templates\base.html:18 msgid "Choose your language" msgstr "Izvēlieties savu valodu" #: .\blogs\templates\base.html:28 msgid "Switch" msgstr "Pārslēgt" #: .\blogs\templates\blogs\home.html:7 msgid "Hello, world!" msgstr "Sveika, pasaule!"
Having done that, don’t forget to produce new MO files:
python manage.py compilemessages
That’s it!
Use Lokalise for Django i18n
As your app grows and you start managing translations for multiple languages, handling .po
files manually can become tedious and error-prone. That’s where Lokalise steps in. Lokalise is a collaborative localization platform that streamlines the process of managing translations.
Setting Up Lokalise
- Create a Lokalise account:
Sign up at app.lokalise.com and create a new project. Choose the project type “Web and Mobile”. - Install the Lokalise CLI tool:
Install Lokalise’s CLI (lokalise2
) on your PC - Generate an API token:
- Go to your Lokalise account settings and create an API token.
- Note down the token; you’ll need it for importing and exporting files.
To upload your .po
files to Lokalise, use the following command:
lokalise2 --token <token> --project-id <project_id> file upload --file "blogs/locale/en/LC_MESSAGES/default.po" --lang-iso en
- Replace
<your_api_token>
and<your_project_id>
with your Lokalise API token and project ID. - Adjust the path to your
.po
file if it’s in a different location. Do not import MO files are these are not meant to be edited directly! - Run the command for each target language (e.g., one for English, one for Latvian, etc.).
To download .po
files back into your project after they’ve been updated on Lokalise, use this command:
lokalise2 --token <token> --project-id <project_id> file download --format po --original-filenames=true --directory-prefix "" --unzip-to "./"
This command should download translation bundle (an archive) and automatically extract it into your Django project.
Conclusion to Django i18n
In this guide, we set up Django i18n to make your app ready for multiple languages. We configured key settings, marked translatable content, managed translation files, and built a language switcher. With these basics in place, your app is ready to offer a localized experience to users.
Thank you for staying with me, and until next time!