0
# Forms Integration
1
2
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.
3
4
## Capabilities
5
6
### Translation Model Forms
7
8
ModelForm subclass that automatically handles translation fields with proper field exclusion and widget configuration.
9
10
```python { .api }
11
class TranslationModelForm(forms.ModelForm):
12
"""
13
ModelForm subclass for translated models.
14
15
Automatically:
16
- Removes translation fields from form (keeps original field names)
17
- Handles language-specific field rendering
18
- Manages validation across translation fields
19
"""
20
21
def __init__(self, *args, **kwargs):
22
"""Initialize form with translation field handling."""
23
```
24
25
**Usage Example:**
26
27
```python
28
from modeltranslation.forms import TranslationModelForm
29
30
class ArticleForm(TranslationModelForm):
31
class Meta:
32
model = Article
33
fields = ['title', 'content', 'author', 'published']
34
35
def clean_title(self):
36
"""Custom validation for title field."""
37
title = self.cleaned_data.get('title')
38
if title and len(title) < 5:
39
raise forms.ValidationError("Title must be at least 5 characters")
40
return title
41
42
# Form automatically handles title_en, title_fr, etc. fields
43
form = ArticleForm(data={
44
'title_en': 'English Title',
45
'title_fr': 'Titre Français',
46
'content_en': 'English content',
47
'content_fr': 'Contenu français',
48
'author': 'John Doe',
49
'published': True
50
})
51
```
52
53
### Nullable Field Types
54
55
Specialized form fields for handling nullable translation values with proper empty value semantics.
56
57
```python { .api }
58
class NullCharField(forms.CharField):
59
"""
60
CharField subclass that returns None for empty values instead of empty string.
61
62
Useful for nullable translation fields where distinction between
63
None and empty string is important.
64
"""
65
66
def to_python(self, value):
67
"""
68
Convert form value to Python value.
69
70
Parameters:
71
- value: Form input value
72
73
Returns:
74
- str | None: Converted value, None for empty inputs
75
"""
76
77
class NullableField(forms.Field):
78
"""
79
Form field mixin that ensures None values are preserved.
80
81
Prevents casting None to other types (like empty string in CharField).
82
Useful as base class for custom nullable form fields.
83
"""
84
85
def to_python(self, value):
86
"""
87
Convert form value, preserving None values.
88
89
Parameters:
90
- value: Form input value
91
92
Returns:
93
- Any | None: Converted value, preserving None
94
"""
95
96
def has_changed(self, initial, data):
97
"""
98
Check if field value has changed, handling None properly.
99
100
Parameters:
101
- initial: Initial field value
102
- data: Current form data value
103
104
Returns:
105
- bool: True if value has changed
106
"""
107
```
108
109
**Usage Example:**
110
111
```python
112
from modeltranslation.forms import NullCharField, NullableField
113
114
class CustomArticleForm(forms.ModelForm):
115
# Use NullCharField for nullable string translations
116
summary_en = NullCharField(required=False, widget=forms.Textarea)
117
summary_fr = NullCharField(required=False, widget=forms.Textarea)
118
119
class Meta:
120
model = Article
121
fields = ['title', 'content', 'summary']
122
```
123
124
### Language-Specific Form Fields
125
126
Create forms with explicit language-specific fields for fine-grained control.
127
128
```python
129
class MultilingualArticleForm(forms.Form):
130
# English fields
131
title_en = forms.CharField(max_length=255, label="Title (English)")
132
content_en = forms.TextField(widget=forms.Textarea, label="Content (English)")
133
134
# French fields
135
title_fr = forms.CharField(max_length=255, required=False, label="Title (French)")
136
content_fr = forms.TextField(widget=forms.Textarea, required=False, label="Content (French)")
137
138
# German fields
139
title_de = forms.CharField(max_length=255, required=False, label="Title (German)")
140
content_de = forms.TextField(widget=forms.Textarea, required=False, label="Content (German)")
141
142
# Common fields
143
author = forms.CharField(max_length=100)
144
published = forms.BooleanField(required=False)
145
146
def clean(self):
147
"""Cross-field validation for translations."""
148
cleaned_data = super().clean()
149
150
# Ensure at least one language version is provided
151
has_en = cleaned_data.get('title_en') and cleaned_data.get('content_en')
152
has_fr = cleaned_data.get('title_fr') and cleaned_data.get('content_fr')
153
has_de = cleaned_data.get('title_de') and cleaned_data.get('content_de')
154
155
if not (has_en or has_fr or has_de):
156
raise forms.ValidationError(
157
"At least one complete translation (title and content) is required."
158
)
159
160
return cleaned_data
161
162
def save(self, commit=True):
163
"""Save form data to Article model."""
164
article = Article(
165
author=self.cleaned_data['author'],
166
published=self.cleaned_data['published']
167
)
168
169
# Set translation fields
170
for lang in ['en', 'fr', 'de']:
171
title_key = f'title_{lang}'
172
content_key = f'content_{lang}'
173
174
if self.cleaned_data.get(title_key):
175
setattr(article, title_key, self.cleaned_data[title_key])
176
if self.cleaned_data.get(content_key):
177
setattr(article, content_key, self.cleaned_data[content_key])
178
179
if commit:
180
article.save()
181
182
return article
183
```
184
185
### Form Widget Integration
186
187
Integration with translation-specific widgets for enhanced user experience.
188
189
```python
190
from modeltranslation.widgets import ClearableWidgetWrapper
191
192
class TranslationForm(forms.ModelForm):
193
class Meta:
194
model = Article
195
fields = ['title', 'content', 'summary']
196
widgets = {
197
'summary_en': ClearableWidgetWrapper(forms.Textarea()),
198
'summary_fr': ClearableWidgetWrapper(forms.Textarea()),
199
'summary_de': ClearableWidgetWrapper(forms.Textarea()),
200
}
201
202
def __init__(self, *args, **kwargs):
203
super().__init__(*args, **kwargs)
204
205
# Add language labels to fields
206
languages = {'en': 'English', 'fr': 'French', 'de': 'German'}
207
208
for field_name in ['title', 'content']:
209
for lang_code, lang_name in languages.items():
210
trans_field = f"{field_name}_{lang_code}"
211
if trans_field in self.fields:
212
original_label = self.fields[trans_field].label or field_name
213
self.fields[trans_field].label = f"{original_label} ({lang_name})"
214
```
215
216
### Required Language Validation
217
218
Enforce required languages in form validation.
219
220
```python
221
class RequiredLanguageForm(TranslationModelForm):
222
class Meta:
223
model = Article
224
fields = ['title', 'content']
225
226
def __init__(self, *args, **kwargs):
227
super().__init__(*args, **kwargs)
228
229
# Mark required language fields
230
required_languages = ['en', 'fr'] # From translation options
231
232
for field_name in ['title', 'content']:
233
for lang in required_languages:
234
trans_field = f"{field_name}_{lang}"
235
if trans_field in self.fields:
236
self.fields[trans_field].required = True
237
238
def clean(self):
239
cleaned_data = super().clean()
240
required_languages = ['en', 'fr']
241
242
# Validate required languages have values
243
for field_name in ['title', 'content']:
244
for lang in required_languages:
245
trans_field = f"{field_name}_{lang}"
246
if not cleaned_data.get(trans_field):
247
self.add_error(
248
trans_field,
249
f"{field_name.title()} in {lang.upper()} is required"
250
)
251
252
return cleaned_data
253
```
254
255
### Formset Integration
256
257
Handle translation fields in Django formsets.
258
259
```python
260
from django.forms import modelformset_factory
261
262
# Create formset for translation forms
263
ArticleFormSet = modelformset_factory(
264
Article,
265
form=TranslationModelForm,
266
fields=['title', 'content'],
267
extra=1
268
)
269
270
# Usage in views
271
def edit_articles(request):
272
if request.method == 'POST':
273
formset = ArticleFormSet(request.POST)
274
if formset.is_valid():
275
formset.save()
276
return redirect('article_list')
277
else:
278
formset = ArticleFormSet(queryset=Article.objects.all())
279
280
return render(request, 'articles/edit.html', {'formset': formset})
281
```
282
283
### Dynamic Field Generation
284
285
Generate form fields dynamically based on available languages.
286
287
```python
288
from modeltranslation.settings import AVAILABLE_LANGUAGES
289
290
class DynamicTranslationForm(forms.Form):
291
def __init__(self, *args, **kwargs):
292
super().__init__(*args, **kwargs)
293
294
# Dynamically add fields for each language
295
for lang in AVAILABLE_LANGUAGES:
296
self.fields[f'title_{lang}'] = forms.CharField(
297
max_length=255,
298
required=(lang == 'en'), # English required, others optional
299
label=f"Title ({lang.upper()})"
300
)
301
302
self.fields[f'content_{lang}'] = forms.CharField(
303
widget=forms.Textarea,
304
required=(lang == 'en'),
305
label=f"Content ({lang.upper()})"
306
)
307
```
308
309
## Advanced Usage
310
311
### Custom Form Field Validation
312
313
Create custom validation rules for translation fields.
314
315
```python
316
class TranslationValidationMixin:
317
def validate_translation_completeness(self, field_base_name):
318
"""Validate that if one language is provided, related languages are too."""
319
primary_lang = 'en'
320
primary_field = f"{field_base_name}_{primary_lang}"
321
322
if self.cleaned_data.get(primary_field):
323
# If primary language is provided, check for required translations
324
required_translations = ['fr'] # Configurable
325
326
for lang in required_translations:
327
trans_field = f"{field_base_name}_{lang}"
328
if not self.cleaned_data.get(trans_field):
329
self.add_error(
330
trans_field,
331
f"Translation required when {primary_lang.upper()} is provided"
332
)
333
334
class ArticleForm(TranslationValidationMixin, TranslationModelForm):
335
class Meta:
336
model = Article
337
fields = ['title', 'content']
338
339
def clean(self):
340
cleaned_data = super().clean()
341
342
# Apply translation validation
343
self.validate_translation_completeness('title')
344
self.validate_translation_completeness('content')
345
346
return cleaned_data
347
```
348
349
### AJAX Form Updates
350
351
Handle AJAX form submissions with translation fields.
352
353
```python
354
import json
355
from django.http import JsonResponse
356
357
def update_translation(request):
358
if request.method == 'POST':
359
form = TranslationModelForm(request.POST)
360
361
if form.is_valid():
362
article = form.save()
363
364
# Return success with updated translation data
365
return JsonResponse({
366
'success': True,
367
'article_id': article.id,
368
'translations': {
369
'title_en': article.title_en,
370
'title_fr': article.title_fr,
371
'content_en': article.content_en,
372
'content_fr': article.content_fr,
373
}
374
})
375
else:
376
# Return validation errors
377
return JsonResponse({
378
'success': False,
379
'errors': form.errors
380
})
381
382
return JsonResponse({'success': False, 'error': 'Invalid request'})
383
```
384
385
### Form Rendering Helpers
386
387
Custom template tags and filters for rendering translation forms.
388
389
```python
390
# In templatetags/translation_tags.py
391
from django import template
392
from modeltranslation.settings import AVAILABLE_LANGUAGES
393
394
register = template.Library()
395
396
@register.filter
397
def translation_fields(form, field_name):
398
"""Get all translation fields for a base field name."""
399
fields = []
400
for lang in AVAILABLE_LANGUAGES:
401
trans_field_name = f"{field_name}_{lang}"
402
if trans_field_name in form.fields:
403
fields.append(form[trans_field_name])
404
return fields
405
406
@register.inclusion_tag('translation/field_tabs.html')
407
def translation_field_tabs(form, field_name):
408
"""Render translation fields as language tabs."""
409
return {
410
'form': form,
411
'field_name': field_name,
412
'languages': AVAILABLE_LANGUAGES,
413
}
414
```
415
416
**Template Usage:**
417
418
```django
419
<!-- Load custom tags -->
420
{% load translation_tags %}
421
422
<!-- Render translation fields as tabs -->
423
{% translation_field_tabs form 'title' %}
424
425
<!-- Or iterate over translation fields -->
426
{% for field in form|translation_fields:'content' %}
427
<div class="field-{{ field.name }}">
428
{{ field.label_tag }}
429
{{ field }}
430
{{ field.errors }}
431
</div>
432
{% endfor %}
433
```