Skip to content

Commit

Permalink
Sync gettextutils from oslo
Browse files Browse the repository at this point in the history
Some Messages, such as those created from Invalid exceptions, use a
Message within a Message, and we were only translating the base Message
but not the Message substitution within.

Also adds test case for cinder case.

Fixes bug: #1221808

Change-Id: Ic3119df23a090cfaa160c1461e955f0af55fe1cf
  • Loading branch information
Luis A. Garcia committed Sep 18, 2013
1 parent 0a339d4 commit afe2a21
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 15 deletions.
74 changes: 60 additions & 14 deletions cinder/openstack/common/gettextutils.py
Expand Up @@ -26,22 +26,41 @@

import copy
import gettext
import logging.handlers
import logging
import os
import re
import UserString
try:
import UserString as _userString
except ImportError:
import collections as _userString

from babel import localedata
import six

_localedir = os.environ.get('cinder'.upper() + '_LOCALEDIR')
_t = gettext.translation('cinder', localedir=_localedir, fallback=True)

_AVAILABLE_LANGUAGES = []
_AVAILABLE_LANGUAGES = {}
USE_LAZY = False


def enable_lazy():
"""Convenience function for configuring _() to use lazy gettext
Call this at the start of execution to enable the gettextutils._
function to use lazy gettext functionality. This is useful if
your project is importing _ directly instead of using the
gettextutils.install() way of importing the _ function.
"""
global USE_LAZY
USE_LAZY = True


def _(msg):
return _t.ugettext(msg)
if USE_LAZY:
return Message(msg, 'cinder')
else:
return _t.ugettext(msg)


def install(domain, lazy=False):
Expand Down Expand Up @@ -95,15 +114,15 @@ def _lazy_gettext(msg):
unicode=True)


class Message(UserString.UserString, object):
class Message(_userString.UserString, object):
"""Class used to encapsulate translatable messages."""
def __init__(self, msg, domain):
# _msg is the gettext msgid and should never change
self._msg = msg
self._left_extra_msg = ''
self._right_extra_msg = ''
self._locale = None
self.params = None
self.locale = None
self.domain = domain

@property
Expand Down Expand Up @@ -132,6 +151,32 @@ def data(self):

return six.text_type(full_msg)

@property
def locale(self):
return self._locale

@locale.setter
def locale(self, value):
self._locale = value
if not self.params:
return

# This Message object may have been constructed with one or more
# Message objects as substitution parameters, given as a single
# Message, or a tuple or Map containing some, so when setting the
# locale for this Message we need to set it for those Messages too.
if isinstance(self.params, Message):
self.params.locale = value
return
if isinstance(self.params, tuple):
for param in self.params:
if isinstance(param, Message):
param.locale = value
return
for param in self.params.values():
if isinstance(param, Message):
param.locale = value

def _save_dictionary_parameter(self, dict_param):
full_msg = self.data
# look for %(blah) fields in string;
Expand Down Expand Up @@ -182,7 +227,7 @@ def __str__(self):

def __getstate__(self):
to_copy = ['_msg', '_right_extra_msg', '_left_extra_msg',
'domain', 'params', 'locale']
'domain', 'params', '_locale']
new_dict = self.__dict__.fromkeys(to_copy)
for attr in to_copy:
new_dict[attr] = copy.deepcopy(self.__dict__[attr])
Expand Down Expand Up @@ -236,16 +281,16 @@ def __getattribute__(self, name):
if name in ops:
return getattr(self.data, name)
else:
return UserString.UserString.__getattribute__(self, name)
return _userString.UserString.__getattribute__(self, name)


def get_available_languages(domain):
"""Lists the available languages for the given translation domain.
:param domain: the domain to get languages for
"""
if _AVAILABLE_LANGUAGES:
return _AVAILABLE_LANGUAGES
if domain in _AVAILABLE_LANGUAGES:
return copy.copy(_AVAILABLE_LANGUAGES[domain])

localedir = '%s_LOCALEDIR' % domain.upper()
find = lambda x: gettext.find(domain,
Expand All @@ -254,7 +299,7 @@ def get_available_languages(domain):

# NOTE(mrodden): en_US should always be available (and first in case
# order matters) since our in-line message strings are en_US
_AVAILABLE_LANGUAGES.append('en_US')
language_list = ['en_US']
# NOTE(luisg): Babel <1.0 used a function called list(), which was
# renamed to locale_identifiers() in >=1.0, the requirements master list
# requires >=0.9.6, uncapped, so defensively work with both. We can remove
Expand All @@ -264,13 +309,14 @@ def get_available_languages(domain):
locale_identifiers = list_identifiers()
for i in locale_identifiers:
if find(i) is not None:
_AVAILABLE_LANGUAGES.append(i)
return _AVAILABLE_LANGUAGES
language_list.append(i)
_AVAILABLE_LANGUAGES[domain] = language_list
return copy.copy(language_list)


def get_localized_message(message, user_locale):
"""Gets a localized version of the given message in the given locale."""
if (isinstance(message, Message)):
if isinstance(message, Message):
if user_locale:
message.locale = user_locale
return unicode(message)
Expand Down
40 changes: 39 additions & 1 deletion cinder/tests/api/middleware/test_faults.py
Expand Up @@ -17,11 +17,14 @@

from xml.dom import minidom

import gettext
import mock
import webob.dec
import webob.exc

from cinder.api import common
from cinder.api.openstack import wsgi
from cinder import exception
from cinder.openstack.common import gettextutils
from cinder.openstack.common import jsonutils
from cinder import test
Expand Down Expand Up @@ -109,7 +112,7 @@ def raiser(req):
self.assertNotIn('resizeNotAllowed', resp.body)
self.assertIn('forbidden', resp.body)

def test_raise_localized_explanation(self):
def test_raise_http_with_localized_explanation(self):
params = ('blah', )
expl = gettextutils.Message("String with params: %s" % params, 'test')

Expand All @@ -130,6 +133,41 @@ def raiser(req):
self.assertIn(("Mensaje traducido"), resp.body)
self.stubs.UnsetAll()

@mock.patch('cinder.openstack.common.gettextutils.gettext.translation')
def test_raise_invalid_with_localized_explanation(self, mock_translation):
msg_template = gettextutils.Message("Invalid input: %(reason)s", "")
reason = gettextutils.Message("Value is invalid", "")

class MockESTranslations(gettext.GNUTranslations):
def ugettext(self, msgid):
if "Invalid input" in msgid:
return "Entrada invalida: %(reason)s"
elif "Value is invalid" in msgid:
return "El valor es invalido"
return msgid

def translation(domain, localedir=None, languages=None, fallback=None):
return MockESTranslations()

mock_translation.side_effect = translation

@webob.dec.wsgify
def raiser(req):
class MyInvalidInput(exception.InvalidInput):
message = msg_template

ex = MyInvalidInput(reason=reason)
raise wsgi.Fault(exception.ConvertedException(code=ex.code,
explanation=ex.msg))

req = webob.Request.blank("/.json")
resp = req.get_response(raiser)
self.assertEqual(resp.content_type, "application/json")
self.assertEqual(resp.status_int, 400)
# This response was comprised of Message objects from two different
# exceptions, here we are testing that both got translated
self.assertIn("Entrada invalida: El valor es invalido", resp.body)

def test_fault_has_status_int(self):
"""Ensure the status_int is set correctly on faults"""
fault = wsgi.Fault(webob.exc.HTTPBadRequest(explanation='what?'))
Expand Down

0 comments on commit afe2a21

Please sign in to comment.