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