diff --git a/cornice/__init__.py b/cornice/__init__.py index 26959eb3..b83caad1 100644 --- a/cornice/__init__.py +++ b/cornice/__init__.py @@ -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 @@ -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 @@ -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 = {} @@ -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) diff --git a/cornice/schemas.py b/cornice/schemas.py index 5dcc042b..b42d0974 100644 --- a/cornice/schemas.py +++ b/cornice/schemas.py @@ -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: diff --git a/cornice/tests/test_validation.py b/cornice/tests/test_validation.py index 9a5a12d0..ce76af57 100644 --- a/cornice/tests/test_validation.py +++ b/cornice/tests/test_validation.py @@ -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/. @@ -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') diff --git a/cornice/tests/validationapp.py b/cornice/tests/validationapp.py index 732e1e2a..c1379040 100644 --- a/cornice/tests/validationapp.py +++ b/cornice/tests/validationapp.py @@ -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()) diff --git a/docs/source/validation.rst b/docs/source/validation.rst index fe8454b2..5230e81d 100644 --- a/docs/source/validation.rst +++ b/docs/source/validation.rst @@ -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 `_ in your settings. +You may also set `pyramid.default_locale_name `_. + Using formencode ~~~~~~~~~~~~~~~~