import gettext
import functools
from warnings import warn
from bottle import (redirect,
request,
response,
template,
BaseTemplate,
DictMixin)
from .lazy import lazy
from .html import quoted_url
from .common import to_unicode
CONTEXT_SEPARATOR = '\x04'
[docs]def dummy_gettext(message):
"""
Mimic ``gettext()`` function. This is a passthrough function with the same
signature as ``gettext()``. It can be used to simulate translation for
applications that are untranslated, without the overhead of calling the
real ``gettext()``.
"""
return message
[docs]def dummy_ngettext(singular, plural, n):
"""
Mimic ``ngettext()`` function. This is a passthrough function with the
same signature as ``ngettext()``. It can be used to simulate translation
for applications that are untranslated, without the overhead of calling the
real ``ngettext()``.
This function returns the verbatim singular message if ``n`` is 1,
otherwise the verbatim plural message.
"""
if n == 1:
return singular
return plural
[docs]def dummy_pgettext(context, message):
"""
Mimic ``pgettext()`` function. This is a passthrough function with the same
signature as ``pgettext()``. It can be used to simulate translation for
applications that are untranslated, without the overhead of calling the
real ``pgettext()`.
"""
return dummy_gettext(message)
[docs]def dummy_npgettext(context, singular, plural, n):
"""
Mimic ``npgettext()`` function. This is a passthrough function with teh
same signature as ``npgettext()``. It can be used to simulate translation
for applications that are untranslated, without the overhead of calling the
real ``npgettext()`` function.
"""
return dummy_ngettext(singular, plural, n)
@lazy
[docs]def lazy_gettext(message):
"""
Lazily evaluated version of ``gettext()``.
This function uses the appropriate Gettext API object based on the value of
``bottle.request.gettext`` set by the plugin. It will fail with
``AttributeError`` exception if the plugin is not installed.
"""
gettext = request.gettext.gettext
return to_unicode(gettext(message))
@lazy
[docs]def lazy_ngettext(singular, plural, n):
"""
Lazily evaluated version of ``ngettext()``.
This function uses the appropriate Gettext API object based on the value of
``bottle.request.gettext`` set by the plugin. It will fail with
``AttributeError`` exception if the plugin is not installed.
"""
ngettext = request.gettext.ngettext
return to_unicode(ngettext(singular, plural, n))
[docs]def lazy_pgettext(context, message):
"""
:py:func:`~lazy_gettext` wrapper with message context.
This function is a wrapper around :py:func:`~lazy_gettext` that provides
message context. It is useful in situations where short messages (usually
one word) are used in several different contexts for which separate
translations may be needed in different languages.
The function itself is not lazily evaluated, but its return value comes
from ``lazy_gettext()`` call, and it is effectively lazy as a result.
"""
message = '%s%s%s' % (context, CONTEXT_SEPARATOR, message)
return lazy_gettext(message)
[docs]def lazy_npgettext(context, singular, plural, n):
"""
:py:func:`bottle_utils.i18n.lazy_ngettext` wrapper with message context.
This function is a wrapper around
:py:func:`bottle_utils.i18n.lazy_ngettext`
that provides message context. It is useful in situations where messages
are used in several different contexts for which separate translations may
be required for different languages.
The function itself is not lazy, but it returns the return value of
``lazy_ngettext()``, and it is effectively lazy. Hence the name.
"""
singular = '%s%s%s' % (context, CONTEXT_SEPARATOR, singular)
plural = '%s%s%s' % (context, CONTEXT_SEPARATOR, plural)
return lazy_ngettext(singular, plural, n)
[docs]def full_path():
"""
Calculate full path including query string for current request. This is a
helper function used by :py:func:`~i18n_path`. It uses the current request
context to obtain information about the path.
"""
path = request.fullpath
qs = request.query_string
if qs:
return '%s?%s' % (path, qs)
return path
@lazy
[docs]def i18n_path(path=None, locale=None):
"""
Return current request path or specified path for given or current locale.
This function can be used to obtain paths for different locales.
If no ``path`` argument is passed, the :py:func:`~full_path` is called to
obtain the full path for current request.
If ``locale`` argument is omitted, current locale is used.
"""
path = path or full_path()
locale = locale or request.locale
if not locale:
# This is a bit unexpected, but it obviously can happen
return path
return '/{}{}'.format(locale, path)
@lazy
[docs]def i18n_url(route, **params):
"""
Return a named route in localized form. This function is a light wrapper
around Bottle's ``get_url()`` function. It passes the result to
:py:func:`~i18n_path`.
If ``locale`` keyword argument is passed, it will be used instead of the
currently selected locale.
"""
locale = params.pop('locale', request.locale)
path = quoted_url(route, **params)
return i18n_path(path, locale=locale)
[docs]def i18n_view(tpl_base_name=None, **defaults):
"""
Renders a template with locale name as suffix. Unlike the normal view
decorator, the template name should not have an extension. The locale names
are appended to the base template name using underscore ('_') as separator,
and lower-case locale identifier.
Any additional keyword arguments are used as default template variables.
For example::
@i18n_view('foo')
def render_foo():
# Renders 'foo_en' for English locale, 'foo_fr' for French, etc.
return
"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
locale = request.locale
tpl_name = '%s_%s' % (tpl_base_name, locale.lower())
except AttributeError:
tpl_name = tpl_base_name
tplvars = defaults.copy()
result = func(*args, **kwargs)
if isinstance(result, (dict, DictMixin)):
tplvars.update(result)
return template(tpl_name, **tplvars)
elif result is None:
return template(tpl_name, **tplvars)
return result
return wrapper
return decorator
class I18NWarning(RuntimeWarning):
pass
[docs]class I18NPlugin(object):
"""
Bottle plugin and WSGI middleware for handling i18n routes. This class is
a middleware. However, if the ``app`` argument is a ``Bottle`` object
(bottle app), it will also install itself as a plugin. The plugin follows
the `version 2 API <http://bottlepy.org/docs/0.12/plugindev.html>`_ and
implements the :py:meth:`~apply` method which applies the plugin to all
routes. The plugin and middleware parts were merged into one class because
they depend on each other and can't really be used separately.
During initialization, the class will set up references to locales,
directory paths, and build a mapping between locale names and appropriate
gettext translation APIs. The translation APIs are created using the
``gettext.translation()`` call. This call tries to access matching .mo file
in the locale directory, and will emit a warning if such file is not found.
If a .mo file does not exist for a given locale, or it is not readable, the
API for that locale will be downgraded to generic `gettext API
<https://docs.python.org/3.4/library/gettext.html>`_.
The class will also update the ``bottle.BaseTemplate.defaults`` dict with
translation-related methods so they are always available in templates (at
least those that are rendered using bottle's API. The following variables
become available in all templates:
- ``_``: alias for :py:func:`~lazy_gettext`
- ``gettext``: alias for :py:func:`~lazy_gettext`
- ``ngettext``: alias for :py:func:`~lazy_ngettext`
- ``pgettext``: alias for :py:func:`~lazy_pgettext`
- ``npgettext``: alias for :py:func:`~lazy_pngettext`
- ``languages``: iterable containing available languages as ``(locale,
name)`` tuples
In addition, two functions for generating i18n-specific paths are added to
the default context:
- :py:func:`~i18n_path`
- :py:func:`~i18n_url`
The middleware itself derives the desired locale from the URL. It does not
read cookies or headers. It only looks for the ``/ll_cc/`` prefix where
``ll`` is the two-ltter language ID, and ``cc`` is country code. If it
finds such a prefix, it will set the locale in the envionment dict
(``LOCALE`` key) and fix the path so it doesn't include the prefix. This
allows the bottle app to have routes matching any number of locales. If it
doesn't find the prefix, it will redirect to the default locale.
If there is no appropriate locale, and ``LOCALE`` key is therfore set to
``None``, the plugin will automatically respond with a 302 redirect to a
location of the default locale.
The plugin reads the ``LOCALE`` key set by the middleware, and aliases the
API for that locale as ``request.gettext``. It also sets ``request.locale``
attribute to the selected locale. These attributes are used by the
:py:func:`~lazy_gettext`` and :py:func:`~lazy_ngettext`, as well as
:py:func:`~i18n_path` and :py:func:`~i18n_url` functions.
The plugin installation during initialization can be competely suppressed,
if you wish (e.g., you wish to apply the plugin yourself some other way).
The locale directory should be in a format which ``gettext.translations()``
understands. This is a path that contains a subtree matching this format::
locale_dir/LANG/LC_MESSAGES/DOMAIN.mo
The ``LANG`` should match any of the supported languages, and ``DOMAIN``
should match the specified domain.
"""
# Bottle plugin name
name = 'i18n'
# Bottle plugin API version
api = 2
def __init__(self, app, langs, default_locale, locale_dir,
domain='messages', noplugin=False):
# The original bottle application object is accessible as ``app``
# attribute after initialization.
self.app = app
# Supported languages as iterable of `(locale, native_name)` tuples.
self.langs = langs
# Supported locales (calculated based on ``langs`` iterable).
self.locales = [lang[0] for lang in langs]
# Default locale.
self.default_locale = default_locale
# Directory that stores ``.po`` and ``.mo`` files.
self.locale_dir = locale_dir
# Domain of the translation.
self.domain = domain
# A dictionary that maps locales to ``gettext.translation()`` objects
# for each locale. Appropriate API object is selected from each
self.gettext_apis = {}
# Prepare gettext class-based APIs for consumption
for locale in self.locales:
try:
api = gettext.translation(domain, locale_dir,
languages=[locale])
except OSError:
api = gettext
warn(I18NWarning("No MO file found for '%s' locale" % locale))
self.gettext_apis[locale] = api
# Provide translation methods to templates
BaseTemplate.defaults.update({
'_': lazy_gettext,
'gettext': lazy_gettext,
'ngettext': lazy_ngettext,
'pgettext': lazy_pgettext,
'npgettext': lazy_npgettext,
'i18n_path': i18n_path,
'i18n_url': i18n_url,
'languages': langs,
})
if noplugin:
return
try:
self.install_plugin()
except AttributeError:
# It's not strictly necessary to install the plugin automatically
# like this, especially if there are other WSGI middleware in the
# stack. We should still warn. It may be unintentional.
warn(I18NWarning('I18NPlugin: Not a bottle app. Skipping '
'plugin installation.'))
def __call__(self, e, h):
path = e['PATH_INFO']
e['LOCALE'] = locale = self.match_locale(path)
e['ORIGINAL_PATH'] = path
if locale:
e['PATH_INFO'] = self.strip_prefix(path, locale)
return self.app(e, h)
def install_plugin(self, app=None):
app = app or self.app
app.install(self)
def apply(self, callback, route):
try:
ignored = route.config.get('no_i18n', False)
except AttributeError:
ignored = False
def wrapper(*args, **kwargs):
request.original_path = request.environ.get('ORIGINAL_PATH',
request.fullpath)
query_string = request.environ.get('QUERY_STRING')
if query_string:
request.original_path = '{0}?{1}'.format(request.original_path,
query_string)
default_locale = request.get_cookie('locale', self.default_locale)
if not ignored:
request.default_locale = default_locale
request.locale = locale = request.environ.get('LOCALE')
if locale:
response.set_cookie('locale', locale, path='/')
if locale not in self.locales:
# If no locale had been specified, redirect to default one
path = request.original_path
redirect(i18n_path(path, default_locale))
else:
request.gettext = self.gettext_apis[locale]
else:
# Dummy translation is used for paths which are excepted from
# i18n plugin.
request.gettext = gettext.NullTranslations()
request.locale = default_locale
return callback(*args, **kwargs)
return wrapper
[docs] def match_locale(self, path):
"""
Match the locale based on prefix in request path. You can customize
this method for a different way of obtaining locale information.
Returning ``None`` from this method causes the plugin to use the
default locale.
The return value of this method is stored in the environment dictionary
as ``LOCALE`` key. It is then used by the plugin part of this class to
provide translation methods to the rest of the app.
"""
path_prefix = path.split('/')[1].lower()
for locale in self.locales:
if path_prefix == locale.lower():
return locale
return None
@staticmethod
[docs] def strip_prefix(path, locale):
"""
Strip the locale prefix from the path. This static method is used to
recalculate the request path that should be passed to Bottle. The
return value of this method replaces the ``PATH_INFO`` key in the
environment dictionary, and the original path is saved in
``ORIGINAL_PATH`` key.
"""
return path[len(locale) + 1:]
[docs] def set_locale(self, locale):
"""
Store the passed in ``locale`` in a 'locale' cookie, which is used to
override the value of the global ``default_locale``.
"""
request.locale = locale
response.set_cookie('locale', locale, path='/')