Skip to content

Commit

Permalink
Merge pull request #206 from makinacorpus/colander-i18n
Browse files Browse the repository at this point in the history
Allow i18n of colander error messages
  • Loading branch information
Natim committed Jan 18, 2016
2 parents 03255df + ccbe88e commit 81b73c2
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 6 deletions.
53 changes: 50 additions & 3 deletions cornice/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
import logging
from functools import partial

from cornice import util
from cornice.errors import Errors # NOQA
Expand All @@ -14,7 +15,8 @@
register_resource_views,
)
from cornice.util import ContentTypePredicate

from pyramid.events import BeforeRender, NewRequest
from pyramid.i18n import get_localizer
from pyramid.httpexceptions import HTTPNotFound, HTTPForbidden
from pyramid.security import NO_PERMISSION_REQUIRED

Expand All @@ -33,11 +35,53 @@ def add_apidoc(config, pattern, func, service, **kwargs):
info['func'] = func


def set_localizer_for_languages(event, available_languages,
default_locale_name):
"""
Sets the current locale based on the incoming Accept-Language header, if
present, and sets a localizer attribute on the request object based on
the current locale.
To be used as an event handler, this function needs to be partially applied
with the available_languages and default_locale_name arguments. The
resulting function will be an event handler which takes an event object as
its only argument.
"""
request = event.request
if request.accept_language:
accepted = request.accept_language
locale = accepted.best_match(available_languages, default_locale_name)
request._LOCALE_ = locale
localizer = get_localizer(request)
request.localizer = localizer


def setup_localization(config):
"""
Setup localization based on the available_languages and
pyramid.default_locale_name settings.
These settings are named after suggestions from the "Internationalization
and Localization" section of the Pyramid documentation.
"""
try:
config.add_translation_dirs('colander:locale/')
settings = config.get_settings()
available_languages = settings['available_languages'].split()
default_locale_name = settings.get('pyramid.default_locale_name', 'en')
set_localizer = partial(set_localizer_for_languages,
available_languages=available_languages,
default_locale_name=default_locale_name)
config.add_subscriber(set_localizer, NewRequest)
except ImportError:
# add_translation_dirs raises an ImportError if colander is not
# installed
pass


def includeme(config):
"""Include the Cornice definitions
"""
from pyramid.events import BeforeRender, NewRequest

# attributes required to maintain services
config.registry.cornice_services = {}

Expand All @@ -61,3 +105,6 @@ def includeme(config):
permission=NO_PERMISSION_REQUIRED)
config.add_view(handle_exceptions, context=HTTPForbidden,
permission=NO_PERMISSION_REQUIRED)

if settings.get('available_languages'):
setup_localization(config)
6 changes: 4 additions & 2 deletions cornice/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,13 @@ def _validate_fields(location, data):
deserialized = attr.deserialize(serialized)
except Invalid as e:
# the struct is invalid
translate = request.localizer.translate
error_dict = e.asdict(translate=translate)
try:
request.errors.add(location, attr.name,
e.asdict()[attr.name])
error_dict[attr.name])
except KeyError:
for k, v in e.asdict().items():
for k, v in error_dict.items():
if k.startswith(attr.name):
request.errors.add(location, k, v)
else:
Expand Down
53 changes: 53 additions & 0 deletions cornice/tests/test_validation.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# -*- encoding: utf-8 -*-
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
Expand Down Expand Up @@ -365,3 +366,55 @@ def low_priority_deserializer(request):
"hello,open,yeah",
headers={'content-type': 'text/dummy'})
self.assertEqual(response.json['test'], 'succeeded')


class TestErrorMessageTranslation(TestCase):

def post(self, settings={}, headers={}):
app = TestApp(main({}, **settings))
return app.post_json('/foobar?yeah=test', {
'foo': 'hello',
'bar': 'open',
'yeah': 'man',
'ipsum': 10,
}, status=400, headers=headers)

def assertErrorDescription(self, response, message):
error_description = response.json['errors'][0]['description']
self.assertEqual(error_description, message)

def test_accept_language_header(self):
response = self.post(
settings={'available_languages': 'fr en'},
headers={'Accept-Language': 'fr'})
self.assertErrorDescription(
response,
u'10 est plus grand que la valeur maximum autorisée (3)')

def test_default_language(self):
response = self.post(settings={
'available_languages': 'fr ja',
'pyramid.default_locale_name': 'ja',
})
self.assertErrorDescription(
response,
u'10 は最大値 3 を超過しています')

def test_default_language_fallback(self):
"""Should fallback to default language if requested language is not
available"""
response = self.post(
settings={
'available_languages': 'ja en',
'pyramid.default_locale_name': 'ja',
},
headers={'Accept-Language': 'ru'})
self.assertErrorDescription(
response,
u'10 は最大値 3 を超過しています')

def test_no_language_settings(self):
response = self.post()
self.assertErrorDescription(
response,
u'10 is greater than maximum value 3')
2 changes: 1 addition & 1 deletion cornice/tests/validationapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,6 @@ def includeme(config):


def main(global_config, **settings):
config = Configurator(settings={})
config = Configurator(settings=settings)
config.include(includeme)
return CatchErrors(config.make_wsgi_app())
4 changes: 4 additions & 0 deletions docs/source/validation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,10 @@ before passing the result to Colander.
View-specific deserializers have priority over global content-type
deserializers.

To enable localization of Colander error messages, you must set
`available_languages <http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/i18n.html#detecting-available-languages>`_ in your settings.
You may also set `pyramid.default_locale_name <http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html#default-locale-name-setting>`_.


Using formencode
~~~~~~~~~~~~~~~~
Expand Down

0 comments on commit 81b73c2

Please sign in to comment.