bpo-2504: Add pgettext() and variants to gettext. (GH-7253)

This commit is contained in:
Cheryl Sabella 2018-11-07 09:12:20 -05:00 committed by Serhiy Storchaka
parent 5598cc90c7
commit 637a33b996
7 changed files with 304 additions and 52 deletions

View File

@ -96,6 +96,18 @@ class-based API instead.
Like :func:`ngettext`, but look the message up in the specified *domain*. Like :func:`ngettext`, but look the message up in the specified *domain*.
.. function:: pgettext(context, message)
.. function:: dpgettext(domain, context, message)
.. function:: npgettext(context, singular, plural, n)
.. function:: dnpgettext(domain, context, singular, plural, n)
Similar to the corresponding functions without the ``p`` in the prefix (that
is, :func:`gettext`, :func:`dgettext`, :func:`ngettext`, :func:`dngettext`),
but the translation is restricted to the given message *context*.
.. versionadded:: 3.8
.. function:: lgettext(message) .. function:: lgettext(message)
.. function:: ldgettext(domain, message) .. function:: ldgettext(domain, message)
.. function:: lngettext(singular, plural, n) .. function:: lngettext(singular, plural, n)
@ -266,6 +278,22 @@ are the methods of :class:`!NullTranslations`:
Overridden in derived classes. Overridden in derived classes.
.. method:: pgettext(context, message)
If a fallback has been set, forward :meth:`pgettext` to the fallback.
Otherwise, return the translated message. Overridden in derived classes.
.. versionadded:: 3.8
.. method:: npgettext(context, singular, plural, n)
If a fallback has been set, forward :meth:`npgettext` to the fallback.
Otherwise, return the translated message. Overridden in derived classes.
.. versionadded:: 3.8
.. method:: lgettext(message) .. method:: lgettext(message)
.. method:: lngettext(singular, plural, n) .. method:: lngettext(singular, plural, n)
@ -316,7 +344,7 @@ are the methods of :class:`!NullTranslations`:
If the *names* parameter is given, it must be a sequence containing the If the *names* parameter is given, it must be a sequence containing the
names of functions you want to install in the builtins namespace in names of functions you want to install in the builtins namespace in
addition to :func:`_`. Supported names are ``'gettext'``, ``'ngettext'``, addition to :func:`_`. Supported names are ``'gettext'``, ``'ngettext'``,
``'lgettext'`` and ``'lngettext'``. ``'pgettext'``, ``'npgettext'``, ``'lgettext'``, and ``'lngettext'``.
Note that this is only one way, albeit the most convenient way, to make Note that this is only one way, albeit the most convenient way, to make
the :func:`_` function available to your application. Because it affects the :func:`_` function available to your application. Because it affects
@ -331,6 +359,9 @@ are the methods of :class:`!NullTranslations`:
This puts :func:`_` only in the module's global namespace and so only This puts :func:`_` only in the module's global namespace and so only
affects calls within this module. affects calls within this module.
.. versionchanged:: 3.8
Added ``'pgettext'`` and ``'npgettext'``.
The :class:`GNUTranslations` class The :class:`GNUTranslations` class
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -394,6 +425,31 @@ unexpected, or if other problems occur while reading the file, instantiating a
n) % {'num': n} n) % {'num': n}
.. method:: pgettext(context, message)
Look up the *context* and *message* id in the catalog and return the
corresponding message string, as a Unicode string. If there is no
entry in the catalog for the *message* id and *context*, and a fallback
has been set, the look up is forwarded to the fallback's
:meth:`pgettext` method. Otherwise, the *message* id is returned.
.. versionadded:: 3.8
.. method:: npgettext(context, singular, plural, n)
Do a plural-forms lookup of a message id. *singular* is used as the
message id for purposes of lookup in the catalog, while *n* is used to
determine which plural form to use.
If the message id for *context* is not found in the catalog, and a
fallback is specified, the request is forwarded to the fallback's
:meth:`npgettext` method. Otherwise, when *n* is 1 *singular* is
returned, and *plural* is returned in all other cases.
.. versionadded:: 3.8
.. method:: lgettext(message) .. method:: lgettext(message)
.. method:: lngettext(singular, plural, n) .. method:: lngettext(singular, plural, n)

View File

@ -131,6 +131,12 @@ asyncio
On Windows, the default event loop is now :class:`~asyncio.ProactorEventLoop`. On Windows, the default event loop is now :class:`~asyncio.ProactorEventLoop`.
gettext
-------
Added :func:`~gettext.pgettext` and its variants.
(Contributed by Franz Glasner, Éric Araujo, and Cheryl Sabella in :issue:`2504`.)
gzip gzip
---- ----

View File

@ -57,6 +57,7 @@ __all__ = ['NullTranslations', 'GNUTranslations', 'Catalog',
'bind_textdomain_codeset', 'bind_textdomain_codeset',
'dgettext', 'dngettext', 'gettext', 'lgettext', 'ldgettext', 'dgettext', 'dngettext', 'gettext', 'lgettext', 'ldgettext',
'ldngettext', 'lngettext', 'ngettext', 'ldngettext', 'lngettext', 'ngettext',
'pgettext', 'dpgettext', 'npgettext', 'dnpgettext',
] ]
_default_localedir = os.path.join(sys.base_prefix, 'share', 'locale') _default_localedir = os.path.join(sys.base_prefix, 'share', 'locale')
@ -311,6 +312,19 @@ class NullTranslations:
return tmsg.encode(self._output_charset) return tmsg.encode(self._output_charset)
return tmsg.encode(locale.getpreferredencoding()) return tmsg.encode(locale.getpreferredencoding())
def pgettext(self, context, message):
if self._fallback:
return self._fallback.pgettext(context, message)
return message
def npgettext(self, context, msgid1, msgid2, n):
if self._fallback:
return self._fallback.npgettext(context, msgid1, msgid2, n)
if n == 1:
return msgid1
else:
return msgid2
def info(self): def info(self):
return self._info return self._info
@ -332,15 +346,11 @@ class NullTranslations:
def install(self, names=None): def install(self, names=None):
import builtins import builtins
builtins.__dict__['_'] = self.gettext builtins.__dict__['_'] = self.gettext
if hasattr(names, "__contains__"): if names is not None:
if "gettext" in names: allowed = {'gettext', 'lgettext', 'lngettext',
builtins.__dict__['gettext'] = builtins.__dict__['_'] 'ngettext', 'npgettext', 'pgettext'}
if "ngettext" in names: for name in allowed & set(names):
builtins.__dict__['ngettext'] = self.ngettext builtins.__dict__[name] = getattr(self, name)
if "lgettext" in names:
builtins.__dict__['lgettext'] = self.lgettext
if "lngettext" in names:
builtins.__dict__['lngettext'] = self.lngettext
class GNUTranslations(NullTranslations): class GNUTranslations(NullTranslations):
@ -348,6 +358,10 @@ class GNUTranslations(NullTranslations):
LE_MAGIC = 0x950412de LE_MAGIC = 0x950412de
BE_MAGIC = 0xde120495 BE_MAGIC = 0xde120495
# The encoding of a msgctxt and a msgid in a .mo file is
# msgctxt + "\x04" + msgid (gettext version >= 0.15)
CONTEXT = "%s\x04%s"
# Acceptable .mo versions # Acceptable .mo versions
VERSIONS = (0, 1) VERSIONS = (0, 1)
@ -493,6 +507,29 @@ class GNUTranslations(NullTranslations):
tmsg = msgid2 tmsg = msgid2
return tmsg return tmsg
def pgettext(self, context, message):
ctxt_msg_id = self.CONTEXT % (context, message)
missing = object()
tmsg = self._catalog.get(ctxt_msg_id, missing)
if tmsg is missing:
if self._fallback:
return self._fallback.pgettext(context, message)
return message
return tmsg
def npgettext(self, context, msgid1, msgid2, n):
ctxt_msg_id = self.CONTEXT % (context, msgid1)
try:
tmsg = self._catalog[ctxt_msg_id, self.plural(n)]
except KeyError:
if self._fallback:
return self._fallback.npgettext(context, msgid1, msgid2, n)
if n == 1:
tmsg = msgid1
else:
tmsg = msgid2
return tmsg
# Locate a .mo file using the gettext strategy # Locate a .mo file using the gettext strategy
def find(domain, localedir=None, languages=None, all=False): def find(domain, localedir=None, languages=None, all=False):
@ -672,6 +709,26 @@ def ldngettext(domain, msgid1, msgid2, n):
DeprecationWarning) DeprecationWarning)
return t.lngettext(msgid1, msgid2, n) return t.lngettext(msgid1, msgid2, n)
def dpgettext(domain, context, message):
try:
t = translation(domain, _localedirs.get(domain, None))
except OSError:
return message
return t.pgettext(context, message)
def dnpgettext(domain, context, msgid1, msgid2, n):
try:
t = translation(domain, _localedirs.get(domain, None))
except OSError:
if n == 1:
return msgid1
else:
return msgid2
return t.npgettext(context, msgid1, msgid2, n)
def gettext(message): def gettext(message):
return dgettext(_current_domain, message) return dgettext(_current_domain, message)
@ -696,6 +753,15 @@ def lngettext(msgid1, msgid2, n):
DeprecationWarning) DeprecationWarning)
return ldngettext(_current_domain, msgid1, msgid2, n) return ldngettext(_current_domain, msgid1, msgid2, n)
def pgettext(context, message):
return dpgettext(_current_domain, context, message)
def npgettext(context, msgid1, msgid2, n):
return dnpgettext(_current_domain, context, msgid1, msgid2, n)
# dcgettext() has been deemed unnecessary and is not implemented. # dcgettext() has been deemed unnecessary and is not implemented.
# James Henstridge's Catalog constructor from GNOME gettext. Documented usage # James Henstridge's Catalog constructor from GNOME gettext. Documented usage

View File

@ -15,23 +15,27 @@ from test import support
# - Tests should have only one assert. # - Tests should have only one assert.
GNU_MO_DATA = b'''\ GNU_MO_DATA = b'''\
3hIElQAAAAAGAAAAHAAAAEwAAAALAAAAfAAAAAAAAACoAAAAFQAAAKkAAAAjAAAAvwAAAKEAAADj 3hIElQAAAAAJAAAAHAAAAGQAAAAAAAAArAAAAAAAAACsAAAAFQAAAK0AAAAjAAAAwwAAAKEAAADn
AAAABwAAAIUBAAALAAAAjQEAAEUBAACZAQAAFgAAAN8CAAAeAAAA9gIAAKEAAAAVAwAABQAAALcD AAAAMAAAAIkBAAAHAAAAugEAABYAAADCAQAAHAAAANkBAAALAAAA9gEAAEIBAAACAgAAFgAAAEUD
AAAJAAAAvQMAAAEAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABQAAAAYAAAACAAAAAFJh AAAeAAAAXAMAAKEAAAB7AwAAMgAAAB0EAAAFAAAAUAQAABsAAABWBAAAIQAAAHIEAAAJAAAAlAQA
eW1vbmQgTHV4dXJ5IFlhY2gtdABUaGVyZSBpcyAlcyBmaWxlAFRoZXJlIGFyZSAlcyBmaWxlcwBU AABSYXltb25kIEx1eHVyeSBZYWNoLXQAVGhlcmUgaXMgJXMgZmlsZQBUaGVyZSBhcmUgJXMgZmls
aGlzIG1vZHVsZSBwcm92aWRlcyBpbnRlcm5hdGlvbmFsaXphdGlvbiBhbmQgbG9jYWxpemF0aW9u ZXMAVGhpcyBtb2R1bGUgcHJvdmlkZXMgaW50ZXJuYXRpb25hbGl6YXRpb24gYW5kIGxvY2FsaXph
CnN1cHBvcnQgZm9yIHlvdXIgUHl0aG9uIHByb2dyYW1zIGJ5IHByb3ZpZGluZyBhbiBpbnRlcmZh dGlvbgpzdXBwb3J0IGZvciB5b3VyIFB5dGhvbiBwcm9ncmFtcyBieSBwcm92aWRpbmcgYW4gaW50
Y2UgdG8gdGhlIEdOVQpnZXR0ZXh0IG1lc3NhZ2UgY2F0YWxvZyBsaWJyYXJ5LgBtdWxsdXNrAG51 ZXJmYWNlIHRvIHRoZSBHTlUKZ2V0dGV4dCBtZXNzYWdlIGNhdGFsb2cgbGlicmFyeS4AV2l0aCBj
ZGdlIG51ZGdlAFByb2plY3QtSWQtVmVyc2lvbjogMi4wClBPLVJldmlzaW9uLURhdGU6IDIwMDAt b250ZXh0BFRoZXJlIGlzICVzIGZpbGUAVGhlcmUgYXJlICVzIGZpbGVzAG11bGx1c2sAbXkgY29u
MDgtMjkgMTI6MTktMDQ6MDAKTGFzdC1UcmFuc2xhdG9yOiBKLiBEYXZpZCBJYsOhw7FleiA8ai1k dGV4dARudWRnZSBudWRnZQBteSBvdGhlciBjb250ZXh0BG51ZGdlIG51ZGdlAG51ZGdlIG51ZGdl
YXZpZEBub29zLmZyPgpMYW5ndWFnZS1UZWFtOiBYWCA8cHl0aG9uLWRldkBweXRob24ub3JnPgpN AFByb2plY3QtSWQtVmVyc2lvbjogMi4wClBPLVJldmlzaW9uLURhdGU6IDIwMDMtMDQtMTEgMTQ6
SU1FLVZlcnNpb246IDEuMApDb250ZW50LVR5cGU6IHRleHQvcGxhaW47IGNoYXJzZXQ9aXNvLTg4 MzItMDQwMApMYXN0LVRyYW5zbGF0b3I6IEouIERhdmlkIEliYW5leiA8ai1kYXZpZEBub29zLmZy
NTktMQpDb250ZW50LVRyYW5zZmVyLUVuY29kaW5nOiBub25lCkdlbmVyYXRlZC1CeTogcHlnZXR0 PgpMYW5ndWFnZS1UZWFtOiBYWCA8cHl0aG9uLWRldkBweXRob24ub3JnPgpNSU1FLVZlcnNpb246
ZXh0LnB5IDEuMQpQbHVyYWwtRm9ybXM6IG5wbHVyYWxzPTI7IHBsdXJhbD1uIT0xOwoAVGhyb2F0 IDEuMApDb250ZW50LVR5cGU6IHRleHQvcGxhaW47IGNoYXJzZXQ9aXNvLTg4NTktMQpDb250ZW50
d29iYmxlciBNYW5ncm92ZQBIYXkgJXMgZmljaGVybwBIYXkgJXMgZmljaGVyb3MAR3V2ZiB6YnFo LVRyYW5zZmVyLUVuY29kaW5nOiA4Yml0CkdlbmVyYXRlZC1CeTogcHlnZXR0ZXh0LnB5IDEuMQpQ
eXIgY2ViaXZxcmYgdmFncmVhbmd2YmFueXZtbmd2YmEgbmFxIHlicG55dm1uZ3ZiYQpmaGNjYmVn bHVyYWwtRm9ybXM6IG5wbHVyYWxzPTI7IHBsdXJhbD1uIT0xOwoAVGhyb2F0d29iYmxlciBNYW5n
IHNiZSBsYmhlIENsZ3ViYSBjZWJ0ZW56ZiBvbCBjZWJpdnF2YXQgbmEgdmFncmVzbnByIGdiIGd1 cm92ZQBIYXkgJXMgZmljaGVybwBIYXkgJXMgZmljaGVyb3MAR3V2ZiB6YnFoeXIgY2ViaXZxcmYg
ciBUQUgKdHJnZ3JrZyB6cmZmbnRyIHBuZ255YnQgeXZvZW5lbC4AYmFjb24Ad2luayB3aW5rAA== dmFncmVhbmd2YmFueXZtbmd2YmEgbmFxIHlicG55dm1uZ3ZiYQpmaGNjYmVnIHNiZSBsYmhlIENs
Z3ViYSBjZWJ0ZW56ZiBvbCBjZWJpdnF2YXQgbmEgdmFncmVzbnByIGdiIGd1ciBUQUgKdHJnZ3Jr
ZyB6cmZmbnRyIHBuZ255YnQgeXZvZW5lbC4ASGF5ICVzIGZpY2hlcm8gKGNvbnRleHQpAEhheSAl
cyBmaWNoZXJvcyAoY29udGV4dCkAYmFjb24Ad2luayB3aW5rIChpbiAibXkgY29udGV4dCIpAHdp
bmsgd2luayAoaW4gIm15IG90aGVyIGNvbnRleHQiKQB3aW5rIHdpbmsA
''' '''
# This data contains an invalid major version number (5) # This data contains an invalid major version number (5)
@ -84,13 +88,13 @@ ciBUQUgKdHJnZ3JrZyB6cmZmbnRyIHBuZ255YnQgeXZvZW5lbC4AYmFjb24Ad2luayB3aW5rAA==
UMO_DATA = b'''\ UMO_DATA = b'''\
3hIElQAAAAACAAAAHAAAACwAAAAFAAAAPAAAAAAAAABQAAAABAAAAFEAAAAPAQAAVgAAAAQAAABm 3hIElQAAAAADAAAAHAAAADQAAAAAAAAAAAAAAAAAAABMAAAABAAAAE0AAAAQAAAAUgAAAA8BAABj
AQAAAQAAAAIAAAAAAAAAAAAAAAAAAAAAYWLDngBQcm9qZWN0LUlkLVZlcnNpb246IDIuMApQTy1S AAAABAAAAHMBAAAWAAAAeAEAAABhYsOeAG15Y29udGV4dMOeBGFiw54AUHJvamVjdC1JZC1WZXJz
ZXZpc2lvbi1EYXRlOiAyMDAzLTA0LTExIDEyOjQyLTA0MDAKTGFzdC1UcmFuc2xhdG9yOiBCYXJy aW9uOiAyLjAKUE8tUmV2aXNpb24tRGF0ZTogMjAwMy0wNC0xMSAxMjo0Mi0wNDAwCkxhc3QtVHJh
eSBBLiBXQXJzYXcgPGJhcnJ5QHB5dGhvbi5vcmc+Ckxhbmd1YWdlLVRlYW06IFhYIDxweXRob24t bnNsYXRvcjogQmFycnkgQS4gV0Fyc2F3IDxiYXJyeUBweXRob24ub3JnPgpMYW5ndWFnZS1UZWFt
ZGV2QHB5dGhvbi5vcmc+Ck1JTUUtVmVyc2lvbjogMS4wCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFp OiBYWCA8cHl0aG9uLWRldkBweXRob24ub3JnPgpNSU1FLVZlcnNpb246IDEuMApDb250ZW50LVR5
bjsgY2hhcnNldD11dGYtOApDb250ZW50LVRyYW5zZmVyLUVuY29kaW5nOiA3Yml0CkdlbmVyYXRl cGU6IHRleHQvcGxhaW47IGNoYXJzZXQ9dXRmLTgKQ29udGVudC1UcmFuc2Zlci1FbmNvZGluZzog
ZC1CeTogbWFudWFsbHkKAMKkeXoA N2JpdApHZW5lcmF0ZWQtQnk6IG1hbnVhbGx5CgDCpHl6AMKkeXogKGNvbnRleHQgdmVyc2lvbikA
''' '''
MMO_DATA = b'''\ MMO_DATA = b'''\
@ -147,7 +151,7 @@ class GettextTestCase1(GettextBaseTest):
GettextBaseTest.setUp(self) GettextBaseTest.setUp(self)
self.localedir = os.curdir self.localedir = os.curdir
self.mofile = MOFILE self.mofile = MOFILE
gettext.install('gettext', self.localedir) gettext.install('gettext', self.localedir, names=['pgettext'])
def test_some_translations(self): def test_some_translations(self):
eq = self.assertEqual eq = self.assertEqual
@ -157,6 +161,13 @@ class GettextTestCase1(GettextBaseTest):
eq(_(r'Raymond Luxury Yach-t'), 'Throatwobbler Mangrove') eq(_(r'Raymond Luxury Yach-t'), 'Throatwobbler Mangrove')
eq(_(r'nudge nudge'), 'wink wink') eq(_(r'nudge nudge'), 'wink wink')
def test_some_translations_with_context(self):
eq = self.assertEqual
eq(pgettext('my context', 'nudge nudge'),
'wink wink (in "my context")')
eq(pgettext('my other context', 'nudge nudge'),
'wink wink (in "my other context")')
def test_double_quotes(self): def test_double_quotes(self):
eq = self.assertEqual eq = self.assertEqual
# double quotes # double quotes
@ -251,6 +262,20 @@ class GettextTestCase2(GettextBaseTest):
eq(self._(r'Raymond Luxury Yach-t'), 'Throatwobbler Mangrove') eq(self._(r'Raymond Luxury Yach-t'), 'Throatwobbler Mangrove')
eq(self._(r'nudge nudge'), 'wink wink') eq(self._(r'nudge nudge'), 'wink wink')
def test_some_translations_with_context(self):
eq = self.assertEqual
eq(gettext.pgettext('my context', 'nudge nudge'),
'wink wink (in "my context")')
eq(gettext.pgettext('my other context', 'nudge nudge'),
'wink wink (in "my other context")')
def test_some_translations_with_context_and_domain(self):
eq = self.assertEqual
eq(gettext.dpgettext('gettext', 'my context', 'nudge nudge'),
'wink wink (in "my context")')
eq(gettext.dpgettext('gettext', 'my other context', 'nudge nudge'),
'wink wink (in "my other context")')
def test_double_quotes(self): def test_double_quotes(self):
eq = self.assertEqual eq = self.assertEqual
# double quotes # double quotes
@ -298,6 +323,15 @@ class PluralFormsTestCase(GettextBaseTest):
x = gettext.ngettext('There is %s file', 'There are %s files', 2) x = gettext.ngettext('There is %s file', 'There are %s files', 2)
eq(x, 'Hay %s ficheros') eq(x, 'Hay %s ficheros')
def test_plural_context_forms1(self):
eq = self.assertEqual
x = gettext.npgettext('With context',
'There is %s file', 'There are %s files', 1)
eq(x, 'Hay %s fichero (context)')
x = gettext.npgettext('With context',
'There is %s file', 'There are %s files', 2)
eq(x, 'Hay %s ficheros (context)')
def test_plural_forms2(self): def test_plural_forms2(self):
eq = self.assertEqual eq = self.assertEqual
with open(self.mofile, 'rb') as fp: with open(self.mofile, 'rb') as fp:
@ -307,6 +341,17 @@ class PluralFormsTestCase(GettextBaseTest):
x = t.ngettext('There is %s file', 'There are %s files', 2) x = t.ngettext('There is %s file', 'There are %s files', 2)
eq(x, 'Hay %s ficheros') eq(x, 'Hay %s ficheros')
def test_plural_context_forms2(self):
eq = self.assertEqual
with open(self.mofile, 'rb') as fp:
t = gettext.GNUTranslations(fp)
x = t.npgettext('With context',
'There is %s file', 'There are %s files', 1)
eq(x, 'Hay %s fichero (context)')
x = t.npgettext('With context',
'There is %s file', 'There are %s files', 2)
eq(x, 'Hay %s ficheros (context)')
# Examples from http://www.gnu.org/software/gettext/manual/gettext.html # Examples from http://www.gnu.org/software/gettext/manual/gettext.html
def test_ja(self): def test_ja(self):
@ -646,6 +691,7 @@ class UnicodeTranslationsTest(GettextBaseTest):
with open(UMOFILE, 'rb') as fp: with open(UMOFILE, 'rb') as fp:
self.t = gettext.GNUTranslations(fp) self.t = gettext.GNUTranslations(fp)
self._ = self.t.gettext self._ = self.t.gettext
self.pgettext = self.t.pgettext
def test_unicode_msgid(self): def test_unicode_msgid(self):
self.assertIsInstance(self._(''), str) self.assertIsInstance(self._(''), str)
@ -653,6 +699,53 @@ class UnicodeTranslationsTest(GettextBaseTest):
def test_unicode_msgstr(self): def test_unicode_msgstr(self):
self.assertEqual(self._('ab\xde'), '\xa4yz') self.assertEqual(self._('ab\xde'), '\xa4yz')
def test_unicode_context_msgstr(self):
t = self.pgettext('mycontext\xde', 'ab\xde')
self.assertTrue(isinstance(t, str))
self.assertEqual(t, '\xa4yz (context version)')
class UnicodeTranslationsPluralTest(GettextBaseTest):
def setUp(self):
GettextBaseTest.setUp(self)
with open(MOFILE, 'rb') as fp:
self.t = gettext.GNUTranslations(fp)
self.ngettext = self.t.ngettext
self.npgettext = self.t.npgettext
def test_unicode_msgid(self):
unless = self.assertTrue
unless(isinstance(self.ngettext('', '', 1), str))
unless(isinstance(self.ngettext('', '', 2), str))
def test_unicode_context_msgid(self):
unless = self.assertTrue
unless(isinstance(self.npgettext('', '', '', 1), str))
unless(isinstance(self.npgettext('', '', '', 2), str))
def test_unicode_msgstr(self):
eq = self.assertEqual
unless = self.assertTrue
t = self.ngettext("There is %s file", "There are %s files", 1)
unless(isinstance(t, str))
eq(t, "Hay %s fichero")
unless(isinstance(t, str))
t = self.ngettext("There is %s file", "There are %s files", 5)
unless(isinstance(t, str))
eq(t, "Hay %s ficheros")
def test_unicode_msgstr_with_context(self):
eq = self.assertEqual
unless = self.assertTrue
t = self.npgettext("With context",
"There is %s file", "There are %s files", 1)
unless(isinstance(t, str))
eq(t, "Hay %s fichero (context)")
t = self.npgettext("With context",
"There is %s file", "There are %s files", 5)
unless(isinstance(t, str))
eq(t, "Hay %s ficheros (context)")
class WeirdMetadataTest(GettextBaseTest): class WeirdMetadataTest(GettextBaseTest):
def setUp(self): def setUp(self):
@ -750,6 +843,14 @@ msgstr ""
msgid "nudge nudge" msgid "nudge nudge"
msgstr "wink wink" msgstr "wink wink"
msgctxt "my context"
msgid "nudge nudge"
msgstr "wink wink (in \"my context\")"
msgctxt "my other context"
msgid "nudge nudge"
msgstr "wink wink (in \"my other context\")"
#: test_gettext.py:16 test_gettext.py:22 test_gettext.py:28 test_gettext.py:34 #: test_gettext.py:16 test_gettext.py:22 test_gettext.py:28 test_gettext.py:34
#: test_gettext.py:77 test_gettext.py:83 test_gettext.py:89 test_gettext.py:95 #: test_gettext.py:77 test_gettext.py:83 test_gettext.py:89 test_gettext.py:95
msgid "albatross" msgid "albatross"
@ -782,6 +883,14 @@ msgid "There is %s file"
msgid_plural "There are %s files" msgid_plural "There are %s files"
msgstr[0] "Hay %s fichero" msgstr[0] "Hay %s fichero"
msgstr[1] "Hay %s ficheros" msgstr[1] "Hay %s ficheros"
# Manually added, as neither pygettext nor xgettext support plural forms
# and context in Python.
msgctxt "With context"
msgid "There is %s file"
msgid_plural "There are %s files"
msgstr[0] "Hay %s fichero (context)"
msgstr[1] "Hay %s ficheros (context)"
''' '''
# Here's the second example po file example, used to generate the UMO_DATA # Here's the second example po file example, used to generate the UMO_DATA
@ -806,6 +915,11 @@ msgstr ""
#: nofile:0 #: nofile:0
msgid "ab\xc3\x9e" msgid "ab\xc3\x9e"
msgstr "\xc2\xa4yz" msgstr "\xc2\xa4yz"
#: nofile:1
msgctxt "mycontext\xc3\x9e"
msgid "ab\xc3\x9e"
msgstr "\xc2\xa4yz (context version)"
''' '''
# Here's the third example po file, used to generate MMO_DATA # Here's the third example po file, used to generate MMO_DATA

View File

@ -559,6 +559,7 @@ Julian Gindi
Yannick Gingras Yannick Gingras
Neil Girdhar Neil Girdhar
Matt Giuca Matt Giuca
Franz Glasner
Wim Glenn Wim Glenn
Michael Goderbauer Michael Goderbauer
Karan Goel Karan Goel

View File

@ -0,0 +1 @@
Add gettext.pgettext() and variants.

View File

@ -5,7 +5,8 @@
This program converts a textual Uniforum-style message catalog (.po file) into This program converts a textual Uniforum-style message catalog (.po file) into
a binary GNU catalog (.mo file). This is essentially the same function as the a binary GNU catalog (.mo file). This is essentially the same function as the
GNU msgfmt program, however, it is a simpler implementation. GNU msgfmt program, however, it is a simpler implementation. Currently it
does not handle plural forms but it does handle message contexts.
Usage: msgfmt.py [OPTIONS] filename.po Usage: msgfmt.py [OPTIONS] filename.po
@ -32,12 +33,11 @@ import struct
import array import array
from email.parser import HeaderParser from email.parser import HeaderParser
__version__ = "1.1" __version__ = "1.2"
MESSAGES = {} MESSAGES = {}
def usage(code, msg=''): def usage(code, msg=''):
print(__doc__, file=sys.stderr) print(__doc__, file=sys.stderr)
if msg: if msg:
@ -45,15 +45,16 @@ def usage(code, msg=''):
sys.exit(code) sys.exit(code)
def add(ctxt, id, str, fuzzy):
def add(id, str, fuzzy):
"Add a non-fuzzy translation to the dictionary." "Add a non-fuzzy translation to the dictionary."
global MESSAGES global MESSAGES
if not fuzzy and str: if not fuzzy and str:
MESSAGES[id] = str if ctxt is None:
MESSAGES[id] = str
else:
MESSAGES[b"%b\x04%b" % (ctxt, id)] = str
def generate(): def generate():
"Return the generated output." "Return the generated output."
global MESSAGES global MESSAGES
@ -95,10 +96,10 @@ def generate():
return output return output
def make(filename, outfile): def make(filename, outfile):
ID = 1 ID = 1
STR = 2 STR = 2
CTXT = 3
# Compute .mo name from .po name and arguments # Compute .mo name from .po name and arguments
if filename.endswith('.po'): if filename.endswith('.po'):
@ -115,7 +116,7 @@ def make(filename, outfile):
print(msg, file=sys.stderr) print(msg, file=sys.stderr)
sys.exit(1) sys.exit(1)
section = None section = msgctxt = None
fuzzy = 0 fuzzy = 0
# Start off assuming Latin-1, so everything decodes without failure, # Start off assuming Latin-1, so everything decodes without failure,
@ -129,8 +130,8 @@ def make(filename, outfile):
lno += 1 lno += 1
# If we get a comment line after a msgstr, this is a new entry # If we get a comment line after a msgstr, this is a new entry
if l[0] == '#' and section == STR: if l[0] == '#' and section == STR:
add(msgid, msgstr, fuzzy) add(msgctxt, msgid, msgstr, fuzzy)
section = None section = msgctxt = None
fuzzy = 0 fuzzy = 0
# Record a fuzzy mark # Record a fuzzy mark
if l[:2] == '#,' and 'fuzzy' in l: if l[:2] == '#,' and 'fuzzy' in l:
@ -138,10 +139,16 @@ def make(filename, outfile):
# Skip comments # Skip comments
if l[0] == '#': if l[0] == '#':
continue continue
# Now we are in a msgid section, output previous section # Now we are in a msgid or msgctxt section, output previous section
if l.startswith('msgid') and not l.startswith('msgid_plural'): if l.startswith('msgctxt'):
if section == STR: if section == STR:
add(msgid, msgstr, fuzzy) add(msgctxt, msgid, msgstr, fuzzy)
section = CTXT
l = l[7:]
msgctxt = b''
elif l.startswith('msgid') and not l.startswith('msgid_plural'):
if section == STR:
add(msgctxt, msgid, msgstr, fuzzy)
if not msgid: if not msgid:
# See whether there is an encoding declaration # See whether there is an encoding declaration
p = HeaderParser() p = HeaderParser()
@ -183,7 +190,9 @@ def make(filename, outfile):
if not l: if not l:
continue continue
l = ast.literal_eval(l) l = ast.literal_eval(l)
if section == ID: if section == CTXT:
msgctxt += l.encode(encoding)
elif section == ID:
msgid += l.encode(encoding) msgid += l.encode(encoding)
elif section == STR: elif section == STR:
msgstr += l.encode(encoding) msgstr += l.encode(encoding)
@ -194,7 +203,7 @@ def make(filename, outfile):
sys.exit(1) sys.exit(1)
# Add last entry # Add last entry
if section == STR: if section == STR:
add(msgid, msgstr, fuzzy) add(msgctxt, msgid, msgstr, fuzzy)
# Compute output # Compute output
output = generate() output = generate()
@ -206,7 +215,6 @@ def make(filename, outfile):
print(msg, file=sys.stderr) print(msg, file=sys.stderr)
def main(): def main():
try: try:
opts, args = getopt.getopt(sys.argv[1:], 'hVo:', opts, args = getopt.getopt(sys.argv[1:], 'hVo:',