Setting up Django's internationalization framework is the foundation for building multilingual applications. This chapter covers the complete configuration process, from basic settings to advanced customization, ensuring your Django project is ready for global deployment with optimal performance and user experience.
# settings.py
import os
from django.utils.translation import gettext_lazy as _
# Enable internationalization
USE_I18N = True
# Enable localization (formatting)
USE_L10N = True
# Enable timezone support
USE_TZ = True
# Default language
LANGUAGE_CODE = 'en'
# Available languages
LANGUAGES = [
('en', _('English')),
('es', _('Spanish')),
('fr', _('French')),
('de', _('German')),
('ja', _('Japanese')),
('zh-hans', _('Simplified Chinese')),
('ar', _('Arabic')),
]
# Locale paths for translation files
LOCALE_PATHS = [
BASE_DIR / 'locale',
BASE_DIR / 'apps' / 'blog' / 'locale', # App-specific translations
]
# Default timezone
TIME_ZONE = 'UTC'
# Middleware configuration
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware', # Add locale middleware
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# Create locale directories
mkdir -p locale
mkdir -p apps/blog/locale
# Project structure
myproject/
├── locale/ # Project-wide translations
│ ├── en/
│ │ └── LC_MESSAGES/
│ ├── es/
│ │ └── LC_MESSAGES/
│ └── fr/
│ └── LC_MESSAGES/
├── apps/
│ └── blog/
│ ├── locale/ # App-specific translations
│ │ ├── en/
│ │ └── es/
│ └── models.py
└── manage.py
# settings/base.py
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Base language configuration
LANGUAGE_CODE = 'en'
TIME_ZONE = 'UTC'
# Common locale paths
LOCALE_PATHS = [
BASE_DIR / 'locale',
]
# settings/development.py
from .base import *
# Development-specific i18n settings
LANGUAGES = [
('en', 'English'),
('es', 'Spanish'),
('fr', 'French'),
]
# Enable translation debugging
ROSETTA_SHOW_AT_ADMIN_PANEL = True
ROSETTA_REQUIRES_AUTH = False
# settings/production.py
from .base import *
# Production language configuration
LANGUAGES = [
('en', 'English'),
('es', 'Español'),
('fr', 'Français'),
('de', 'Deutsch'),
('ja', '日本語'),
('zh-hans', '简体中文'),
('ar', 'العربية'),
]
# Performance optimizations
LOCALE_PATHS = [
BASE_DIR / 'locale',
]
# Cache translations in production
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'KEY_PREFIX': 'translations',
}
}
# middleware/language.py
from django.middleware.locale import LocaleMiddleware
from django.utils import translation
from django.conf import settings
import re
class CustomLocaleMiddleware(LocaleMiddleware):
"""Enhanced locale middleware with custom detection logic."""
def process_request(self, request):
# Check for language in URL path
urlconf = getattr(request, 'urlconf', settings.ROOT_URLCONF)
i18n_patterns_used, prefixed_default_language = is_language_prefix_patterns_used(urlconf)
language = translation.get_language_from_request(
request,
check_path=i18n_patterns_used
)
# Custom detection logic
if not language:
language = self.detect_language_from_custom_sources(request)
# Fallback to default
if not language:
language = settings.LANGUAGE_CODE
translation.activate(language)
request.LANGUAGE_CODE = translation.get_language()
def detect_language_from_custom_sources(self, request):
"""Custom language detection logic."""
# Check user profile
if hasattr(request, 'user') and request.user.is_authenticated:
profile = getattr(request.user, 'profile', None)
if profile and hasattr(profile, 'language'):
return profile.language
# Check subdomain
subdomain = self.get_subdomain(request)
if subdomain in dict(settings.LANGUAGES):
return subdomain
# Check custom header
custom_lang = request.META.get('HTTP_X_LANGUAGE')
if custom_lang in dict(settings.LANGUAGES):
return custom_lang
return None
def get_subdomain(self, request):
"""Extract subdomain from request."""
host = request.get_host().split(':')[0] # Remove port
parts = host.split('.')
if len(parts) > 2:
return parts[0]
return None
# urls.py
from django.conf import settings
from django.conf.urls.i18n import i18n_patterns
from django.contrib import admin
from django.urls import path, include
# Non-translatable URLs (no language prefix)
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('api.urls')), # API endpoints
path('health/', include('health.urls')), # Health checks
]
# Translatable URLs (with language prefix)
urlpatterns += i18n_patterns(
path('', include('blog.urls')),
path('accounts/', include('accounts.urls')),
path('shop/', include('shop.urls')),
prefix_default_language=False, # Don't prefix default language
)
# Serve media files in development
if settings.DEBUG:
from django.conf.urls.static import static
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# blog/urls.py
from django.urls import path
from django.utils.translation import gettext_lazy as _
from . import views
app_name = 'blog'
urlpatterns = [
path('', views.PostListView.as_view(), name='post_list'),
path(_('post/<slug:slug>/'), views.PostDetailView.as_view(), name='post_detail'),
path(_('category/<slug:slug>/'), views.CategoryView.as_view(), name='category'),
path(_('tag/<slug:slug>/'), views.TagView.as_view(), name='tag'),
path(_('archive/<int:year>/'), views.YearArchiveView.as_view(), name='year_archive'),
path(_('search/'), views.SearchView.as_view(), name='search'),
]
# Create translation files for the entire project
python manage.py makemessages -l es
python manage.py makemessages -l fr
python manage.py makemessages -l de
# Create translations for specific apps
python manage.py makemessages -l es --ignore=venv/*
python manage.py makemessages -l fr --extension=html,txt,py
# Create JavaScript translations
python manage.py makemessages -d djangojs -l es
# locale/es/LC_MESSAGES/django.po
msgid ""
msgstr ""
"Project-Id-Version: MyProject 1.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-01-15 10:30+0000\n"
"PO-Revision-Date: 2024-01-15 11:00+0000\n"
"Last-Translator: John Doe <john@example.com>\n"
"Language-Team: Spanish <es@example.com>\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: blog/models.py:15
msgid "Title"
msgstr "Título"
#: blog/models.py:16
msgid "Content"
msgstr "Contenido"
#: blog/models.py:17
msgid "Published"
msgstr "Publicado"
#: templates/blog/post_list.html:10
msgid "Latest Posts"
msgstr "Últimas Publicaciones"
#: templates/blog/post_list.html:25
#, python-format
msgid "%(count)d post found"
msgid_plural "%(count)d posts found"
msgstr[0] "%(count)d publicación encontrada"
msgstr[1] "%(count)d publicaciones encontradas"
# Compile all translations
python manage.py compilemessages
# Compile specific language
python manage.py compilemessages -l es
# Compile with optimization
python manage.py compilemessages --use-fuzzy
# management/commands/update_translations.py
from django.core.management.base import BaseCommand
from django.core.management import call_command
from django.conf import settings
import os
class Command(BaseCommand):
help = 'Update all translation files'
def add_arguments(self, parser):
parser.add_argument(
'--languages',
nargs='+',
default=[lang[0] for lang in settings.LANGUAGES if lang[0] != 'en'],
help='Languages to update'
)
parser.add_argument(
'--compile',
action='store_true',
help='Compile translations after updating'
)
def handle(self, *args, **options):
languages = options['languages']
self.stdout.write('Updating translation files...')
for language in languages:
self.stdout.write(f'Processing {language}...')
# Make messages for this language
call_command('makemessages',
locale=language,
ignore_patterns=['venv/*', 'node_modules/*'],
verbosity=0)
# Make JavaScript messages
call_command('makemessages',
domain='djangojs',
locale=language,
verbosity=0)
if options['compile']:
self.stdout.write('Compiling translations...')
call_command('compilemessages', verbosity=0)
self.stdout.write(
self.style.SUCCESS('Successfully updated translations')
)
# management/commands/translation_stats.py
from django.core.management.base import BaseCommand
from django.conf import settings
import os
import polib
class Command(BaseCommand):
help = 'Show translation statistics'
def handle(self, *args, **options):
self.stdout.write('Translation Statistics\n' + '=' * 50)
for language_code, language_name in settings.LANGUAGES:
if language_code == 'en': # Skip source language
continue
po_file_path = os.path.join(
settings.BASE_DIR,
'locale',
language_code,
'LC_MESSAGES',
'django.po'
)
if os.path.exists(po_file_path):
po = polib.pofile(po_file_path)
total = len(po)
translated = len(po.translated_entries())
untranslated = len(po.untranslated_entries())
fuzzy = len(po.fuzzy_entries())
percentage = (translated / total * 100) if total > 0 else 0
self.stdout.write(f'\n{language_name} ({language_code}):')
self.stdout.write(f' Total strings: {total}')
self.stdout.write(f' Translated: {translated} ({percentage:.1f}%)')
self.stdout.write(f' Untranslated: {untranslated}')
self.stdout.write(f' Fuzzy: {fuzzy}')
else:
self.stdout.write(f'\n{language_name} ({language_code}): No translation file found')
# settings.py
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
},
'translations': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/2',
'KEY_PREFIX': 'trans',
'TIMEOUT': 86400, # 24 hours
}
}
# Custom translation loading
def get_translation_cache_key(language):
"""Generate cache key for translation."""
return f'translation_{language}_{settings.VERSION}'
# Cache translation files
TRANSLATION_CACHE_TIMEOUT = 86400 # 24 hours
# utils/translation.py
from django.utils.translation import gettext_lazy as _
from django.core.cache import cache
from django.conf import settings
import os
class TranslationManager:
"""Manage translation loading and caching."""
def __init__(self):
self._translations = {}
def get_translation(self, language_code):
"""Get translation for language with caching."""
cache_key = f'translation_{language_code}'
translation = cache.get(cache_key)
if translation is None:
translation = self._load_translation(language_code)
cache.set(cache_key, translation, timeout=86400)
return translation
def _load_translation(self, language_code):
"""Load translation from file."""
# Implementation for loading translation files
pass
def invalidate_cache(self, language_code=None):
"""Invalidate translation cache."""
if language_code:
cache.delete(f'translation_{language_code}')
else:
# Invalidate all translations
for lang_code, _ in settings.LANGUAGES:
cache.delete(f'translation_{lang_code}')
translation_manager = TranslationManager()
# models.py
from django.db import models
from django.contrib.auth.models import User
from django.conf import settings
class UserProfile(models.Model):
"""Extended user profile with language preferences."""
user = models.OneToOneField(User, on_delete=models.CASCADE)
language = models.CharField(
max_length=10,
choices=settings.LANGUAGES,
default=settings.LANGUAGE_CODE,
verbose_name=_('Preferred Language')
)
timezone = models.CharField(
max_length=50,
default=settings.TIME_ZONE,
verbose_name=_('Timezone')
)
date_format = models.CharField(
max_length=20,
default='%Y-%m-%d',
verbose_name=_('Date Format')
)
class Meta:
verbose_name = _('User Profile')
verbose_name_plural = _('User Profiles')
def __str__(self):
return f"{self.user.username}'s profile"
# Signal to create profile
from django.db.models.signals import post_save
from django.dispatch import receiver
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(user=instance)
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
instance.userprofile.save()
# models.py
from django.db import models
from django.utils.translation import gettext_lazy as _
class TranslatableModel(models.Model):
"""Base model for translatable content."""
class Meta:
abstract = True
def get_translation(self, language_code=None):
"""Get translation for specific language."""
if not language_code:
from django.utils import translation
language_code = translation.get_language()
try:
return self.translations.get(language_code=language_code)
except:
# Fallback to default language
return self.translations.get(language_code=settings.LANGUAGE_CODE)
class BlogPost(TranslatableModel):
"""Blog post with translation support."""
slug = models.SlugField(unique=True)
author = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
published = models.BooleanField(default=False)
class Meta:
verbose_name = _('Blog Post')
verbose_name_plural = _('Blog Posts')
ordering = ['-created_at']
class BlogPostTranslation(models.Model):
"""Translation model for blog posts."""
post = models.ForeignKey(
BlogPost,
on_delete=models.CASCADE,
related_name='translations'
)
language_code = models.CharField(max_length=10, choices=settings.LANGUAGES)
title = models.CharField(max_length=200, verbose_name=_('Title'))
content = models.TextField(verbose_name=_('Content'))
meta_description = models.CharField(
max_length=160,
blank=True,
verbose_name=_('Meta Description')
)
class Meta:
unique_together = ['post', 'language_code']
verbose_name = _('Blog Post Translation')
verbose_name_plural = _('Blog Post Translations')
# tests/test_translations.py
from django.test import TestCase, override_settings
from django.utils import translation
from django.urls import reverse
from django.contrib.auth.models import User
class TranslationTestCase(TestCase):
def setUp(self):
self.user = User.objects.create_user(
username='testuser',
password='testpass'
)
def test_language_detection_from_url(self):
"""Test language detection from URL prefix."""
response = self.client.get('/es/')
self.assertEqual(response.status_code, 200)
# Check if Spanish is activated
with translation.override('es'):
self.assertEqual(translation.get_language(), 'es')
def test_language_switching(self):
"""Test language switching functionality."""
# Start with English
response = self.client.get('/')
self.assertContains(response, 'English content')
# Switch to Spanish
response = self.client.get('/es/')
self.assertContains(response, 'Contenido en español')
@override_settings(LANGUAGE_CODE='es')
def test_default_language_override(self):
"""Test default language override."""
with translation.override('es'):
self.assertEqual(translation.get_language(), 'es')
def test_user_language_preference(self):
"""Test user language preference."""
# Set user language preference
profile = self.user.userprofile
profile.language = 'fr'
profile.save()
self.client.login(username='testuser', password='testpass')
response = self.client.get('/')
# Should use user's preferred language
self.assertEqual(response.context['LANGUAGE_CODE'], 'fr')
Proper translation setup is crucial for building globally accessible Django applications. Start with basic configuration and gradually add advanced features like custom language detection, performance optimization, and user preferences. The foundation you build here will support all subsequent internationalization features while maintaining excellent performance and user experience.
Internationalization and Localization
Building applications that serve users across different languages, cultures, and time zones requires careful planning and implementation of internationalization (i18n) and localization (l10n) features. Django provides comprehensive built-in support for creating multilingual applications that adapt to users' linguistic and cultural preferences while maintaining excellent performance and user experience.
Translating Text in Code and Templates
Marking strings for translation is the core of Django's internationalization system. This chapter covers comprehensive techniques for translating text in Python code and Django templates, including advanced patterns for pluralization, context-specific translations, and dynamic content localization.