Merge "Support contextual and plural form of gettext functions"
This commit is contained in:
commit
f2570c1475
@ -15,6 +15,36 @@ to the user. Those do not need to be translated.
|
||||
* :doc:`usage`
|
||||
* :doc:`api`
|
||||
|
||||
Gettext Contextual Form and Plural Form
|
||||
=======================================
|
||||
|
||||
Sometimes under different contexts, the same word should be
|
||||
translated into different phrases using
|
||||
:py:attr:`TranslatorFactory.contextual_form <oslo_i18n.TranslatorFactory.contextual_form>`.
|
||||
|
||||
And recommend the following code to use contextual form::
|
||||
|
||||
# The contextual translation function using the name "_C"
|
||||
_C = _translators.contextual_form
|
||||
|
||||
...
|
||||
msg = _C('context', 'string')
|
||||
|
||||
In some languages, sometimes the translated strings are different
|
||||
with different item counts using
|
||||
:py:attr:`TranslatorFactory.plural_form <oslo_i18n.TranslatorFactory.plural_form>`
|
||||
|
||||
And recommend the following code to use plural form::
|
||||
|
||||
# The plural translation function using the name "_P"
|
||||
_P = _translators.plural_form
|
||||
|
||||
...
|
||||
msg = _P('single', 'plural', count)
|
||||
|
||||
The contextual form and plural form are used only when needed.
|
||||
By default, the translation should use the ``_()``.
|
||||
|
||||
Log Translation
|
||||
===============
|
||||
|
||||
|
@ -30,6 +30,12 @@ the marker functions the factory creates.
|
||||
# The primary translation function using the well-known name "_"
|
||||
_ = _translators.primary
|
||||
|
||||
# The contextual translation function using the name "_C"
|
||||
_C = _translators.contextual_form
|
||||
|
||||
# The plural translation function using the name "_P"
|
||||
_P = _translators.plural_form
|
||||
|
||||
# Translators for log levels.
|
||||
#
|
||||
# The abbreviated names are meant to reflect the usual use of a short
|
||||
|
@ -30,6 +30,9 @@ __all__ = [
|
||||
'TranslatorFactory',
|
||||
]
|
||||
|
||||
# magic gettext number to separate context from message
|
||||
CONTEXT_SEPARATOR = "\x04"
|
||||
|
||||
|
||||
class TranslatorFactory(object):
|
||||
"Create translator functions"
|
||||
@ -81,11 +84,99 @@ class TranslatorFactory(object):
|
||||
return m(msg)
|
||||
return f
|
||||
|
||||
def _make_contextual_translation_func(self, domain=None):
|
||||
"""Return a translation function ready for use with context messages.
|
||||
|
||||
The returned function takes two values, the context of
|
||||
the unicode string, the unicode string to be translated.
|
||||
The returned type is the same as
|
||||
:method:`TranslatorFactory._make_translation_func`.
|
||||
|
||||
The domain argument is the same as
|
||||
:method:`TranslatorFactory._make_translation_func`.
|
||||
|
||||
"""
|
||||
if domain is None:
|
||||
domain = self.domain
|
||||
t = gettext.translation(domain,
|
||||
localedir=self.localedir,
|
||||
fallback=True)
|
||||
# Use the appropriate method of the translation object based
|
||||
# on the python version.
|
||||
m = t.gettext if six.PY3 else t.ugettext
|
||||
|
||||
def f(ctx, msg):
|
||||
"""oslo.i18n.gettextutils translation with context function."""
|
||||
if _lazy.USE_LAZY:
|
||||
msgid = (ctx, msg)
|
||||
return _message.Message(msgid, domain=domain,
|
||||
has_contextual_form=True)
|
||||
|
||||
msgctx = "%s%s%s" % (ctx, CONTEXT_SEPARATOR, msg)
|
||||
s = m(msgctx)
|
||||
if CONTEXT_SEPARATOR in s:
|
||||
# Translation not found
|
||||
return msg
|
||||
return s
|
||||
return f
|
||||
|
||||
def _make_plural_translation_func(self, domain=None):
|
||||
"""Return a plural translation function ready for use with messages.
|
||||
|
||||
The returned function takes three values, the single form of
|
||||
the unicode string, the plural form of the unicode string,
|
||||
the count of items to be translated.
|
||||
The returned type is the same as
|
||||
:method:`TranslatorFactory._make_translation_func`.
|
||||
|
||||
The domain argument is the same as
|
||||
:method:`TranslatorFactory._make_translation_func`.
|
||||
|
||||
"""
|
||||
if domain is None:
|
||||
domain = self.domain
|
||||
t = gettext.translation(domain,
|
||||
localedir=self.localedir,
|
||||
fallback=True)
|
||||
# Use the appropriate method of the translation object based
|
||||
# on the python version.
|
||||
m = t.ngettext if six.PY3 else t.ungettext
|
||||
|
||||
def f(msgsingle, msgplural, msgcount):
|
||||
"""oslo.i18n.gettextutils plural translation function."""
|
||||
if _lazy.USE_LAZY:
|
||||
msgid = (msgsingle, msgplural, msgcount)
|
||||
return _message.Message(msgid, domain=domain,
|
||||
has_plural_form=True)
|
||||
return m(msgsingle, msgplural, msgcount)
|
||||
return f
|
||||
|
||||
@property
|
||||
def primary(self):
|
||||
"The default translation function."
|
||||
return self._make_translation_func()
|
||||
|
||||
@property
|
||||
def contextual_form(self):
|
||||
"""The contextual translation function.
|
||||
|
||||
The returned function takes two values, the context of
|
||||
the unicode string, the unicode string to be translated.
|
||||
|
||||
"""
|
||||
return self._make_contextual_translation_func()
|
||||
|
||||
@property
|
||||
def plural_form(self):
|
||||
"""The plural translation function.
|
||||
|
||||
The returned function takes three values, the single form of
|
||||
the unicode string, the plural form of the unicode string,
|
||||
the count of items to be translated.
|
||||
|
||||
"""
|
||||
return self._make_plural_translation_func()
|
||||
|
||||
def _make_log_translation_func(self, level):
|
||||
return self._make_translation_func(self.domain + '-log-' + level)
|
||||
|
||||
|
@ -26,6 +26,9 @@ import six
|
||||
from oslo_i18n import _locale
|
||||
from oslo_i18n import _translate
|
||||
|
||||
# magic gettext number to separate context from message
|
||||
CONTEXT_SEPARATOR = "\x04"
|
||||
|
||||
|
||||
class Message(six.text_type):
|
||||
"""A Message object is a unicode object that can be translated.
|
||||
@ -36,7 +39,8 @@ class Message(six.text_type):
|
||||
"""
|
||||
|
||||
def __new__(cls, msgid, msgtext=None, params=None,
|
||||
domain='oslo', *args):
|
||||
domain='oslo', has_contextual_form=False,
|
||||
has_plural_form=False, *args):
|
||||
"""Create a new Message object.
|
||||
|
||||
In order for translation to work gettext requires a message ID, this
|
||||
@ -55,6 +59,8 @@ class Message(six.text_type):
|
||||
msg.msgid = msgid
|
||||
msg.domain = domain
|
||||
msg.params = params
|
||||
msg.has_contextual_form = has_contextual_form
|
||||
msg.has_plural_form = has_plural_form
|
||||
return msg
|
||||
|
||||
def translate(self, desired_locale=None):
|
||||
@ -69,7 +75,10 @@ class Message(six.text_type):
|
||||
|
||||
translated_message = Message._translate_msgid(self.msgid,
|
||||
self.domain,
|
||||
desired_locale)
|
||||
desired_locale,
|
||||
self.has_contextual_form,
|
||||
self.has_plural_form)
|
||||
|
||||
if self.params is None:
|
||||
# No need for more translation
|
||||
return translated_message
|
||||
@ -86,7 +95,8 @@ class Message(six.text_type):
|
||||
return translated_message
|
||||
|
||||
@staticmethod
|
||||
def _translate_msgid(msgid, domain, desired_locale=None):
|
||||
def _translate_msgid(msgid, domain, desired_locale=None,
|
||||
has_contextual_form=False, has_plural_form=False):
|
||||
if not desired_locale:
|
||||
system_locale = locale.getdefaultlocale()
|
||||
# If the system locale is not available to the runtime use English
|
||||
@ -99,11 +109,42 @@ class Message(six.text_type):
|
||||
localedir=locale_dir,
|
||||
languages=[desired_locale],
|
||||
fallback=True)
|
||||
|
||||
# Primary translation function
|
||||
if not has_contextual_form and not has_plural_form:
|
||||
translator = lang.gettext if six.PY3 else lang.ugettext
|
||||
|
||||
translated_message = translator(msgid)
|
||||
return translated_message
|
||||
|
||||
# Contextual translation function
|
||||
if has_contextual_form and not has_plural_form:
|
||||
(msgctx, msgtxt) = msgid
|
||||
translator = lang.gettext if six.PY3 else lang.ugettext
|
||||
|
||||
msg_with_ctx = "%s%s%s" % (msgctx, CONTEXT_SEPARATOR, msgtxt)
|
||||
translated_message = translator(msg_with_ctx)
|
||||
|
||||
if CONTEXT_SEPARATOR in translated_message:
|
||||
# Translation not found
|
||||
translated_message = msgtxt
|
||||
|
||||
return translated_message
|
||||
|
||||
# Plural translation function
|
||||
if not has_contextual_form and has_plural_form:
|
||||
(msgsingle, msgplural, msgcount) = msgid
|
||||
translator = lang.ngettext if six.PY3 else lang.ungettext
|
||||
|
||||
translated_message = translator(msgsingle, msgplural, msgcount)
|
||||
return translated_message
|
||||
|
||||
# Reserved for contextual and plural translation function
|
||||
if has_contextual_form and has_plural_form:
|
||||
raise ValueError("Unimplemented.")
|
||||
|
||||
raise TypeError("Unknown msgid type.")
|
||||
|
||||
def __mod__(self, other):
|
||||
# When we mod a Message we want the actual operation to be performed
|
||||
# by the parent class (i.e. unicode()), the only thing we do here is
|
||||
|
@ -22,6 +22,9 @@ from oslo_i18n import _factory
|
||||
from oslo_i18n import _lazy
|
||||
from oslo_i18n import _message
|
||||
|
||||
# magic gettext number to separate context from message
|
||||
CONTEXT_SEPARATOR = "\x04"
|
||||
|
||||
|
||||
class TranslatorFactoryTest(test_base.BaseTestCase):
|
||||
|
||||
@ -89,3 +92,57 @@ class TranslatorFactoryTest(test_base.BaseTestCase):
|
||||
tf = _factory.TranslatorFactory('domain')
|
||||
tf._make_log_translation_func('mylevel')
|
||||
mtf.assert_called_with('domain-log-mylevel')
|
||||
|
||||
def test_contextual_form_py2(self):
|
||||
_lazy.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')
|
||||
trans.ugettext.return_value = "some text"
|
||||
tf = _factory.TranslatorFactory('domain')
|
||||
tf.contextual_form('context', 'some text')
|
||||
trans.ugettext.assert_called_with(
|
||||
"%s%s%s" % ('context', CONTEXT_SEPARATOR, 'some text'))
|
||||
|
||||
def test_contextual_form_py3(self):
|
||||
_lazy.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')
|
||||
trans.gettext.return_value = "some text"
|
||||
tf = _factory.TranslatorFactory('domain')
|
||||
tf.contextual_form('context', 'some text')
|
||||
trans.gettext.assert_called_with(
|
||||
"%s%s%s" % ('context', CONTEXT_SEPARATOR, 'some text'))
|
||||
|
||||
def test_plural_form_py2(self):
|
||||
_lazy.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.ngettext.side_effect = AssertionError(
|
||||
'should have called ungettext')
|
||||
tf = _factory.TranslatorFactory('domain')
|
||||
tf.plural_form('single', 'plural', 1)
|
||||
trans.ungettext.assert_called_with(
|
||||
'single', 'plural', 1)
|
||||
|
||||
def test_plural_form_py3(self):
|
||||
_lazy.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.ungettext.side_effect = AssertionError(
|
||||
'should have called ngettext')
|
||||
tf = _factory.TranslatorFactory('domain')
|
||||
tf.plural_form('single', 'plural', 1)
|
||||
trans.ngettext.assert_called_with(
|
||||
'single', 'plural', 1)
|
||||
|
@ -41,7 +41,7 @@ output_dir = oslo.i18n/locale
|
||||
input_file = oslo.i18n/locale/oslo.i18n.pot
|
||||
|
||||
[extract_messages]
|
||||
keywords = _ gettext ngettext l_ lazy_gettext
|
||||
keywords = _ gettext ngettext l_ lazy_gettext _C:1c,2 _P:1,2
|
||||
mapping_file = babel.cfg
|
||||
output_file = oslo.i18n/locale/oslo.i18n.pot
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user