[validation error] refactor validation error handling so translation is done on the web side
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Mon, 17 Sep 2012 17:48:55 +0200
changeset 8556 bbe0d6985e59
parent 8555 c747242d22a6
child 8557 8756f932ca83
[validation error] refactor validation error handling so translation is done on the web side Users should now use cubicweb.validation_error helper function that will activate the feature with other handy behaviours. Also test testing for message in errors should call exception.tr(unicode) before comparing. Using bare ValidationError keep backward compat.
__init__.py
_exceptions.py
hooks/integrity.py
hooks/syncschema.py
hooks/syncsession.py
hooks/syncsources.py
hooks/test/unittest_hooks.py
hooks/test/unittest_syncsession.py
hooks/workflow.py
server/edition.py
server/test/unittest_undo.py
web/application.py
web/test/unittest_views_basecontrollers.py
web/views/basecontrollers.py
web/views/editcontroller.py
--- a/__init__.py	Tue Sep 11 22:32:01 2012 +0200
+++ b/__init__.py	Mon Sep 17 17:48:55 2012 +0200
@@ -199,3 +199,26 @@
         CW_EVENT_MANAGER.bind(event, func, *args, **kwargs)
         return func
     return _decorator
+
+
+from yams.schema import role_name as rname
+
+def validation_error(entity, errors, substitutions=None, i18nvalues=None):
+    """easy way to retrieve a :class:`cubicweb.ValidationError` for an entity or eid.
+
+    You may also have 2-tuple as error keys, :func:`yams.role_name` will be
+    called automatically for them.
+
+    Messages in errors **should not be translated yet**, though marked for
+    internationalization. You may give an additional substition dictionary that
+    will be used for interpolation after the translation.
+    """
+    if substitutions is None:
+        # set empty dict else translation won't be done for backward
+        # compatibility reason (see ValidationError.tr method)
+        substitutions = {}
+    for key in errors.keys():
+        if isinstance(key, tuple):
+            errors[rname(*key)] = errors.pop(key)
+    return ValidationError(getattr(entity, 'eid', entity), errors,
+                           substitutions, i18nvalues)
--- a/_exceptions.py	Tue Sep 11 22:32:01 2012 +0200
+++ b/_exceptions.py	Mon Sep 17 17:48:55 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -19,7 +19,7 @@
 
 __docformat__ = "restructuredtext en"
 
-from yams import ValidationError
+from yams import ValidationError as ValidationError
 
 # abstract exceptions #########################################################
 
--- a/hooks/integrity.py	Tue Sep 11 22:32:01 2012 +0200
+++ b/hooks/integrity.py	Mon Sep 17 17:48:55 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -20,12 +20,11 @@
 """
 
 __docformat__ = "restructuredtext en"
+_ = unicode
 
 from threading import Lock
 
-from yams.schema import role_name
-
-from cubicweb import ValidationError
+from cubicweb import validation_error
 from cubicweb.schema import (META_RTYPES, WORKFLOW_RTYPES,
                              RQLConstraint, RQLUniqueConstraint)
 from cubicweb.predicates import is_instance
@@ -87,11 +86,11 @@
                 continue
             if not session.execute(self.base_rql % rtype, {'x': eid}):
                 etype = session.describe(eid)[0]
-                _ = session._
                 msg = _('at least one relation %(rtype)s is required on '
                         '%(etype)s (%(eid)s)')
-                msg %= {'rtype': _(rtype), 'etype': _(etype), 'eid': eid}
-                raise ValidationError(eid, {role_name(rtype, self.role): msg})
+                raise validation_error(eid, {(rtype, self.role): msg},
+                                       {'rtype': rtype, 'etype': etype, 'eid': eid},
+                                       ['rtype', 'etype'])
 
 
 class _CheckSRelationOp(_CheckRequiredRelationOperation):
@@ -231,9 +230,9 @@
                 rql = '%s X WHERE X %s %%(val)s' % (entity.e_schema, attr)
                 rset = self._cw.execute(rql, {'val': val})
                 if rset and rset[0][0] != entity.eid:
-                    msg = self._cw._('the value "%s" is already used, use another one')
-                    qname = role_name(attr, 'subject')
-                    raise ValidationError(entity.eid, {qname: msg % val})
+                    msg = _('the value "%s" is already used, use another one')
+                    raise validation_error(entity, {(attr, 'subject'): msg},
+                                           (val,))
 
 
 class DontRemoveOwnersGroupHook(IntegrityHook):
@@ -246,15 +245,12 @@
     def __call__(self):
         entity = self.entity
         if self.event == 'before_delete_entity' and entity.name == 'owners':
-            msg = self._cw._('can\'t be deleted')
-            raise ValidationError(entity.eid, {None: msg})
+            raise validation_error(entity, {None: _("can't be deleted")})
         elif self.event == 'before_update_entity' \
                  and 'name' in entity.cw_edited:
             oldname, newname = entity.cw_edited.oldnewvalue('name')
             if oldname == 'owners' and newname != oldname:
-                qname = role_name('name', 'subject')
-                msg = self._cw._('can\'t be changed')
-                raise ValidationError(entity.eid, {qname: msg})
+                raise validation_error(entity, {('name', 'subject'): _("can't be changed")})
 
 
 class TidyHtmlFields(IntegrityHook):
--- a/hooks/syncschema.py	Tue Sep 11 22:32:01 2012 +0200
+++ b/hooks/syncschema.py	Mon Sep 17 17:48:55 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -24,6 +24,7 @@
 """
 
 __docformat__ = "restructuredtext en"
+_ = unicode
 
 from copy import copy
 from yams.schema import BASE_TYPES, RelationSchema, RelationDefinitionSchema
@@ -31,7 +32,7 @@
 
 from logilab.common.decorators import clear_cache
 
-from cubicweb import ValidationError
+from cubicweb import validation_error
 from cubicweb.predicates import is_instance
 from cubicweb.schema import (SCHEMA_TYPES, META_RTYPES, VIRTUAL_RTYPES,
                              CONSTRAINTS, ETYPE_NAME_MAP, display_name)
@@ -127,10 +128,9 @@
         if attr in ro_attrs:
             origval, newval = entity.cw_edited.oldnewvalue(attr)
             if newval != origval:
-                errors[attr] = session._("can't change the %s attribute") % \
-                               display_name(session, attr)
+                errors[attr] = _("can't change this attribute")
     if errors:
-        raise ValidationError(entity.eid, errors)
+        raise validation_error(entity, errors)
 
 
 class _MockEntity(object): # XXX use a named tuple with python 2.6
@@ -913,7 +913,7 @@
         # final entities can't be deleted, don't care about that
         name = self.entity.name
         if name in CORE_TYPES:
-            raise ValidationError(self.entity.eid, {None: self._cw._('can\'t be deleted')})
+            raise validation_error(self.entity, {None: _("can't be deleted")})
         # delete every entities of this type
         if name not in ETYPE_NAME_MAP:
             self._cw.execute('DELETE %s X' % name)
@@ -983,7 +983,7 @@
     def __call__(self):
         name = self.entity.name
         if name in CORE_TYPES:
-            raise ValidationError(self.entity.eid, {None: self._cw._('can\'t be deleted')})
+            raise validation_error(self.entity, {None: _("can't be deleted")})
         # delete relation definitions using this relation type
         self._cw.execute('DELETE CWAttribute X WHERE X relation_type Y, Y eid %(x)s',
                         {'x': self.entity.eid})
--- a/hooks/syncsession.py	Tue Sep 11 22:32:01 2012 +0200
+++ b/hooks/syncsession.py	Mon Sep 17 17:48:55 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -18,9 +18,9 @@
 """Core hooks: synchronize living session on persistent data changes"""
 
 __docformat__ = "restructuredtext en"
+_ = unicode
 
-from yams.schema import role_name
-from cubicweb import UnknownProperty, ValidationError, BadConnectionId
+from cubicweb import UnknownProperty, BadConnectionId, validation_error
 from cubicweb.predicates import is_instance
 from cubicweb.server import hook
 
@@ -165,13 +165,11 @@
         try:
             value = session.vreg.typed_value(key, value)
         except UnknownProperty:
-            qname = role_name('pkey', 'subject')
-            msg = session._('unknown property key %s') % key
-            raise ValidationError(self.entity.eid, {qname: msg})
+            msg = _('unknown property key %s')
+            raise validation_error(self.entity, {('pkey', 'subject'): msg}, (key,))
         except ValueError, ex:
-            qname = role_name('value', 'subject')
-            raise ValidationError(self.entity.eid,
-                                  {qname: session._(str(ex))})
+            raise validation_error(self.entity,
+                                  {('value', 'subject'): str(ex)})
         if not session.user.matching_groups('managers'):
             session.add_relation(self.entity.eid, 'for_user', session.user.eid)
         else:
@@ -196,8 +194,7 @@
         except UnknownProperty:
             return
         except ValueError, ex:
-            qname = role_name('value', 'subject')
-            raise ValidationError(entity.eid, {qname: session._(str(ex))})
+            raise validation_error(entity, {('value', 'subject'): str(ex)})
         if entity.for_user:
             for session_ in get_user_sessions(session.repo, entity.for_user[0].eid):
                 _ChangeCWPropertyOp(session, cwpropdict=session_.user.properties,
@@ -237,10 +234,8 @@
         key, value = session.execute('Any K,V WHERE P eid %(x)s,P pkey K,P value V',
                                      {'x': eidfrom})[0]
         if session.vreg.property_info(key)['sitewide']:
-            qname = role_name('for_user', 'subject')
-            msg = session._("site-wide property can't be set for user")
-            raise ValidationError(eidfrom,
-                                  {qname: msg})
+            msg = _("site-wide property can't be set for user")
+            raise validation_error(eidfrom, {('for_user', 'subject'): msg})
         for session_ in get_user_sessions(session.repo, self.eidto):
             _ChangeCWPropertyOp(session, cwpropdict=session_.user.properties,
                               key=key, value=value)
--- a/hooks/syncsources.py	Tue Sep 11 22:32:01 2012 +0200
+++ b/hooks/syncsources.py	Mon Sep 17 17:48:55 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2010-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2010-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -17,12 +17,13 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """hooks for repository sources synchronization"""
 
+_ = unicode
+
 from socket import gethostname
 
 from logilab.common.decorators import clear_cache
-from yams.schema import role_name
 
-from cubicweb import ValidationError
+from cubicweb import validation_error
 from cubicweb.predicates import is_instance
 from cubicweb.server import SOURCE_TYPES, hook
 
@@ -46,9 +47,8 @@
         try:
             sourcecls = SOURCE_TYPES[self.entity.type]
         except KeyError:
-            msg = self._cw._('unknown source type')
-            raise ValidationError(self.entity.eid,
-                                  {role_name('type', 'subject'): msg})
+            msg = _('unknown source type')
+            raise validation_error(self.entity, {('type', 'subject'): msg})
         sourcecls.check_conf_dict(self.entity.eid, self.entity.host_config,
                                   fail_if_unknown=not self._cw.vreg.config.repairing)
         SourceAddedOp(self._cw, entity=self.entity)
@@ -65,7 +65,8 @@
     events = ('before_delete_entity',)
     def __call__(self):
         if self.entity.name == 'system':
-            raise ValidationError(self.entity.eid, {None: 'cant remove system source'})
+            msg = _("You cannot remove the system source")
+            raise validation_error(self.entity, {None: msg})
         SourceRemovedOp(self._cw, uri=self.entity.name)
 
 
@@ -154,8 +155,8 @@
     events = ('before_add_relation',)
     def __call__(self):
         if not self._cw.added_in_transaction(self.eidfrom):
-            msg = self._cw._("can't change this relation")
-            raise ValidationError(self.eidfrom, {self.rtype: msg})
+            msg = _("You can't change this relation")
+            raise validation_error(self.eidfrom, {self.rtype: msg})
 
 
 class SourceMappingChangedOp(hook.DataOperationMixIn, hook.Operation):
--- a/hooks/test/unittest_hooks.py	Tue Sep 11 22:32:01 2012 +0200
+++ b/hooks/test/unittest_hooks.py	Mon Sep 17 17:48:55 2012 +0200
@@ -170,6 +170,7 @@
         try:
             self.execute('INSERT CWUser X: X login "admin"')
         except ValidationError, ex:
+            ex.tr(unicode)
             self.assertIsInstance(ex.entity, int)
             self.assertEqual(ex.errors, {'login-subject': 'the value "admin" is already used, use another one'})
 
--- a/hooks/test/unittest_syncsession.py	Tue Sep 11 22:32:01 2012 +0200
+++ b/hooks/test/unittest_syncsession.py	Mon Sep 17 17:48:55 2012 +0200
@@ -31,9 +31,11 @@
     def test_unexistant_cwproperty(self):
         with self.assertRaises(ValidationError) as cm:
             self.execute('INSERT CWProperty X: X pkey "bla.bla", X value "hop", X for_user U')
+        cm.exception.tr(unicode)
         self.assertEqual(cm.exception.errors, {'pkey-subject': 'unknown property key bla.bla'})
         with self.assertRaises(ValidationError) as cm:
             self.execute('INSERT CWProperty X: X pkey "bla.bla", X value "hop"')
+        cm.exception.tr(unicode)
         self.assertEqual(cm.exception.errors, {'pkey-subject': 'unknown property key bla.bla'})
 
     def test_site_wide_cwproperty(self):
--- a/hooks/workflow.py	Tue Sep 11 22:32:01 2012 +0200
+++ b/hooks/workflow.py	Mon Sep 17 17:48:55 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -18,12 +18,12 @@
 """Core hooks: workflow related hooks"""
 
 __docformat__ = "restructuredtext en"
+_ = unicode
 
 from datetime import datetime
 
-from yams.schema import role_name
 
-from cubicweb import RepositoryError, ValidationError
+from cubicweb import RepositoryError, validation_error
 from cubicweb.predicates import is_instance, adaptable
 from cubicweb.server import hook
 
@@ -92,9 +92,8 @@
         if mainwf.eid == self.wfeid:
             deststate = mainwf.initial
             if not deststate:
-                qname = role_name('custom_workflow', 'subject')
-                msg = session._('workflow has no initial state')
-                raise ValidationError(entity.eid, {qname: msg})
+                msg = _('workflow has no initial state')
+                raise validation_error(entity, {('custom_workflow', 'subject'): msg})
             if mainwf.state_by_eid(iworkflowable.current_state.eid):
                 # nothing to do
                 return
@@ -119,9 +118,8 @@
         outputs = set()
         for ep in tr.subworkflow_exit:
             if ep.subwf_state.eid in outputs:
-                qname = role_name('subworkflow_exit', 'subject')
-                msg = self.session._("can't have multiple exits on the same state")
-                raise ValidationError(self.treid, {qname: msg})
+                msg = _("can't have multiple exits on the same state")
+                raise validation_error(self.treid, {('subworkflow_exit', 'subject'): msg})
             outputs.add(ep.subwf_state.eid)
 
 
@@ -137,13 +135,12 @@
         wftr = iworkflowable.subworkflow_input_transition()
         if wftr is None:
             # inconsistency detected
-            qname = role_name('to_state', 'subject')
-            msg = session._("state doesn't belong to entity's current workflow")
-            raise ValidationError(self.trinfo.eid, {'to_state': msg})
+            msg = _("state doesn't belong to entity's current workflow")
+            raise validation_error(self.trinfo, {('to_state', 'subject'): msg})
         tostate = wftr.get_exit_point(forentity, trinfo.cw_attr_cache['to_state'])
         if tostate is not None:
             # reached an exit point
-            msg = session._('exiting from subworkflow %s')
+            msg = _('exiting from subworkflow %s')
             msg %= session._(iworkflowable.current_workflow.name)
             session.transaction_data[(forentity.eid, 'subwfentrytr')] = True
             iworkflowable.change_state(tostate, msg, u'text/plain', tr=wftr)
@@ -186,9 +183,8 @@
         try:
             foreid = entity.cw_attr_cache['wf_info_for']
         except KeyError:
-            qname = role_name('wf_info_for', 'subject')
-            msg = session._('mandatory relation')
-            raise ValidationError(entity.eid, {qname: msg})
+            msg = _('mandatory relation')
+            raise validation_error(entity, {('wf_info_for', 'subject'): msg})
         forentity = session.entity_from_eid(foreid)
         # see comment in the TrInfo entity definition
         entity.cw_edited['tr_count']=len(forentity.reverse_wf_info_for)
@@ -201,13 +197,13 @@
         else:
             wf = iworkflowable.current_workflow
         if wf is None:
-            msg = session._('related entity has no workflow set')
-            raise ValidationError(entity.eid, {None: msg})
+            msg = _('related entity has no workflow set')
+            raise validation_error(entity, {None: msg})
         # then check it has a state set
         fromstate = iworkflowable.current_state
         if fromstate is None:
-            msg = session._('related entity has no state')
-            raise ValidationError(entity.eid, {None: msg})
+            msg = _('related entity has no state')
+            raise validation_error(entity, {None: msg})
         # True if we are coming back from subworkflow
         swtr = session.transaction_data.pop((forentity.eid, 'subwfentrytr'), None)
         cowpowers = (session.user.is_in_group('managers')
@@ -219,47 +215,42 @@
             # no transition set, check user is a manager and destination state
             # is specified (and valid)
             if not cowpowers:
-                qname = role_name('by_transition', 'subject')
-                msg = session._('mandatory relation')
-                raise ValidationError(entity.eid, {qname: msg})
+                msg = _('mandatory relation')
+                raise validation_error(entity, {('by_transition', 'subject'): msg})
             deststateeid = entity.cw_attr_cache.get('to_state')
             if not deststateeid:
-                qname = role_name('by_transition', 'subject')
-                msg = session._('mandatory relation')
-                raise ValidationError(entity.eid, {qname: msg})
+                msg = _('mandatory relation')
+                raise validation_error(entity, {('by_transition', 'subject'): msg})
             deststate = wf.state_by_eid(deststateeid)
             if deststate is None:
-                qname = role_name('to_state', 'subject')
-                msg = session._("state doesn't belong to entity's workflow")
-                raise ValidationError(entity.eid, {qname: msg})
+                msg = _("state doesn't belong to entity's workflow")
+                raise validation_error(entity, {('to_state', 'subject'): msg})
         else:
             # check transition is valid and allowed, unless we're coming back
             # from subworkflow
             tr = session.entity_from_eid(treid)
             if swtr is None:
-                qname = role_name('by_transition', 'subject')
+                qname = ('by_transition', 'subject')
                 if tr is None:
-                    msg = session._("transition doesn't belong to entity's workflow")
-                    raise ValidationError(entity.eid, {qname: msg})
+                    msg = _("transition doesn't belong to entity's workflow")
+                    raise validation_error(entity, {qname: msg})
                 if not tr.has_input_state(fromstate):
-                    msg = session._("transition %(tr)s isn't allowed from %(st)s") % {
-                        'tr': session._(tr.name), 'st': session._(fromstate.name)}
-                    raise ValidationError(entity.eid, {qname: msg})
+                    msg = _("transition %(tr)s isn't allowed from %(st)s")
+                    raise validation_error(entity, {qname: msg}, {
+                            'tr': tr.name, 'st': fromstate.name}, ['tr', 'st'])
                 if not tr.may_be_fired(foreid):
-                    msg = session._("transition may not be fired")
-                    raise ValidationError(entity.eid, {qname: msg})
+                    msg = _("transition may not be fired")
+                    raise validation_error(entity, {qname: msg})
             deststateeid = entity.cw_attr_cache.get('to_state')
             if deststateeid is not None:
                 if not cowpowers and deststateeid != tr.destination(forentity).eid:
-                    qname = role_name('by_transition', 'subject')
-                    msg = session._("transition isn't allowed")
-                    raise ValidationError(entity.eid, {qname: msg})
+                    msg = _("transition isn't allowed")
+                    raise validation_error(entity, {('by_transition', 'subject'): msg})
                 if swtr is None:
                     deststate = session.entity_from_eid(deststateeid)
                     if not cowpowers and deststate is None:
-                        qname = role_name('to_state', 'subject')
-                        msg = session._("state doesn't belong to entity's workflow")
-                        raise ValidationError(entity.eid, {qname: msg})
+                        msg = _("state doesn't belong to entity's workflow")
+                        raise validation_error(entity, {('to_state', 'subject'): msg})
             else:
                 deststateeid = tr.destination(forentity).eid
         # everything is ok, add missing information on the trinfo entity
@@ -307,20 +298,18 @@
         iworkflowable = entity.cw_adapt_to('IWorkflowable')
         mainwf = iworkflowable.main_workflow
         if mainwf is None:
-            msg = session._('entity has no workflow set')
-            raise ValidationError(entity.eid, {None: msg})
+            msg = _('entity has no workflow set')
+            raise validation_error(entity, {None: msg})
         for wf in mainwf.iter_workflows():
             if wf.state_by_eid(self.eidto):
                 break
         else:
-            qname = role_name('in_state', 'subject')
-            msg = session._("state doesn't belong to entity's workflow. You may "
-                            "want to set a custom workflow for this entity first.")
-            raise ValidationError(self.eidfrom, {qname: msg})
+            msg = _("state doesn't belong to entity's workflow. You may "
+                    "want to set a custom workflow for this entity first.")
+            raise validation_error(self.eidfrom, {('in_state', 'subject'): msg})
         if iworkflowable.current_workflow and wf.eid != iworkflowable.current_workflow.eid:
-            qname = role_name('in_state', 'subject')
-            msg = session._("state doesn't belong to entity's current workflow")
-            raise ValidationError(self.eidfrom, {qname: msg})
+            msg = _("state doesn't belong to entity's current workflow")
+            raise validation_error(self.eidfrom, {('in_state', 'subject'): msg})
 
 
 class SetModificationDateOnStateChange(WorkflowHook):
--- a/server/edition.py	Tue Sep 11 22:32:01 2012 +0200
+++ b/server/edition.py	Mon Sep 17 17:48:55 2012 +0200
@@ -143,8 +143,7 @@
                          for rtype in self]
         try:
             entity.e_schema.check(dict_protocol_catcher(entity),
-                                  creation=creation, _=entity._cw._,
-                                  relations=relations)
+                                  creation=creation, relations=relations)
         except ValidationError, ex:
             ex.entity = self.entity
             raise
--- a/server/test/unittest_undo.py	Tue Sep 11 22:32:01 2012 +0200
+++ b/server/test/unittest_undo.py	Mon Sep 17 17:48:55 2012 +0200
@@ -228,6 +228,7 @@
             "%s doesn't exist anymore." % g.eid])
         with self.assertRaises(ValidationError) as cm:
             self.commit()
+        cm.exception.tr(unicode)
         self.assertEqual(cm.exception.entity, self.toto.eid)
         self.assertEqual(cm.exception.errors,
                           {'in_group-subject': u'at least one relation in_group is '
--- a/web/application.py	Tue Sep 11 22:32:01 2012 +0200
+++ b/web/application.py	Mon Sep 17 17:48:55 2012 +0200
@@ -511,7 +511,7 @@
         return ''
 
     def validation_error_handler(self, req, ex):
-        ex.errors = dict((k, v) for k, v in ex.errors.items())
+        ex.tr(req._) # translate messages using ui language
         if '__errorurl' in req.form:
             forminfo = {'error': ex,
                         'values': req.form,
--- a/web/test/unittest_views_basecontrollers.py	Tue Sep 11 22:32:01 2012 +0200
+++ b/web/test/unittest_views_basecontrollers.py	Mon Sep 17 17:48:55 2012 +0200
@@ -77,6 +77,7 @@
                     }
         with self.assertRaises(ValidationError) as cm:
             self.ctrl_publish(req)
+        cm.exception.tr(unicode)
         self.assertEqual(cm.exception.errors, {'login-subject': 'the value "admin" is already used, use another one'})
 
     def test_user_editing_itself(self):
@@ -249,6 +250,7 @@
                 }
         with self.assertRaises(ValidationError) as cm:
             self.ctrl_publish(req)
+        cm.exception.tr(unicode)
         self.assertEqual(cm.exception.errors, {'amount-subject': 'value -10 must be >= 0'})
         req = self.request(rollbackfirst=True)
         req.form = {'eid': ['X'],
@@ -259,6 +261,7 @@
                     }
         with self.assertRaises(ValidationError) as cm:
             self.ctrl_publish(req)
+        cm.exception.tr(unicode)
         self.assertEqual(cm.exception.errors, {'amount-subject': 'value 110 must be <= 100'})
         req = self.request(rollbackfirst=True)
         req.form = {'eid': ['X'],
--- a/web/views/basecontrollers.py	Tue Sep 11 22:32:01 2012 +0200
+++ b/web/views/basecontrollers.py	Mon Sep 17 17:48:55 2012 +0200
@@ -190,6 +190,7 @@
 
 def _validation_error(req, ex):
     req.cnx.rollback()
+    ex.tr(req._) # translate messages using ui language
     # XXX necessary to remove existant validation error?
     # imo (syt), it's not necessary
     req.session.data.pop(req.form.get('__errorurl'), None)
--- a/web/views/editcontroller.py	Tue Sep 11 22:32:01 2012 +0200
+++ b/web/views/editcontroller.py	Mon Sep 17 17:48:55 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.