]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Enable localizable REST API responses via the Accept-Language header
authorLuis A. Garcia <luis@linux.vnet.ibm.com>
Tue, 30 Jul 2013 18:52:05 +0000 (18:52 +0000)
committerLuis A. Garcia <luis@linux.vnet.ibm.com>
Wed, 7 Aug 2013 21:19:25 +0000 (21:19 +0000)
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
neutron/server/__init__.py
neutron/tests/unit/test_api_v2_resource.py
neutron/wsgi.py

index 744a7d940d2e7ef0a56b09f18a7a75b45a613654..529f519edb8304c42202275cfca18a8f251a0b0e 100644 (file)
@@ -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)
index 72a52b22e0917872928e12ca01e7b4ae2455681a..a31cdbe403ff68d8857b1f9b24b0209d77886a8c 100755 (executable)
@@ -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()
index f4f7289ed9f609de220c4b7d2e831c0eea043a6e..91ac57117a9e6da641d6c33577b745be98c1bf21 100644 (file)
@@ -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()
index 56e909712f1de719413843646fab15c5d291350e..029b7bdfe2f50a25d99fa962f3955af160b505c2 100644 (file)
@@ -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: