Source code for aioxmpp.i18n

########################################################################
# File name: i18n.py
# This file is part of: aioxmpp
#
# LICENSE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program.  If not, see
# <http://www.gnu.org/licenses/>.
#
########################################################################
"""
:mod:`~aioxmpp.i18n` -- Helper functions for localizing text
############################################################

This module provides facilities to faciliate the internationalization of
applications using :mod:`aioxmpp`.

.. autoclass:: LocalizingFormatter

.. autoclass:: LocalizableString

Shorthand functions
===================

.. autofunction:: _

.. autofunction:: ngettext

"""

import numbers
import string

from datetime import datetime, timedelta, date, time

import babel
import babel.dates
import babel.numbers
import tzlocal


[docs]class LocalizingFormatter(string.Formatter): """ This is an alternative implementation on top of :class:`string.Formatter`. It is designed to work well with :mod:`babel`, which also means that some things work differently when compared with the default :class:`string.Formatter`. Most notably, all objects from :mod:`datetime` are handled without using their :meth:`__format__` method. Depending on their type, they are forwarded to the respective formatting method in :mod:`babel.dates`. +----------------------------+------------------------------------+-----------------+ |Type |Babel function |Timezone support | +============================+====================================+=================+ |:class:`~datetime.datetime` |:func:`babel.dates.format_datetime` |yes | +----------------------------+------------------------------------+-----------------+ |:class:`~datetime.timedelta`|:func:`babel.dates.format_timedelta`|no | +----------------------------+------------------------------------+-----------------+ |:class:`~datetime.date` |:func:`babel.dates.format_date` |no | +----------------------------+------------------------------------+-----------------+ |:class:`~datetime.time` |:func:`babel.dates.format_time` |no | +----------------------------+------------------------------------+-----------------+ If the format specification is empty, the default format as babel defines it is used. In addition to date and time formatting, numbers which use the ``n`` format type are also formatted with babel. If the format specificaton is empty (except for the trailing ``n``), :func:`babel.numbers.format_number` is used. Otherwise, the remainder of the format specification is passed as format to :func:`babel.numbers.format_decimal`. Examples:: >>> import pytz, babel, datetime, aioxmpp.i18n >>> tz = pytz.timezone("Europe/Berlin") >>> dt = datetime.datetime(year=2015, 5, 5, 15, 55, 55, tzinfo=tz) >>> fmt = aioxmpp.i18n.LocalizingFormatter(locale=babel.Locale("en_GB")) >>> fmt.format("{}", dt) '5 May 2015 15:55:55' >>> fmt.format("{:full}", dt) 'Tuesday, 5 May 2015 15:55:55 GMT+00:00' >>> fmt.format("{:##.###n}, 120.3) >>> fmt.format("{:##.###n}", 12.3) '12.3' >>> fmt.format("{:##.###;-(#)n}", -1.234) '-(1.234)' >>> fmt.format("{:n}", -10000) '-10,000' """ def __init__(self, locale=None, tzinfo=None): super().__init__() self.locale = locale if locale is not None else babel.default_locale() self.tzinfo = tzinfo if tzinfo is not None else tzlocal.get_localzone() def format_field(self, value, format_spec, locale=None, tzinfo=None): if tzinfo is None: tzinfo = self.tzinfo if locale is None: locale = self.locale if isinstance(value, datetime): if value.tzinfo is not None: value = tzinfo.normalize(value) if format_spec: return babel.dates.format_datetime(value, locale=locale, format=format_spec) else: return babel.dates.format_datetime(value, locale=locale) elif isinstance(value, timedelta): if format_spec: return babel.dates.format_timedelta(value, locale=locale, format=format_spec) else: return babel.dates.format_timedelta(value, locale=locale) elif isinstance(value, date): if format_spec: return babel.dates.format_date(value, locale=locale, format=format_spec) else: return babel.dates.format_date(value, locale=locale) elif isinstance(value, time): if format_spec: return babel.dates.format_time(value, locale=locale, format=format_spec) else: return babel.dates.format_time(value, locale=locale) elif isinstance(value, numbers.Real) and format_spec.endswith("n"): if len(format_spec) > 1: return babel.numbers.format_decimal(value, format=format_spec[:-1], locale=locale) else: return babel.numbers.format_number(value, locale=locale) else: return super().format_field(value, format_spec) def convert_field(self, value, conversion, locale=None, tzinfo=None): if conversion != "s": return super().convert_field(value, conversion) if locale is None: locale = self.locale if tzinfo is None: tzinfo = self.tzinfo if isinstance(value, datetime): return babel.dates.format_datetime( tzinfo.normalize(value), locale=locale) elif isinstance(value, timedelta): return babel.dates.format_timedelta(value, locale=locale) elif isinstance(value, date): return babel.dates.format_date(value, locale=locale) elif isinstance(value, time): return babel.dates.format_time( value, locale=locale) return super().convert_field(value, conversion)
[docs]class LocalizableString: """ This class can be used for lazily translated localizable strings. `singular` must be a :class:`str`. If `plural` is not set, the string will be localized using `gettext`; otherwise, `ngettext` will be used. The detailed process on localizing a string is described in the documentation of :meth:`localize`. Localizable strings compare equal if their `singular`, `plural` and `number_index` values all match. The :func:`str` of a localizable string is its singular string. The :func:`repr` depends on whether `plural` is set and refers to the usage of :func:`_` and :func:`ngettext`. The arguments are stored in attributes named like the arguments. :class:`LocalizableString` instances are immutable and hashable. Examples:: >>> import aioxmpp.i18n, pytz, babel, gettext >>> fmt = aioxmpp.i18n.LocalizingFormatter() >>> translator = gettext.NullTranslations() >>> s1 = aioxmpp.i18n.LocalizableString( ... "{count} thing", ... "{count} things", "count") >>> s1.localize(fmt, translator, count=1) '1 thing' >>> s1.localize(fmt, translator, count=10) '10 things' .. automethod:: localize """ __slots__ = ("_singular", "_plural", "_number_index") def __init__(self, singular, plural=None, number_index=None): if plural is None and number_index is not None: raise ValueError("plural is required if number_index is given") self._singular = singular self._plural = plural if plural is not None: if number_index is None: number_index = "0" self._number_index = str(number_index) else: self._number_index = None @property def singular(self): return self._singular @property def plural(self): return self._plural @property def number_index(self): return self._number_index def __eq__(self, other): if not isinstance(other, LocalizableString): return NotImplemented return (self.singular == other.singular and self.plural == other.plural and self.number_index == other.number_index) def __ne__(self, other): return not (self == other) def __hash__(self): return hash((self._singular, self._plural, self._number_index))
[docs] def localize(self, formatter, translator, *args, **kwargs): """ Localize and format the string using the given `formatter` and `translator`. The remaining args are passed to the :meth:`~LocalizingFormatter.format` method of the `formatter`. The `translator` must be an object supporting the :class:`gettext.NullTranslations` interface. If :attr:`plural` is not :data:`None`, the number which will be passed to the `ngettext` method of `translator` is first extracted from the `args` or `kwargs`, depending on :attr:`number_index`. The whole semantics of all three are described in :meth:`string.Formatter.get_field`, which is used by this method (:attr:`number_index` is passed as `field_name`). The value returned by :meth:`~string.Formatter.get_field` is then used as third argument to `ngettext`, while the others are sourced from :attr:`singular` and :attr:`plural`. If :attr:`plural` is :data:`None`, the `gettext` method of `translator` is used with :attr:`singular` as its only argument. After the translation step, the `formatter` is used with the translated string and `args` and `kwargs` to obtain a formatted version of the string which is then returned. All of this works best when using a :class:`LocalizingFormatter`. """ if self.plural is not None: n, _ = formatter.get_field(self.number_index, args, kwargs) translated = translator.ngettext(self.singular, self.plural, n) else: translated = translator.gettext(self.singular) return formatter.vformat(translated, args, kwargs)
def __str__(self): return self.singular def __repr__(self): if self.plural is not None: return "ngettext({!r}, {!r}, {!r})".format( self.singular, self.plural, self.number_index ) return "_({!r})".format(self.singular)
[docs]def _(s): """ Return a new singular :class:`LocalizableString` using `s` as singular form. """ return LocalizableString(s)
[docs]def ngettext(singular, plural, number_index): """ Return a new plural :class:`LocalizableString` with the given arguments; these are passed to the constructor of :class:`LocalizableString`. """ return LocalizableString(singular, plural, number_index)