• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""Internationalization and localization support.
2
3This module provides internationalization (I18N) and localization (L10N)
4support for your Python programs by providing an interface to the GNU gettext
5message catalog library.
6
7I18N refers to the operation by which a program is made aware of multiple
8languages.  L10N refers to the adaptation of your program, once
9internationalized, to the local language and cultural habits.
10
11"""
12
13# This module represents the integration of work, contributions, feedback, and
14# suggestions from the following people:
15#
16# Martin von Loewis, who wrote the initial implementation of the underlying
17# C-based libintlmodule (later renamed _gettext), along with a skeletal
18# gettext.py implementation.
19#
20# Peter Funk, who wrote fintl.py, a fairly complete wrapper around intlmodule,
21# which also included a pure-Python implementation to read .mo files if
22# intlmodule wasn't available.
23#
24# James Henstridge, who also wrote a gettext.py module, which has some
25# interesting, but currently unsupported experimental features: the notion of
26# a Catalog class and instances, and the ability to add to a catalog file via
27# a Python API.
28#
29# Barry Warsaw integrated these modules, wrote the .install() API and code,
30# and conformed all C and Python code to Python's coding standards.
31#
32# Francois Pinard and Marc-Andre Lemburg also contributed valuably to this
33# module.
34#
35# J. David Ibanez implemented plural forms. Bruno Haible fixed some bugs.
36#
37# TODO:
38# - Lazy loading of .mo files.  Currently the entire catalog is loaded into
39#   memory, but that's probably bad for large translated programs.  Instead,
40#   the lexical sort of original strings in GNU .mo files should be exploited
41#   to do binary searches and lazy initializations.  Or you might want to use
42#   the undocumented double-hash algorithm for .mo files with hash tables, but
43#   you'll need to study the GNU gettext code to do this.
44#
45# - Support Solaris .mo file formats.  Unfortunately, we've been unable to
46#   find this format documented anywhere.
47
48
49import locale
50import os
51import re
52import sys
53
54
55__all__ = ['NullTranslations', 'GNUTranslations', 'Catalog',
56           'find', 'translation', 'install', 'textdomain', 'bindtextdomain',
57           'bind_textdomain_codeset',
58           'dgettext', 'dngettext', 'gettext', 'lgettext', 'ldgettext',
59           'ldngettext', 'lngettext', 'ngettext',
60           'pgettext', 'dpgettext', 'npgettext', 'dnpgettext',
61           ]
62
63_default_localedir = os.path.join(sys.base_prefix, 'share', 'locale')
64
65# Expression parsing for plural form selection.
66#
67# The gettext library supports a small subset of C syntax.  The only
68# incompatible difference is that integer literals starting with zero are
69# decimal.
70#
71# https://www.gnu.org/software/gettext/manual/gettext.html#Plural-forms
72# http://git.savannah.gnu.org/cgit/gettext.git/tree/gettext-runtime/intl/plural.y
73
74_token_pattern = re.compile(r"""
75        (?P<WHITESPACES>[ \t]+)                    | # spaces and horizontal tabs
76        (?P<NUMBER>[0-9]+\b)                       | # decimal integer
77        (?P<NAME>n\b)                              | # only n is allowed
78        (?P<PARENTHESIS>[()])                      |
79        (?P<OPERATOR>[-*/%+?:]|[><!]=?|==|&&|\|\|) | # !, *, /, %, +, -, <, >,
80                                                     # <=, >=, ==, !=, &&, ||,
81                                                     # ? :
82                                                     # unary and bitwise ops
83                                                     # not allowed
84        (?P<INVALID>\w+|.)                           # invalid token
85    """, re.VERBOSE|re.DOTALL)
86
87def _tokenize(plural):
88    for mo in re.finditer(_token_pattern, plural):
89        kind = mo.lastgroup
90        if kind == 'WHITESPACES':
91            continue
92        value = mo.group(kind)
93        if kind == 'INVALID':
94            raise ValueError('invalid token in plural form: %s' % value)
95        yield value
96    yield ''
97
98def _error(value):
99    if value:
100        return ValueError('unexpected token in plural form: %s' % value)
101    else:
102        return ValueError('unexpected end of plural form')
103
104_binary_ops = (
105    ('||',),
106    ('&&',),
107    ('==', '!='),
108    ('<', '>', '<=', '>='),
109    ('+', '-'),
110    ('*', '/', '%'),
111)
112_binary_ops = {op: i for i, ops in enumerate(_binary_ops, 1) for op in ops}
113_c2py_ops = {'||': 'or', '&&': 'and', '/': '//'}
114
115def _parse(tokens, priority=-1):
116    result = ''
117    nexttok = next(tokens)
118    while nexttok == '!':
119        result += 'not '
120        nexttok = next(tokens)
121
122    if nexttok == '(':
123        sub, nexttok = _parse(tokens)
124        result = '%s(%s)' % (result, sub)
125        if nexttok != ')':
126            raise ValueError('unbalanced parenthesis in plural form')
127    elif nexttok == 'n':
128        result = '%s%s' % (result, nexttok)
129    else:
130        try:
131            value = int(nexttok, 10)
132        except ValueError:
133            raise _error(nexttok) from None
134        result = '%s%d' % (result, value)
135    nexttok = next(tokens)
136
137    j = 100
138    while nexttok in _binary_ops:
139        i = _binary_ops[nexttok]
140        if i < priority:
141            break
142        # Break chained comparisons
143        if i in (3, 4) and j in (3, 4):  # '==', '!=', '<', '>', '<=', '>='
144            result = '(%s)' % result
145        # Replace some C operators by their Python equivalents
146        op = _c2py_ops.get(nexttok, nexttok)
147        right, nexttok = _parse(tokens, i + 1)
148        result = '%s %s %s' % (result, op, right)
149        j = i
150    if j == priority == 4:  # '<', '>', '<=', '>='
151        result = '(%s)' % result
152
153    if nexttok == '?' and priority <= 0:
154        if_true, nexttok = _parse(tokens, 0)
155        if nexttok != ':':
156            raise _error(nexttok)
157        if_false, nexttok = _parse(tokens)
158        result = '%s if %s else %s' % (if_true, result, if_false)
159        if priority == 0:
160            result = '(%s)' % result
161
162    return result, nexttok
163
164def _as_int(n):
165    try:
166        i = round(n)
167    except TypeError:
168        raise TypeError('Plural value must be an integer, got %s' %
169                        (n.__class__.__name__,)) from None
170    import warnings
171    warnings.warn('Plural value must be an integer, got %s' %
172                  (n.__class__.__name__,),
173                  DeprecationWarning, 4)
174    return n
175
176def c2py(plural):
177    """Gets a C expression as used in PO files for plural forms and returns a
178    Python function that implements an equivalent expression.
179    """
180
181    if len(plural) > 1000:
182        raise ValueError('plural form expression is too long')
183    try:
184        result, nexttok = _parse(_tokenize(plural))
185        if nexttok:
186            raise _error(nexttok)
187
188        depth = 0
189        for c in result:
190            if c == '(':
191                depth += 1
192                if depth > 20:
193                    # Python compiler limit is about 90.
194                    # The most complex example has 2.
195                    raise ValueError('plural form expression is too complex')
196            elif c == ')':
197                depth -= 1
198
199        ns = {'_as_int': _as_int}
200        exec('''if True:
201            def func(n):
202                if not isinstance(n, int):
203                    n = _as_int(n)
204                return int(%s)
205            ''' % result, ns)
206        return ns['func']
207    except RecursionError:
208        # Recursion error can be raised in _parse() or exec().
209        raise ValueError('plural form expression is too complex')
210
211
212def _expand_lang(loc):
213    loc = locale.normalize(loc)
214    COMPONENT_CODESET   = 1 << 0
215    COMPONENT_TERRITORY = 1 << 1
216    COMPONENT_MODIFIER  = 1 << 2
217    # split up the locale into its base components
218    mask = 0
219    pos = loc.find('@')
220    if pos >= 0:
221        modifier = loc[pos:]
222        loc = loc[:pos]
223        mask |= COMPONENT_MODIFIER
224    else:
225        modifier = ''
226    pos = loc.find('.')
227    if pos >= 0:
228        codeset = loc[pos:]
229        loc = loc[:pos]
230        mask |= COMPONENT_CODESET
231    else:
232        codeset = ''
233    pos = loc.find('_')
234    if pos >= 0:
235        territory = loc[pos:]
236        loc = loc[:pos]
237        mask |= COMPONENT_TERRITORY
238    else:
239        territory = ''
240    language = loc
241    ret = []
242    for i in range(mask+1):
243        if not (i & ~mask):  # if all components for this combo exist ...
244            val = language
245            if i & COMPONENT_TERRITORY: val += territory
246            if i & COMPONENT_CODESET:   val += codeset
247            if i & COMPONENT_MODIFIER:  val += modifier
248            ret.append(val)
249    ret.reverse()
250    return ret
251
252
253
254class NullTranslations:
255    def __init__(self, fp=None):
256        self._info = {}
257        self._charset = None
258        self._output_charset = None
259        self._fallback = None
260        if fp is not None:
261            self._parse(fp)
262
263    def _parse(self, fp):
264        pass
265
266    def add_fallback(self, fallback):
267        if self._fallback:
268            self._fallback.add_fallback(fallback)
269        else:
270            self._fallback = fallback
271
272    def gettext(self, message):
273        if self._fallback:
274            return self._fallback.gettext(message)
275        return message
276
277    def lgettext(self, message):
278        import warnings
279        warnings.warn('lgettext() is deprecated, use gettext() instead',
280                      DeprecationWarning, 2)
281        if self._fallback:
282            with warnings.catch_warnings():
283                warnings.filterwarnings('ignore', r'.*\blgettext\b.*',
284                                        DeprecationWarning)
285                return self._fallback.lgettext(message)
286        if self._output_charset:
287            return message.encode(self._output_charset)
288        return message.encode(locale.getpreferredencoding())
289
290    def ngettext(self, msgid1, msgid2, n):
291        if self._fallback:
292            return self._fallback.ngettext(msgid1, msgid2, n)
293        if n == 1:
294            return msgid1
295        else:
296            return msgid2
297
298    def lngettext(self, msgid1, msgid2, n):
299        import warnings
300        warnings.warn('lngettext() is deprecated, use ngettext() instead',
301                      DeprecationWarning, 2)
302        if self._fallback:
303            with warnings.catch_warnings():
304                warnings.filterwarnings('ignore', r'.*\blngettext\b.*',
305                                        DeprecationWarning)
306                return self._fallback.lngettext(msgid1, msgid2, n)
307        if n == 1:
308            tmsg = msgid1
309        else:
310            tmsg = msgid2
311        if self._output_charset:
312            return tmsg.encode(self._output_charset)
313        return tmsg.encode(locale.getpreferredencoding())
314
315    def pgettext(self, context, message):
316        if self._fallback:
317            return self._fallback.pgettext(context, message)
318        return message
319
320    def npgettext(self, context, msgid1, msgid2, n):
321        if self._fallback:
322            return self._fallback.npgettext(context, msgid1, msgid2, n)
323        if n == 1:
324            return msgid1
325        else:
326            return msgid2
327
328    def info(self):
329        return self._info
330
331    def charset(self):
332        return self._charset
333
334    def output_charset(self):
335        import warnings
336        warnings.warn('output_charset() is deprecated',
337                      DeprecationWarning, 2)
338        return self._output_charset
339
340    def set_output_charset(self, charset):
341        import warnings
342        warnings.warn('set_output_charset() is deprecated',
343                      DeprecationWarning, 2)
344        self._output_charset = charset
345
346    def install(self, names=None):
347        import builtins
348        builtins.__dict__['_'] = self.gettext
349        if names is not None:
350            allowed = {'gettext', 'lgettext', 'lngettext',
351                       'ngettext', 'npgettext', 'pgettext'}
352            for name in allowed & set(names):
353                builtins.__dict__[name] = getattr(self, name)
354
355
356class GNUTranslations(NullTranslations):
357    # Magic number of .mo files
358    LE_MAGIC = 0x950412de
359    BE_MAGIC = 0xde120495
360
361    # The encoding of a msgctxt and a msgid in a .mo file is
362    # msgctxt + "\x04" + msgid (gettext version >= 0.15)
363    CONTEXT = "%s\x04%s"
364
365    # Acceptable .mo versions
366    VERSIONS = (0, 1)
367
368    def _get_versions(self, version):
369        """Returns a tuple of major version, minor version"""
370        return (version >> 16, version & 0xffff)
371
372    def _parse(self, fp):
373        """Override this method to support alternative .mo formats."""
374        # Delay struct import for speeding up gettext import when .mo files
375        # are not used.
376        from struct import unpack
377        filename = getattr(fp, 'name', '')
378        # Parse the .mo file header, which consists of 5 little endian 32
379        # bit words.
380        self._catalog = catalog = {}
381        self.plural = lambda n: int(n != 1) # germanic plural by default
382        buf = fp.read()
383        buflen = len(buf)
384        # Are we big endian or little endian?
385        magic = unpack('<I', buf[:4])[0]
386        if magic == self.LE_MAGIC:
387            version, msgcount, masteridx, transidx = unpack('<4I', buf[4:20])
388            ii = '<II'
389        elif magic == self.BE_MAGIC:
390            version, msgcount, masteridx, transidx = unpack('>4I', buf[4:20])
391            ii = '>II'
392        else:
393            raise OSError(0, 'Bad magic number', filename)
394
395        major_version, minor_version = self._get_versions(version)
396
397        if major_version not in self.VERSIONS:
398            raise OSError(0, 'Bad version number ' + str(major_version), filename)
399
400        # Now put all messages from the .mo file buffer into the catalog
401        # dictionary.
402        for i in range(0, msgcount):
403            mlen, moff = unpack(ii, buf[masteridx:masteridx+8])
404            mend = moff + mlen
405            tlen, toff = unpack(ii, buf[transidx:transidx+8])
406            tend = toff + tlen
407            if mend < buflen and tend < buflen:
408                msg = buf[moff:mend]
409                tmsg = buf[toff:tend]
410            else:
411                raise OSError(0, 'File is corrupt', filename)
412            # See if we're looking at GNU .mo conventions for metadata
413            if mlen == 0:
414                # Catalog description
415                lastk = None
416                for b_item in tmsg.split(b'\n'):
417                    item = b_item.decode().strip()
418                    if not item:
419                        continue
420                    # Skip over comment lines:
421                    if item.startswith('#-#-#-#-#') and item.endswith('#-#-#-#-#'):
422                        continue
423                    k = v = None
424                    if ':' in item:
425                        k, v = item.split(':', 1)
426                        k = k.strip().lower()
427                        v = v.strip()
428                        self._info[k] = v
429                        lastk = k
430                    elif lastk:
431                        self._info[lastk] += '\n' + item
432                    if k == 'content-type':
433                        self._charset = v.split('charset=')[1]
434                    elif k == 'plural-forms':
435                        v = v.split(';')
436                        plural = v[1].split('plural=')[1]
437                        self.plural = c2py(plural)
438            # Note: we unconditionally convert both msgids and msgstrs to
439            # Unicode using the character encoding specified in the charset
440            # parameter of the Content-Type header.  The gettext documentation
441            # strongly encourages msgids to be us-ascii, but some applications
442            # require alternative encodings (e.g. Zope's ZCML and ZPT).  For
443            # traditional gettext applications, the msgid conversion will
444            # cause no problems since us-ascii should always be a subset of
445            # the charset encoding.  We may want to fall back to 8-bit msgids
446            # if the Unicode conversion fails.
447            charset = self._charset or 'ascii'
448            if b'\x00' in msg:
449                # Plural forms
450                msgid1, msgid2 = msg.split(b'\x00')
451                tmsg = tmsg.split(b'\x00')
452                msgid1 = str(msgid1, charset)
453                for i, x in enumerate(tmsg):
454                    catalog[(msgid1, i)] = str(x, charset)
455            else:
456                catalog[str(msg, charset)] = str(tmsg, charset)
457            # advance to next entry in the seek tables
458            masteridx += 8
459            transidx += 8
460
461    def lgettext(self, message):
462        import warnings
463        warnings.warn('lgettext() is deprecated, use gettext() instead',
464                      DeprecationWarning, 2)
465        missing = object()
466        tmsg = self._catalog.get(message, missing)
467        if tmsg is missing:
468            if self._fallback:
469                return self._fallback.lgettext(message)
470            tmsg = message
471        if self._output_charset:
472            return tmsg.encode(self._output_charset)
473        return tmsg.encode(locale.getpreferredencoding())
474
475    def lngettext(self, msgid1, msgid2, n):
476        import warnings
477        warnings.warn('lngettext() is deprecated, use ngettext() instead',
478                      DeprecationWarning, 2)
479        try:
480            tmsg = self._catalog[(msgid1, self.plural(n))]
481        except KeyError:
482            if self._fallback:
483                return self._fallback.lngettext(msgid1, msgid2, n)
484            if n == 1:
485                tmsg = msgid1
486            else:
487                tmsg = msgid2
488        if self._output_charset:
489            return tmsg.encode(self._output_charset)
490        return tmsg.encode(locale.getpreferredencoding())
491
492    def gettext(self, message):
493        missing = object()
494        tmsg = self._catalog.get(message, missing)
495        if tmsg is missing:
496            if self._fallback:
497                return self._fallback.gettext(message)
498            return message
499        return tmsg
500
501    def ngettext(self, msgid1, msgid2, n):
502        try:
503            tmsg = self._catalog[(msgid1, self.plural(n))]
504        except KeyError:
505            if self._fallback:
506                return self._fallback.ngettext(msgid1, msgid2, n)
507            if n == 1:
508                tmsg = msgid1
509            else:
510                tmsg = msgid2
511        return tmsg
512
513    def pgettext(self, context, message):
514        ctxt_msg_id = self.CONTEXT % (context, message)
515        missing = object()
516        tmsg = self._catalog.get(ctxt_msg_id, missing)
517        if tmsg is missing:
518            if self._fallback:
519                return self._fallback.pgettext(context, message)
520            return message
521        return tmsg
522
523    def npgettext(self, context, msgid1, msgid2, n):
524        ctxt_msg_id = self.CONTEXT % (context, msgid1)
525        try:
526            tmsg = self._catalog[ctxt_msg_id, self.plural(n)]
527        except KeyError:
528            if self._fallback:
529                return self._fallback.npgettext(context, msgid1, msgid2, n)
530            if n == 1:
531                tmsg = msgid1
532            else:
533                tmsg = msgid2
534        return tmsg
535
536
537# Locate a .mo file using the gettext strategy
538def find(domain, localedir=None, languages=None, all=False):
539    # Get some reasonable defaults for arguments that were not supplied
540    if localedir is None:
541        localedir = _default_localedir
542    if languages is None:
543        languages = []
544        for envar in ('LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG'):
545            val = os.environ.get(envar)
546            if val:
547                languages = val.split(':')
548                break
549        if 'C' not in languages:
550            languages.append('C')
551    # now normalize and expand the languages
552    nelangs = []
553    for lang in languages:
554        for nelang in _expand_lang(lang):
555            if nelang not in nelangs:
556                nelangs.append(nelang)
557    # select a language
558    if all:
559        result = []
560    else:
561        result = None
562    for lang in nelangs:
563        if lang == 'C':
564            break
565        mofile = os.path.join(localedir, lang, 'LC_MESSAGES', '%s.mo' % domain)
566        if os.path.exists(mofile):
567            if all:
568                result.append(mofile)
569            else:
570                return mofile
571    return result
572
573
574
575# a mapping between absolute .mo file path and Translation object
576_translations = {}
577_unspecified = ['unspecified']
578
579def translation(domain, localedir=None, languages=None,
580                class_=None, fallback=False, codeset=_unspecified):
581    if class_ is None:
582        class_ = GNUTranslations
583    mofiles = find(domain, localedir, languages, all=True)
584    if not mofiles:
585        if fallback:
586            return NullTranslations()
587        from errno import ENOENT
588        raise FileNotFoundError(ENOENT,
589                                'No translation file found for domain', domain)
590    # Avoid opening, reading, and parsing the .mo file after it's been done
591    # once.
592    result = None
593    for mofile in mofiles:
594        key = (class_, os.path.abspath(mofile))
595        t = _translations.get(key)
596        if t is None:
597            with open(mofile, 'rb') as fp:
598                t = _translations.setdefault(key, class_(fp))
599        # Copy the translation object to allow setting fallbacks and
600        # output charset. All other instance data is shared with the
601        # cached object.
602        # Delay copy import for speeding up gettext import when .mo files
603        # are not used.
604        import copy
605        t = copy.copy(t)
606        if codeset is not _unspecified:
607            import warnings
608            warnings.warn('parameter codeset is deprecated',
609                          DeprecationWarning, 2)
610            if codeset:
611                with warnings.catch_warnings():
612                    warnings.filterwarnings('ignore', r'.*\bset_output_charset\b.*',
613                                            DeprecationWarning)
614                    t.set_output_charset(codeset)
615        if result is None:
616            result = t
617        else:
618            result.add_fallback(t)
619    return result
620
621
622def install(domain, localedir=None, codeset=_unspecified, names=None):
623    t = translation(domain, localedir, fallback=True, codeset=codeset)
624    t.install(names)
625
626
627
628# a mapping b/w domains and locale directories
629_localedirs = {}
630# a mapping b/w domains and codesets
631_localecodesets = {}
632# current global domain, `messages' used for compatibility w/ GNU gettext
633_current_domain = 'messages'
634
635
636def textdomain(domain=None):
637    global _current_domain
638    if domain is not None:
639        _current_domain = domain
640    return _current_domain
641
642
643def bindtextdomain(domain, localedir=None):
644    global _localedirs
645    if localedir is not None:
646        _localedirs[domain] = localedir
647    return _localedirs.get(domain, _default_localedir)
648
649
650def bind_textdomain_codeset(domain, codeset=None):
651    import warnings
652    warnings.warn('bind_textdomain_codeset() is deprecated',
653                  DeprecationWarning, 2)
654    global _localecodesets
655    if codeset is not None:
656        _localecodesets[domain] = codeset
657    return _localecodesets.get(domain)
658
659
660def dgettext(domain, message):
661    try:
662        t = translation(domain, _localedirs.get(domain, None))
663    except OSError:
664        return message
665    return t.gettext(message)
666
667def ldgettext(domain, message):
668    import warnings
669    warnings.warn('ldgettext() is deprecated, use dgettext() instead',
670                  DeprecationWarning, 2)
671    codeset = _localecodesets.get(domain)
672    try:
673        with warnings.catch_warnings():
674            warnings.filterwarnings('ignore', r'.*\bparameter codeset\b.*',
675                                    DeprecationWarning)
676            t = translation(domain, _localedirs.get(domain, None), codeset=codeset)
677    except OSError:
678        return message.encode(codeset or locale.getpreferredencoding())
679    with warnings.catch_warnings():
680        warnings.filterwarnings('ignore', r'.*\blgettext\b.*',
681                                DeprecationWarning)
682        return t.lgettext(message)
683
684def dngettext(domain, msgid1, msgid2, n):
685    try:
686        t = translation(domain, _localedirs.get(domain, None))
687    except OSError:
688        if n == 1:
689            return msgid1
690        else:
691            return msgid2
692    return t.ngettext(msgid1, msgid2, n)
693
694def ldngettext(domain, msgid1, msgid2, n):
695    import warnings
696    warnings.warn('ldngettext() is deprecated, use dngettext() instead',
697                  DeprecationWarning, 2)
698    codeset = _localecodesets.get(domain)
699    try:
700        with warnings.catch_warnings():
701            warnings.filterwarnings('ignore', r'.*\bparameter codeset\b.*',
702                                    DeprecationWarning)
703            t = translation(domain, _localedirs.get(domain, None), codeset=codeset)
704    except OSError:
705        if n == 1:
706            tmsg = msgid1
707        else:
708            tmsg = msgid2
709        return tmsg.encode(codeset or locale.getpreferredencoding())
710    with warnings.catch_warnings():
711        warnings.filterwarnings('ignore', r'.*\blngettext\b.*',
712                                DeprecationWarning)
713        return t.lngettext(msgid1, msgid2, n)
714
715
716def dpgettext(domain, context, message):
717    try:
718        t = translation(domain, _localedirs.get(domain, None))
719    except OSError:
720        return message
721    return t.pgettext(context, message)
722
723
724def dnpgettext(domain, context, msgid1, msgid2, n):
725    try:
726        t = translation(domain, _localedirs.get(domain, None))
727    except OSError:
728        if n == 1:
729            return msgid1
730        else:
731            return msgid2
732    return t.npgettext(context, msgid1, msgid2, n)
733
734
735def gettext(message):
736    return dgettext(_current_domain, message)
737
738def lgettext(message):
739    import warnings
740    warnings.warn('lgettext() is deprecated, use gettext() instead',
741                  DeprecationWarning, 2)
742    with warnings.catch_warnings():
743        warnings.filterwarnings('ignore', r'.*\bldgettext\b.*',
744                                DeprecationWarning)
745        return ldgettext(_current_domain, message)
746
747def ngettext(msgid1, msgid2, n):
748    return dngettext(_current_domain, msgid1, msgid2, n)
749
750def lngettext(msgid1, msgid2, n):
751    import warnings
752    warnings.warn('lngettext() is deprecated, use ngettext() instead',
753                  DeprecationWarning, 2)
754    with warnings.catch_warnings():
755        warnings.filterwarnings('ignore', r'.*\bldngettext\b.*',
756                                DeprecationWarning)
757        return ldngettext(_current_domain, msgid1, msgid2, n)
758
759
760def pgettext(context, message):
761    return dpgettext(_current_domain, context, message)
762
763
764def npgettext(context, msgid1, msgid2, n):
765    return dnpgettext(_current_domain, context, msgid1, msgid2, n)
766
767
768# dcgettext() has been deemed unnecessary and is not implemented.
769
770# James Henstridge's Catalog constructor from GNOME gettext.  Documented usage
771# was:
772#
773#    import gettext
774#    cat = gettext.Catalog(PACKAGE, localedir=LOCALEDIR)
775#    _ = cat.gettext
776#    print _('Hello World')
777
778# The resulting catalog object currently don't support access through a
779# dictionary API, which was supported (but apparently unused) in GNOME
780# gettext.
781
782Catalog = translation
783