Internationalization and Localization

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.

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.

Basic String Translation

Python Code Translation

# views.py
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _lazy
from django.utils.translation import ngettext
from django.http import JsonResponse
from django.shortcuts import render

def blog_view(request):
    """Example view with translated strings."""
    # Basic translation
    title = _('Welcome to our blog')
    
    # Lazy translation (for model fields, form labels)
    description = _lazy('Latest articles and insights')
    
    # Pluralization
    post_count = 5
    message = ngettext(
        'You have %(count)d new post',
        'You have %(count)d new posts',
        post_count
    ) % {'count': post_count}
    
    context = {
        'title': title,
        'description': description,
        'message': message,
    }
    return render(request, 'blog/index.html', context)

Model Translation

# models.py
from django.db import models
from django.utils.translation import gettext_lazy as _

class BlogPost(models.Model):
    """Blog post model with translated field labels."""
    title = models.CharField(
        max_length=200,
        verbose_name=_('Title'),
        help_text=_('Enter the post title')
    )
    content = models.TextField(
        verbose_name=_('Content'),
        help_text=_('Write your blog post content here')
    )
    status = models.CharField(
        max_length=20,
        choices=[
            ('draft', _('Draft')),
            ('published', _('Published')),
            ('archived', _('Archived')),
        ],
        default='draft',
        verbose_name=_('Status')
    )
    created_at = models.DateTimeField(
        auto_now_add=True,
        verbose_name=_('Created at')
    )
    
    class Meta:
        verbose_name = _('Blog Post')
        verbose_name_plural = _('Blog Posts')
        ordering = ['-created_at']
    
    def __str__(self):
        return self.title
    
    def get_status_display_translated(self):
        """Get translated status display."""
        status_dict = {
            'draft': _('Draft'),
            'published': _('Published'),
            'archived': _('Archived'),
        }
        return status_dict.get(self.status, self.status)

Form Translation

# forms.py
from django import forms
from django.utils.translation import gettext_lazy as _
from .models import BlogPost

class BlogPostForm(forms.ModelForm):
    """Blog post form with translated labels and help text."""
    
    class Meta:
        model = BlogPost
        fields = ['title', 'content', 'status']
        labels = {
            'title': _('Post Title'),
            'content': _('Post Content'),
            'status': _('Publication Status'),
        }
        help_texts = {
            'title': _('Choose a descriptive title for your post'),
            'content': _('Write your post content using Markdown'),
            'status': _('Select the publication status'),
        }
        widgets = {
            'content': forms.Textarea(attrs={
                'placeholder': _('Start writing your post...'),
                'rows': 10
            }),
        }
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        
        # Dynamic field customization
        self.fields['title'].widget.attrs.update({
            'placeholder': _('Enter post title'),
            'class': 'form-control'
        })
        
        # Custom validation messages
        self.fields['title'].error_messages = {
            'required': _('Title is required'),
            'max_length': _('Title is too long'),
        }
    
    def clean_title(self):
        """Custom validation with translated error messages."""
        title = self.cleaned_data.get('title')
        if title and len(title) < 5:
            raise forms.ValidationError(
                _('Title must be at least 5 characters long')
            )
        return title

class ContactForm(forms.Form):
    """Contact form with comprehensive translation."""
    name = forms.CharField(
        max_length=100,
        label=_('Your Name'),
        help_text=_('Enter your full name'),
        error_messages={
            'required': _('Name is required'),
            'max_length': _('Name is too long'),
        }
    )
    email = forms.EmailField(
        label=_('Email Address'),
        help_text=_('We will use this to respond to you'),
        error_messages={
            'required': _('Email is required'),
            'invalid': _('Please enter a valid email address'),
        }
    )
    subject = forms.ChoiceField(
        choices=[
            ('general', _('General Inquiry')),
            ('support', _('Technical Support')),
            ('business', _('Business Inquiry')),
            ('feedback', _('Feedback')),
        ],
        label=_('Subject'),
        help_text=_('Select the topic of your message')
    )
    message = forms.CharField(
        widget=forms.Textarea(attrs={
            'rows': 5,
            'placeholder': _('Type your message here...')
        }),
        label=_('Message'),
        help_text=_('Describe your inquiry in detail'),
        error_messages={
            'required': _('Message is required'),
        }
    )
    
    def send_email(self):
        """Send email with translated content."""
        from django.core.mail import send_mail
        from django.template.loader import render_to_string
        
        subject = _('New contact form submission: %(subject)s') % {
            'subject': self.cleaned_data['subject']
        }
        
        message = render_to_string('emails/contact_form.txt', {
            'form_data': self.cleaned_data,
            'greeting': _('Hello'),
            'closing': _('Best regards'),
        })
        
        send_mail(
            subject=subject,
            message=message,
            from_email='noreply@example.com',
            recipient_list=['admin@example.com'],
        )

Template Translation

Basic Template Translation

<!-- templates/blog/post_list.html -->
{% load i18n %}

<!DOCTYPE html>
<html lang="{{ LANGUAGE_CODE }}">
<head>
    <meta charset="UTF-8">
    <title>{% trans "Blog Posts" %} - {% trans "My Website" %}</title>
    <meta name="description" content="{% trans 'Latest blog posts and articles' %}">
</head>
<body>
    <header>
        <h1>{% trans "Welcome to Our Blog" %}</h1>
        <nav>
            <a href="{% url 'blog:post_list' %}">{% trans "Home" %}</a>
            <a href="{% url 'blog:about' %}">{% trans "About" %}</a>
            <a href="{% url 'blog:contact' %}">{% trans "Contact" %}</a>
        </nav>
    </header>
    
    <main>
        <section class="posts">
            <h2>{% trans "Latest Posts" %}</h2>
            
            {% if posts %}
                {% for post in posts %}
                    <article class="post">
                        <h3><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h3>
                        <p class="meta">
                            {% blocktrans with author=post.author.username date=post.created_at %}
                                By {{ author }} on {{ date }}
                            {% endblocktrans %}
                        </p>
                        <p>{{ post.excerpt }}</p>
                        <a href="{{ post.get_absolute_url }}">
                            {% trans "Read more" %}
                        </a>
                    </article>
                {% endfor %}
                
                <!-- Pagination with translation -->
                {% if is_paginated %}
                    <div class="pagination">
                        {% if page_obj.has_previous %}
                            <a href="?page={{ page_obj.previous_page_number }}">
                                {% trans "Previous" %}
                            </a>
                        {% endif %}
                        
                        <span class="current">
                            {% blocktrans with current=page_obj.number total=page_obj.paginator.num_pages %}
                                Page {{ current }} of {{ total }}
                            {% endblocktrans %}
                        </span>
                        
                        {% if page_obj.has_next %}
                            <a href="?page={{ page_obj.next_page_number }}">
                                {% trans "Next" %}
                            </a>
                        {% endif %}
                    </div>
                {% endif %}
            {% else %}
                <p>{% trans "No posts available yet." %}</p>
            {% endif %}
        </section>
    </main>
</body>
</html>

Advanced Template Translation

<!-- templates/blog/post_detail.html -->
{% load i18n %}

{% get_current_language as LANGUAGE_CODE %}
{% get_available_languages as LANGUAGES %}

<article class="post-detail">
    <header>
        <h1>{{ post.title }}</h1>
        <div class="meta">
            {% blocktrans with author=post.author.get_full_name|default:post.author.username date=post.created_at|date:"F j, Y" %}
                Published by {{ author }} on {{ date }}
            {% endblocktrans %}
            
            {% if post.updated_at != post.created_at %}
                <span class="updated">
                    {% blocktrans with date=post.updated_at|date:"F j, Y" %}
                        (Updated on {{ date }})
                    {% endblocktrans %}
                </span>
            {% endif %}
        </div>
        
        <!-- Language switcher -->
        <div class="language-switcher">
            <span>{% trans "Available in:" %}</span>
            {% for lang_code, lang_name in LANGUAGES %}
                {% if lang_code != LANGUAGE_CODE %}
                    <a href="{% url 'set_language' %}?language={{ lang_code }}&next={{ request.get_full_path }}">
                        {{ lang_name }}
                    </a>
                {% else %}
                    <strong>{{ lang_name }}</strong>
                {% endif %}
            {% endfor %}
        </div>
    </header>
    
    <div class="content">
        {{ post.content|safe }}
    </div>
    
    <footer>
        {% if post.tags.exists %}
            <div class="tags">
                <span>{% trans "Tags:" %}</span>
                {% for tag in post.tags.all %}
                    <a href="{% url 'blog:tag' tag.slug %}" class="tag">{{ tag.name }}</a>
                {% endfor %}
            </div>
        {% endif %}
        
        <div class="actions">
            <a href="{% url 'blog:post_list' %}">
                {% trans "← Back to posts" %}
            </a>
            
            {% if user.is_authenticated and user == post.author %}
                <a href="{% url 'blog:post_edit' post.slug %}">
                    {% trans "Edit post" %}
                </a>
            {% endif %}
        </div>
    </footer>
</article>

<!-- Comments section -->
<section class="comments">
    <h3>
        {% blocktrans count counter=post.comments.count %}
            {{ counter }} Comment
        {% plural %}
            {{ counter }} Comments
        {% endblocktrans %}
    </h3>
    
    {% for comment in post.comments.all %}
        <div class="comment">
            <div class="comment-meta">
                {% blocktrans with author=comment.author.username date=comment.created_at|timesince %}
                    {{ author }}, {{ date }} ago
                {% endblocktrans %}
            </div>
            <div class="comment-content">
                {{ comment.content|linebreaks }}
            </div>
        </div>
    {% empty %}
        <p>{% trans "No comments yet. Be the first to comment!" %}</p>
    {% endfor %}
</section>

Pluralization

Complex Pluralization Rules

# views.py
from django.utils.translation import ngettext, gettext as _

def notification_view(request):
    """Handle complex pluralization scenarios."""
    
    # Simple pluralization
    message_count = 3
    message = ngettext(
        'You have %(count)d unread message',
        'You have %(count)d unread messages',
        message_count
    ) % {'count': message_count}
    
    # Complex pluralization with context
    like_count = 1
    comment_count = 5
    
    if like_count == 1 and comment_count == 1:
        activity = _('%(likes)d person liked and %(comments)d person commented on your post') % {
            'likes': like_count,
            'comments': comment_count
        }
    elif like_count == 1:
        activity = ngettext(
            '%(likes)d person liked and %(comments)d person commented on your post',
            '%(likes)d person liked and %(comments)d people commented on your post',
            comment_count
        ) % {'likes': like_count, 'comments': comment_count}
    else:
        activity = ngettext(
            '%(likes)d people liked and %(comments)d person commented on your post',
            '%(likes)d people liked and %(comments)d people commented on your post',
            comment_count
        ) % {'likes': like_count, 'comments': comment_count}
    
    return render(request, 'notifications.html', {
        'message': message,
        'activity': activity,
    })

Template Pluralization

<!-- templates/notifications.html -->
{% load i18n %}

<div class="notifications">
    <!-- Simple pluralization -->
    {% blocktrans count counter=unread_count %}
        You have {{ counter }} unread notification
    {% plural %}
        You have {{ counter }} unread notifications
    {% endblocktrans %}
    
    <!-- Complex pluralization with multiple variables -->
    {% with likes=post.likes.count comments=post.comments.count %}
        {% if likes and comments %}
            {% blocktrans count like_count=likes %}
                {{ like_count }} person liked
            {% plural %}
                {{ like_count }} people liked
            {% endblocktrans %}
            {% trans "and" %}
            {% blocktrans count comment_count=comments %}
                {{ comment_count }} person commented
            {% plural %}
                {{ comment_count }} people commented
            {% endblocktrans %}
            {% trans "on your post" %}
        {% endif %}
    {% endwith %}
</div>

Context-Specific Translation

Translation with Context

# views.py
from django.utils.translation import pgettext, pgettext_lazy

def context_translation_example(request):
    """Examples of context-specific translations."""
    
    # Same word, different contexts
    may_month = pgettext('month name', 'May')
    may_permission = pgettext('permission', 'May')
    
    # Button contexts
    save_button = pgettext('button', 'Save')
    save_file = pgettext('file action', 'Save')
    
    # Status contexts
    active_user = pgettext('user status', 'Active')
    active_session = pgettext('session status', 'Active')
    
    return render(request, 'context_example.html', {
        'may_month': may_month,
        'may_permission': may_permission,
        'save_button': save_button,
        'save_file': save_file,
    })

Template Context Translation

<!-- templates/context_example.html -->
{% load i18n %}

<div class="calendar">
    <h3>{% pgettext "calendar header" "May" %} 2024</h3>
</div>

<div class="permissions">
    <p>{% pgettext "permission text" "You may edit this document" %}</p>
</div>

<form>
    <button type="submit">
        {% pgettext "form button" "Save" %}
    </button>
</form>

<div class="file-menu">
    <a href="#" onclick="saveFile()">
        {% pgettext "file menu" "Save" %}
    </a>
</div>

Dynamic Content Translation

Database Content Translation

# models.py
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.utils import translation

class TranslatableContent(models.Model):
    """Model for storing translatable content."""
    key = models.CharField(max_length=100, unique=True)
    
    class Meta:
        verbose_name = _('Translatable Content')
        verbose_name_plural = _('Translatable Contents')

class ContentTranslation(models.Model):
    """Translation storage for dynamic content."""
    content = models.ForeignKey(TranslatableContent, on_delete=models.CASCADE, related_name='translations')
    language_code = models.CharField(max_length=10)
    text = models.TextField()
    
    class Meta:
        unique_together = ['content', 'language_code']

def get_translated_content(key, default=''):
    """Get translated content from database."""
    try:
        content = TranslatableContent.objects.get(key=key)
        current_language = translation.get_language()
        
        try:
            translation_obj = content.translations.get(language_code=current_language)
            return translation_obj.text
        except ContentTranslation.DoesNotExist:
            # Fallback to default language
            try:
                fallback = content.translations.get(language_code=settings.LANGUAGE_CODE)
                return fallback.text
            except ContentTranslation.DoesNotExist:
                return default
    except TranslatableContent.DoesNotExist:
        return default

Template Tags for Dynamic Translation

# templatetags/translation_tags.py
from django import template
from django.utils.translation import gettext as _
from ..models import get_translated_content

register = template.Library()

@register.simple_tag
def translate_content(key, default=''):
    """Template tag for dynamic content translation."""
    return get_translated_content(key, default)

@register.simple_tag(takes_context=True)
def translate_with_fallback(context, key, fallback_key='', default=''):
    """Template tag with fallback translation."""
    content = get_translated_content(key)
    if not content and fallback_key:
        content = get_translated_content(fallback_key)
    return content or default

@register.filter
def translate_choice(value, choices_dict):
    """Filter to translate choice field values."""
    return choices_dict.get(value, value)
<!-- templates/dynamic_content.html -->
{% load translation_tags i18n %}

<div class="hero">
    <h1>{% translate_content "hero.title" "Welcome" %}</h1>
    <p>{% translate_content "hero.subtitle" "Default subtitle" %}</p>
</div>

<div class="features">
    {% translate_with_fallback "features.title" "section.title" "Features" %}
</div>

JavaScript Translation

Exposing Translations to JavaScript

# views.py
from django.http import JsonResponse
from django.utils.translation import gettext as _
from django.views.decorators.http import require_GET

@require_GET
def javascript_translations(request):
    """Provide translations for JavaScript."""
    translations = {
        'loading': _('Loading...'),
        'error': _('An error occurred'),
        'success': _('Success!'),
        'confirm_delete': _('Are you sure you want to delete this item?'),
        'cancel': _('Cancel'),
        'delete': _('Delete'),
        'save': _('Save'),
        'edit': _('Edit'),
        'close': _('Close'),
        'search_placeholder': _('Search...'),
        'no_results': _('No results found'),
        'load_more': _('Load more'),
    }
    return JsonResponse(translations)
<!-- templates/base.html -->
{% load i18n %}

<script>
// Expose translations to JavaScript
window.translations = {
    loading: "{% trans 'Loading...' %}",
    error: "{% trans 'An error occurred' %}",
    success: "{% trans 'Success!' %}",
    confirmDelete: "{% trans 'Are you sure you want to delete this item?' %}",
    cancel: "{% trans 'Cancel' %}",
    delete: "{% trans 'Delete' %}",
    save: "{% trans 'Save' %}",
    edit: "{% trans 'Edit' %}",
    close: "{% trans 'Close' %}"
};

// Translation function for JavaScript
function _(key) {
    return window.translations[key] || key;
}
</script>

Django JavaScript Catalog

# urls.py
from django.views.i18n import JavaScriptCatalog

urlpatterns = [
    path('jsi18n/', JavaScriptCatalog.as_view(), name='javascript-catalog'),
]
<!-- templates/base.html -->
<script src="{% url 'javascript-catalog' %}"></script>
<script>
// Use Django's JavaScript translation functions
console.log(gettext('Hello world'));
console.log(ngettext('item', 'items', count));
console.log(interpolate(gettext('Hello %(name)s'), {name: 'Django'}, true));
</script>

Effective text translation in Django requires understanding both the technical implementation and the linguistic nuances of your target languages. Use lazy translation for model fields and form labels, implement proper pluralization rules, and provide context for ambiguous terms. This foundation enables building truly multilingual applications that provide excellent user experiences across all supported languages.