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

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 @@
    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 @@
       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 @@
       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 @@
       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 @@
              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 @@
 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 @@
            '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 @@
             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 @@
     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 @@
     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 @@
                 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 @@
                                 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 @@
                                 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 @@
 #  - 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 @@
 
 
 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 @@
         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 @@
         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 @@
         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 @@
         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 @@
         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 @@
         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 @@
     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 @@
 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_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 @@
 #: 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 @@
 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 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 @@
     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 @@
     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 @@
         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 @@
         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 @@
         # 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(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:
                     # See whether there is an encoding declaration
                     p = HeaderParser()
@@ -183,7 +190,9 @@
         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 @@
             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 @@
         print(msg, file=sys.stderr)
 
 
-
 def main():
     try:
         opts, args = getopt.getopt(sys.argv[1:], 'hVo:',