Add support for API message localization

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 now
possible to use the resolved language to translate an exception message
to the user requested language and return that translation from the API.

Partially implements bp user-locale-api

Change-Id: Ib2c8360372996d53b50542df54a52d92b07295ca
This commit is contained in:
Luis A. Garcia
2013-07-08 23:11:05 +00:00
parent 881c73263c
commit 2086a91059
7 changed files with 66 additions and 10 deletions

View File

@@ -44,7 +44,7 @@ if os.path.exists(os.path.join(possible_topdir, "cinder", "__init__.py")):
sys.path.insert(0, possible_topdir)
from cinder.openstack.common import gettextutils
gettextutils.install('cinder')
gettextutils.install('cinder', lazy=True)
from cinder.common import config # Need to register global_opts
from cinder.openstack.common import log as logging

View File

@@ -24,7 +24,6 @@
# eventlet is updated/released to fix the root issue
import eventlet
eventlet.monkey_patch()
import os
@@ -39,7 +38,7 @@ if os.path.exists(os.path.join(possible_topdir, "cinder", "__init__.py")):
sys.path.insert(0, possible_topdir)
from cinder.openstack.common import gettextutils
gettextutils.install('cinder')
gettextutils.install('cinder', lazy=True)
from cinder.common import config # Need to register global_opts
from cinder.openstack.common import log as logging

View File

@@ -1,6 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC.
# Copyright 2013 IBM Corp.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -21,6 +22,7 @@ import time
import webob
from cinder import exception
from cinder.openstack.common import gettextutils
from cinder.openstack.common import jsonutils
from cinder.openstack.common import log as logging
from cinder import utils
@@ -101,6 +103,12 @@ class Request(webob.Request):
return content_type
def best_match_language(self):
"""Determines best available locale from the Accept-Language header."""
all_languages = gettextutils.get_available_languages('cinder')
return self.accept_language.best_match(all_languages,
default_match='en_US')
class ActionDispatcher(object):
"""Maps method name to local methods through action name."""
@@ -1069,12 +1077,15 @@ class Fault(webob.exc.HTTPException):
def __call__(self, req):
"""Generate a WSGI response based on the exception passed to ctor."""
# Replace the body with fault details.
locale = req.best_match_language()
code = self.wrapped_exc.status_int
fault_name = self._fault_names.get(code, "computeFault")
explanation = self.wrapped_exc.explanation
fault_data = {
fault_name: {
'code': code,
'message': self.wrapped_exc.explanation}}
'message': gettextutils.get_localized_message(explanation,
locale)}}
if code == 413:
retry = self.wrapped_exc.headers['Retry-After']
fault_data[fault_name]['retryAfter'] = retry
@@ -1134,13 +1145,19 @@ class OverLimitFault(webob.exc.HTTPException):
@webob.dec.wsgify(RequestClass=Request)
def __call__(self, request):
"""
Return the wrapped exception with a serialized body conforming to our
error format.
"""
"""Serializes the wrapped exception conforming to our error format."""
content_type = request.best_match_content_type()
metadata = {"attributes": {"overLimitFault": "code"}}
def translate(msg):
locale = request.best_match_language()
return gettextutils.get_localized_message(msg, locale)
self.content['overLimitFault']['message'] = \
translate(self.content['overLimitFault']['message'])
self.content['overLimitFault']['details'] = \
translate(self.content['overLimitFault']['details'])
xml_serializer = XMLDictSerializer(metadata, XMLNS_V1)
serializer = {
'application/xml': xml_serializer,

View File

@@ -17,12 +17,12 @@
from xml.dom import minidom
import webob
import webob.dec
import webob.exc
from cinder.api import common
from cinder.api.openstack import wsgi
from cinder.openstack.common import gettextutils
from cinder.openstack.common import jsonutils
from cinder import test
@@ -109,6 +109,27 @@ class TestFaults(test.TestCase):
self.assertTrue('resizeNotAllowed' not in resp.body)
self.assertTrue('forbidden' in resp.body)
def test_raise_localized_explanation(self):
params = ('blah', )
expl = gettextutils.Message("String with params: %s" % params, 'test')
def _mock_translation(msg, locale):
return "Mensaje traducido"
self.stubs.Set(gettextutils,
"get_localized_message", _mock_translation)
@webob.dec.wsgify
def raiser(req):
raise wsgi.Fault(webob.exc.HTTPNotFound(explanation=expl))
req = webob.Request.blank('/.xml')
resp = req.get_response(raiser)
self.assertEqual(resp.content_type, "application/xml")
self.assertEqual(resp.status_int, 404)
self.assertTrue(("Mensaje traducido") in resp.body)
self.stubs.UnsetAll()
def test_fault_has_status_int(self):
"""Ensure the status_int is set correctly on faults"""
fault = wsgi.Fault(webob.exc.HTTPBadRequest(explanation='what?'))

View File

@@ -87,6 +87,21 @@ class RequestTest(test.TestCase):
result = request.best_match_content_type()
self.assertEqual(result, "application/json")
def test_best_match_language(self):
# Here we test that we are actually invoking language negotiation
# by webob and also that the default locale always available is en-US
request = wsgi.Request.blank('/')
accepted = 'unknown-lang'
request.headers = {'Accept-Language': accepted}
def fake_best_match(self, offers, default_match=None):
return default_match
self.stubs.SmartSet(request.accept_language,
'best_match', fake_best_match)
self.assertEqual(request.best_match_language(), 'en_US')
class ActionDispatcherTest(test.TestCase):
def test_dispatch(self):

View File

@@ -48,6 +48,7 @@ from oslo.config import cfg
from cinder import exception
from cinder.openstack.common import excutils
from cinder.openstack.common import gettextutils
from cinder.openstack.common import importutils
from cinder.openstack.common import lockutils
from cinder.openstack.common import log as logging
@@ -659,6 +660,8 @@ def utf8(value):
"""
if isinstance(value, unicode):
return value.encode('utf-8')
elif isinstance(value, gettextutils.Message):
return unicode(value).encode('utf-8')
elif isinstance(value, str):
return value
else:

View File

@@ -204,7 +204,8 @@ class Server(object):
backlog=backlog)
self._server = eventlet.spawn(self._start)
(self._host, self._port) = self._socket.getsockname()[0:2]
LOG.info(_("Started %(name)s on %(_host)s:%(_port)s") % self.__dict__)
LOG.info(_("Started %(name)s on %(host)s:%(port)s") %
{'name': self.name, 'host': self.host, 'port': self.port})
@property
def host(self):