"""Internationalization and localization support.
This module provides internationalization (I18N) and localization (L10N)
support for your Python programs by providing an interface to the GNU gettext
I18N refers to the operation by which a program is made aware of multiple
languages. L10N refers to the adaptation of your program, once
internationalized, to the local language and cultural habits.
# This module represents the integration of work, contributions, feedback, and
# suggestions from the following people:
# Martin von Loewis, who wrote the initial implementation of the underlying
# C-based libintlmodule (later renamed _gettext), along with a skeletal
# gettext.py implementation.
# Peter Funk, who wrote fintl.py, a fairly complete wrapper around intlmodule,
# which also included a pure-Python implementation to read .mo files if
# intlmodule wasn't available.
# James Henstridge, who also wrote a gettext.py module, which has some
# interesting, but currently unsupported experimental features: the notion of
# a Catalog class and instances, and the ability to add to a catalog file via
# Barry Warsaw integrated these modules, wrote the .install() API and code,
# and conformed all C and Python code to Python's coding standards.
# Francois Pinard and Marc-Andre Lemburg also contributed valuably to this
# J. David Ibanez implemented plural forms. Bruno Haible fixed some bugs.
# - Lazy loading of .mo files. Currently the entire catalog is loaded into
# memory, but that's probably bad for large translated programs. Instead,
# the lexical sort of original strings in GNU .mo files should be exploited
# to do binary searches and lazy initializations. Or you might want to use
# the undocumented double-hash algorithm for .mo files with hash tables, but
# you'll need to study the GNU gettext code to do this.
# - Support Solaris .mo file formats. Unfortunately, we've been unable to
# find this format documented anywhere.
import locale, copy, io, os, re, struct, sys
__all__ = ['NullTranslations', 'GNUTranslations', 'Catalog',
'find', 'translation', 'install', 'textdomain', 'bindtextdomain',
'bind_textdomain_codeset',
'dgettext', 'dngettext', 'gettext', 'lgettext', 'ldgettext',
'ldngettext', 'lngettext', 'ngettext',
_default_localedir = os.path.join(sys.base_prefix, 'share', 'locale')
# Expression parsing for plural form selection.
# The gettext library supports a small subset of C syntax. The only
# incompatible difference is that integer literals starting with zero are
# https://www.gnu.org/software/gettext/manual/gettext.html#Plural-forms
# http://git.savannah.gnu.org/cgit/gettext.git/tree/gettext-runtime/intl/plural.y
_token_pattern = re.compile(r"""
(?P<WHITESPACES>[ \t]+) | # spaces and horizontal tabs
(?P<NUMBER>[0-9]+\b) | # decimal integer
(?P<NAME>n\b) | # only n is allowed
(?P<OPERATOR>[-*/%+?:]|[><!]=?|==|&&|\|\|) | # !, *, /, %, +, -, <, >,
# <=, >=, ==, !=, &&, ||,
(?P<INVALID>\w+|.) # invalid token
""", re.VERBOSE|re.DOTALL)
for mo in re.finditer(_token_pattern, plural):
if kind == 'WHITESPACES':
raise ValueError('invalid token in plural form: %s' % value)
return ValueError('unexpected token in plural form: %s' % value)
return ValueError('unexpected end of plural form')
_binary_ops = {op: i for i, ops in enumerate(_binary_ops, 1) for op in ops}
_c2py_ops = {'||': 'or', '&&': 'and', '/': '//'}
def _parse(tokens, priority=-1):
sub, nexttok = _parse(tokens)
result = '%s(%s)' % (result, sub)
raise ValueError('unbalanced parenthesis in plural form')
result = '%s%s' % (result, nexttok)
raise _error(nexttok) from None
result = '%s%d' % (result, value)
while nexttok in _binary_ops:
# Break chained comparisons
if i in (3, 4) and j in (3, 4): # '==', '!=', '<', '>', '<=', '>='
# Replace some C operators by their Python equivalents
op = _c2py_ops.get(nexttok, nexttok)
right, nexttok = _parse(tokens, i + 1)
result = '%s %s %s' % (result, op, right)
if j == priority == 4: # '<', '>', '<=', '>='
if nexttok == '?' and priority <= 0:
if_true, nexttok = _parse(tokens, 0)
if_false, nexttok = _parse(tokens)
result = '%s if %s else %s' % (if_true, result, if_false)
raise TypeError('Plural value must be an integer, got %s' %
(n.__class__.__name__,)) from None
"""Gets a C expression as used in PO files for plural forms and returns a
Python function that implements an equivalent expression.
raise ValueError('plural form expression is too long')
result, nexttok = _parse(_tokenize(plural))
# Python compiler limit is about 90.
# The most complex example has 2.
raise ValueError('plural form expression is too complex')
ns = {'_as_int': _as_int}
if not isinstance(n, int):
# Recursion error can be raised in _parse() or exec().
raise ValueError('plural form expression is too complex')
loc = locale.normalize(loc)
COMPONENT_CODESET = 1 << 0
COMPONENT_TERRITORY = 1 << 1
COMPONENT_MODIFIER = 1 << 2
# split up the locale into its base components
mask |= COMPONENT_MODIFIER
mask |= COMPONENT_CODESET
mask |= COMPONENT_TERRITORY
if not (i & ~mask): # if all components for this combo exist ...
if i & COMPONENT_TERRITORY: val += territory
if i & COMPONENT_CODESET: val += codeset
if i & COMPONENT_MODIFIER: val += modifier
def __init__(self, fp=None):
self._output_charset = None
def add_fallback(self, fallback):
self._fallback.add_fallback(fallback)
self._fallback = fallback
def gettext(self, message):
return self._fallback.gettext(message)
def lgettext(self, message):
return self._fallback.lgettext(message)
return message.encode(self._output_charset)
return message.encode(locale.getpreferredencoding())
def ngettext(self, msgid1, msgid2, n):
return self._fallback.ngettext(msgid1, msgid2, n)
def lngettext(self, msgid1, msgid2, n):
return self._fallback.lngettext(msgid1, msgid2, n)
return tmsg.encode(self._output_charset)
return tmsg.encode(locale.getpreferredencoding())
def output_charset(self):
return self._output_charset
def set_output_charset(self, charset):
self._output_charset = charset
def install(self, names=None):
builtins.__dict__['_'] = self.gettext
if hasattr(names, "__contains__"):
builtins.__dict__['gettext'] = builtins.__dict__['_']
builtins.__dict__['ngettext'] = self.ngettext
builtins.__dict__['lgettext'] = self.lgettext
builtins.__dict__['lngettext'] = self.lngettext
class GNUTranslations(NullTranslations):
# Magic number of .mo files
# Acceptable .mo versions
def _get_versions(self, version):
"""Returns a tuple of major version, minor version"""
return (version >> 16, version & 0xffff)
"""Override this method to support alternative .mo formats."""
filename = getattr(fp, 'name', '')
# Parse the .mo file header, which consists of 5 little endian 32
self._catalog = catalog = {}
self.plural = lambda n: int(n != 1) # germanic plural by default
# Are we big endian or little endian?
magic = unpack('<I', buf[:4])[0]
if magic == self.LE_MAGIC:
version, msgcount, masteridx, transidx = unpack('<4I', buf[4:20])
elif magic == self.BE_MAGIC:
version, msgcount, masteridx, transidx = unpack('>4I', buf[4:20])
raise OSError(0, 'Bad magic number', filename)
major_version, minor_version = self._get_versions(version)
if major_version not in self.VERSIONS:
raise OSError(0, 'Bad version number ' + str(major_version), filename)
# Now put all messages from the .mo file buffer into the catalog
for i in range(0, msgcount):
mlen, moff = unpack(ii, buf[masteridx:masteridx+8])
tlen, toff = unpack(ii, buf[transidx:transidx+8])
if mend < buflen and tend < buflen:
raise OSError(0, 'File is corrupt', filename)
# See if we're looking at GNU .mo conventions for metadata
for b_item in tmsg.split(b'\n'):
item = b_item.decode().strip()
k, v = item.split(':', 1)
self._info[lastk] += '\n' + item
self._charset = v.split('charset=')[1]
elif k == 'plural-forms':
plural = v[1].split('plural=')[1]
self.plural = c2py(plural)
# Note: we unconditionally convert both msgids and msgstrs to
# Unicode using the character encoding specified in the charset
# parameter of the Content-Type header. The gettext documentation
# strongly encourages msgids to be us-ascii, but some applications
# require alternative encodings (e.g. Zope's ZCML and ZPT). For
# traditional gettext applications, the msgid conversion will
# cause no problems since us-ascii should always be a subset of
# the charset encoding. We may want to fall back to 8-bit msgids
# if the Unicode conversion fails.
charset = self._charset or 'ascii'
msgid1, msgid2 = msg.split(b'\x00')
tmsg = tmsg.split(b'\x00')
msgid1 = str(msgid1, charset)
for i, x in enumerate(tmsg):
catalog[(msgid1, i)] = str(x, charset)
catalog[str(msg, charset)] = str(tmsg, charset)
# advance to next entry in the seek tables
def lgettext(self, message):
tmsg = self._catalog.get(message, missing)
return self._fallback.lgettext(message)
return tmsg.encode(self._output_charset)
return tmsg.encode(locale.getpreferredencoding())
def lngettext(self, msgid1, msgid2, n):
tmsg = self._catalog[(msgid1, self.plural(n))]
return self._fallback.lngettext(msgid1, msgid2, n)
return tmsg.encode(self._output_charset)
return tmsg.encode(locale.getpreferredencoding())
def gettext(self, message):
tmsg = self._catalog.get(message, missing)
return self._fallback.gettext(message)
def ngettext(self, msgid1, msgid2, n):
tmsg = self._catalog[(msgid1, self.plural(n))]
return self._fallback.ngettext(msgid1, msgid2, n)
# Locate a .mo file using the gettext strategy
def find(domain, localedir=None, languages=None, all=False):
# Get some reasonable defaults for arguments that were not supplied
localedir = _default_localedir
for envar in ('LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG'):
val = os.environ.get(envar)
languages = val.split(':')
# now normalize and expand the languages
for nelang in _expand_lang(lang):
if nelang not in nelangs:
mofile = os.path.join(localedir, lang, 'LC_MESSAGES', '%s.mo' % domain)
if os.path.exists(mofile):