Check the lazy flag at runtime

Update lazy flag handling to check it at runtime when each message is
translated, instead of once at startup when the translation function is
created. This allows a library to support lazy translation, without
requiring an application that uses the library to support it.

Change-Id: Iae22668119c8d0f5fb9c486436bfc35cdb88ac58
This commit is contained in:
Doug Hellmann 2014-06-04 12:38:01 -07:00
parent 38acb78b01
commit 67ac9babdf
5 changed files with 119 additions and 69 deletions

View File

@ -2,6 +2,9 @@
Usage
=======
Integration Module
==================
To use in a project, create a small integration module containing:
::
@ -37,3 +40,28 @@ for your case:
# ...
raise RuntimeError(_('exception message'))
Lazy Translation
================
Lazy translation delays converting a message string to the translated
form as long as possible, including possibly never if the message is
not logged or delivered to the user in some other way. It also
supports logging translated messages in multiple languages, by
configuring separate log handlers.
Lazy translation is implemented by returning a special object from the
translation function, instead of a unicode string. That special
message object supports some, but not all, string manipulation
APIs. For example, concatenation with addition is not supported, but
interpolation of variables is supported. Depending on how translated
strings are used in an application, these restrictions may mean that
lazy translation cannot be used, and so it is not enabled by default.
To enable lazy translation, call :func:`enable_lazy`.
::
from oslo.i18n import gettextutils
gettextutils.enable_lazy()

View File

@ -18,7 +18,6 @@
"""
import copy
import functools
import gettext
import locale
from logging import handlers
@ -29,8 +28,7 @@ import six
_AVAILABLE_LANGUAGES = {}
# FIXME(dhellmann): Remove this when moving to oslo.i18n.
USE_LAZY = False
_USE_LAZY = False
def _get_locale_dir_variable_name(domain):
@ -44,49 +42,53 @@ class TranslatorFactory(object):
"""Create translator functions
"""
def __init__(self, domain, lazy=False, localedir=None):
def __init__(self, domain, localedir=None):
"""Establish a set of translation functions for the domain.
:param domain: Name of translation domain,
specifying a message catalog.
:type domain: str
:param lazy: Delays translation until a message is emitted.
Defaults to False.
:type lazy: Boolean
:param localedir: Directory with translation catalogs.
:type localedir: str
"""
self.domain = domain
self.lazy = lazy
if localedir is None:
localedir = os.environ.get(_get_locale_dir_variable_name(domain))
self.localedir = localedir
def _make_translation_func(self, domain=None):
"""Return a new translation function ready for use.
"""Return a translation function ready for use with messages.
Takes into account whether or not lazy translation is being
done.
The returned function takes a single value, the unicode string
to be translated. The return type varies depending on whether
lazy translation is being done. When lazy translation is
enabled, :class:`Message` objects are returned instead of
regular :class:`unicode` strings.
The domain can be specified to override the default from the
factory, but the localedir from the factory is always used
because we assume the log-level translation catalogs are
The domain argument can be specified to override the default
from the factory, but the localedir from the factory is always
used because we assume the log-level translation catalogs are
installed in the same directory as the main application
catalog.
"""
if domain is None:
domain = self.domain
if self.lazy:
return functools.partial(Message, domain=domain)
t = gettext.translation(
domain,
localedir=self.localedir,
fallback=True,
)
if six.PY3:
return t.gettext
return t.ugettext
# Use the appropriate method of the translation object based
# on the python version.
m = t.gettext if six.PY3 else t.ugettext
def f(msg):
"""oslo.i18n.gettextutils translation function."""
if _USE_LAZY:
return Message(msg, domain=domain)
return m(msg)
return f
@property
def primary(self):
@ -141,27 +143,24 @@ _LC = _translators.log_critical
# integration module.
def enable_lazy():
def enable_lazy(enable=True):
"""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.
:param enable: Flag indicating whether lazy translation should be
turned on or off. Defaults to True.
:type enable: bool
"""
# FIXME(dhellmann): This function will be removed in oslo.i18n,
# because the TranslatorFactory makes it superfluous.
global _, _LI, _LW, _LE, _LC, USE_LAZY
tf = TranslatorFactory('oslo', lazy=True)
_ = tf.primary
_LI = tf.log_info
_LW = tf.log_warning
_LE = tf.log_error
_LC = tf.log_critical
USE_LAZY = True
global _USE_LAZY
_USE_LAZY = enable
def install(domain, lazy=False):
def install(domain):
"""Install a _() function using the given translation domain.
Given a translation domain, install a _() function using gettext's
@ -179,19 +178,9 @@ def install(domain, lazy=False):
instead of strings, which can then be lazily translated into
any available locale.
"""
if lazy:
from six import moves
tf = TranslatorFactory(domain, lazy=True)
moves.builtins.__dict__['_'] = tf.primary
else:
localedir = '%s_LOCALEDIR' % domain.upper()
if six.PY3:
gettext.install(domain,
localedir=os.environ.get(localedir))
else:
gettext.install(domain,
localedir=os.environ.get(localedir),
unicode=True)
from six import moves
tf = TranslatorFactory(domain)
moves.builtins.__dict__['_'] = tf.primary
class Message(six.text_type):

View File

@ -24,37 +24,67 @@ from oslo.i18n import gettextutils
class TranslatorFactoryTest(test_base.BaseTestCase):
def setUp(self):
super(TranslatorFactoryTest, self).setUp()
# remember so we can reset to it later in case it changes
self._USE_LAZY = gettextutils._USE_LAZY
def tearDown(self):
# reset to value before test
gettextutils._USE_LAZY = self._USE_LAZY
super(TranslatorFactoryTest, self).tearDown()
def test_lazy(self):
gettextutils.enable_lazy(True)
with mock.patch.object(gettextutils, 'Message') as msg:
tf = gettextutils.TranslatorFactory('domain', lazy=True)
tf = gettextutils.TranslatorFactory('domain')
tf.primary('some text')
msg.assert_called_with('some text', domain='domain')
def test_not_lazy(self):
gettextutils.enable_lazy(False)
with mock.patch.object(gettextutils, 'Message') as msg:
msg.side_effect = AssertionError('should not use Message')
tf = gettextutils.TranslatorFactory('domain')
tf.primary('some text')
def test_change_lazy(self):
gettextutils.enable_lazy(True)
tf = gettextutils.TranslatorFactory('domain')
r = tf.primary('some text')
self.assertIsInstance(r, gettextutils.Message)
gettextutils.enable_lazy(False)
r = tf.primary('some text')
# Python 2.6 doesn't have assertNotIsInstance().
self.assertFalse(isinstance(r, gettextutils.Message))
def test_py2(self):
gettextutils.enable_lazy(False)
with mock.patch.object(six, 'PY3', False):
with mock.patch('gettext.translation') as translation:
trans = mock.Mock()
translation.return_value = trans
trans.gettext.side_effect = AssertionError(
'should have called ugettext')
tf = gettextutils.TranslatorFactory('domain', lazy=False)
tf = gettextutils.TranslatorFactory('domain')
tf.primary('some text')
trans.ugettext.assert_called_with('some text')
def test_py3(self):
gettextutils.enable_lazy(False)
with mock.patch.object(six, 'PY3', True):
with mock.patch('gettext.translation') as translation:
trans = mock.Mock()
translation.return_value = trans
trans.ugettext.side_effect = AssertionError(
'should have called gettext')
tf = gettextutils.TranslatorFactory('domain', lazy=False)
tf = gettextutils.TranslatorFactory('domain')
tf.primary('some text')
trans.gettext.assert_called_with('some text')
def test_log_level_domain_name(self):
with mock.patch.object(gettextutils.TranslatorFactory,
'_make_translation_func') as mtf:
tf = gettextutils.TranslatorFactory('domain', lazy=False)
tf = gettextutils.TranslatorFactory('domain')
tf._make_log_translation_func('mylevel')
mtf.assert_called_with('domain-log-mylevel')

View File

@ -40,49 +40,52 @@ class GettextTest(test_base.BaseTestCase):
moxfixture = self.useFixture(moxstubout.MoxStubout())
self.stubs = moxfixture.stubs
self.mox = moxfixture.mox
# remember so we can reset to it later
self._USE_LAZY = gettextutils.USE_LAZY
# remember so we can reset to it later in case it changes
self._USE_LAZY = gettextutils._USE_LAZY
def tearDown(self):
# reset to value before test
gettextutils.USE_LAZY = self._USE_LAZY
gettextutils._USE_LAZY = self._USE_LAZY
super(GettextTest, self).tearDown()
def test_enable_lazy(self):
gettextutils.USE_LAZY = False
gettextutils._USE_LAZY = False
gettextutils.enable_lazy()
# assert now enabled
self.assertTrue(gettextutils.USE_LAZY)
self.assertTrue(gettextutils._USE_LAZY)
def test_disable_lazy(self):
gettextutils._USE_LAZY = True
gettextutils.enable_lazy(False)
self.assertFalse(gettextutils._USE_LAZY)
def test_gettext_does_not_blow_up(self):
LOG.info(gettextutils._('test'))
def test_gettextutils_install(self):
gettextutils.install('blaa')
gettextutils.enable_lazy(False)
self.assertTrue(isinstance(_('A String'), six.text_type)) # noqa
gettextutils.install('blaa', lazy=True)
gettextutils.install('blaa')
gettextutils.enable_lazy(True)
self.assertTrue(isinstance(_('A Message'), # noqa
gettextutils.Message))
def test_gettext_install_looks_up_localedir(self):
with mock.patch('os.environ.get') as environ_get:
with mock.patch('gettext.install') as gettext_install:
with mock.patch('gettext.install'):
environ_get.return_value = '/foo/bar'
gettextutils.install('blaa')
environ_get.assert_calls([mock.call('BLAA_LOCALEDIR')])
environ_get.assert_called_once_with('BLAA_LOCALEDIR')
if six.PY3:
gettext_install.assert_called_once_with(
'blaa',
localedir='/foo/bar')
else:
gettext_install.assert_called_once_with(
'blaa',
localedir='/foo/bar',
unicode=True)
def test_gettext_install_updates_builtins(self):
with mock.patch('os.environ.get') as environ_get:
with mock.patch('gettext.install'):
environ_get.return_value = '/foo/bar'
if '_' in six.moves.builtins.__dict__:
del six.moves.builtins.__dict__['_']
gettextutils.install('blaa')
self.assertIn('_', six.moves.builtins.__dict__)
def test_get_available_languages(self):
# All the available languages for which locale data is available

View File

@ -37,6 +37,6 @@ class LogLevelTranslationsTest(test_base.BaseTestCase):
def _test(self, level):
with mock.patch.object(gettextutils.TranslatorFactory,
'_make_translation_func') as mtf:
tf = gettextutils.TranslatorFactory('domain', lazy=False)
tf = gettextutils.TranslatorFactory('domain')
getattr(tf, 'log_%s' % level)
mtf.assert_called_with('domain-log-%s' % level)