summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorCheryl Sabella <cheryl.sabella@gmail.com>2018-11-07 14:12:20 (GMT)
committerSerhiy Storchaka <storchaka@gmail.com>2018-11-07 14:12:20 (GMT)
commit637a33b99685fd5d1032670fbe29c7c8a8f0ff63 (patch)
treee72cdff2802199ccd6cde261644fa630ee36be6f
parent5598cc90c745dab827e55fadded42dbe85e31d33 (diff)
downloadcpython-637a33b99685fd5d1032670fbe29c7c8a8f0ff63.zip
cpython-637a33b99685fd5d1032670fbe29c7c8a8f0ff63.tar.gz
cpython-637a33b99685fd5d1032670fbe29c7c8a8f0ff63.tar.bz2
bpo-2504: Add pgettext() and variants to gettext. (GH-7253)
-rw-r--r--Doc/library/gettext.rst58
-rw-r--r--Doc/whatsnew/3.8.rst6
-rw-r--r--Lib/gettext.py84
-rw-r--r--Lib/test/test_gettext.py164
-rw-r--r--Misc/ACKS1
-rw-r--r--Misc/NEWS.d/next/Library/2018-05-30-16-00-06.bpo-2504.BynUvU.rst1
-rwxr-xr-xTools/i18n/msgfmt.py42
7 files changed, 304 insertions, 52 deletions
diff --git a/Doc/library/gettext.rst b/Doc/library/gettext.rst
index 38515eb..7f4eab5 100644
--- a/Doc/library/gettext.rst
+++ b/Doc/library/gettext.rst
@@ -96,6 +96,18 @@ class-based API instead.
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:: ldgettext(domain, message)
.. function:: lngettext(singular, plural, n)
@@ -266,6 +278,22 @@ are the methods of :class:`!NullTranslations`:
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:: 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
names of functions you want to install in the builtins namespace in
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
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
affects calls within this module.
+ .. versionchanged:: 3.8
+ Added ``'pgettext'`` and ``'npgettext'``.
+
The :class:`GNUTranslations` class
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -394,6 +425,31 @@ unexpected, or if other problems occur while reading the file, instantiating a
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:: lngettext(singular, plural, n)
diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst
index 3bacbab..7b9a940 100644
--- a/Doc/whatsnew/3.8.rst
+++ b/Doc/whatsnew/3.8.rst
@@ -131,6 +131,12 @@ asyncio
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
----
diff --git a/Lib/gettext.py b/Lib/gettext.py
index 920742c..72a313a 100644
--- a/Lib/gettext.py
+++ b/Lib/gettext.py
@@ -57,6 +57,7 @@ __all__ = ['NullTranslations', 'GNUTranslations', 'Catalog',
'bind_textdomain_codeset',
'dgettext', 'dngettext', 'gettext', 'lgettext', 'ldgettext',
'ldngettext', 'lngettext', 'ngettext',
+ 'pgettext', 'dpgettext', 'npgettext', 'dnpgettext',
]
_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(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):
return self._info
@@ -332,15 +346,11 @@ class NullTranslations:
def install(self, names=None):
import builtins
builtins.__dict__['_'] = self.gettext
- if hasattr(names, "__contains__"):
- if "gettext" in names:
- builtins.__dict__['gettext'] = builtins.__dict__['_']
- if "ngettext" in names:
- builtins.__dict__['ngettext'] = self.ngettext
- if "lgettext" in names:
- builtins.__dict__['lgettext'] = self.lgettext
- if "lngettext" in names:
- builtins.__dict__['lngettext'] = self.lngettext
+ if names is not None:
+ allowed = {'gettext', 'lgettext', 'lngettext',
+ 'ngettext', 'npgettext', 'pgettext'}
+ for name in allowed & set(names):
+ builtins.__dict__[name] = getattr(self, name)
class GNUTranslations(NullTranslations):
@@ -348,6 +358,10 @@ class GNUTranslations(NullTranslations):
LE_MAGIC = 0x950412de
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
VERSIONS = (0, 1)
@@ -493,6 +507,29 @@ class GNUTranslations(NullTranslations):
tmsg = msgid2
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
def find(domain, localedir=None, languages=None, all=False):
@@ -672,6 +709,26 @@ def ldngettext(domain, msgid1, msgid2, n):
DeprecationWarning)
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):
return dgettext(_current_domain, message)
@@ -696,6 +753,15 @@ def lngettext(msgid1, msgid2, n):
DeprecationWarning)
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.
# James Henstridge's Catalog constructor from GNOME gettext. Documented usage
diff --git a/Lib/test/test_gettext.py b/Lib/test/test_gettext.py
index bbad102..8c0250e 100644
--- a/Lib/test/test_gettext.py
+++ b/Lib/test/test_gettext.py
@@ -15,23 +15,27 @@ from test import support
# - Tests should have only one assert.
GNU_MO_DATA = b'''\
-3hIElQAAAAAGAAAAHAAAAEwAAAALAAAAfAAAAAAAAACoAAAAFQAAAKkAAAAjAAAAvwAAAKEAAADj
-AAAABwAAAIUBAAALAAAAjQEAAEUBAACZAQAAFgAAAN8CAAAeAAAA9gIAAKEAAAAVAwAABQAAALcD
-AAAJAAAAvQMAAAEAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABQAAAAYAAAACAAAAAFJh
-eW1vbmQgTHV4dXJ5IFlhY2gtdABUaGVyZSBpcyAlcyBmaWxlAFRoZXJlIGFyZSAlcyBmaWxlcwBU
-aGlzIG1vZHVsZSBwcm92aWRlcyBpbnRlcm5hdGlvbmFsaXphdGlvbiBhbmQgbG9jYWxpemF0aW9u
-CnN1cHBvcnQgZm9yIHlvdXIgUHl0aG9uIHByb2dyYW1zIGJ5IHByb3ZpZGluZyBhbiBpbnRlcmZh
-Y2UgdG8gdGhlIEdOVQpnZXR0ZXh0IG1lc3NhZ2UgY2F0YWxvZyBsaWJyYXJ5LgBtdWxsdXNrAG51
-ZGdlIG51ZGdlAFByb2plY3QtSWQtVmVyc2lvbjogMi4wClBPLVJldmlzaW9uLURhdGU6IDIwMDAt
-MDgtMjkgMTI6MTktMDQ6MDAKTGFzdC1UcmFuc2xhdG9yOiBKLiBEYXZpZCBJYsOhw7FleiA8ai1k
-YXZpZEBub29zLmZyPgpMYW5ndWFnZS1UZWFtOiBYWCA8cHl0aG9uLWRldkBweXRob24ub3JnPgpN
-SU1FLVZlcnNpb246IDEuMApDb250ZW50LVR5cGU6IHRleHQvcGxhaW47IGNoYXJzZXQ9aXNvLTg4
-NTktMQpDb250ZW50LVRyYW5zZmVyLUVuY29kaW5nOiBub25lCkdlbmVyYXRlZC1CeTogcHlnZXR0
-ZXh0LnB5IDEuMQpQbHVyYWwtRm9ybXM6IG5wbHVyYWxzPTI7IHBsdXJhbD1uIT0xOwoAVGhyb2F0
-d29iYmxlciBNYW5ncm92ZQBIYXkgJXMgZmljaGVybwBIYXkgJXMgZmljaGVyb3MAR3V2ZiB6YnFo
-eXIgY2ViaXZxcmYgdmFncmVhbmd2YmFueXZtbmd2YmEgbmFxIHlicG55dm1uZ3ZiYQpmaGNjYmVn
-IHNiZSBsYmhlIENsZ3ViYSBjZWJ0ZW56ZiBvbCBjZWJpdnF2YXQgbmEgdmFncmVzbnByIGdiIGd1
-ciBUQUgKdHJnZ3JrZyB6cmZmbnRyIHBuZ255YnQgeXZvZW5lbC4AYmFjb24Ad2luayB3aW5rAA==
+3hIElQAAAAAJAAAAHAAAAGQAAAAAAAAArAAAAAAAAACsAAAAFQAAAK0AAAAjAAAAwwAAAKEAAADn
+AAAAMAAAAIkBAAAHAAAAugEAABYAAADCAQAAHAAAANkBAAALAAAA9gEAAEIBAAACAgAAFgAAAEUD
+AAAeAAAAXAMAAKEAAAB7AwAAMgAAAB0EAAAFAAAAUAQAABsAAABWBAAAIQAAAHIEAAAJAAAAlAQA
+AABSYXltb25kIEx1eHVyeSBZYWNoLXQAVGhlcmUgaXMgJXMgZmlsZQBUaGVyZSBhcmUgJXMgZmls
+ZXMAVGhpcyBtb2R1bGUgcHJvdmlkZXMgaW50ZXJuYXRpb25hbGl6YXRpb24gYW5kIGxvY2FsaXph
+dGlvbgpzdXBwb3J0IGZvciB5b3VyIFB5dGhvbiBwcm9ncmFtcyBieSBwcm92aWRpbmcgYW4gaW50
+ZXJmYWNlIHRvIHRoZSBHTlUKZ2V0dGV4dCBtZXNzYWdlIGNhdGFsb2cgbGlicmFyeS4AV2l0aCBj
+b250ZXh0BFRoZXJlIGlzICVzIGZpbGUAVGhlcmUgYXJlICVzIGZpbGVzAG11bGx1c2sAbXkgY29u
+dGV4dARudWRnZSBudWRnZQBteSBvdGhlciBjb250ZXh0BG51ZGdlIG51ZGdlAG51ZGdlIG51ZGdl
+AFByb2plY3QtSWQtVmVyc2lvbjogMi4wClBPLVJldmlzaW9uLURhdGU6IDIwMDMtMDQtMTEgMTQ6
+MzItMDQwMApMYXN0LVRyYW5zbGF0b3I6IEouIERhdmlkIEliYW5leiA8ai1kYXZpZEBub29zLmZy
+PgpMYW5ndWFnZS1UZWFtOiBYWCA8cHl0aG9uLWRldkBweXRob24ub3JnPgpNSU1FLVZlcnNpb246
+IDEuMApDb250ZW50LVR5cGU6IHRleHQvcGxhaW47IGNoYXJzZXQ9aXNvLTg4NTktMQpDb250ZW50
+LVRyYW5zZmVyLUVuY29kaW5nOiA4Yml0CkdlbmVyYXRlZC1CeTogcHlnZXR0ZXh0LnB5IDEuMQpQ
+bHVyYWwtRm9ybXM6IG5wbHVyYWxzPTI7IHBsdXJhbD1uIT0xOwoAVGhyb2F0d29iYmxlciBNYW5n
+cm92ZQBIYXkgJXMgZmljaGVybwBIYXkgJXMgZmljaGVyb3MAR3V2ZiB6YnFoeXIgY2ViaXZxcmYg
+dmFncmVhbmd2YmFueXZtbmd2YmEgbmFxIHlicG55dm1uZ3ZiYQpmaGNjYmVnIHNiZSBsYmhlIENs
+Z3ViYSBjZWJ0ZW56ZiBvbCBjZWJpdnF2YXQgbmEgdmFncmVzbnByIGdiIGd1ciBUQUgKdHJnZ3Jr
+ZyB6cmZmbnRyIHBuZ255YnQgeXZvZW5lbC4ASGF5ICVzIGZpY2hlcm8gKGNvbnRleHQpAEhheSAl
+cyBmaWNoZXJvcyAoY29udGV4dCkAYmFjb24Ad2luayB3aW5rIChpbiAibXkgY29udGV4dCIpAHdp
+bmsgd2luayAoaW4gIm15IG90aGVyIGNvbnRleHQiKQB3aW5rIHdpbmsA
'''
# This data contains an invalid major version number (5)
@@ -84,13 +88,13 @@ ciBUQUgKdHJnZ3JrZyB6cmZmbnRyIHBuZ255YnQgeXZvZW5lbC4AYmFjb24Ad2luayB3aW5rAA==
UMO_DATA = b'''\
-3hIElQAAAAACAAAAHAAAACwAAAAFAAAAPAAAAAAAAABQAAAABAAAAFEAAAAPAQAAVgAAAAQAAABm
-AQAAAQAAAAIAAAAAAAAAAAAAAAAAAAAAYWLDngBQcm9qZWN0LUlkLVZlcnNpb246IDIuMApQTy1S
-ZXZpc2lvbi1EYXRlOiAyMDAzLTA0LTExIDEyOjQyLTA0MDAKTGFzdC1UcmFuc2xhdG9yOiBCYXJy
-eSBBLiBXQXJzYXcgPGJhcnJ5QHB5dGhvbi5vcmc+Ckxhbmd1YWdlLVRlYW06IFhYIDxweXRob24t
-ZGV2QHB5dGhvbi5vcmc+Ck1JTUUtVmVyc2lvbjogMS4wCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFp
-bjsgY2hhcnNldD11dGYtOApDb250ZW50LVRyYW5zZmVyLUVuY29kaW5nOiA3Yml0CkdlbmVyYXRl
-ZC1CeTogbWFudWFsbHkKAMKkeXoA
+3hIElQAAAAADAAAAHAAAADQAAAAAAAAAAAAAAAAAAABMAAAABAAAAE0AAAAQAAAAUgAAAA8BAABj
+AAAABAAAAHMBAAAWAAAAeAEAAABhYsOeAG15Y29udGV4dMOeBGFiw54AUHJvamVjdC1JZC1WZXJz
+aW9uOiAyLjAKUE8tUmV2aXNpb24tRGF0ZTogMjAwMy0wNC0xMSAxMjo0Mi0wNDAwCkxhc3QtVHJh
+bnNsYXRvcjogQmFycnkgQS4gV0Fyc2F3IDxiYXJyeUBweXRob24ub3JnPgpMYW5ndWFnZS1UZWFt
+OiBYWCA8cHl0aG9uLWRldkBweXRob24ub3JnPgpNSU1FLVZlcnNpb246IDEuMApDb250ZW50LVR5
+cGU6IHRleHQvcGxhaW47IGNoYXJzZXQ9dXRmLTgKQ29udGVudC1UcmFuc2Zlci1FbmNvZGluZzog
+N2JpdApHZW5lcmF0ZWQtQnk6IG1hbnVhbGx5CgDCpHl6AMKkeXogKGNvbnRleHQgdmVyc2lvbikA
'''
MMO_DATA = b'''\
@@ -147,7 +151,7 @@ class GettextTestCase1(GettextBaseTest):
GettextBaseTest.setUp(self)
self.localedir = os.curdir
self.mofile = MOFILE
- gettext.install('gettext', self.localedir)
+ gettext.install('gettext', self.localedir, names=['pgettext'])
def test_some_translations(self):
eq = self.assertEqual
@@ -157,6 +161,13 @@ class GettextTestCase1(GettextBaseTest):
eq(_(r'Raymond Luxury Yach-t'), 'Throatwobbler Mangrove')
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):
eq = self.assertEqual
# double quotes
@@ -251,6 +262,20 @@ class GettextTestCase2(GettextBaseTest):
eq(self._(r'Raymond Luxury Yach-t'), 'Throatwobbler Mangrove')
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):
eq = self.assertEqual
# double quotes
@@ -298,6 +323,15 @@ class PluralFormsTestCase(GettextBaseTest):
x = gettext.ngettext('There is %s file', 'There are %s files', 2)
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):
eq = self.assertEqual
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)
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
def test_ja(self):
@@ -646,6 +691,7 @@ class UnicodeTranslationsTest(GettextBaseTest):
with open(UMOFILE, 'rb') as fp:
self.t = gettext.GNUTranslations(fp)
self._ = self.t.gettext
+ self.pgettext = self.t.pgettext
def test_unicode_msgid(self):
self.assertIsInstance(self._(''), str)
@@ -653,6 +699,53 @@ class UnicodeTranslationsTest(GettextBaseTest):
def test_unicode_msgstr(self):
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):
def setUp(self):
@@ -750,6 +843,14 @@ msgstr ""
msgid "nudge nudge"
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:77 test_gettext.py:83 test_gettext.py:89 test_gettext.py:95
msgid "albatross"
@@ -782,6 +883,14 @@ msgid "There is %s file"
msgid_plural "There are %s files"
msgstr[0] "Hay %s fichero"
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
@@ -806,6 +915,11 @@ msgstr ""
#: nofile:0
msgid "ab\xc3\x9e"
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
diff --git a/Misc/ACKS b/Misc/ACKS
index aba6094..3e5fa0a 100644
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -559,6 +559,7 @@ Julian Gindi
Yannick Gingras
Neil Girdhar
Matt Giuca
+Franz Glasner
Wim Glenn
Michael Goderbauer
Karan Goel
diff --git a/Misc/NEWS.d/next/Library/2018-05-30-16-00-06.bpo-2504.BynUvU.rst b/Misc/NEWS.d/next/Library/2018-05-30-16-00-06.bpo-2504.BynUvU.rst
new file mode 100644
index 0000000..72b0f70
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2018-05-30-16-00-06.bpo-2504.BynUvU.rst
@@ -0,0 +1 @@
+Add gettext.pgettext() and variants.
diff --git a/Tools/i18n/msgfmt.py b/Tools/i18n/msgfmt.py
index 63d52d1..3f731e9 100755
--- a/Tools/i18n/msgfmt.py
+++ b/Tools/i18n/msgfmt.py
@@ -5,7 +5,8 @@
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
-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
@@ -32,12 +33,11 @@ import struct
import array
from email.parser import HeaderParser
-__version__ = "1.1"
+__version__ = "1.2"
MESSAGES = {}
-
def usage(code, msg=''):
print(__doc__, file=sys.stderr)
if msg:
@@ -45,15 +45,16 @@ def usage(code, msg=''):
sys.exit(code)
-
-def add(id, str, fuzzy):
+def add(ctxt, id, str, fuzzy):
"Add a non-fuzzy translation to the dictionary."
global MESSAGES
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():
"Return the generated output."
global MESSAGES
@@ -95,10 +96,10 @@ def generate():
return output
-
def make(filename, outfile):
ID = 1
STR = 2
+ CTXT = 3
# Compute .mo name from .po name and arguments
if filename.endswith('.po'):
@@ -115,7 +116,7 @@ def make(filename, outfile):
print(msg, file=sys.stderr)
sys.exit(1)
- section = None
+ section = msgctxt = None
fuzzy = 0
# Start off assuming Latin-1, so everything decodes without failure,
@@ -129,8 +130,8 @@ def make(filename, outfile):
lno += 1
# If we get a comment line after a msgstr, this is a new entry
if l[0] == '#' and section == STR:
- add(msgid, msgstr, fuzzy)
- section = None
+ add(msgctxt, msgid, msgstr, fuzzy)
+ section = msgctxt = None
fuzzy = 0
# Record a fuzzy mark
if l[:2] == '#,' and 'fuzzy' in l:
@@ -138,10 +139,16 @@ def make(filename, outfile):
# Skip comments
if l[0] == '#':
continue
- # Now we are in a msgid section, output previous section
- if l.startswith('msgid') and not l.startswith('msgid_plural'):
+ # Now we are in a msgid or msgctxt section, output previous section
+ if l.startswith('msgctxt'):
+ if section == STR:
+ 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(msgid, msgstr, fuzzy)
+ add(msgctxt, msgid, msgstr, fuzzy)
if not msgid:
# See whether there is an encoding declaration
p = HeaderParser()
@@ -183,7 +190,9 @@ def make(filename, outfile):
if not l:
continue
l = ast.literal_eval(l)
- if section == ID:
+ if section == CTXT:
+ msgctxt += l.encode(encoding)
+ elif section == ID:
msgid += l.encode(encoding)
elif section == STR:
msgstr += l.encode(encoding)
@@ -194,7 +203,7 @@ def make(filename, outfile):
sys.exit(1)
# Add last entry
if section == STR:
- add(msgid, msgstr, fuzzy)
+ add(msgctxt, msgid, msgstr, fuzzy)
# Compute output
output = generate()
@@ -206,7 +215,6 @@ def make(filename, outfile):
print(msg, file=sys.stderr)
-
def main():
try:
opts, args = getopt.getopt(sys.argv[1:], 'hVo:',