Support contextual and plural form of gettext functions

This draft patch is created for more-gettext-support blueprint review.

Refer URL:
https://blueprints.launchpad.net/oslo.i18n/+spec/more-gettext-support
https://review.openstack.org/#q,topic:bp/more-gettext-support,n,z

Implements: blueprint more-gettext-support

Depends-on: I258eac447ecc7b71fb02952077cf3ef3ecfe12bb

Change-Id: Ic16d902ddfe94cfb5cfabe1c4f612ff001a8fa53
This commit is contained in:
Peng Wu 2014-10-30 11:54:05 +08:00
parent e9429faafe
commit 9e3132bc2f
6 changed files with 232 additions and 7 deletions

View File

@ -12,6 +12,36 @@ be installed as ``_()`` in the integration module.
* :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
===============

View File

@ -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

View File

@ -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)

View File

@ -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,10 +109,41 @@ class Message(six.text_type):
localedir=locale_dir,
languages=[desired_locale],
fallback=True)
translator = lang.gettext if six.PY3 else lang.ugettext
translated_message = translator(msgid)
return translated_message
# 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

View File

@ -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)

View File

@ -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