Repository: flask-admin/flask-admin

Issue:
Validation errors from wtforms are not translated.
Although flask-admin and its strings are properly localized, form validation errors from wtforms appear untranslated.



Discussion:
I tried using lazy_gettext, but that didn't work.

These links might help in figuring out the fix:
https://github.com/lepture/flask-wtf/blob/master/flask_wtf/i18n.py
https://github.com/lepture/flask-wtf/blob/8879e9827b0160776031bd06ef083b81282904e5/flask_wtf/form.py#L168
https://gist.github.com/satiani/7490106

I tried something similar to the solution here: https://gist.github.com/satiani/7490106

But, the problem with that solution is that it will no longer use WTForms's translations. So, non-custom validation errors like "This field is required." will show as english.

I think the easiest workaround for this will be using flask-wtf and overriding the base form in the modelview.

The permanent fix will probably be similar to this:
- https://github.com/lepture/flask-wtf/blob/8879e9827b0160776031bd06ef083b81282904e5/flask_wtf/form.py#L168
- https://github.com/lepture/flask-wtf/blob/master/flask_wtf/i18n.py


Resolving patch:
diff --git a/flask_admin/babel.py b/flask_admin/babel.py
--- a/flask_admin/babel.py
+++ b/flask_admin/babel.py
@@ -6,11 +6,19 @@ def gettext(string, **variables):
         return string % variables
 
     def ngettext(singular, plural, num, **variables):
+        variables.setdefault('num', num)
         return (singular if num == 1 else plural) % variables
 
     def lazy_gettext(string, **variables):
         return gettext(string, **variables)
 
+    class Translations(object):
+        ''' dummy Translations class for WTForms, no translation support '''
+        def gettext(self, string):
+            return gettext(string)
+
+        def ngettext(self, singular, plural, n):
+            return ngettext(singular, plural, n)
 else:
     from flask_admin import translations
 
@@ -34,5 +42,21 @@ def get_translations_path(self, ctx):
     ngettext = domain.ngettext
     lazy_gettext = domain.lazy_gettext
 
+    try:
+        from wtforms.i18n import messages_path
+    except ImportError:
+        from wtforms.ext.i18n.utils import messages_path
+
+    wtforms_domain = Domain(messages_path(), domain='wtforms')
+
+    class Translations(object):
+        ''' Fixes WTForms translation support and uses wtforms translations '''
+        def gettext(self, string):
+            return wtforms_domain.gettext(string)
+
+        def ngettext(self, singular, plural, n):
+            return wtforms_domain.ngettext(singular, plural, n)
+
+
 # lazy imports
 from .helpers import get_current_view
diff --git a/flask_admin/contrib/sqla/validators.py b/flask_admin/contrib/sqla/validators.py
--- a/flask_admin/contrib/sqla/validators.py
+++ b/flask_admin/contrib/sqla/validators.py
@@ -58,10 +58,10 @@ def __call__(self, form, field):
         if len(field.data) < self.min:
             if self.message is None:
                 message = field.ngettext(
-                    u"At least %d item is required",
-                    u"At least %d items are required",
+                    u"At least %(num)d item is required",
+                    u"At least %(num)d items are required",
                     self.min
-                ) % (self.min,)
+                )
             else:
                 message = self.message
 
diff --git a/flask_admin/form/__init__.py b/flask_admin/form/__init__.py
--- a/flask_admin/form/__init__.py
+++ b/flask_admin/form/__init__.py
@@ -1,5 +1,6 @@
 from wtforms import form, __version__ as wtforms_version
 from wtforms.fields.core import UnboundField
+from flask_admin.babel import Translations
 
 from .fields import *
 from .widgets import *
@@ -7,11 +8,16 @@
 
 
 class BaseForm(form.Form):
+    _translations = Translations()
+
     def __init__(self, formdata=None, obj=None, prefix=u'', **kwargs):
         self._obj = obj
 
         super(BaseForm, self).__init__(formdata=formdata, obj=obj, prefix=prefix, **kwargs)
 
+    def _get_translations(self):
+        return self._translations
+
 
 class FormOpts(object):
     __slots__ = ['widget_args', 'form_rules']


Test patch: