From 2086a91059f022344f74143ffb2ce9d0c2d6691c Mon Sep 17 00:00:00 2001 From: "Luis A. Garcia" Date: Mon, 8 Jul 2013 23:11:05 +0000 Subject: [PATCH] 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 --- bin/cinder-all | 2 +- bin/cinder-api | 3 +-- cinder/api/openstack/wsgi.py | 27 ++++++++++++++++++---- cinder/tests/api/middleware/test_faults.py | 23 +++++++++++++++++- cinder/tests/api/openstack/test_wsgi.py | 15 ++++++++++++ cinder/utils.py | 3 +++ cinder/wsgi.py | 3 ++- 7 files changed, 66 insertions(+), 10 deletions(-) diff --git a/bin/cinder-all b/bin/cinder-all index 8e19ab5f4..33ae0ea84 100755 --- a/bin/cinder-all +++ b/bin/cinder-all @@ -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 diff --git a/bin/cinder-api b/bin/cinder-api index 7005d24ce..7c296c6bf 100755 --- a/bin/cinder-api +++ b/bin/cinder-api @@ -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 diff --git a/cinder/api/openstack/wsgi.py b/cinder/api/openstack/wsgi.py index a2101c196..89a4356ae 100644 --- a/cinder/api/openstack/wsgi.py +++ b/cinder/api/openstack/wsgi.py @@ -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, diff --git a/cinder/tests/api/middleware/test_faults.py b/cinder/tests/api/middleware/test_faults.py index aff1dfbcc..7be3b05fc 100644 --- a/cinder/tests/api/middleware/test_faults.py +++ b/cinder/tests/api/middleware/test_faults.py @@ -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?')) diff --git a/cinder/tests/api/openstack/test_wsgi.py b/cinder/tests/api/openstack/test_wsgi.py index 75e0fa215..0413a842e 100644 --- a/cinder/tests/api/openstack/test_wsgi.py +++ b/cinder/tests/api/openstack/test_wsgi.py @@ -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): diff --git a/cinder/utils.py b/cinder/utils.py index fa05e73e6..8942c273d 100644 --- a/cinder/utils.py +++ b/cinder/utils.py @@ -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: diff --git a/cinder/wsgi.py b/cinder/wsgi.py index 098c1d3a6..50e71038a 100644 --- a/cinder/wsgi.py +++ b/cinder/wsgi.py @@ -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): -- 2.45.2