import gettext
import os
from translationstring import Pluralizer, Translator
from translationstring import TranslationString # API
from translationstring import TranslationStringFactory # API
from pyramid.decorator import reify
from pyramid.interfaces import (
ILocaleNegotiator,
ILocalizer,
ITranslationDirectories,
)
from pyramid.threadlocal import get_current_registry
TranslationString = TranslationString # PyFlakes
TranslationStringFactory = TranslationStringFactory # PyFlakes
DEFAULT_PLURAL = lambda n: int(n != 1)
[docs]
class Localizer:
"""
An object providing translation and pluralizations related to
the current request's locale name. A
:class:`pyramid.i18n.Localizer` object is created using the
:func:`pyramid.i18n.get_localizer` function.
"""
def __init__(self, locale_name, translations):
self.locale_name = locale_name
self.translations = translations
self.pluralizer = None
self.translator = None
[docs]
def translate(self, tstring, domain=None, mapping=None):
"""
Translate a :term:`translation string` to the current language
and interpolate any *replacement markers* in the result. The
``translate`` method accepts three arguments: ``tstring``
(required), ``domain`` (optional) and ``mapping`` (optional).
When called, it will translate the ``tstring`` translation
string using the current locale. If the current locale could not be
determined, the result of interpolation of the default value is
returned. The optional ``domain`` argument can be used to specify
or override the domain of the ``tstring`` (useful when ``tstring``
is a normal string rather than a translation string). The optional
``mapping`` argument can specify or override the ``tstring``
interpolation mapping, useful when the ``tstring`` argument is
a simple string instead of a translation string.
Example::
from pyramid.i18n import TranslationString
ts = TranslationString('Add ${item}', domain='mypackage',
mapping={'item':'Item'})
translated = localizer.translate(ts)
Example::
translated = localizer.translate('Add ${item}', domain='mypackage',
mapping={'item':'Item'})
"""
if self.translator is None:
self.translator = Translator(self.translations)
return self.translator(tstring, domain=domain, mapping=mapping)
[docs]
def pluralize(self, singular, plural, n, domain=None, mapping=None):
"""
Return a string translation by using two
:term:`message identifier` objects as a singular/plural pair
and an ``n`` value representing the number that appears in the
message using gettext plural forms support. The ``singular``
and ``plural`` objects should be strings. There is no
reason to use translation string objects as arguments as all
metadata is ignored.
``n`` represents the number of elements. ``domain`` is the
translation domain to use to do the pluralization, and ``mapping``
is the interpolation mapping that should be used on the result. If
the ``domain`` is not supplied, a default domain is used (usually
``messages``).
Example::
num = 1
translated = localizer.pluralize('Add ${num} item',
'Add ${num} items',
num,
mapping={'num':num})
If using the gettext plural support, which is required for
languages that have pluralisation rules other than n != 1, the
``singular`` argument must be the message_id defined in the
translation file. The plural argument is not used in this case.
Example::
num = 1
translated = localizer.pluralize('item_plural',
'',
num,
mapping={'num':num})
"""
if self.pluralizer is None:
self.pluralizer = Pluralizer(self.translations)
return self.pluralizer(
singular, plural, n, domain=domain, mapping=mapping
)
[docs]
def default_locale_negotiator(request):
"""The default :term:`locale negotiator`. Returns a locale name
or ``None``.
- First, the negotiator looks for the ``_LOCALE_`` attribute of
the request object (possibly set by a view or a listener for an
:term:`event`). If the attribute exists and it is not ``None``,
its value will be used.
- Then it looks for the ``request.params['_LOCALE_']`` value.
- Then it looks for the ``request.cookies['_LOCALE_']`` value.
- Finally, the negotiator returns ``None`` if the locale could not
be determined via any of the previous checks (when a locale
negotiator returns ``None``, it signifies that the
:term:`default locale name` should be used.)
"""
name = '_LOCALE_'
locale_name = getattr(request, name, None)
if locale_name is None:
locale_name = request.params.get(name)
if locale_name is None:
locale_name = request.cookies.get(name)
return locale_name
[docs]
def negotiate_locale_name(request):
"""Negotiate and return the :term:`locale name` associated with
the current request."""
try:
registry = request.registry
except AttributeError:
registry = get_current_registry()
negotiator = registry.queryUtility(
ILocaleNegotiator, default=default_locale_negotiator
)
locale_name = negotiator(request)
if locale_name is None:
settings = registry.settings or {}
locale_name = settings.get('default_locale_name', 'en')
return locale_name
[docs]
def get_locale_name(request):
"""
.. deprecated:: 1.5
Use :attr:`pyramid.request.Request.locale_name` directly instead.
Return the :term:`locale name` associated with the current request.
"""
return request.locale_name
[docs]
def make_localizer(current_locale_name, translation_directories):
"""Create a :class:`pyramid.i18n.Localizer` object
corresponding to the provided locale name from the
translations found in the list of translation directories."""
translations = Translations()
translations._catalog = {}
locales_to_try = []
if '_' in current_locale_name:
locales_to_try = [current_locale_name.split('_')[0]]
locales_to_try.append(current_locale_name)
# intent: order locales left to right in least specific to most specific,
# e.g. ['de', 'de_DE']. This services the intent of creating a
# translations object that returns a "more specific" translation for a
# region, but will fall back to a "less specific" translation for the
# locale if necessary. Ordering from least specific to most specific
# allows us to call translations.add in the below loop to get this
# behavior.
for tdir in translation_directories:
locale_dirs = []
for lname in locales_to_try:
ldir = os.path.realpath(os.path.join(tdir, lname))
if os.path.isdir(ldir):
locale_dirs.append(ldir)
for locale_dir in locale_dirs:
messages_dir = os.path.join(locale_dir, 'LC_MESSAGES')
if not os.path.isdir(os.path.realpath(messages_dir)):
continue
for mofile in os.listdir(messages_dir):
mopath = os.path.realpath(os.path.join(messages_dir, mofile))
if mofile.endswith('.mo') and os.path.isfile(mopath):
with open(mopath, 'rb') as mofp:
domain = mofile[:-3]
dtrans = Translations(mofp, domain)
translations.add(dtrans)
return Localizer(
locale_name=current_locale_name, translations=translations
)
[docs]
def get_localizer(request):
"""
.. deprecated:: 1.5
Use the :attr:`pyramid.request.Request.localizer` attribute directly
instead. Retrieve a :class:`pyramid.i18n.Localizer` object
corresponding to the current request's locale name.
"""
return request.localizer
class Translations(gettext.GNUTranslations):
"""An extended translation catalog class (ripped off from Babel) """
DEFAULT_DOMAIN = 'messages'
def __init__(self, fileobj=None, domain=DEFAULT_DOMAIN):
"""Initialize the translations catalog.
:param fileobj: the file-like object the translation should be read
from
"""
# germanic plural by default; self.plural will be overwritten by
# GNUTranslations._parse (called as a side effect if fileobj is
# passed to GNUTranslations.__init__) with a "real" self.plural for
# this domain; see https://github.com/Pylons/pyramid/issues/235
# It is only overridden the first time a new message file is found
# for a given domain, so all message files must have matching plural
# rules if they are in the same domain. We keep track of if we have
# overridden so we can special case the default domain, which is always
# instantiated before a message file is read.
# See also https://github.com/Pylons/pyramid/pull/2102
self.plural = DEFAULT_PLURAL
gettext.GNUTranslations.__init__(self, fp=fileobj)
self.files = list(filter(None, [getattr(fileobj, 'name', None)]))
self.domain = domain
self._domains = {}
@classmethod
def load(cls, dirname=None, locales=None, domain=DEFAULT_DOMAIN):
"""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
:return: the loaded catalog, or a ``NullTranslations`` instance if no
matching translations were found
:rtype: `Translations`
"""
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 gettext.NullTranslations()
with open(filename, 'rb') as fp:
return cls(fileobj=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
:return: the `Translations` instance (``self``) so that `merge` calls
can be easily chained
:rtype: `Translations`
"""
domain = getattr(translations, 'domain', self.DEFAULT_DOMAIN)
if domain == self.DEFAULT_DOMAIN and self.plural is DEFAULT_PLURAL:
self.plural = translations.plural
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
:return: the `Translations` instance (``self``) so that `merge` calls
can be easily chained
:rtype: `Translations`
"""
if isinstance(translations, gettext.GNUTranslations):
self._catalog.update(translations._catalog)
if isinstance(translations, Translations):
self.files.extend(translations.files)
return self
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 dugettext(self, domain, message):
"""Like ``ugettext()``, but look the message up in the specified
domain.
"""
return self._domains.get(domain, self).gettext(message)
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 dungettext(self, domain, singular, plural, num):
"""Like ``ungettext()`` but look the message up in the specified
domain.
"""
return self._domains.get(domain, self).ngettext(singular, plural, num)
class LocalizerRequestMixin:
@reify
def localizer(self):
""" Convenience property to return a localizer """
registry = self.registry
current_locale_name = self.locale_name
localizer = registry.queryUtility(ILocalizer, name=current_locale_name)
if localizer is None:
# no localizer utility registered yet
tdirs = registry.queryUtility(ITranslationDirectories, default=[])
localizer = make_localizer(current_locale_name, tdirs)
registry.registerUtility(
localizer, ILocalizer, name=current_locale_name
)
return localizer
@reify
def locale_name(self):
locale_name = negotiate_locale_name(self)
return locale_name