IntlPull
Tutorial
13 min read

Django i18n: Complete Python Localization Guide for 2026

Master Django internationalization with gettext, .po files, makemessages, template tags, JavaScript i18n, and model translation. Complete guide for Python developers in 2026.

IntlPull Team
IntlPull Team
Feb 12, 2026
On this page
Summary

Master Django internationalization with gettext, .po files, makemessages, template tags, JavaScript i18n, and model translation. Complete guide for Python developers in 2026.

Django i18n (internationalization) is a comprehensive framework for building multilingual web applications using Python and the Django web framework. Django's i18n system leverages gettext, the industry-standard translation tool, to manage message catalogs in .po (Portable Object) files that translators can edit using specialized tools like Poedit or web-based platforms. The framework provides the makemessages command to extract translatable strings from Python code and templates, and compilemessages to convert .po files into optimized .mo (Machine Object) binary files used at runtime. Django's i18n system extends beyond simple string translation to include URL pattern localization, timezone-aware date formatting, currency and number formatting, and pluralization rules that respect language-specific grammar. The django.utils.translation module offers gettext() and gettext_lazy() functions for runtime translation lookup, while template tags like {% trans %} and {% blocktrans %} enable declarative translation in templates. Middleware components automatically detect user language preferences from browser headers, session data, or cookies. For database content, third-party packages like django-modeltranslation and django-parler enable translatable model fields. This guide covers Django i18n from initial configuration through production deployment, including JavaScript internationalization, testing strategies, and integration with modern translation management systems.

Django i18n Architecture

Django's internationalization system operates through a multi-layer architecture that separates translatable content from application logic while maintaining performance through compiled message catalogs.

At the foundation, Django uses GNU gettext as its translation engine. When you mark strings as translatable using gettext() or template tags, Django records these strings and their context. The makemessages management command scans your codebase to extract all translatable strings into .po files—human-readable text files organized by language code.

The translation workflow follows this pattern:

  1. Development: Mark strings as translatable using gettext(), _(), or template tags
  2. Extraction: Run makemessages to create/update .po files
  3. Translation: Translators edit .po files with translations
  4. Compilation: Run compilemessages to create optimized .mo binary files
  5. Runtime: Django loads .mo files and serves translations based on user locale

Django's LocaleMiddleware determines the active language by checking (in order):

  1. Language prefix in URL (if using i18n_patterns)
  2. Session key LANGUAGE_SESSION_KEY
  3. Cookie named django_language
  4. Accept-Language HTTP header
  5. Global LANGUAGE_CODE setting

The framework caches translations aggressively for performance. Translation functions like gettext_lazy() return proxy objects that only perform lookup when rendered, enabling translations in module-level code.

Initial Django i18n Setup

Enable internationalization in your Django project settings:

Python
1# settings.py
2
3from django.utils.translation import gettext_lazy as _
4
5# Enable i18n and l10n (localization)
6USE_I18N = True
7USE_L10N = True
8USE_TZ = True  # Timezone support
9
10# Default language
11LANGUAGE_CODE = 'en-us'
12
13# Supported languages
14LANGUAGES = [
15    ('en', _('English')),
16    ('es', _('Spanish')),
17    ('fr', _('French')),
18    ('de', _('German')),
19]
20
21# Translation file paths
22LOCALE_PATHS = [
23    BASE_DIR / 'locale',
24]
25
26# Middleware configuration (order matters)
27MIDDLEWARE = [
28    'django.middleware.security.SecurityMiddleware',
29    'django.contrib.sessions.middleware.SessionMiddleware',
30    'django.middleware.locale.LocaleMiddleware',  # After SessionMiddleware
31    'django.middleware.common.CommonMiddleware',
32    # ... other middleware
33]

Create the locale directory structure:

Terminal
mkdir -p locale/{es,fr,de}/LC_MESSAGES

The directory structure will look like:

myproject/
├── locale/
│   ├── es/
│   │   └── LC_MESSAGES/
│   │       ├── django.po
│   │       └── django.mo
│   ├── fr/
│   │   └── LC_MESSAGES/
│   └── de/
│       └── LC_MESSAGES/
├── myapp/
│   ├── locale/  # App-specific translations
│   │   └── es/
│   │       └── LC_MESSAGES/
│   ├── views.py
│   └── models.py
└── manage.py

Marking Strings for Translation

Django provides multiple functions for marking translatable strings depending on the context.

In Python Code

Python
1from django.utils.translation import gettext as _
2from django.utils.translation import gettext_lazy
3from django.utils.translation import ngettext
4
5def my_view(request):
6    # Simple translation (evaluated immediately)
7    message = _('Welcome to our site')
8
9    # Lazy translation (evaluated when rendered)
10    # Use for module-level code, model fields, form fields
11    help_text = gettext_lazy('Enter your email address')
12
13    # Plural handling
14    count = request.GET.get('count', 0)
15    items_text = ngettext(
16        'You have %(count)d item',
17        'You have %(count)d items',
18        count
19    ) % {'count': count}
20
21    return render(request, 'template.html', {
22        'message': message,
23        'help_text': help_text,
24        'items_text': items_text,
25    })

In Models

Always use gettext_lazy() in model definitions:

Python
1from django.db import models
2from django.utils.translation import gettext_lazy as _
3
4class Product(models.Model):
5    name = models.CharField(
6        max_length=200,
7        verbose_name=_('Product Name'),
8        help_text=_('Enter the product name')
9    )
10    description = models.TextField(
11        verbose_name=_('Description'),
12        blank=True
13    )
14    price = models.DecimalField(
15        max_digits=10,
16        decimal_places=2,
17        verbose_name=_('Price')
18    )
19
20    class Meta:
21        verbose_name = _('Product')
22        verbose_name_plural = _('Products')
23
24    def __str__(self):
25        return self.name

In Forms

Python
1from django import forms
2from django.utils.translation import gettext_lazy as _
3
4class ContactForm(forms.Form):
5    name = forms.CharField(
6        label=_('Your Name'),
7        max_length=100,
8        help_text=_('Enter your full name')
9    )
10    email = forms.EmailField(
11        label=_('Email Address'),
12        help_text=_('We will never share your email')
13    )
14    message = forms.CharField(
15        label=_('Message'),
16        widget=forms.Textarea,
17        help_text=_('Enter your message here')
18    )
19
20    def clean_message(self):
21        message = self.cleaned_data['message']
22        if len(message) < 10:
23            raise forms.ValidationError(
24                _('Message must be at least 10 characters long')
25            )
26        return message

With Context

Provide context when the same English word has different translations:

Python
1from django.utils.translation import pgettext
2
3# "May" the month vs "may" the verb
4month = pgettext('month name', 'May')
5permission = pgettext('verb', 'may')
6
7# In .po file, these become:
8# msgctxt "month name"
9# msgid "May"
10# msgstr "Mayo"
11#
12# msgctxt "verb"
13# msgid "may"
14# msgstr "poder"

Template Translation Tags

Django templates use special tags for translation.

Basic Translation

DJANGO
1{% load i18n %}
2
3<!DOCTYPE html>
4<html>
5<head>
6    <title>{% trans "Welcome to Our Site" %}</title>
7</head>
8<body>
9    <h1>{% trans "Hello, World!" %}</h1>
10
11    {# Translation with variable substitution #}
12    <p>{% blocktrans with name=user.first_name %}
13        Hello, {{ name }}!
14    {% endblocktrans %}</p>
15
16    {# Pluralization in templates #}
17    <p>{% blocktrans count counter=items|length %}
18        You have {{ counter }} item in your cart.
19    {% plural %}
20        You have {{ counter }} items in your cart.
21    {% endblocktrans %}</p>
22
23    {# Context-aware translation #}
24    <p>{% trans "May" context "month name" %}</p>
25</body>
26</html>

Language Selection

Create a language switcher:

DJANGO
1{% load i18n %}
2
3<form action="{% url 'set_language' %}" method="post">
4    {% csrf_token %}
5    <input name="next" type="hidden" value="{{ redirect_to }}" />
6    <select name="language" onchange="this.form.submit()">
7        {% get_current_language as CURRENT_LANGUAGE %}
8        {% get_available_languages as AVAILABLE_LANGUAGES %}
9        {% for lang_code, lang_name in AVAILABLE_LANGUAGES %}
10            <option value="{{ lang_code }}"
11                {% if lang_code == CURRENT_LANGUAGE %}selected{% endif %}>
12                {{ lang_name }}
13            </option>
14        {% endfor %}
15    </select>
16</form>

Configure URL pattern in urls.py:

Python
1from django.conf.urls.i18n import i18n_patterns
2from django.urls import path, include
3
4urlpatterns = [
5    path('i18n/', include('django.conf.urls.i18n')),
6]

Translation Variables

DJANGO
1{% load i18n %}
2
3{# Get current language #}
4{% get_current_language as LANGUAGE_CODE %}
5<p>Current language: {{ LANGUAGE_CODE }}</p>
6
7{# Get available languages #}
8{% get_available_languages as LANGUAGES %}
9<ul>
10    {% for lang_code, lang_name in LANGUAGES %}
11        <li>{{ lang_code }}: {{ lang_name }}</li>
12    {% endfor %}
13</ul>
14
15{# Get language info #}
16{% get_language_info for LANGUAGE_CODE as lang_info %}
17<p>
18    Language: {{ lang_info.name_local }}
19    ({{ lang_info.code }})
20    {% if lang_info.bidi %}[RTL]{% endif %}
21</p>

makemessages and compilemessages

Extract and compile translations using Django management commands.

Extracting Messages

Run makemessages to scan code for translatable strings:

Terminal
1# Create/update .po files for all languages
2python manage.py makemessages -a
3
4# Create/update for specific language
5python manage.py makemessages -l es
6python manage.py makemessages -l fr
7
8# Include JavaScript files
9python manage.py makemessages -d djangojs -l es
10
11# Ignore virtual environment
12python manage.py makemessages -l es --ignore=venv/*
13
14# Update existing translations without fuzzy matching
15python manage.py makemessages -l es --no-obsolete

This creates files like locale/es/LC_MESSAGES/django.po:

# locale/es/LC_MESSAGES/django.po
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

#: views.py:15
msgid "Welcome to our site"
msgstr "Bienvenido a nuestro sitio"

#: models.py:10
msgid "Product Name"
msgstr "Nombre del Producto"

#: templates/home.html:5
msgid "Hello, World!"
msgstr "¡Hola, Mundo!"

#: templates/cart.html:12
#, python-format
msgid "You have %(count)d item in your cart."
msgid_plural "You have %(count)d items in your cart."
msgstr[0] "Tienes %(count)d artículo en tu carrito."
msgstr[1] "Tienes %(count)d artículos en tu carrito."

Compiling Messages

Convert .po files to optimized .mo binary files:

Terminal
1# Compile all languages
2python manage.py compilemessages
3
4# Compile specific language
5python manage.py compilemessages -l es
6
7# Compile and check for errors
8python manage.py compilemessages --use-fuzzy

Always compile after editing .po files. Django only reads .mo files at runtime.

Automation Script

Create a translation workflow script:

Terminal
1#!/bin/bash
2# scripts/update_translations.sh
3
4set -e
5
6echo "Extracting messages..."
7python manage.py makemessages -a --ignore=venv/*
8python manage.py makemessages -d djangojs -a --ignore=venv/* --ignore=node_modules/*
9
10echo "Compiling messages..."
11python manage.py compilemessages
12
13echo "Translation files updated successfully!"

URL Internationalization

Translate URL patterns using i18n_patterns.

Basic URL i18n

Python
1# urls.py
2from django.conf.urls.i18n import i18n_patterns
3from django.urls import path
4from . import views
5
6urlpatterns = i18n_patterns(
7    path('about/', views.about, name='about'),
8    path('contact/', views.contact, name='contact'),
9    path('products/', views.product_list, name='product_list'),
10    path('products/<slug:slug>/', views.product_detail, name='product_detail'),
11)

With this configuration, URLs become:

  • /en/about/ (English)
  • /es/acerca-de/ (Spanish, if translated)
  • /fr/a-propos/ (French, if translated)

Translating URL Patterns

Create translated URL patterns:

Python
1from django.urls import path
2from django.conf.urls.i18n import i18n_patterns
3from django.utils.translation import gettext_lazy as _
4from . import views
5
6urlpatterns = i18n_patterns(
7    path(_('about/'), views.about, name='about'),
8    path(_('contact/'), views.contact, name='contact'),
9    path(_('products/'), views.product_list, name='product_list'),
10    path(_('products/<slug:slug>/'), views.product_detail, name='product_detail'),
11)

Add translations to .po file:

msgid "about/"
msgstr "acerca-de/"

msgid "contact/"
msgstr "contacto/"

msgid "products/"
msgstr "productos/"

msgid "products/<slug:slug>/"
msgstr "productos/<slug:slug>/"

Reverse URL Resolution

Use reverse() to generate translated URLs:

Python
1from django.urls import reverse
2from django.utils.translation import activate
3
4# Activate Spanish
5activate('es')
6
7# Generate translated URL
8url = reverse('product_list')  # Returns '/es/productos/'
9
10# In templates
11{% url 'product_list' %}  # Automatically uses current language

Language Prefix Configuration

Control language prefix behavior:

Python
1# settings.py
2
3# Prefix default language URLs (e.g., /en/about/)
4# If False, default language has no prefix (e.g., /about/)
5PREFIX_DEFAULT_LANGUAGE = False

JavaScript Internationalization

Django provides i18n support for frontend JavaScript code.

JavaScript Catalog View

Include the JavaScript catalog in your template:

DJANGO
<script src="{% url 'javascript-catalog' %}"></script>
<script src="{% static 'js/app.js' %}"></script>

Configure the catalog URL:

Python
1# urls.py
2from django.views.i18n import JavaScriptCatalog
3
4urlpatterns = [
5    path('jsi18n/', JavaScriptCatalog.as_view(), name='javascript-catalog'),
6]

Using Translations in JavaScript

JavaScript
1// static/js/app.js
2
3// Simple translation
4const message = gettext('Welcome to our site');
5console.log(message);
6
7// String interpolation
8const greeting = interpolate(
9    gettext('Hello, %s!'),
10    [userName]
11);
12
13// Plural handling
14const itemsText = ngettext(
15    'You have %s item',
16    'You have %s items',
17    itemCount
18);
19const formatted = interpolate(itemsText, [itemCount]);
20
21// Get current language
22const currentLang = get_format('LANGUAGE_CODE');

Extracting JavaScript Translations

Create a separate catalog for JavaScript:

Terminal
1# Extract JavaScript translations
2python manage.py makemessages -d djangojs -l es
3
4# This creates locale/es/LC_MESSAGES/djangojs.po

Configure which packages to include:

Python
1# urls.py
2from django.views.i18n import JavaScriptCatalog
3
4urlpatterns = [
5    path('jsi18n/',
6        JavaScriptCatalog.as_view(packages=['myapp']),
7        name='javascript-catalog'
8    ),
9]

Modern JavaScript i18n

For modern JavaScript frameworks, export Django translations to JSON:

Python
1# myapp/views.py
2from django.http import JsonResponse
3from django.utils.translation import get_language, gettext as _
4
5def translations_json(request):
6    """Export translations as JSON for frontend frameworks."""
7    translations = {
8        'welcome_message': _('Welcome to our site'),
9        'login_button': _('Log In'),
10        'signup_button': _('Sign Up'),
11        'error_required': _('This field is required'),
12        # Add all needed translations
13    }
14
15    return JsonResponse({
16        'language': get_language(),
17        'translations': translations
18    })

Use in React/Vue:

JavaScript
1// Fetch translations on app init
2fetch('/api/translations/')
3    .then(res => res.json())
4    .then(data => {
5        window.translations = data.translations;
6        window.currentLanguage = data.language;
7    });
8
9// Usage
10const t = (key) => window.translations[key] || key;
11console.log(t('welcome_message'));

Model Translation

Translate database content using third-party packages.

Using django-modeltranslation

Install and configure:

Terminal
pip install django-modeltranslation
Python
1# settings.py
2
3INSTALLED_APPS = [
4    'modeltranslation',  # Before django.contrib.admin
5    'django.contrib.admin',
6    # ... other apps
7]
8
9LANGUAGES = [
10    ('en', 'English'),
11    ('es', 'Spanish'),
12    ('fr', 'French'),
13]

Define translation fields:

Python
1# myapp/translation.py
2from modeltranslation.translator import register, TranslationOptions
3from .models import Product
4
5@register(Product)
6class ProductTranslationOptions(TranslationOptions):
7    fields = ('name', 'description')

This creates additional fields in the database:

  • name_en, name_es, name_fr
  • description_en, description_es, description_fr

Usage:

Python
1from django.utils.translation import activate
2from myapp.models import Product
3
4# Create product
5product = Product.objects.create(
6    name_en='Laptop',
7    name_es='Portátil',
8    name_fr='Ordinateur portable',
9    description_en='High-performance laptop',
10    description_es='Portátil de alto rendimiento',
11    description_fr='Ordinateur portable haute performance'
12)
13
14# Access translated fields
15activate('es')
16print(product.name)  # Returns 'Portátil'
17
18activate('fr')
19print(product.name)  # Returns 'Ordinateur portable'

Run migration after configuring:

Terminal
1python manage.py makemigrations
2python manage.py migrate
3
4# Sync existing data
5python manage.py update_translation_fields

Using django-parler

Alternative approach with separate translation tables:

Terminal
pip install django-parler
Python
1# models.py
2from django.db import models
3from parler.models import TranslatableModel, TranslatedFields
4
5class Product(TranslatableModel):
6    sku = models.CharField(max_length=50, unique=True)
7    price = models.DecimalField(max_digits=10, decimal_places=2)
8
9    translations = TranslatedFields(
10        name=models.CharField(max_length=200),
11        description=models.TextField()
12    )
13
14    def __str__(self):
15        return self.name

Usage:

Python
1from django.utils.translation import activate
2
3# Create with translations
4product = Product.objects.create(sku='LAP001', price=999.99)
5product.set_current_language('en')
6product.name = 'Laptop'
7product.description = 'High-performance laptop'
8product.save()
9
10product.set_current_language('es')
11product.name = 'Portátil'
12product.description = 'Portátil de alto rendimiento'
13product.save()
14
15# Query translated fields
16activate('es')
17products = Product.objects.translated('es')

IntlPull CLI Integration

Integrate Django with IntlPull for collaborative translation management.

Installation

Terminal
pip install intlpull

Export to IntlPull

Terminal
1# Export Django .po files to IntlPull
2intlpull import \
3    --project-id your-project-id \
4    --format po \
5    --file locale/es/LC_MESSAGES/django.po \
6    --language es
7
8# Bulk import all languages
9for lang in es fr de; do
10    intlpull import \
11        --project-id your-project-id \
12        --format po \
13        --file locale/$lang/LC_MESSAGES/django.po \
14        --language $lang
15done

Import from IntlPull

Terminal
1# Download updated translations
2intlpull export \
3    --project-id your-project-id \
4    --format po \
5    --language es \
6    --output locale/es/LC_MESSAGES/django.po
7
8# Compile after import
9python manage.py compilemessages -l es

CI/CD Integration

Automate translation sync in GitHub Actions:

YAML
1# .github/workflows/translations.yml
2name: Sync Translations
3
4on:
5  schedule:
6    - cron: '0 0 * * *'  # Daily at midnight
7  workflow_dispatch:
8
9jobs:
10  sync:
11    runs-on: ubuntu-latest
12    steps:
13      - uses: actions/checkout@v3
14
15      - name: Set up Python
16        uses: actions/setup-python@v4
17        with:
18          python-version: '3.11'
19
20      - name: Install dependencies
21        run: |
22          pip install django intlpull
23
24      - name: Extract messages
25        run: python manage.py makemessages -a
26
27      - name: Upload to IntlPull
28        env:
29          INTLPULL_API_KEY: ${{ secrets.INTLPULL_API_KEY }}
30        run: |
31          for lang in es fr de; do
32            intlpull import \
33              --project-id ${{ secrets.INTLPULL_PROJECT_ID }} \
34              --format po \
35              --file locale/$lang/LC_MESSAGES/django.po \
36              --language $lang
37          done
38
39      - name: Download translations
40        env:
41          INTLPULL_API_KEY: ${{ secrets.INTLPULL_API_KEY }}
42        run: |
43          for lang in es fr de; do
44            intlpull export \
45              --project-id ${{ secrets.INTLPULL_PROJECT_ID }} \
46              --format po \
47              --language $lang \
48              --output locale/$lang/LC_MESSAGES/django.po
49          done
50
51      - name: Compile messages
52        run: python manage.py compilemessages
53
54      - name: Create Pull Request
55        uses: peter-evans/create-pull-request@v5
56        with:
57          commit-message: 'chore: update translations'
58          title: 'Update translations from IntlPull'
59          branch: translations-update

Testing i18n

Ensure translations work correctly across all supported languages.

Unit Testing

Python
1# tests.py
2from django.test import TestCase
3from django.utils.translation import activate, gettext as _
4
5class TranslationTests(TestCase):
6
7    def test_welcome_message_spanish(self):
8        activate('es')
9        message = _('Welcome to our site')
10        self.assertEqual(message, 'Bienvenido a nuestro sitio')
11
12    def test_welcome_message_french(self):
13        activate('fr')
14        message = _('Welcome to our site')
15        self.assertEqual(message, 'Bienvenue sur notre site')
16
17    def test_pluralization(self):
18        from django.utils.translation import ngettext
19
20        activate('es')
21
22        # Singular
23        text = ngettext(
24            'You have %(count)d item',
25            'You have %(count)d items',
26            1
27        ) % {'count': 1}
28        self.assertIn('artículo', text)
29
30        # Plural
31        text = ngettext(
32            'You have %(count)d item',
33            'You have %(count)d items',
34            5
35        ) % {'count': 5}
36        self.assertIn('artículos', text)

View Testing

Python
1from django.test import TestCase, Client
2from django.urls import reverse
3
4class LocalizedViewTests(TestCase):
5
6    def test_home_page_spanish(self):
7        response = self.client.get('/es/', follow=True)
8        self.assertEqual(response.status_code, 200)
9        self.assertContains(response, 'Bienvenido')
10
11    def test_language_switcher(self):
12        # Start in English
13        response = self.client.get('/')
14        self.assertContains(response, 'Welcome')
15
16        # Switch to Spanish
17        response = self.client.post(
18            reverse('set_language'),
19            {'language': 'es', 'next': '/'}
20        )
21        self.assertEqual(response.status_code, 302)
22
23        # Verify Spanish content
24        response = self.client.get('/')
25        self.assertContains(response, 'Bienvenido')

Translation Coverage

Check for missing translations:

Python
1# scripts/check_translations.py
2import os
3import polib
4
5def check_translation_coverage(lang_code):
6    po_path = f'locale/{lang_code}/LC_MESSAGES/django.po'
7    if not os.path.exists(po_path):
8        print(f"Missing .po file for {lang_code}")
9        return
10
11    po = polib.pofile(po_path)
12    total = len(po)
13    translated = len(po.translated_entries())
14    untranslated = len(po.untranslated_entries())
15    fuzzy = len(po.fuzzy_entries())
16
17    coverage = (translated / total * 100) if total > 0 else 0
18
19    print(f"\n{lang_code} Translation Coverage:")
20    print(f"  Total entries: {total}")
21    print(f"  Translated: {translated}")
22    print(f"  Untranslated: {untranslated}")
23    print(f"  Fuzzy: {fuzzy}")
24    print(f"  Coverage: {coverage:.1f}%")
25
26    if untranslated > 0:
27        print(f"\n  Missing translations:")
28        for entry in po.untranslated_entries()[:10]:
29            print(f"    - {entry.msgid}")
30
31if __name__ == '__main__':
32    for lang in ['es', 'fr', 'de']:
33        check_translation_coverage(lang)

Best Practices

1. Use Lazy Translation for Module-Level Code

Python
1# ❌ Bad - evaluated at module import
2from django.utils.translation import gettext as _
3ERROR_MESSAGE = _('An error occurred')
4
5# ✅ Good - evaluated when accessed
6from django.utils.translation import gettext_lazy as _
7ERROR_MESSAGE = _('An error occurred')

2. Provide Context for Translators

Python
1# ❌ Bad - no context
2label = _('Name')
3
4# ✅ Good - clear context
5label = _('Product Name')
6
7# ✅ Better - with pgettext
8label = pgettext('product field', 'Name')

3. Never Concatenate Translated Strings

Python
1# ❌ Bad - word order varies by language
2message = _('Hello') + ', ' + user.name + '!'
3
4# ✅ Good - use format strings
5message = _('Hello, %(name)s!') % {'name': user.name}

4. Keep .po Files in Version Control

Commit .po files but consider excluding .mo files:

# .gitignore
*.mo

Compile .mo files during deployment.

5. Use Format Strings Consistently

Python
1# ✅ Named placeholders (preferred)
2_('Welcome, %(username)s!') % {'username': user.name}
3
4# ✅ Positional placeholders (okay)
5_('Welcome, %s!') % user.name
6
7# ❌ Mixed styles (confusing)
8_('Welcome, %(name)s! You have %d messages') % {'name': user.name, ...}

6. Organize Translations by App

Use app-specific locale directories:

myproject/
├── myapp/
│   ├── locale/
│   │   ├── es/
│   │   └── fr/
│   └── views.py
└── settings.py

This keeps translations modular and reusable.

Production Deployment

Pre-Deployment Checklist

  • All .po files translated and reviewed
  • compilemessages run successfully
  • .mo files included in deployment
  • Language middleware configured
  • URL patterns tested in all languages
  • JavaScript catalog generated
  • Translation coverage > 95% for launch languages
  • RTL languages tested (if applicable)

Deployment Script

Terminal
1#!/bin/bash
2# scripts/deploy.sh
3
4set -e
5
6echo "Compiling translations..."
7python manage.py compilemessages
8
9echo "Collecting static files..."
10python manage.py collectstatic --noinput
11
12echo "Running migrations..."
13python manage.py migrate
14
15echo "Deployment complete!"

Performance Optimization

Enable translation caching:

Python
1# settings.py
2
3CACHES = {
4    'default': {
5        'BACKEND': 'django.core.cache.backends.redis.RedisCache',
6        'LOCATION': 'redis://127.0.0.1:6379/1',
7    }
8}
9
10# Cache translations
11LOCALE_PATHS = [BASE_DIR / 'locale']

Frequently Asked Questions

Q: Should I use gettext() or gettext_lazy()?

A: Use gettext_lazy() for module-level code, model fields, and form fields. Use gettext() inside functions and views where immediate evaluation is acceptable. Lazy translation prevents issues with language detection at import time.

Q: How do I handle user-generated content translation?

A: Use django-modeltranslation or django-parler for database content. For user comments/posts, consider separate translation tables or integration with translation services via API rather than storing all translations upfront.

Q: Can I change language without URL prefix?

A: Yes, use session or cookie-based language detection without i18n_patterns. Set language via the set_language view and store preference in session. This keeps URLs language-neutral.

Q: How do I translate emails?

A: Use gettext() in email template context, activate the user's preferred language before rendering, and create separate email templates per language if HTML structure differs significantly.

Q: What's the difference between .po and .mo files?

A: .po files are human-readable text format edited by translators. .mo files are binary format compiled from .po files for runtime efficiency. Always edit .po files, never .mo files directly.

Q: How do I handle date and number formatting?

A: Django's USE_L10N = True enables automatic formatting based on locale. Use template filters like {{ value|date }} and {{ value|floatformat }} which respect the current language's formatting rules.

Q: Can I mix static and dynamic translations?

A: Yes, combine gettext for UI strings with model translation for database content. Use different .po files (django.po for code, djangojs.po for JavaScript) to organize translations logically.

Q: How do I test RTL languages?

A: Add RTL languages (Arabic, Hebrew) to LANGUAGES, activate them in tests, and check template rendering with {% if LANGUAGE_BIDI %} conditionals. Use browser dev tools to verify RTL layout.

IntlPull streamlines Django localization workflows by providing collaborative translation management, automated .po file sync, and version control for translations. Whether building a new Django application or internationalizing an existing project, proper i18n setup ensures your application serves global audiences effectively. Start with Django's built-in i18n framework, integrate IntlPull for translation collaboration, and follow best practices for maintainable multilingual applications.

Tags
django
python
i18n
localization
gettext
backend
web-framework
IntlPull Team
IntlPull Team
Engineering

Building tools to help teams ship products globally. Follow us for more insights on localization and i18n.