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
This commit is contained in:
parent
44fdf47f53
commit
1d988b4c22
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user