Form validation and rendering for Python web development.
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
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.
Core functions for managing translations and message catalogs.
def get_translations(languages=None, getter=None) -> object:
"""
Get translation object for specified languages.
Parameters:
- languages: List of language codes (e.g., ['en_US', 'en'])
- getter: Function to retrieve translations (default: get_builtin_gnu_translations)
Returns:
Translation object with gettext/ngettext methods
"""
def get_builtin_gnu_translations(languages=None) -> object:
"""
Get built-in GNU gettext translations from WTForms message catalogs.
Parameters:
- languages: List of language codes to load
Returns:
GNUTranslations object or NullTranslations if no translations found
"""
def messages_path() -> str:
"""
Get path to WTForms built-in message catalog directory.
Returns:
str: Path to locale directory containing .mo files
"""Translation wrapper classes for different translation backends.
class DefaultTranslations:
"""
Wrapper for gettext translations with fallback support.
Provides gettext and ngettext methods for message translation.
Parameters:
- translations: GNUTranslations object or compatible translation backend
"""
def __init__(self, translations): ...
def gettext(self, string) -> str:
"""
Get translated string.
Parameters:
- string: String to translate
Returns:
str: Translated string or original if no translation found
"""
def ngettext(self, singular, plural, n) -> str:
"""
Get translated string with pluralization.
Parameters:
- singular: Singular form string
- plural: Plural form string
- n: Number for pluralization
Returns:
str: Translated string in appropriate plural form
"""
class DummyTranslations:
"""
No-op translation class that returns strings unmodified.
Used as default when no translations are configured.
"""
def __init__(self): ...
def gettext(self, string) -> str:
"""Return string unmodified."""
return string
def ngettext(self, singular, plural, n) -> str:
"""Return singular or plural form based on n."""
return singular if n == 1 else pluralfrom wtforms import Form, StringField, validators
from wtforms.i18n import get_translations
class MultilingualForm(Form):
class Meta:
locales = ['es_ES', 'en_US'] # Spanish, English fallback
cache_translations = True
name = StringField('Name', [validators.DataRequired()])
email = StringField('Email', [validators.Email()])
# Form will use Spanish translations for validation messages
form = MultilingualForm(formdata=request.form)
if not form.validate():
for field, errors in form.errors.items():
for error in errors:
print(error) # Error messages in Spanishfrom wtforms.i18n import DefaultTranslations
import gettext
class CustomForm(Form):
class Meta:
def get_translations(self, form):
# Custom translation loading
domain = 'my_app'
localedir = '/path/to/my/locales'
language = get_current_language() # Your language detection
try:
translations = gettext.translation(
domain, localedir, languages=[language]
)
return DefaultTranslations(translations)
except IOError:
# Fallback to built-in translations
from wtforms.i18n import get_builtin_gnu_translations
return get_builtin_gnu_translations([language])
username = StringField('Username', [validators.DataRequired()])class LocalizedForm(Form):
username = StringField('Username', [validators.DataRequired()])
email = StringField('Email', [validators.Email()])
def create_form(request):
# Detect language from request
accepted_languages = request.headers.get('Accept-Language', '')
user_language = detect_language(accepted_languages)
# Create form with specific language
form = LocalizedForm(
formdata=request.form,
meta={
'locales': [user_language, 'en'], # User language + English fallback
'cache_translations': False # Don't cache for per-request languages
}
)
return form
@app.route('/register', methods=['GET', 'POST'])
def register():
form = create_form(request)
if form.validate():
# Process registration
pass
return render_template('register.html', form=form)WTForms includes built-in translations for common validation messages:
# Available built-in languages (as of version 3.2.1)
SUPPORTED_LANGUAGES = [
'ar', # Arabic
'bg', # Bulgarian
'ca', # Catalan
'cs', # Czech
'cy', # Welsh
'da', # Danish
'de', # German
'el', # Greek
'en', # English
'es', # Spanish
'et', # Estonian
'fa', # Persian
'fi', # Finnish
'fr', # French
'he', # Hebrew
'hu', # Hungarian
'it', # Italian
'ja', # Japanese
'ko', # Korean
'nb', # Norwegian Bokmål
'nl', # Dutch
'pl', # Polish
'pt', # Portuguese
'ru', # Russian
'sk', # Slovak
'sv', # Swedish
'tr', # Turkish
'uk', # Ukrainian
'zh', # Chinese
]
class MultiLanguageForm(Form):
class Meta:
# Try user's preferred language, fallback to English
locales = ['de', 'en'] # German with English fallback
email = StringField('E-Mail', [validators.Email()])
password = StringField('Passwort', [validators.DataRequired()])
# Validation messages will be in German
form = MultiLanguageForm(formdata={'email': 'invalid'})
form.validate()
print(form.email.errors) # ['Ungültige E-Mail-Adresse.']class CustomMessageForm(Form):
username = StringField('Username', [
validators.DataRequired(message='El nombre de usuario es obligatorio'),
validators.Length(min=3, message='Mínimo 3 caracteres')
])
email = StringField('Email', [
validators.Email(message='Formato de email inválido')
])
def validate_username(self, field):
if User.exists(field.data):
raise ValidationError('Este nombre de usuario ya existe')
# Custom messages override translation system
form = CustomMessageForm(formdata={'username': 'ab'})
form.validate()
print(form.username.errors) # ['Mínimo 3 caracteres']from wtforms.validators import ValidationError
class TranslatedValidator:
"""Custom validator with translatable messages."""
def __init__(self, message_key='custom_validation_error'):
self.message_key = message_key
def __call__(self, form, field):
if not self.validate_logic(field.data):
# Get translated message using form's translation system
message = field.gettext(self.message_key)
raise ValidationError(message)
def validate_logic(self, value):
# Your validation logic here
return len(value) > 5
class TranslatedForm(Form):
class Meta:
locales = ['es', 'en']
data = StringField('Data', [TranslatedValidator()])
# Validation messages will be translated
form = TranslatedForm(formdata={'data': 'short'})<!-- Jinja2 template with translated labels -->
<form method="POST">
{{ form.csrf_token }}
<div class="form-group">
{{ form.username.label(class="form-label") }}
{{ form.username(class="form-control") }}
{% for error in form.username.errors %}
<div class="error">{{ error }}</div>
{% endfor %}
</div>
<div class="form-group">
{{ form.email.label(class="form-label") }}
{{ form.email(class="form-control") }}
{% for error in form.email.errors %}
<div class="error">{{ error }}</div>
{% endfor %}
</div>
<button type="submit">{{ _('Submit') }}</button>
</form># Create message catalog for your application
# 1. Extract messages from Python files
pybabel extract -F babel.cfg -k gettext -k ngettext -o messages.pot .
# 2. Initialize Spanish translation
pybabel init -i messages.pot -d translations -l es
# 3. Update existing translations
pybabel update -i messages.pot -d translations
# 4. Compile translations
pybabel compile -d translationsExample babel.cfg:
[python: **.py]
[jinja2: **/templates/**.html]
extensions=jinja2.ext.autoescape,jinja2.ext.with_from contextlib import contextmanager
from flask_babel import get_locale
@contextmanager
def form_language(language_code):
"""Context manager for temporary language switching."""
original_locale = get_locale()
try:
# Set temporary locale
set_locale(language_code)
yield
finally:
# Restore original locale
set_locale(original_locale)
class FlexibleForm(Form):
message = StringField('Message', [validators.DataRequired()])
# Use form with specific language
with form_language('fr'):
form = FlexibleForm(formdata=request.form)
if not form.validate():
# Error messages in French
errors = form.errorsimport os
import gettext
from wtforms.i18n import DefaultTranslations, DummyTranslations
class AdvancedI18nForm(Form):
class Meta:
@staticmethod
def get_translations(form):
"""Advanced translation configuration."""
# Get language from various sources
language = (
getattr(form, '_language', None) or # Form-specific
os.environ.get('FORM_LANGUAGE') or # Environment
request.headers.get('Accept-Language', '')[:2] or # Browser
'en' # Default
)
# Try multiple translation sources
translation_sources = [
('myapp', '/opt/myapp/translations'), # App-specific
('wtforms', get_wtforms_locale_path()), # WTForms built-in
]
for domain, locale_dir in translation_sources:
try:
trans = gettext.translation(
domain, locale_dir,
languages=[language, 'en'],
fallback=True
)
return DefaultTranslations(trans)
except IOError:
continue
# Final fallback
return DummyTranslations()
name = StringField('Name', [validators.DataRequired()])
# Set language per form instance
form = AdvancedI18nForm(formdata=request.form)
form._language = 'de' # Germanfrom wtforms.validators import ValidationError
class ItemCountValidator:
"""Validator demonstrating pluralization."""
def __init__(self, min_items=1):
self.min_items = min_items
def __call__(self, form, field):
items = field.data or []
count = len(items)
if count < self.min_items:
# Use ngettext for proper pluralization
message = field.ngettext(
'You must select at least %(num)d item.',
'You must select at least %(num)d items.',
self.min_items
) % {'num': self.min_items}
raise ValidationError(message)
class ItemSelectionForm(Form):
class Meta:
locales = ['en', 'de', 'fr']
items = SelectMultipleField('Select Items', [
ItemCountValidator(min_items=2)
])
# Error messages will be properly pluralized in selected language
form = ItemSelectionForm(formdata={'items': ['one']})
form.validate()
# English: "You must select at least 2 items."
# German: "Sie müssen mindestens 2 Elemente auswählen."from flask import Flask
from flask_babel import Babel, get_locale
from wtforms.i18n import get_translations
app = Flask(__name__)
babel = Babel(app)
class FlaskBabelForm(Form):
class Meta:
def get_translations(self, form):
return get_translations([str(get_locale())])
message = StringField('Message', [validators.DataRequired()])
@babel.localeselector
def get_locale():
return request.args.get('lang') or 'en'from django.utils.translation import get_language
from django.utils.translation import gettext_lazy as _
class DjangoI18nForm(Form):
class Meta:
def get_translations(self, form):
from wtforms.i18n import get_translations
return get_translations([get_language()])
# Use Django's lazy translation for labels
message = StringField(_('Message'), [validators.DataRequired()])This comprehensive internationalization system allows WTForms to be used effectively in multilingual applications across different web frameworks and deployment scenarios.
Install with Tessl CLI
npx tessl i tessl/pypi-wtforms