Translates Django models using a registration approach without modifying original model classes.
—
Form classes and field types specifically designed for handling translations in Django forms, providing automatic translation field inclusion and specialized widgets for multilingual content management.
ModelForm subclass that automatically handles translation fields with proper field exclusion and widget configuration.
class TranslationModelForm(forms.ModelForm):
"""
ModelForm subclass for translated models.
Automatically:
- Removes translation fields from form (keeps original field names)
- Handles language-specific field rendering
- Manages validation across translation fields
"""
def __init__(self, *args, **kwargs):
"""Initialize form with translation field handling."""Usage Example:
from modeltranslation.forms import TranslationModelForm
class ArticleForm(TranslationModelForm):
class Meta:
model = Article
fields = ['title', 'content', 'author', 'published']
def clean_title(self):
"""Custom validation for title field."""
title = self.cleaned_data.get('title')
if title and len(title) < 5:
raise forms.ValidationError("Title must be at least 5 characters")
return title
# Form automatically handles title_en, title_fr, etc. fields
form = ArticleForm(data={
'title_en': 'English Title',
'title_fr': 'Titre Français',
'content_en': 'English content',
'content_fr': 'Contenu français',
'author': 'John Doe',
'published': True
})Specialized form fields for handling nullable translation values with proper empty value semantics.
class NullCharField(forms.CharField):
"""
CharField subclass that returns None for empty values instead of empty string.
Useful for nullable translation fields where distinction between
None and empty string is important.
"""
def to_python(self, value):
"""
Convert form value to Python value.
Parameters:
- value: Form input value
Returns:
- str | None: Converted value, None for empty inputs
"""
class NullableField(forms.Field):
"""
Form field mixin that ensures None values are preserved.
Prevents casting None to other types (like empty string in CharField).
Useful as base class for custom nullable form fields.
"""
def to_python(self, value):
"""
Convert form value, preserving None values.
Parameters:
- value: Form input value
Returns:
- Any | None: Converted value, preserving None
"""
def has_changed(self, initial, data):
"""
Check if field value has changed, handling None properly.
Parameters:
- initial: Initial field value
- data: Current form data value
Returns:
- bool: True if value has changed
"""Usage Example:
from modeltranslation.forms import NullCharField, NullableField
class CustomArticleForm(forms.ModelForm):
# Use NullCharField for nullable string translations
summary_en = NullCharField(required=False, widget=forms.Textarea)
summary_fr = NullCharField(required=False, widget=forms.Textarea)
class Meta:
model = Article
fields = ['title', 'content', 'summary']Create forms with explicit language-specific fields for fine-grained control.
class MultilingualArticleForm(forms.Form):
# English fields
title_en = forms.CharField(max_length=255, label="Title (English)")
content_en = forms.TextField(widget=forms.Textarea, label="Content (English)")
# French fields
title_fr = forms.CharField(max_length=255, required=False, label="Title (French)")
content_fr = forms.TextField(widget=forms.Textarea, required=False, label="Content (French)")
# German fields
title_de = forms.CharField(max_length=255, required=False, label="Title (German)")
content_de = forms.TextField(widget=forms.Textarea, required=False, label="Content (German)")
# Common fields
author = forms.CharField(max_length=100)
published = forms.BooleanField(required=False)
def clean(self):
"""Cross-field validation for translations."""
cleaned_data = super().clean()
# Ensure at least one language version is provided
has_en = cleaned_data.get('title_en') and cleaned_data.get('content_en')
has_fr = cleaned_data.get('title_fr') and cleaned_data.get('content_fr')
has_de = cleaned_data.get('title_de') and cleaned_data.get('content_de')
if not (has_en or has_fr or has_de):
raise forms.ValidationError(
"At least one complete translation (title and content) is required."
)
return cleaned_data
def save(self, commit=True):
"""Save form data to Article model."""
article = Article(
author=self.cleaned_data['author'],
published=self.cleaned_data['published']
)
# Set translation fields
for lang in ['en', 'fr', 'de']:
title_key = f'title_{lang}'
content_key = f'content_{lang}'
if self.cleaned_data.get(title_key):
setattr(article, title_key, self.cleaned_data[title_key])
if self.cleaned_data.get(content_key):
setattr(article, content_key, self.cleaned_data[content_key])
if commit:
article.save()
return articleIntegration with translation-specific widgets for enhanced user experience.
from modeltranslation.widgets import ClearableWidgetWrapper
class TranslationForm(forms.ModelForm):
class Meta:
model = Article
fields = ['title', 'content', 'summary']
widgets = {
'summary_en': ClearableWidgetWrapper(forms.Textarea()),
'summary_fr': ClearableWidgetWrapper(forms.Textarea()),
'summary_de': ClearableWidgetWrapper(forms.Textarea()),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Add language labels to fields
languages = {'en': 'English', 'fr': 'French', 'de': 'German'}
for field_name in ['title', 'content']:
for lang_code, lang_name in languages.items():
trans_field = f"{field_name}_{lang_code}"
if trans_field in self.fields:
original_label = self.fields[trans_field].label or field_name
self.fields[trans_field].label = f"{original_label} ({lang_name})"Enforce required languages in form validation.
class RequiredLanguageForm(TranslationModelForm):
class Meta:
model = Article
fields = ['title', 'content']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Mark required language fields
required_languages = ['en', 'fr'] # From translation options
for field_name in ['title', 'content']:
for lang in required_languages:
trans_field = f"{field_name}_{lang}"
if trans_field in self.fields:
self.fields[trans_field].required = True
def clean(self):
cleaned_data = super().clean()
required_languages = ['en', 'fr']
# Validate required languages have values
for field_name in ['title', 'content']:
for lang in required_languages:
trans_field = f"{field_name}_{lang}"
if not cleaned_data.get(trans_field):
self.add_error(
trans_field,
f"{field_name.title()} in {lang.upper()} is required"
)
return cleaned_dataHandle translation fields in Django formsets.
from django.forms import modelformset_factory
# Create formset for translation forms
ArticleFormSet = modelformset_factory(
Article,
form=TranslationModelForm,
fields=['title', 'content'],
extra=1
)
# Usage in views
def edit_articles(request):
if request.method == 'POST':
formset = ArticleFormSet(request.POST)
if formset.is_valid():
formset.save()
return redirect('article_list')
else:
formset = ArticleFormSet(queryset=Article.objects.all())
return render(request, 'articles/edit.html', {'formset': formset})Generate form fields dynamically based on available languages.
from modeltranslation.settings import AVAILABLE_LANGUAGES
class DynamicTranslationForm(forms.Form):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Dynamically add fields for each language
for lang in AVAILABLE_LANGUAGES:
self.fields[f'title_{lang}'] = forms.CharField(
max_length=255,
required=(lang == 'en'), # English required, others optional
label=f"Title ({lang.upper()})"
)
self.fields[f'content_{lang}'] = forms.CharField(
widget=forms.Textarea,
required=(lang == 'en'),
label=f"Content ({lang.upper()})"
)Create custom validation rules for translation fields.
class TranslationValidationMixin:
def validate_translation_completeness(self, field_base_name):
"""Validate that if one language is provided, related languages are too."""
primary_lang = 'en'
primary_field = f"{field_base_name}_{primary_lang}"
if self.cleaned_data.get(primary_field):
# If primary language is provided, check for required translations
required_translations = ['fr'] # Configurable
for lang in required_translations:
trans_field = f"{field_base_name}_{lang}"
if not self.cleaned_data.get(trans_field):
self.add_error(
trans_field,
f"Translation required when {primary_lang.upper()} is provided"
)
class ArticleForm(TranslationValidationMixin, TranslationModelForm):
class Meta:
model = Article
fields = ['title', 'content']
def clean(self):
cleaned_data = super().clean()
# Apply translation validation
self.validate_translation_completeness('title')
self.validate_translation_completeness('content')
return cleaned_dataHandle AJAX form submissions with translation fields.
import json
from django.http import JsonResponse
def update_translation(request):
if request.method == 'POST':
form = TranslationModelForm(request.POST)
if form.is_valid():
article = form.save()
# Return success with updated translation data
return JsonResponse({
'success': True,
'article_id': article.id,
'translations': {
'title_en': article.title_en,
'title_fr': article.title_fr,
'content_en': article.content_en,
'content_fr': article.content_fr,
}
})
else:
# Return validation errors
return JsonResponse({
'success': False,
'errors': form.errors
})
return JsonResponse({'success': False, 'error': 'Invalid request'})Custom template tags and filters for rendering translation forms.
# In templatetags/translation_tags.py
from django import template
from modeltranslation.settings import AVAILABLE_LANGUAGES
register = template.Library()
@register.filter
def translation_fields(form, field_name):
"""Get all translation fields for a base field name."""
fields = []
for lang in AVAILABLE_LANGUAGES:
trans_field_name = f"{field_name}_{lang}"
if trans_field_name in form.fields:
fields.append(form[trans_field_name])
return fields
@register.inclusion_tag('translation/field_tabs.html')
def translation_field_tabs(form, field_name):
"""Render translation fields as language tabs."""
return {
'form': form,
'field_name': field_name,
'languages': AVAILABLE_LANGUAGES,
}Template Usage:
<!-- Load custom tags -->
{% load translation_tags %}
<!-- Render translation fields as tabs -->
{% translation_field_tabs form 'title' %}
<!-- Or iterate over translation fields -->
{% for field in form|translation_fields:'content' %}
<div class="field-{{ field.name }}">
{{ field.label_tag }}
{{ field }}
{{ field.errors }}
</div>
{% endfor %}Install with Tessl CLI
npx tessl i tessl/pypi-django-modeltranslation