or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

admin-integration.mdcore-registration.mdfield-management.mdforms-integration.mdindex.mdquery-interface.mdutils-configuration.md

forms-integration.mddocs/

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

```