Django's locale middleware automatically detects and activates the appropriate language for each request, providing seamless internationalization without requiring manual language management. This chapter covers configuring, customizing, and optimizing locale middleware for sophisticated multilingual applications with advanced language detection and user preference management.
Django's LocaleMiddleware follows a specific order to determine the appropriate language:
/es/blog/)LANGUAGE_CODE setting# settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware', # Add after SessionMiddleware
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# Language configuration
LANGUAGE_CODE = 'en'
LANGUAGES = [
('en', 'English'),
('es', 'Español'),
('fr', 'Français'),
('de', 'Deutsch'),
('ja', '日本語'),
]
# Language cookie settings
LANGUAGE_COOKIE_NAME = 'django_language'
LANGUAGE_COOKIE_AGE = 365 * 24 * 60 * 60 # 1 year
LANGUAGE_COOKIE_DOMAIN = None
LANGUAGE_COOKIE_PATH = '/'
LANGUAGE_COOKIE_SECURE = False # Set to True in production with HTTPS
LANGUAGE_COOKIE_HTTPONLY = False
LANGUAGE_COOKIE_SAMESITE = 'Lax'
# Session language key
LANGUAGE_SESSION_KEY = 'django_language'
# middleware/locale.py
from django.middleware.locale import LocaleMiddleware
from django.utils import translation
from django.conf import settings
from django.utils.cache import patch_vary_headers
from django.utils.deprecation import MiddlewareMixin
import re
import logging
logger = logging.getLogger(__name__)
class EnhancedLocaleMiddleware(LocaleMiddleware):
"""Enhanced locale middleware with additional features."""
def __init__(self, get_response):
super().__init__(get_response)
self.get_response = get_response
def process_request(self, request):
"""Process request with enhanced language detection."""
# Store original language for comparison
original_language = translation.get_language()
# Custom language detection
language = self.get_language_from_request(request)
# Activate the detected language
translation.activate(language)
request.LANGUAGE_CODE = translation.get_language()
# Log language detection for debugging
if settings.DEBUG:
logger.debug(f'Language detected: {language} for {request.path}')
# Store language change for analytics
if hasattr(request, 'user') and request.user.is_authenticated:
self.track_language_usage(request, language)
def process_response(self, request, response):
"""Process response with language-specific optimizations."""
language = getattr(request, 'LANGUAGE_CODE', None)
if language:
# Set language cookie if changed
current_cookie_lang = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME)
if current_cookie_lang != language:
response.set_cookie(
settings.LANGUAGE_COOKIE_NAME,
language,
max_age=settings.LANGUAGE_COOKIE_AGE,
path=settings.LANGUAGE_COOKIE_PATH,
domain=settings.LANGUAGE_COOKIE_DOMAIN,
secure=settings.LANGUAGE_COOKIE_SECURE,
httponly=settings.LANGUAGE_COOKIE_HTTPONLY,
samesite=settings.LANGUAGE_COOKIE_SAMESITE,
)
# Add language to response headers
response['Content-Language'] = language
# Add Vary header for caching
patch_vary_headers(response, ('Accept-Language', 'Cookie'))
return response
def get_language_from_request(self, request):
"""Enhanced language detection with custom logic."""
# 1. Check URL language prefix (handled by parent class)
language = super().get_language_from_request(request)
if language:
return language
# 2. Check user profile language preference
if hasattr(request, 'user') and request.user.is_authenticated:
try:
profile_language = request.user.userprofile.language
if profile_language and self.is_language_supported(profile_language):
return profile_language
except AttributeError:
pass
# 3. Check subdomain-based language
subdomain_language = self.get_language_from_subdomain(request)
if subdomain_language:
return subdomain_language
# 4. Check custom header (for API clients)
header_language = request.META.get('HTTP_X_LANGUAGE')
if header_language and self.is_language_supported(header_language):
return header_language
# 5. Check GeoIP-based language detection
geoip_language = self.get_language_from_geoip(request)
if geoip_language:
return geoip_language
# 6. Fall back to default detection
return translation.get_language_from_request(request, check_path=True)
def get_language_from_subdomain(self, request):
"""Extract language from subdomain."""
host = request.get_host().split(':')[0] # Remove port
parts = host.split('.')
if len(parts) > 2: # Has subdomain
subdomain = parts[0]
if self.is_language_supported(subdomain):
return subdomain
return None
def get_language_from_geoip(self, request):
"""Get language based on user's geographic location."""
try:
from django.contrib.gis.geoip2 import GeoIP2
# Get client IP
ip = self.get_client_ip(request)
if not ip:
return None
# Get country from IP
g = GeoIP2()
country = g.country_code(ip)
# Map countries to languages
country_language_map = {
'ES': 'es', # Spain -> Spanish
'MX': 'es', # Mexico -> Spanish
'AR': 'es', # Argentina -> Spanish
'FR': 'fr', # France -> French
'CA': 'fr', # Canada -> French (could be 'en' too)
'DE': 'de', # Germany -> German
'AT': 'de', # Austria -> German
'JP': 'ja', # Japan -> Japanese
'CN': 'zh-hans', # China -> Simplified Chinese
'TW': 'zh-hant', # Taiwan -> Traditional Chinese
}
language = country_language_map.get(country)
if language and self.is_language_supported(language):
return language
except Exception as e:
logger.warning(f'GeoIP language detection failed: {e}')
return None
def get_client_ip(self, request):
"""Get client IP address."""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
def is_language_supported(self, language_code):
"""Check if language is supported."""
supported_languages = [lang[0] for lang in settings.LANGUAGES]
return language_code in supported_languages
def track_language_usage(self, request, language):
"""Track language usage for analytics."""
# This could be implemented to track language preferences
# for analytics or user behavior analysis
pass
class APILocaleMiddleware(MiddlewareMixin):
"""Specialized locale middleware for API endpoints."""
def process_request(self, request):
"""Process API requests with language detection."""
# Only process API requests
if not request.path.startswith('/api/'):
return
# Get language from various sources
language = (
request.META.get('HTTP_ACCEPT_LANGUAGE_OVERRIDE') or
request.GET.get('lang') or
request.META.get('HTTP_ACCEPT_LANGUAGE', '').split(',')[0].split('-')[0] or
settings.LANGUAGE_CODE
)
# Validate and activate language
if language in [lang[0] for lang in settings.LANGUAGES]:
translation.activate(language)
request.LANGUAGE_CODE = language
else:
translation.activate(settings.LANGUAGE_CODE)
request.LANGUAGE_CODE = settings.LANGUAGE_CODE
def process_response(self, request, response):
"""Add language headers to API responses."""
if hasattr(request, 'LANGUAGE_CODE'):
response['Content-Language'] = request.LANGUAGE_CODE
return response
# models.py
from django.db import models
from django.contrib.auth.models import User
from django.conf import settings
from django.utils.translation import gettext_lazy as _
class UserLanguagePreference(models.Model):
"""Track user language preferences and history."""
user = models.OneToOneField(
User,
on_delete=models.CASCADE,
related_name='language_preference'
)
primary_language = models.CharField(
max_length=10,
choices=settings.LANGUAGES,
default=settings.LANGUAGE_CODE,
verbose_name=_('Primary Language')
)
secondary_languages = models.JSONField(
default=list,
blank=True,
verbose_name=_('Secondary Languages'),
help_text=_('Additional languages the user understands')
)
auto_detect = models.BooleanField(
default=True,
verbose_name=_('Auto-detect language'),
help_text=_('Automatically detect language from browser')
)
last_detected_language = models.CharField(
max_length=10,
blank=True,
verbose_name=_('Last Detected Language')
)
detection_source = models.CharField(
max_length=20,
choices=[
('manual', _('Manual Selection')),
('browser', _('Browser Detection')),
('geoip', _('Geographic Location')),
('subdomain', _('Subdomain')),
('profile', _('User Profile')),
],
blank=True,
verbose_name=_('Detection Source')
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = _('User Language Preference')
verbose_name_plural = _('User Language Preferences')
def get_preferred_language(self, available_languages=None):
"""Get user's preferred language from available options."""
if available_languages is None:
available_languages = [lang[0] for lang in settings.LANGUAGES]
# Check primary language
if self.primary_language in available_languages:
return self.primary_language
# Check secondary languages
for lang in self.secondary_languages:
if lang in available_languages:
return lang
# Fall back to default
return settings.LANGUAGE_CODE
class LanguageUsageLog(models.Model):
"""Log language usage for analytics."""
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
null=True,
blank=True
)
session_key = models.CharField(max_length=40, blank=True)
language_code = models.CharField(max_length=10)
detection_method = models.CharField(
max_length=20,
choices=[
('url', 'URL Prefix'),
('session', 'Session'),
('cookie', 'Cookie'),
('header', 'Accept-Language Header'),
('profile', 'User Profile'),
('geoip', 'GeoIP'),
('subdomain', 'Subdomain'),
('default', 'Default'),
]
)
ip_address = models.GenericIPAddressField(null=True, blank=True)
user_agent = models.TextField(blank=True)
path = models.CharField(max_length=500)
timestamp = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = _('Language Usage Log')
verbose_name_plural = _('Language Usage Logs')
indexes = [
models.Index(fields=['timestamp', 'language_code']),
models.Index(fields=['user', 'timestamp']),
]
# utils/language_detection.py
from django.conf import settings
from django.utils import translation
import re
from collections import Counter
class SmartLanguageDetector:
"""Advanced language detection with machine learning capabilities."""
def __init__(self):
self.language_patterns = self.build_language_patterns()
def build_language_patterns(self):
"""Build patterns for language detection from content."""
return {
'en': [
r'\b(the|and|or|but|in|on|at|to|for|of|with|by)\b',
r'\b(this|that|these|those|here|there|where|when|what|how)\b',
],
'es': [
r'\b(el|la|los|las|un|una|y|o|pero|en|de|con|por|para)\b',
r'\b(este|esta|estos|estas|aquí|allí|donde|cuando|qué|cómo)\b',
],
'fr': [
r'\b(le|la|les|un|une|et|ou|mais|dans|de|avec|par|pour)\b',
r'\b(ce|cette|ces|ici|là|où|quand|que|comment)\b',
],
'de': [
r'\b(der|die|das|ein|eine|und|oder|aber|in|von|mit|für)\b',
r'\b(dieser|diese|dieses|hier|dort|wo|wann|was|wie)\b',
],
}
def detect_from_content(self, text):
"""Detect language from text content."""
if not text:
return None
text = text.lower()
scores = {}
for lang, patterns in self.language_patterns.items():
score = 0
for pattern in patterns:
matches = re.findall(pattern, text, re.IGNORECASE)
score += len(matches)
scores[lang] = score
if scores:
return max(scores, key=scores.get)
return None
def detect_from_user_behavior(self, user):
"""Detect language from user's historical behavior."""
if not user.is_authenticated:
return None
try:
# Get user's recent language usage
from .models import LanguageUsageLog
recent_logs = LanguageUsageLog.objects.filter(
user=user
).order_by('-timestamp')[:50]
if recent_logs:
languages = [log.language_code for log in recent_logs]
most_common = Counter(languages).most_common(1)
return most_common[0][0] if most_common else None
except Exception:
pass
return None
def detect_from_browser_settings(self, request):
"""Enhanced browser language detection."""
accept_language = request.META.get('HTTP_ACCEPT_LANGUAGE', '')
if not accept_language:
return None
# Parse Accept-Language header
languages = []
for item in accept_language.split(','):
if ';' in item:
lang, quality = item.split(';', 1)
try:
q = float(quality.split('=')[1])
except (IndexError, ValueError):
q = 1.0
else:
lang, q = item, 1.0
lang = lang.strip().lower()
# Handle language variants (e.g., en-US -> en)
if '-' in lang:
lang = lang.split('-')[0]
# Check if language is supported
if lang in [l[0] for l in settings.LANGUAGES]:
languages.append((lang, q))
# Sort by quality and return best match
if languages:
languages.sort(key=lambda x: x[1], reverse=True)
return languages[0][0]
return None
detector = SmartLanguageDetector()
# views.py
from django.shortcuts import redirect
from django.urls import reverse
from django.utils import translation
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.decorators import login_required
from django.conf import settings
import json
@require_POST
def set_language(request):
"""Set user's language preference."""
language = request.POST.get('language')
next_url = request.POST.get('next', request.META.get('HTTP_REFERER', '/'))
if language and language in [lang[0] for lang in settings.LANGUAGES]:
# Activate language
translation.activate(language)
# Store in session
request.session[settings.LANGUAGE_SESSION_KEY] = language
# Store in user profile if authenticated
if request.user.is_authenticated:
try:
preference, created = UserLanguagePreference.objects.get_or_create(
user=request.user
)
preference.primary_language = language
preference.detection_source = 'manual'
preference.save()
except Exception:
pass
# Log language change
log_language_usage(request, language, 'manual')
# Redirect to next URL
response = redirect(next_url)
# Set language cookie
if language:
response.set_cookie(
settings.LANGUAGE_COOKIE_NAME,
language,
max_age=settings.LANGUAGE_COOKIE_AGE,
path=settings.LANGUAGE_COOKIE_PATH,
secure=settings.LANGUAGE_COOKIE_SECURE,
httponly=settings.LANGUAGE_COOKIE_HTTPONLY,
samesite=settings.LANGUAGE_COOKIE_SAMESITE,
)
return response
@csrf_exempt
def api_set_language(request):
"""API endpoint for setting language."""
if request.method != 'POST':
return JsonResponse({'error': 'Method not allowed'}, status=405)
try:
data = json.loads(request.body)
language = data.get('language')
except (json.JSONDecodeError, AttributeError):
language = request.POST.get('language')
if not language:
return JsonResponse({'error': 'Language not specified'}, status=400)
if language not in [lang[0] for lang in settings.LANGUAGES]:
return JsonResponse({'error': 'Language not supported'}, status=400)
# Set language in session
request.session[settings.LANGUAGE_SESSION_KEY] = language
# Update user profile if authenticated
if request.user.is_authenticated:
try:
preference, created = UserLanguagePreference.objects.get_or_create(
user=request.user
)
preference.primary_language = language
preference.detection_source = 'manual'
preference.save()
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
# Log language change
log_language_usage(request, language, 'api')
return JsonResponse({
'success': True,
'language': language,
'message': f'Language set to {language}'
})
@login_required
def language_preferences(request):
"""Manage user language preferences."""
try:
preference = request.user.language_preference
except UserLanguagePreference.DoesNotExist:
preference = UserLanguagePreference.objects.create(user=request.user)
if request.method == 'POST':
primary_language = request.POST.get('primary_language')
secondary_languages = request.POST.getlist('secondary_languages')
auto_detect = request.POST.get('auto_detect') == 'on'
if primary_language in [lang[0] for lang in settings.LANGUAGES]:
preference.primary_language = primary_language
preference.secondary_languages = secondary_languages
preference.auto_detect = auto_detect
preference.save()
# Activate new language
translation.activate(primary_language)
request.session[settings.LANGUAGE_SESSION_KEY] = primary_language
return redirect('language_preferences')
context = {
'preference': preference,
'available_languages': settings.LANGUAGES,
}
return render(request, 'accounts/language_preferences.html', context)
def log_language_usage(request, language, method):
"""Log language usage for analytics."""
try:
from .models import LanguageUsageLog
LanguageUsageLog.objects.create(
user=request.user if request.user.is_authenticated else None,
session_key=request.session.session_key,
language_code=language,
detection_method=method,
ip_address=get_client_ip(request),
user_agent=request.META.get('HTTP_USER_AGENT', ''),
path=request.path,
)
except Exception:
# Don't let logging errors break the application
pass
def get_client_ip(request):
"""Get client IP address."""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
<!-- templates/includes/language_switcher.html -->
{% load i18n %}
<div class="language-switcher">
<div class="current-language">
<span class="language-icon">🌐</span>
<span class="language-name">{{ LANGUAGE_CODE|upper }}</span>
<span class="dropdown-arrow">▼</span>
</div>
<div class="language-dropdown">
{% get_available_languages as LANGUAGES %}
{% get_current_language as LANGUAGE_CODE %}
<form action="{% url 'set_language' %}" method="post" class="language-form">
{% csrf_token %}
<input name="next" type="hidden" value="{{ request.get_full_path }}" />
{% for language_code, language_name in LANGUAGES %}
<button
type="submit"
name="language"
value="{{ language_code }}"
class="language-option {% if language_code == LANGUAGE_CODE %}active{% endif %}"
{% if language_code == LANGUAGE_CODE %}disabled{% endif %}
>
<span class="language-code">{{ language_code|upper }}</span>
<span class="language-name">{{ language_name }}</span>
{% if language_code == LANGUAGE_CODE %}
<span class="current-indicator">✓</span>
{% endif %}
</button>
{% endfor %}
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const switcher = document.querySelector('.language-switcher');
const dropdown = switcher.querySelector('.language-dropdown');
const currentLang = switcher.querySelector('.current-language');
// Toggle dropdown
currentLang.addEventListener('click', function() {
dropdown.classList.toggle('show');
});
// Close dropdown when clicking outside
document.addEventListener('click', function(event) {
if (!switcher.contains(event.target)) {
dropdown.classList.remove('show');
}
});
// Handle language selection via AJAX
const form = switcher.querySelector('.language-form');
const buttons = form.querySelectorAll('.language-option');
buttons.forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
const language = this.value;
const formData = new FormData();
formData.append('language', language);
formData.append('next', window.location.pathname);
formData.append('csrfmiddlewaretoken', form.querySelector('[name=csrfmiddlewaretoken]').value);
fetch('{% url "api_set_language" %}', {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Reload page to apply new language
window.location.reload();
} else {
console.error('Language change failed:', data.error);
}
})
.catch(error => {
console.error('Error:', error);
// Fall back to form submission
form.submit();
});
});
});
});
</script>
<style>
.language-switcher {
position: relative;
display: inline-block;
}
.current-language {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
cursor: pointer;
user-select: none;
}
.current-language:hover {
background: #e9ecef;
}
.language-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
z-index: 1000;
display: none;
}
.language-dropdown.show {
display: block;
}
.language-form {
margin: 0;
}
.language-option {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 0.75rem 1rem;
border: none;
background: none;
text-align: left;
cursor: pointer;
transition: background-color 0.15s ease-in-out;
}
.language-option:hover:not(:disabled) {
background: #f8f9fa;
}
.language-option.active {
background: #e7f3ff;
font-weight: 600;
}
.language-option:disabled {
cursor: default;
}
.current-indicator {
color: #0d6efd;
font-weight: bold;
}
</style>
<!-- templates/accounts/language_preferences.html -->
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Language Preferences" %}{% endblock %}
{% block content %}
<div class="language-preferences">
<h1>{% trans "Language Preferences" %}</h1>
<form method="post" class="preferences-form">
{% csrf_token %}
<div class="form-group">
<label for="primary_language">{% trans "Primary Language" %}</label>
<select name="primary_language" id="primary_language" class="form-control">
{% for language_code, language_name in available_languages %}
<option
value="{{ language_code }}"
{% if language_code == preference.primary_language %}selected{% endif %}
>
{{ language_name }}
</option>
{% endfor %}
</select>
<small class="form-text">{% trans "Your preferred language for the interface" %}</small>
</div>
<div class="form-group">
<label>{% trans "Secondary Languages" %}</label>
<div class="checkbox-group">
{% for language_code, language_name in available_languages %}
<div class="form-check">
<input
type="checkbox"
name="secondary_languages"
value="{{ language_code }}"
id="secondary_{{ language_code }}"
class="form-check-input"
{% if language_code in preference.secondary_languages %}checked{% endif %}
>
<label for="secondary_{{ language_code }}" class="form-check-label">
{{ language_name }}
</label>
</div>
{% endfor %}
</div>
<small class="form-text">{% trans "Languages you understand (used for fallback content)" %}</small>
</div>
<div class="form-group">
<div class="form-check">
<input
type="checkbox"
name="auto_detect"
id="auto_detect"
class="form-check-input"
{% if preference.auto_detect %}checked{% endif %}
>
<label for="auto_detect" class="form-check-label">
{% trans "Auto-detect language from browser" %}
</label>
</div>
<small class="form-text">
{% trans "Automatically detect your preferred language from browser settings" %}
</small>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
{% trans "Save Preferences" %}
</button>
<a href="{% url 'profile' %}" class="btn btn-secondary">
{% trans "Cancel" %}
</a>
</div>
</form>
<div class="language-info">
<h3>{% trans "Current Language Information" %}</h3>
<dl class="info-list">
<dt>{% trans "Active Language" %}</dt>
<dd>{{ LANGUAGE_CODE|upper }} - {% get_language_info for LANGUAGE_CODE as lang_info %}{{ lang_info.name_local }}</dd>
<dt>{% trans "Last Detection Source" %}</dt>
<dd>{{ preference.get_detection_source_display|default:"—" }}</dd>
<dt>{% trans "Last Detected Language" %}</dt>
<dd>{{ preference.last_detected_language|upper|default:"—" }}</dd>
<dt>{% trans "Browser Languages" %}</dt>
<dd id="browser-languages">{% trans "Detecting..." %}</dd>
</dl>
</div>
</div>
<script>
// Display browser language preferences
document.addEventListener('DOMContentLoaded', function() {
const browserLangs = navigator.languages || [navigator.language];
const browserLangsElement = document.getElementById('browser-languages');
if (browserLangs.length > 0) {
browserLangsElement.textContent = browserLangs.join(', ');
} else {
browserLangsElement.textContent = '{% trans "Not available" %}';
}
// Handle primary language change
const primarySelect = document.getElementById('primary_language');
const secondaryCheckboxes = document.querySelectorAll('input[name="secondary_languages"]');
primarySelect.addEventListener('change', function() {
const selectedPrimary = this.value;
// Uncheck the primary language from secondary languages
secondaryCheckboxes.forEach(checkbox => {
if (checkbox.value === selectedPrimary) {
checkbox.checked = false;
checkbox.disabled = true;
} else {
checkbox.disabled = false;
}
});
});
// Trigger change event on load
primarySelect.dispatchEvent(new Event('change'));
});
</script>
{% endblock %}
# urls.py
from django.conf import settings
from django.conf.urls.i18n import i18n_patterns
from django.urls import path, include
from django.views.i18n import set_language
from . import views
# Non-translatable URLs
urlpatterns = [
# Language switching
path('set-language/', set_language, name='set_language'),
path('api/set-language/', views.api_set_language, name='api_set_language'),
# API endpoints (no language prefix)
path('api/', include('api.urls')),
# Health checks and admin
path('health/', include('health.urls')),
path('admin/', admin.site.urls),
]
# Translatable URLs with language prefix
urlpatterns += i18n_patterns(
path('', include('blog.urls')),
path('accounts/', include('accounts.urls')),
path('preferences/language/', views.language_preferences, name='language_preferences'),
# Prefix default language in URLs (optional)
prefix_default_language=False,
)
# Custom 404 handler for language-aware URLs
handler404 = 'myapp.views.custom_404'
# 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'),
# Translatable URL patterns
path(_('posts/'), views.PostListView.as_view(), name='posts'),
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(_('archive/<int:year>/<int:month>/'), views.MonthArchiveView.as_view(), name='month_archive'),
path(_('search/'), views.SearchView.as_view(), name='search'),
path(_('feed/'), views.PostFeedView.as_view(), name='feed'),
]
# utils/cache.py
from django.core.cache import cache
from django.utils import translation
from django.conf import settings
import hashlib
def get_language_cache_key(base_key, language=None):
"""Generate cache key with language suffix."""
if language is None:
language = translation.get_language()
return f"{base_key}_{language}"
def cache_per_language(timeout=3600):
"""Decorator to cache function results per language."""
def decorator(func):
def wrapper(*args, **kwargs):
# Generate cache key
key_parts = [func.__name__]
key_parts.extend(str(arg) for arg in args)
key_parts.extend(f"{k}={v}" for k, v in sorted(kwargs.items()))
base_key = hashlib.md5('_'.join(key_parts).encode()).hexdigest()
cache_key = get_language_cache_key(base_key)
# Try to get from cache
result = cache.get(cache_key)
if result is not None:
return result
# Execute function and cache result
result = func(*args, **kwargs)
cache.set(cache_key, result, timeout)
return result
return wrapper
return decorator
# Cache configuration for multilingual content
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'KEY_PREFIX': f'myapp_{settings.LANGUAGE_CODE}',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
}
},
'translations': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/2',
'KEY_PREFIX': 'translations',
'TIMEOUT': 86400, # 24 hours
}
}
# middleware/optimized_locale.py
from django.middleware.locale import LocaleMiddleware
from django.utils import translation
from django.core.cache import cache
from django.conf import settings
class OptimizedLocaleMiddleware(LocaleMiddleware):
"""Optimized locale middleware with caching."""
def __init__(self, get_response):
super().__init__(get_response)
self.cache_timeout = getattr(settings, 'LANGUAGE_DETECTION_CACHE_TIMEOUT', 300)
def process_request(self, request):
"""Optimized language detection with caching."""
# Generate cache key for this request
cache_key = self.get_cache_key(request)
# Try to get language from cache
language = cache.get(cache_key)
if language is None:
# Detect language using parent method
language = translation.get_language_from_request(request, check_path=True)
# Cache the result
cache.set(cache_key, language, self.cache_timeout)
# Activate language
translation.activate(language)
request.LANGUAGE_CODE = language
def get_cache_key(self, request):
"""Generate cache key for language detection."""
key_parts = [
'lang_detect',
request.path_info,
request.META.get('HTTP_ACCEPT_LANGUAGE', ''),
request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME, ''),
]
# Add user ID if authenticated
if hasattr(request, 'user') and request.user.is_authenticated:
key_parts.append(str(request.user.id))
return '_'.join(str(part) for part in key_parts if part)
# tests/test_locale_middleware.py
from django.test import TestCase, RequestFactory, override_settings
from django.contrib.auth.models import User
from django.utils import translation
from django.conf import settings
from middleware.locale import EnhancedLocaleMiddleware
class LocaleMiddlewareTestCase(TestCase):
def setUp(self):
self.factory = RequestFactory()
self.middleware = EnhancedLocaleMiddleware(lambda r: None)
self.user = User.objects.create_user(
username='testuser',
password='testpass'
)
def test_url_language_detection(self):
"""Test language detection from URL prefix."""
request = self.factory.get('/es/blog/')
request.user = self.user
self.middleware.process_request(request)
self.assertEqual(request.LANGUAGE_CODE, 'es')
def test_user_profile_language(self):
"""Test language detection from user profile."""
# Set user's preferred language
profile = self.user.userprofile
profile.language = 'fr'
profile.save()
request = self.factory.get('/blog/')
request.user = self.user
request.session = {}
request.COOKIES = {}
self.middleware.process_request(request)
self.assertEqual(request.LANGUAGE_CODE, 'fr')
def test_cookie_language_detection(self):
"""Test language detection from cookie."""
request = self.factory.get('/blog/')
request.user = self.user
request.session = {}
request.COOKIES = {settings.LANGUAGE_COOKIE_NAME: 'de'}
self.middleware.process_request(request)
self.assertEqual(request.LANGUAGE_CODE, 'de')
def test_accept_language_header(self):
"""Test language detection from Accept-Language header."""
request = self.factory.get('/blog/', HTTP_ACCEPT_LANGUAGE='ja,en;q=0.9')
request.user = self.user
request.session = {}
request.COOKIES = {}
self.middleware.process_request(request)
self.assertEqual(request.LANGUAGE_CODE, 'ja')
def test_subdomain_language_detection(self):
"""Test language detection from subdomain."""
request = self.factory.get('/blog/', HTTP_HOST='es.example.com')
request.user = self.user
request.session = {}
request.COOKIES = {}
language = self.middleware.get_language_from_subdomain(request)
self.assertEqual(language, 'es')
@override_settings(LANGUAGES=[('en', 'English'), ('es', 'Spanish')])
def test_unsupported_language_fallback(self):
"""Test fallback to default language for unsupported languages."""
request = self.factory.get('/blog/', HTTP_ACCEPT_LANGUAGE='zh-cn')
request.user = self.user
request.session = {}
request.COOKIES = {}
self.middleware.process_request(request)
self.assertEqual(request.LANGUAGE_CODE, settings.LANGUAGE_CODE)
def test_language_cookie_setting(self):
"""Test that language cookie is set in response."""
request = self.factory.get('/blog/')
request.user = self.user
request.session = {}
request.COOKIES = {}
request.LANGUAGE_CODE = 'es'
from django.http import HttpResponse
response = HttpResponse()
response = self.middleware.process_response(request, response)
# Check if cookie is set
self.assertIn(settings.LANGUAGE_COOKIE_NAME, response.cookies)
self.assertEqual(response.cookies[settings.LANGUAGE_COOKIE_NAME].value, 'es')
# analytics/language_analytics.py
from django.db.models import Count, Q
from django.utils import timezone
from datetime import timedelta
from .models import LanguageUsageLog
class LanguageAnalytics:
"""Analytics for language usage patterns."""
def get_language_distribution(self, days=30):
"""Get language usage distribution over time period."""
since = timezone.now() - timedelta(days=days)
return LanguageUsageLog.objects.filter(
timestamp__gte=since
).values('language_code').annotate(
count=Count('id')
).order_by('-count')
def get_detection_method_stats(self, days=30):
"""Get statistics on language detection methods."""
since = timezone.now() - timedelta(days=days)
return LanguageUsageLog.objects.filter(
timestamp__gte=since
).values('detection_method').annotate(
count=Count('id')
).order_by('-count')
def get_user_language_preferences(self):
"""Get user language preference statistics."""
from .models import UserLanguagePreference
return UserLanguagePreference.objects.values(
'primary_language'
).annotate(
count=Count('id')
).order_by('-count')
def get_geographic_language_distribution(self):
"""Get language distribution by geographic location."""
# This would require GeoIP integration
pass
def generate_language_report(self, days=30):
"""Generate comprehensive language usage report."""
return {
'language_distribution': self.get_language_distribution(days),
'detection_methods': self.get_detection_method_stats(days),
'user_preferences': self.get_user_language_preferences(),
'total_requests': LanguageUsageLog.objects.filter(
timestamp__gte=timezone.now() - timedelta(days=days)
).count(),
'unique_users': LanguageUsageLog.objects.filter(
timestamp__gte=timezone.now() - timedelta(days=days),
user__isnull=False
).values('user').distinct().count(),
}
analytics = LanguageAnalytics()
Django's locale middleware provides the foundation for sophisticated multilingual applications. By customizing language detection logic, implementing user preferences, and optimizing performance through caching, you can create applications that automatically adapt to users' language preferences while maintaining excellent performance. The key is balancing automatic detection with user control, ensuring that language switching is both seamless and predictable.
Timezone Support
Modern web applications serve users across multiple time zones, making proper timezone handling essential for accurate time display and scheduling. Django provides comprehensive timezone support that automatically handles timezone conversion, user preferences, and daylight saving time transitions while maintaining data integrity and user experience.
Caching
Caching is one of the most effective techniques for improving web application performance, reducing database load, and enhancing user experience. Django provides a comprehensive caching framework that supports multiple backends, granular caching strategies, and sophisticated cache invalidation patterns. This guide covers everything from basic cache configuration to advanced deployment-level caching architectures.