123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617 |
- # -*- coding: utf-8 -*-
- """
- babel.support
- ~~~~~~~~~~~~~
- Several classes and functions that help with integrating and using Babel
- in applications.
- .. note: the code in this module is not used by Babel itself
- :copyright: (c) 2013-2021 by the Babel Team.
- :license: BSD, see LICENSE for more details.
- """
- import gettext
- import locale
- from babel.core import Locale
- from babel.dates import format_date, format_datetime, format_time, \
- format_timedelta
- from babel.numbers import format_number, format_decimal, format_currency, \
- format_percent, format_scientific
- from babel._compat import PY2, text_type, text_to_native
- class Format(object):
- """Wrapper class providing the various date and number formatting functions
- bound to a specific locale and time-zone.
- >>> from babel.util import UTC
- >>> from datetime import date
- >>> fmt = Format('en_US', UTC)
- >>> fmt.date(date(2007, 4, 1))
- u'Apr 1, 2007'
- >>> fmt.decimal(1.2345)
- u'1.234'
- """
- def __init__(self, locale, tzinfo=None):
- """Initialize the formatter.
- :param locale: the locale identifier or `Locale` instance
- :param tzinfo: the time-zone info (a `tzinfo` instance or `None`)
- """
- self.locale = Locale.parse(locale)
- self.tzinfo = tzinfo
- def date(self, date=None, format='medium'):
- """Return a date formatted according to the given pattern.
- >>> from datetime import date
- >>> fmt = Format('en_US')
- >>> fmt.date(date(2007, 4, 1))
- u'Apr 1, 2007'
- """
- return format_date(date, format, locale=self.locale)
- def datetime(self, datetime=None, format='medium'):
- """Return a date and time formatted according to the given pattern.
- >>> from datetime import datetime
- >>> from pytz import timezone
- >>> fmt = Format('en_US', tzinfo=timezone('US/Eastern'))
- >>> fmt.datetime(datetime(2007, 4, 1, 15, 30))
- u'Apr 1, 2007, 11:30:00 AM'
- """
- return format_datetime(datetime, format, tzinfo=self.tzinfo,
- locale=self.locale)
- def time(self, time=None, format='medium'):
- """Return a time formatted according to the given pattern.
- >>> from datetime import datetime
- >>> from pytz import timezone
- >>> fmt = Format('en_US', tzinfo=timezone('US/Eastern'))
- >>> fmt.time(datetime(2007, 4, 1, 15, 30))
- u'11:30:00 AM'
- """
- return format_time(time, format, tzinfo=self.tzinfo, locale=self.locale)
- def timedelta(self, delta, granularity='second', threshold=.85,
- format='long', add_direction=False):
- """Return a time delta according to the rules of the given locale.
- >>> from datetime import timedelta
- >>> fmt = Format('en_US')
- >>> fmt.timedelta(timedelta(weeks=11))
- u'3 months'
- """
- return format_timedelta(delta, granularity=granularity,
- threshold=threshold,
- format=format, add_direction=add_direction,
- locale=self.locale)
- def number(self, number):
- """Return an integer number formatted for the locale.
- >>> fmt = Format('en_US')
- >>> fmt.number(1099)
- u'1,099'
- """
- return format_number(number, locale=self.locale)
- def decimal(self, number, format=None):
- """Return a decimal number formatted for the locale.
- >>> fmt = Format('en_US')
- >>> fmt.decimal(1.2345)
- u'1.234'
- """
- return format_decimal(number, format, locale=self.locale)
- def currency(self, number, currency):
- """Return a number in the given currency formatted for the locale.
- """
- return format_currency(number, currency, locale=self.locale)
- def percent(self, number, format=None):
- """Return a number formatted as percentage for the locale.
- >>> fmt = Format('en_US')
- >>> fmt.percent(0.34)
- u'34%'
- """
- return format_percent(number, format, locale=self.locale)
- def scientific(self, number):
- """Return a number formatted using scientific notation for the locale.
- """
- return format_scientific(number, locale=self.locale)
- class LazyProxy(object):
- """Class for proxy objects that delegate to a specified function to evaluate
- the actual object.
- >>> def greeting(name='world'):
- ... return 'Hello, %s!' % name
- >>> lazy_greeting = LazyProxy(greeting, name='Joe')
- >>> print(lazy_greeting)
- Hello, Joe!
- >>> u' ' + lazy_greeting
- u' Hello, Joe!'
- >>> u'(%s)' % lazy_greeting
- u'(Hello, Joe!)'
- This can be used, for example, to implement lazy translation functions that
- delay the actual translation until the string is actually used. The
- rationale for such behavior is that the locale of the user may not always
- be available. In web applications, you only know the locale when processing
- a request.
- The proxy implementation attempts to be as complete as possible, so that
- the lazy objects should mostly work as expected, for example for sorting:
- >>> greetings = [
- ... LazyProxy(greeting, 'world'),
- ... LazyProxy(greeting, 'Joe'),
- ... LazyProxy(greeting, 'universe'),
- ... ]
- >>> greetings.sort()
- >>> for greeting in greetings:
- ... print(greeting)
- Hello, Joe!
- Hello, universe!
- Hello, world!
- """
- __slots__ = ['_func', '_args', '_kwargs', '_value', '_is_cache_enabled', '_attribute_error']
- def __init__(self, func, *args, **kwargs):
- is_cache_enabled = kwargs.pop('enable_cache', True)
- # Avoid triggering our own __setattr__ implementation
- object.__setattr__(self, '_func', func)
- object.__setattr__(self, '_args', args)
- object.__setattr__(self, '_kwargs', kwargs)
- object.__setattr__(self, '_is_cache_enabled', is_cache_enabled)
- object.__setattr__(self, '_value', None)
- object.__setattr__(self, '_attribute_error', None)
- @property
- def value(self):
- if self._value is None:
- try:
- value = self._func(*self._args, **self._kwargs)
- except AttributeError as error:
- object.__setattr__(self, '_attribute_error', error)
- raise
- if not self._is_cache_enabled:
- return value
- object.__setattr__(self, '_value', value)
- return self._value
- def __contains__(self, key):
- return key in self.value
- def __nonzero__(self):
- return bool(self.value)
- def __dir__(self):
- return dir(self.value)
- def __iter__(self):
- return iter(self.value)
- def __len__(self):
- return len(self.value)
- def __str__(self):
- return str(self.value)
- def __unicode__(self):
- return unicode(self.value)
- def __add__(self, other):
- return self.value + other
- def __radd__(self, other):
- return other + self.value
- def __mod__(self, other):
- return self.value % other
- def __rmod__(self, other):
- return other % self.value
- def __mul__(self, other):
- return self.value * other
- def __rmul__(self, other):
- return other * self.value
- def __call__(self, *args, **kwargs):
- return self.value(*args, **kwargs)
- def __lt__(self, other):
- return self.value < other
- def __le__(self, other):
- return self.value <= other
- def __eq__(self, other):
- return self.value == other
- def __ne__(self, other):
- return self.value != other
- def __gt__(self, other):
- return self.value > other
- def __ge__(self, other):
- return self.value >= other
- def __delattr__(self, name):
- delattr(self.value, name)
- def __getattr__(self, name):
- if self._attribute_error is not None:
- raise self._attribute_error
- return getattr(self.value, name)
- def __setattr__(self, name, value):
- setattr(self.value, name, value)
- def __delitem__(self, key):
- del self.value[key]
- def __getitem__(self, key):
- return self.value[key]
- def __setitem__(self, key, value):
- self.value[key] = value
- def __copy__(self):
- return LazyProxy(
- self._func,
- enable_cache=self._is_cache_enabled,
- *self._args,
- **self._kwargs
- )
- def __deepcopy__(self, memo):
- from copy import deepcopy
- return LazyProxy(
- deepcopy(self._func, memo),
- enable_cache=deepcopy(self._is_cache_enabled, memo),
- *deepcopy(self._args, memo),
- **deepcopy(self._kwargs, memo)
- )
- class NullTranslations(gettext.NullTranslations, object):
- DEFAULT_DOMAIN = None
- def __init__(self, fp=None):
- """Initialize a simple translations class which is not backed by a
- real catalog. Behaves similar to gettext.NullTranslations but also
- offers Babel's on *gettext methods (e.g. 'dgettext()').
- :param fp: a file-like object (ignored in this class)
- """
- # These attributes are set by gettext.NullTranslations when a catalog
- # is parsed (fp != None). Ensure that they are always present because
- # some *gettext methods (including '.gettext()') rely on the attributes.
- self._catalog = {}
- self.plural = lambda n: int(n != 1)
- super(NullTranslations, self).__init__(fp=fp)
- self.files = list(filter(None, [getattr(fp, 'name', None)]))
- self.domain = self.DEFAULT_DOMAIN
- self._domains = {}
- def dgettext(self, domain, message):
- """Like ``gettext()``, but look the message up in the specified
- domain.
- """
- return self._domains.get(domain, self).gettext(message)
- def ldgettext(self, domain, message):
- """Like ``lgettext()``, but look the message up in the specified
- domain.
- """
- return self._domains.get(domain, self).lgettext(message)
- def udgettext(self, domain, message):
- """Like ``ugettext()``, but look the message up in the specified
- domain.
- """
- return self._domains.get(domain, self).ugettext(message)
- # backward compatibility with 0.9
- dugettext = udgettext
- def dngettext(self, domain, singular, plural, num):
- """Like ``ngettext()``, but look the message up in the specified
- domain.
- """
- return self._domains.get(domain, self).ngettext(singular, plural, num)
- def ldngettext(self, domain, singular, plural, num):
- """Like ``lngettext()``, but look the message up in the specified
- domain.
- """
- return self._domains.get(domain, self).lngettext(singular, plural, num)
- def udngettext(self, domain, singular, plural, num):
- """Like ``ungettext()`` but look the message up in the specified
- domain.
- """
- return self._domains.get(domain, self).ungettext(singular, plural, num)
- # backward compatibility with 0.9
- dungettext = udngettext
- # Most of the downwards code, until it get's included in stdlib, from:
- # https://bugs.python.org/file10036/gettext-pgettext.patch
- #
- # The encoding of a msgctxt and a msgid in a .mo file is
- # msgctxt + "\x04" + msgid (gettext version >= 0.15)
- CONTEXT_ENCODING = '%s\x04%s'
- def pgettext(self, context, message):
- """Look up the `context` and `message` id in the catalog and return the
- corresponding message string, as an 8-bit string encoded with the
- catalog's charset encoding, if known. 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 ``pgettext()``
- method. Otherwise, the `message` id is returned.
- """
- ctxt_msg_id = self.CONTEXT_ENCODING % (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
- # Encode the Unicode tmsg back to an 8-bit string, if possible
- if self._output_charset:
- return text_to_native(tmsg, self._output_charset)
- elif self._charset:
- return text_to_native(tmsg, self._charset)
- return tmsg
- def lpgettext(self, context, message):
- """Equivalent to ``pgettext()``, but the translation is returned in the
- preferred system encoding, if no other encoding was explicitly set with
- ``bind_textdomain_codeset()``.
- """
- ctxt_msg_id = self.CONTEXT_ENCODING % (context, message)
- missing = object()
- tmsg = self._catalog.get(ctxt_msg_id, missing)
- if tmsg is missing:
- if self._fallback:
- return self._fallback.lpgettext(context, message)
- return message
- if self._output_charset:
- return tmsg.encode(self._output_charset)
- return tmsg.encode(locale.getpreferredencoding())
- def npgettext(self, context, singular, plural, num):
- """Do a plural-forms lookup of a message id. `singular` is used as the
- message id for purposes of lookup in the catalog, while `num` is used to
- determine which plural form to use. The returned message string is an
- 8-bit string encoded with the catalog's charset encoding, if known.
- 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
- ``npgettext()`` method. Otherwise, when ``num`` is 1 ``singular`` is
- returned, and ``plural`` is returned in all other cases.
- """
- ctxt_msg_id = self.CONTEXT_ENCODING % (context, singular)
- try:
- tmsg = self._catalog[(ctxt_msg_id, self.plural(num))]
- if self._output_charset:
- return text_to_native(tmsg, self._output_charset)
- elif self._charset:
- return text_to_native(tmsg, self._charset)
- return tmsg
- except KeyError:
- if self._fallback:
- return self._fallback.npgettext(context, singular, plural, num)
- if num == 1:
- return singular
- else:
- return plural
- def lnpgettext(self, context, singular, plural, num):
- """Equivalent to ``npgettext()``, but the translation is returned in the
- preferred system encoding, if no other encoding was explicitly set with
- ``bind_textdomain_codeset()``.
- """
- ctxt_msg_id = self.CONTEXT_ENCODING % (context, singular)
- try:
- tmsg = self._catalog[(ctxt_msg_id, self.plural(num))]
- if self._output_charset:
- return tmsg.encode(self._output_charset)
- return tmsg.encode(locale.getpreferredencoding())
- except KeyError:
- if self._fallback:
- return self._fallback.lnpgettext(context, singular, plural, num)
- if num == 1:
- return singular
- else:
- return plural
- def upgettext(self, 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 ``upgettext()``
- method. Otherwise, the `message` id is returned.
- """
- ctxt_message_id = self.CONTEXT_ENCODING % (context, message)
- missing = object()
- tmsg = self._catalog.get(ctxt_message_id, missing)
- if tmsg is missing:
- if self._fallback:
- return self._fallback.upgettext(context, message)
- return text_type(message)
- return tmsg
- def unpgettext(self, context, singular, plural, num):
- """Do a plural-forms lookup of a message id. `singular` is used as the
- message id for purposes of lookup in the catalog, while `num` is used to
- determine which plural form to use. The returned message string is a
- Unicode string.
- 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
- ``unpgettext()`` method. Otherwise, when `num` is 1 `singular` is
- returned, and `plural` is returned in all other cases.
- """
- ctxt_message_id = self.CONTEXT_ENCODING % (context, singular)
- try:
- tmsg = self._catalog[(ctxt_message_id, self.plural(num))]
- except KeyError:
- if self._fallback:
- return self._fallback.unpgettext(context, singular, plural, num)
- if num == 1:
- tmsg = text_type(singular)
- else:
- tmsg = text_type(plural)
- return tmsg
- def dpgettext(self, domain, context, message):
- """Like `pgettext()`, but look the message up in the specified
- `domain`.
- """
- return self._domains.get(domain, self).pgettext(context, message)
- def udpgettext(self, domain, context, message):
- """Like `upgettext()`, but look the message up in the specified
- `domain`.
- """
- return self._domains.get(domain, self).upgettext(context, message)
- # backward compatibility with 0.9
- dupgettext = udpgettext
- def ldpgettext(self, domain, context, message):
- """Equivalent to ``dpgettext()``, but the translation is returned in the
- preferred system encoding, if no other encoding was explicitly set with
- ``bind_textdomain_codeset()``.
- """
- return self._domains.get(domain, self).lpgettext(context, message)
- def dnpgettext(self, domain, context, singular, plural, num):
- """Like ``npgettext``, but look the message up in the specified
- `domain`.
- """
- return self._domains.get(domain, self).npgettext(context, singular,
- plural, num)
- def udnpgettext(self, domain, context, singular, plural, num):
- """Like ``unpgettext``, but look the message up in the specified
- `domain`.
- """
- return self._domains.get(domain, self).unpgettext(context, singular,
- plural, num)
- # backward compatibility with 0.9
- dunpgettext = udnpgettext
- def ldnpgettext(self, domain, context, singular, plural, num):
- """Equivalent to ``dnpgettext()``, but the translation is returned in
- the preferred system encoding, if no other encoding was explicitly set
- with ``bind_textdomain_codeset()``.
- """
- return self._domains.get(domain, self).lnpgettext(context, singular,
- plural, num)
- if not PY2:
- ugettext = gettext.NullTranslations.gettext
- ungettext = gettext.NullTranslations.ngettext
- class Translations(NullTranslations, gettext.GNUTranslations):
- """An extended translation catalog class."""
- DEFAULT_DOMAIN = 'messages'
- def __init__(self, fp=None, domain=None):
- """Initialize the translations catalog.
- :param fp: the file-like object the translation should be read from
- :param domain: the message domain (default: 'messages')
- """
- super(Translations, self).__init__(fp=fp)
- self.domain = domain or self.DEFAULT_DOMAIN
- if not PY2:
- ugettext = gettext.GNUTranslations.gettext
- ungettext = gettext.GNUTranslations.ngettext
- @classmethod
- def load(cls, dirname=None, locales=None, domain=None):
- """Load translations from the given directory.
- :param dirname: the directory containing the ``MO`` files
- :param locales: the list of locales in order of preference (items in
- this list can be either `Locale` objects or locale
- strings)
- :param domain: the message domain (default: 'messages')
- """
- if locales is not None:
- if not isinstance(locales, (list, tuple)):
- locales = [locales]
- locales = [str(locale) for locale in locales]
- if not domain:
- domain = cls.DEFAULT_DOMAIN
- filename = gettext.find(domain, dirname, locales)
- if not filename:
- return NullTranslations()
- with open(filename, 'rb') as fp:
- return cls(fp=fp, domain=domain)
- def __repr__(self):
- return '<%s: "%s">' % (type(self).__name__,
- self._info.get('project-id-version'))
- def add(self, translations, merge=True):
- """Add the given translations to the catalog.
- If the domain of the translations is different than that of the
- current catalog, they are added as a catalog that is only accessible
- by the various ``d*gettext`` functions.
- :param translations: the `Translations` instance with the messages to
- add
- :param merge: whether translations for message domains that have
- already been added should be merged with the existing
- translations
- """
- domain = getattr(translations, 'domain', self.DEFAULT_DOMAIN)
- if merge and domain == self.domain:
- return self.merge(translations)
- existing = self._domains.get(domain)
- if merge and existing is not None:
- existing.merge(translations)
- else:
- translations.add_fallback(self)
- self._domains[domain] = translations
- return self
- def merge(self, translations):
- """Merge the given translations into the catalog.
- Message translations in the specified catalog override any messages
- with the same identifier in the existing catalog.
- :param translations: the `Translations` instance with the messages to
- merge
- """
- if isinstance(translations, gettext.GNUTranslations):
- self._catalog.update(translations._catalog)
- if isinstance(translations, Translations):
- self.files.extend(translations.files)
- return self
|