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