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 + 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): 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)