From 1d988b4c225d81a4a105fadc4cce3f1134ad2b77 Mon Sep 17 00:00:00 2001 From: "Luis A. Garcia" Date: Tue, 30 Jul 2013 18:52:05 +0000 Subject: [PATCH] Enable localizable REST API responses via the Accept-Language header Add support for doing language resolution for a request, based on the Accept-Language HTTP header. Using the lazy gettext functionality, from oslo gettextutils, it is possible to use the resolved language to translate exception messages to the user requested language and return that translation from the API. Partially implements bp user-locale-api. Change-Id: I63edc8463836bfff257daa8a2c66ed5d3a444254 --- neutron/api/v2/resource.py | 27 +++++++++ neutron/server/__init__.py | 3 + neutron/tests/unit/test_api_v2_resource.py | 64 ++++++++++++++++++++++ neutron/wsgi.py | 7 +++ 4 files changed, 101 insertions(+) diff --git a/neutron/api/v2/resource.py b/neutron/api/v2/resource.py index 744a7d940d..529f519edb 100644 --- a/neutron/api/v2/resource.py +++ b/neutron/api/v2/resource.py @@ -23,6 +23,7 @@ import webob.exc from neutron.api.v2 import attributes from neutron.common import exceptions +from neutron.openstack.common import gettextutils from neutron.openstack.common import log as logging from neutron import wsgi @@ -70,6 +71,7 @@ def Resource(controller, faults=None, deserializers=None, serializers=None): action = args.pop('action', None) content_type = format_types.get(fmt, request.best_match_content_type()) + language = request.best_match_language() deserializer = deserializers.get(content_type) serializer = serializers.get(content_type) @@ -83,6 +85,7 @@ def Resource(controller, faults=None, deserializers=None, serializers=None): except (exceptions.NeutronException, netaddr.AddrFormatError) as e: LOG.exception(_('%s failed'), action) + e = translate(e, language) body = serializer.serialize({'NeutronError': e}) kwargs = {'body': body, 'content_type': content_type} for fault in faults: @@ -91,10 +94,12 @@ def Resource(controller, faults=None, deserializers=None, serializers=None): raise webob.exc.HTTPInternalServerError(**kwargs) except webob.exc.HTTPException as e: LOG.exception(_('%s failed'), action) + translate(e, language) e.body = serializer.serialize({'NeutronError': e}) e.content_type = content_type raise except NotImplementedError as e: + e = translate(e, language) # NOTE(armando-migliaccio): from a client standpoint # it makes sense to receive these errors, because # extensions may or may not be implemented by @@ -111,6 +116,7 @@ def Resource(controller, faults=None, deserializers=None, serializers=None): # Do not expose details of 500 error to clients. msg = _('Request Failed: internal server error while ' 'processing your request.') + msg = translate(msg, language) body = serializer.serialize({'NeutronError': msg}) kwargs = {'body': body, 'content_type': content_type} raise webob.exc.HTTPInternalServerError(**kwargs) @@ -126,3 +132,24 @@ def Resource(controller, faults=None, deserializers=None, serializers=None): content_type=content_type, body=body) return resource + + +def translate(translatable, locale): + """Translates the object to the given locale. + + If the object is an exception its translatable elements are translated + in place, if the object is a translatable string it is translated and + returned. Otherwise, the object is returned as-is. + + :param translatable: the object to be translated + :param locale: the locale to translate to + :returns: the translated object, or the object as-is if it + was not translated + """ + localize = gettextutils.get_localized_message + if isinstance(translatable, Exception): + translatable.message = localize(translatable.message, locale) + if isinstance(translatable, webob.exc.HTTPError): + translatable.detail = localize(translatable.detail, locale) + return translatable + return localize(translatable, locale) diff --git a/neutron/server/__init__.py b/neutron/server/__init__.py index 72a52b22e0..a31cdbe403 100755 --- a/neutron/server/__init__.py +++ b/neutron/server/__init__.py @@ -27,6 +27,9 @@ from oslo.config import cfg from neutron.common import config from neutron import service +from neutron.openstack.common import gettextutils +gettextutils.install('neutron', lazy=True) + def main(): eventlet.monkey_patch() diff --git a/neutron/tests/unit/test_api_v2_resource.py b/neutron/tests/unit/test_api_v2_resource.py index f4f7289ed9..91ac57117a 100644 --- a/neutron/tests/unit/test_api_v2_resource.py +++ b/neutron/tests/unit/test_api_v2_resource.py @@ -25,6 +25,7 @@ import webtest from neutron.api.v2 import resource as wsgi_resource from neutron.common import exceptions as q_exc from neutron import context +from neutron.openstack.common import gettextutils from neutron.tests import base from neutron import wsgi @@ -98,8 +99,23 @@ class RequestTestCase(base.BaseTestCase): def test_context_without_neutron_context(self): self.assertTrue(self.req.context.is_admin) + def test_best_match_language(self): + # Here we test that we are actually invoking language negotiation + # by webop and also that the default locale always available is en-US + request = wsgi.Request.blank('/') + gettextutils.get_available_languages = mock.MagicMock() + gettextutils.get_available_languages.return_value = ['known-language', + 'es', 'zh'] + request.headers['Accept-Language'] = 'known-language' + language = request.best_match_language() + self.assertEqual(language, 'known-language') + request.headers['Accept-Language'] = 'unknown-language' + language = request.best_match_language() + self.assertEqual(language, 'en_US') + class ResourceTestCase(base.BaseTestCase): + def test_unmapped_neutron_error_with_json(self): msg = u'\u7f51\u7edc' @@ -136,6 +152,29 @@ class ResourceTestCase(base.BaseTestCase): self.assertEqual(wsgi.XMLDeserializer().deserialize(res.body), expected_res) + @mock.patch('neutron.openstack.common.gettextutils.Message.data', + new_callable=mock.PropertyMock) + def test_unmapped_neutron_error_localized(self, mock_translation): + gettextutils.install('blaa', lazy=True) + msg_translation = 'Translated error' + mock_translation.return_value = msg_translation + msg = _('Unmapped error') + + class TestException(q_exc.NeutronException): + message = msg + + controller = mock.MagicMock() + controller.test.side_effect = TestException() + resource = webtest.TestApp(wsgi_resource.Resource(controller)) + + environ = {'wsgiorg.routing_args': (None, {'action': 'test', + 'format': 'json'})} + + res = resource.get('', extra_environ=environ, expect_errors=True) + self.assertEqual(res.status_int, exc.HTTPInternalServerError.code) + self.assertIn(msg_translation, + str(wsgi.JSONDeserializer().deserialize(res.body))) + def test_mapped_neutron_error_with_json(self): msg = u'\u7f51\u7edc' @@ -176,6 +215,31 @@ class ResourceTestCase(base.BaseTestCase): self.assertEqual(wsgi.XMLDeserializer().deserialize(res.body), expected_res) + @mock.patch('neutron.openstack.common.gettextutils.Message.data', + new_callable=mock.PropertyMock) + def test_mapped_neutron_error_localized(self, mock_translation): + gettextutils.install('blaa', lazy=True) + msg_translation = 'Translated error' + mock_translation.return_value = msg_translation + msg = _('Unmapped error') + + class TestException(q_exc.NeutronException): + message = msg + + controller = mock.MagicMock() + controller.test.side_effect = TestException() + faults = {TestException: exc.HTTPGatewayTimeout} + resource = webtest.TestApp(wsgi_resource.Resource(controller, + faults=faults)) + + environ = {'wsgiorg.routing_args': (None, {'action': 'test', + 'format': 'json'})} + + res = resource.get('', extra_environ=environ, expect_errors=True) + self.assertEqual(res.status_int, exc.HTTPGatewayTimeout.code) + self.assertIn(msg_translation, + str(wsgi.JSONDeserializer().deserialize(res.body))) + def test_http_error(self): controller = mock.MagicMock() controller.test.side_effect = exc.HTTPGatewayTimeout() diff --git a/neutron/wsgi.py b/neutron/wsgi.py index 56e909712f..029b7bdfe2 100644 --- a/neutron/wsgi.py +++ b/neutron/wsgi.py @@ -37,6 +37,7 @@ import webob.exc from neutron.common import constants from neutron.common import exceptions as exception from neutron import context +from neutron.openstack.common import gettextutils from neutron.openstack.common import jsonutils from neutron.openstack.common import log as logging @@ -299,6 +300,12 @@ class Request(webob.Request): return _type return None + def best_match_language(self): + """Determine language for returned response.""" + all_languages = gettextutils.get_available_languages('neutron') + return self.accept_language.best_match(all_languages, + default_match='en_US') + @property def context(self): if 'neutron.context' not in self.environ: