summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDoug Hellmann <doug.hellmann@dreamhost.com>2014-06-04 12:38:01 -0700
committerDoug Hellmann <doug.hellmann@dreamhost.com>2014-06-04 12:44:10 -0700
commit67ac9babdfd3f9350f6dd2ab043519000a853ae0 (patch)
tree52e21bc16c863a9626a5a08a52fdd5126d9a5d15
parent38acb78b01aee8e2df9b5a2d9d12e5ce1a1ab44f (diff)
downloadoslo-i18n-67ac9babdfd3f9350f6dd2ab043519000a853ae0.tar.gz
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
-rw-r--r--doc/source/usage.rst28
-rw-r--r--oslo/i18n/gettextutils.py81
-rw-r--r--tests/test_factory.py38
-rw-r--r--tests/test_gettextutils.py43
-rw-r--r--tests/test_logging.py2
5 files changed, 121 insertions, 71 deletions
diff --git a/doc/source/usage.rst b/doc/source/usage.rst
index 9ea3fa0..55ebe00 100644
--- a/doc/source/usage.rst
+++ b/doc/source/usage.rst
@@ -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()
diff --git a/oslo/i18n/gettextutils.py b/oslo/i18n/gettextutils.py
index 30eb273..a8134fc 100644
--- a/oslo/i18n/gettextutils.py
+++ b/oslo/i18n/gettextutils.py
@@ -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
-
-
-def install(domain, lazy=False):
+ global _USE_LAZY
+ _USE_LAZY = enable
+
+
+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):
diff --git a/tests/test_factory.py b/tests/test_factory.py
index 2cf031a..58af03d 100644
--- a/tests/test_factory.py
+++ b/tests/test_factory.py
@@ -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')
diff --git a/tests/test_gettextutils.py b/tests/test_gettextutils.py
index 210dd94..543e897 100644
--- a/tests/test_gettextutils.py
+++ b/tests/test_gettextutils.py
@@ -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
diff --git a/tests/test_logging.py b/tests/test_logging.py
index b4273d6..f750488 100644
--- a/tests/test_logging.py
+++ b/tests/test_logging.py
@@ -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)