0
# Internationalization
1
2
Multi-language support with built-in message translations and customizable translation backends for form labels, validation messages, and error text. WTForms provides comprehensive internationalization (i18n) capabilities that integrate with standard gettext workflows.
3
4
## Capabilities
5
6
### Translation Functions
7
8
Core functions for managing translations and message catalogs.
9
10
```python { .api }
11
def get_translations(languages=None, getter=None) -> object:
12
"""
13
Get translation object for specified languages.
14
15
Parameters:
16
- languages: List of language codes (e.g., ['en_US', 'en'])
17
- getter: Function to retrieve translations (default: get_builtin_gnu_translations)
18
19
Returns:
20
Translation object with gettext/ngettext methods
21
"""
22
23
def get_builtin_gnu_translations(languages=None) -> object:
24
"""
25
Get built-in GNU gettext translations from WTForms message catalogs.
26
27
Parameters:
28
- languages: List of language codes to load
29
30
Returns:
31
GNUTranslations object or NullTranslations if no translations found
32
"""
33
34
def messages_path() -> str:
35
"""
36
Get path to WTForms built-in message catalog directory.
37
38
Returns:
39
str: Path to locale directory containing .mo files
40
"""
41
```
42
43
### Translation Classes
44
45
Translation wrapper classes for different translation backends.
46
47
```python { .api }
48
class DefaultTranslations:
49
"""
50
Wrapper for gettext translations with fallback support.
51
Provides gettext and ngettext methods for message translation.
52
53
Parameters:
54
- translations: GNUTranslations object or compatible translation backend
55
"""
56
def __init__(self, translations): ...
57
58
def gettext(self, string) -> str:
59
"""
60
Get translated string.
61
62
Parameters:
63
- string: String to translate
64
65
Returns:
66
str: Translated string or original if no translation found
67
"""
68
69
def ngettext(self, singular, plural, n) -> str:
70
"""
71
Get translated string with pluralization.
72
73
Parameters:
74
- singular: Singular form string
75
- plural: Plural form string
76
- n: Number for pluralization
77
78
Returns:
79
str: Translated string in appropriate plural form
80
"""
81
82
class DummyTranslations:
83
"""
84
No-op translation class that returns strings unmodified.
85
Used as default when no translations are configured.
86
"""
87
def __init__(self): ...
88
89
def gettext(self, string) -> str:
90
"""Return string unmodified."""
91
return string
92
93
def ngettext(self, singular, plural, n) -> str:
94
"""Return singular or plural form based on n."""
95
return singular if n == 1 else plural
96
```
97
98
## Internationalization Usage Examples
99
100
### Basic Translation Setup
101
102
```python
103
from wtforms import Form, StringField, validators
104
from wtforms.i18n import get_translations
105
106
class MultilingualForm(Form):
107
class Meta:
108
locales = ['es_ES', 'en_US'] # Spanish, English fallback
109
cache_translations = True
110
111
name = StringField('Name', [validators.DataRequired()])
112
email = StringField('Email', [validators.Email()])
113
114
# Form will use Spanish translations for validation messages
115
form = MultilingualForm(formdata=request.form)
116
if not form.validate():
117
for field, errors in form.errors.items():
118
for error in errors:
119
print(error) # Error messages in Spanish
120
```
121
122
### Custom Translation Backend
123
124
```python
125
from wtforms.i18n import DefaultTranslations
126
import gettext
127
128
class CustomForm(Form):
129
class Meta:
130
def get_translations(self, form):
131
# Custom translation loading
132
domain = 'my_app'
133
localedir = '/path/to/my/locales'
134
language = get_current_language() # Your language detection
135
136
try:
137
translations = gettext.translation(
138
domain, localedir, languages=[language]
139
)
140
return DefaultTranslations(translations)
141
except IOError:
142
# Fallback to built-in translations
143
from wtforms.i18n import get_builtin_gnu_translations
144
return get_builtin_gnu_translations([language])
145
146
username = StringField('Username', [validators.DataRequired()])
147
```
148
149
### Per-Request Language Selection
150
151
```python
152
class LocalizedForm(Form):
153
username = StringField('Username', [validators.DataRequired()])
154
email = StringField('Email', [validators.Email()])
155
156
def create_form(request):
157
# Detect language from request
158
accepted_languages = request.headers.get('Accept-Language', '')
159
user_language = detect_language(accepted_languages)
160
161
# Create form with specific language
162
form = LocalizedForm(
163
formdata=request.form,
164
meta={
165
'locales': [user_language, 'en'], # User language + English fallback
166
'cache_translations': False # Don't cache for per-request languages
167
}
168
)
169
return form
170
171
@app.route('/register', methods=['GET', 'POST'])
172
def register():
173
form = create_form(request)
174
if form.validate():
175
# Process registration
176
pass
177
return render_template('register.html', form=form)
178
```
179
180
### Built-in Language Support
181
182
WTForms includes built-in translations for common validation messages:
183
184
```python
185
# Available built-in languages (as of version 3.2.1)
186
SUPPORTED_LANGUAGES = [
187
'ar', # Arabic
188
'bg', # Bulgarian
189
'ca', # Catalan
190
'cs', # Czech
191
'cy', # Welsh
192
'da', # Danish
193
'de', # German
194
'el', # Greek
195
'en', # English
196
'es', # Spanish
197
'et', # Estonian
198
'fa', # Persian
199
'fi', # Finnish
200
'fr', # French
201
'he', # Hebrew
202
'hu', # Hungarian
203
'it', # Italian
204
'ja', # Japanese
205
'ko', # Korean
206
'nb', # Norwegian Bokmål
207
'nl', # Dutch
208
'pl', # Polish
209
'pt', # Portuguese
210
'ru', # Russian
211
'sk', # Slovak
212
'sv', # Swedish
213
'tr', # Turkish
214
'uk', # Ukrainian
215
'zh', # Chinese
216
]
217
218
class MultiLanguageForm(Form):
219
class Meta:
220
# Try user's preferred language, fallback to English
221
locales = ['de', 'en'] # German with English fallback
222
223
email = StringField('E-Mail', [validators.Email()])
224
password = StringField('Passwort', [validators.DataRequired()])
225
226
# Validation messages will be in German
227
form = MultiLanguageForm(formdata={'email': 'invalid'})
228
form.validate()
229
print(form.email.errors) # ['Ungültige E-Mail-Adresse.']
230
```
231
232
### Custom Validation Messages
233
234
```python
235
class CustomMessageForm(Form):
236
username = StringField('Username', [
237
validators.DataRequired(message='El nombre de usuario es obligatorio'),
238
validators.Length(min=3, message='Mínimo 3 caracteres')
239
])
240
241
email = StringField('Email', [
242
validators.Email(message='Formato de email inválido')
243
])
244
245
def validate_username(self, field):
246
if User.exists(field.data):
247
raise ValidationError('Este nombre de usuario ya existe')
248
249
# Custom messages override translation system
250
form = CustomMessageForm(formdata={'username': 'ab'})
251
form.validate()
252
print(form.username.errors) # ['Mínimo 3 caracteres']
253
```
254
255
### Dynamic Message Translation
256
257
```python
258
from wtforms.validators import ValidationError
259
260
class TranslatedValidator:
261
"""Custom validator with translatable messages."""
262
263
def __init__(self, message_key='custom_validation_error'):
264
self.message_key = message_key
265
266
def __call__(self, form, field):
267
if not self.validate_logic(field.data):
268
# Get translated message using form's translation system
269
message = field.gettext(self.message_key)
270
raise ValidationError(message)
271
272
def validate_logic(self, value):
273
# Your validation logic here
274
return len(value) > 5
275
276
class TranslatedForm(Form):
277
class Meta:
278
locales = ['es', 'en']
279
280
data = StringField('Data', [TranslatedValidator()])
281
282
# Validation messages will be translated
283
form = TranslatedForm(formdata={'data': 'short'})
284
```
285
286
### Template Integration
287
288
```html
289
<!-- Jinja2 template with translated labels -->
290
<form method="POST">
291
{{ form.csrf_token }}
292
293
<div class="form-group">
294
{{ form.username.label(class="form-label") }}
295
{{ form.username(class="form-control") }}
296
{% for error in form.username.errors %}
297
<div class="error">{{ error }}</div>
298
{% endfor %}
299
</div>
300
301
<div class="form-group">
302
{{ form.email.label(class="form-label") }}
303
{{ form.email(class="form-control") }}
304
{% for error in form.email.errors %}
305
<div class="error">{{ error }}</div>
306
{% endfor %}
307
</div>
308
309
<button type="submit">{{ _('Submit') }}</button>
310
</form>
311
```
312
313
### Creating Custom Translation Files
314
315
```bash
316
# Create message catalog for your application
317
# 1. Extract messages from Python files
318
pybabel extract -F babel.cfg -k gettext -k ngettext -o messages.pot .
319
320
# 2. Initialize Spanish translation
321
pybabel init -i messages.pot -d translations -l es
322
323
# 3. Update existing translations
324
pybabel update -i messages.pot -d translations
325
326
# 4. Compile translations
327
pybabel compile -d translations
328
```
329
330
Example `babel.cfg`:
331
```ini
332
[python: **.py]
333
[jinja2: **/templates/**.html]
334
extensions=jinja2.ext.autoescape,jinja2.ext.with_
335
```
336
337
### Translation with Context Managers
338
339
```python
340
from contextlib import contextmanager
341
from flask_babel import get_locale
342
343
@contextmanager
344
def form_language(language_code):
345
"""Context manager for temporary language switching."""
346
original_locale = get_locale()
347
try:
348
# Set temporary locale
349
set_locale(language_code)
350
yield
351
finally:
352
# Restore original locale
353
set_locale(original_locale)
354
355
class FlexibleForm(Form):
356
message = StringField('Message', [validators.DataRequired()])
357
358
# Use form with specific language
359
with form_language('fr'):
360
form = FlexibleForm(formdata=request.form)
361
if not form.validate():
362
# Error messages in French
363
errors = form.errors
364
```
365
366
### Advanced Translation Configuration
367
368
```python
369
import os
370
import gettext
371
from wtforms.i18n import DefaultTranslations, DummyTranslations
372
373
class AdvancedI18nForm(Form):
374
class Meta:
375
@staticmethod
376
def get_translations(form):
377
"""Advanced translation configuration."""
378
379
# Get language from various sources
380
language = (
381
getattr(form, '_language', None) or # Form-specific
382
os.environ.get('FORM_LANGUAGE') or # Environment
383
request.headers.get('Accept-Language', '')[:2] or # Browser
384
'en' # Default
385
)
386
387
# Try multiple translation sources
388
translation_sources = [
389
('myapp', '/opt/myapp/translations'), # App-specific
390
('wtforms', get_wtforms_locale_path()), # WTForms built-in
391
]
392
393
for domain, locale_dir in translation_sources:
394
try:
395
trans = gettext.translation(
396
domain, locale_dir,
397
languages=[language, 'en'],
398
fallback=True
399
)
400
return DefaultTranslations(trans)
401
except IOError:
402
continue
403
404
# Final fallback
405
return DummyTranslations()
406
407
name = StringField('Name', [validators.DataRequired()])
408
409
# Set language per form instance
410
form = AdvancedI18nForm(formdata=request.form)
411
form._language = 'de' # German
412
```
413
414
### Pluralization Support
415
416
```python
417
from wtforms.validators import ValidationError
418
419
class ItemCountValidator:
420
"""Validator demonstrating pluralization."""
421
422
def __init__(self, min_items=1):
423
self.min_items = min_items
424
425
def __call__(self, form, field):
426
items = field.data or []
427
count = len(items)
428
429
if count < self.min_items:
430
# Use ngettext for proper pluralization
431
message = field.ngettext(
432
'You must select at least %(num)d item.',
433
'You must select at least %(num)d items.',
434
self.min_items
435
) % {'num': self.min_items}
436
raise ValidationError(message)
437
438
class ItemSelectionForm(Form):
439
class Meta:
440
locales = ['en', 'de', 'fr']
441
442
items = SelectMultipleField('Select Items', [
443
ItemCountValidator(min_items=2)
444
])
445
446
# Error messages will be properly pluralized in selected language
447
form = ItemSelectionForm(formdata={'items': ['one']})
448
form.validate()
449
# English: "You must select at least 2 items."
450
# German: "Sie müssen mindestens 2 Elemente auswählen."
451
```
452
453
### Framework Integration Examples
454
455
#### Flask-Babel Integration
456
457
```python
458
from flask import Flask
459
from flask_babel import Babel, get_locale
460
from wtforms.i18n import get_translations
461
462
app = Flask(__name__)
463
babel = Babel(app)
464
465
class FlaskBabelForm(Form):
466
class Meta:
467
def get_translations(self, form):
468
return get_translations([str(get_locale())])
469
470
message = StringField('Message', [validators.DataRequired()])
471
472
@babel.localeselector
473
def get_locale():
474
return request.args.get('lang') or 'en'
475
```
476
477
#### Django Integration
478
479
```python
480
from django.utils.translation import get_language
481
from django.utils.translation import gettext_lazy as _
482
483
class DjangoI18nForm(Form):
484
class Meta:
485
def get_translations(self, form):
486
from wtforms.i18n import get_translations
487
return get_translations([get_language()])
488
489
# Use Django's lazy translation for labels
490
message = StringField(_('Message'), [validators.DataRequired()])
491
```
492
493
This comprehensive internationalization system allows WTForms to be used effectively in multilingual applications across different web frameworks and deployment scenarios.