Internationalization and Localization

Enabling Translation

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.

Enabling Translation

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.

Basic Translation Setup

Django Settings Configuration

# 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',
]

Directory Structure Setup

# 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

Advanced Configuration

Environment-Specific Settings

# 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',
    }
}

Custom Language Detection

# 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

URL Configuration

Language Prefix URLs

# 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)

Custom URL Patterns

# 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'),
]

Translation File Management

Creating Message Files

# 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

Message File Structure

# 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"

Compiling Translations

# 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

Custom Translation Commands

# 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')
        )

Translation Statistics Command

# 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')

Performance Optimization

Translation Caching

# 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

Lazy Translation Loading

# 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()

Database Configuration

User Language Preferences

# 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()

Multilingual Content Models

# 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')

Testing Translation Setup

Translation Tests

# 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.