Commit 637a33b9 authored by Cheryl Sabella's avatar Cheryl Sabella Committed by Serhiy Storchaka

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

parent 5598cc90
...@@ -96,6 +96,18 @@ class-based API instead. ...@@ -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`: ...@@ -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`: ...@@ -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`: ...@@ -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 ...@@ -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)
......
...@@ -131,6 +131,12 @@ asyncio ...@@ -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
---- ----
......
...@@ -57,6 +57,7 @@ __all__ = ['NullTranslations', 'GNUTranslations', 'Catalog', ...@@ -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: ...@@ -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: ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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
......
This diff is collapsed.
...@@ -559,6 +559,7 @@ Julian Gindi ...@@ -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
......
...@@ -5,7 +5,8 @@ ...@@ -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 ...@@ -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=''): ...@@ -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(): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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:
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: if section == STR:
add(msgid, msgstr, fuzzy) 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): ...@@ -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): ...@@ -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): ...@@ -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:',
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment