CherryPy Project Download

Tool to integrate babel (localization and gettext translations) in CherryPy.

# -*- coding: utf-8 -*-

"""Internationalization and Localization for CherryPy

This tool provides locales and loads translations based on the
HTTP-ACCEPT-LANGUAGE header. If no header is send or the given language
is not supported by the application, it falls back to
`tools.I18nTool.default`. Set `default` to the native language used in your
code for strings, so you must not provide a .mo file for it.

The tool uses `babel<http://babel.edgewall.org>`_ for localization and
handling translations. Within your Python code you can use four functions
defined in this module and the loaded locale provided as
`cherrypy.response.i18n.locale`.

Example::

    from i18n_tool import ugettext as _, ungettext

    class MyController(object):
        @cherrypy.expose
        def index(self):
            loc = cherrypy.response.i18n.locale
            s1 = _(u'Translateable string')
            s2 = ungettext(u'There is one string.',
                           u'There are more strings.', 2)
            return u'<br />'.join([s1, s2, loc.display_name])

If you have code (e.g. database models) that is executed before the response
object is available, use the *_lazy functions to mark the strings
translateable. They will be translated later on, when the text is used (and
hopefully the response object is available then).

Example::

    from i18n_tool import ugettext_lazy

    class Model:
        def __init__(self):
            name = ugettext_lazy(u'Name of the model')

For your templates read the documentation of your template engine how to
integrate babel with it. I think `Genshi<http://genshi.edgewall.org>`_ and
`Jinja 2<http://jinja.pocoo.org`_ support it out of the box.


Settings for the CherryPy configuration::

    [/]
    tools.I18nTool.on = True
    tools.I18nTool.default = Your language with territory (e.g. 'en_US')
    tools.I18nTool.mo_dir = Directory holding the locale directories
    tools.I18nTool.domain = Your gettext domain (e.g. application name)

The mo_dir must contain subdirectories named with the language prefix
for all translations, containing a LC_MESSAGES dir with the compiled
catalog file in it.

Example::

    [/]
    tools.I18nTool.on = True
    tools.I18nTool.default = 'en_US'
    tools.I18nTool.mo_dir = '/home/user/web/myapp/i18n'
    tools.I18nTool.domain = 'myapp'

    Now the tool will look for a file called myapp.mo in
    /home/user/web/myapp/i18n/en/LC_MESSACES/
    or generic: <mo_dir>/<language>/LC_MESSAGES/<domain>.mo

That's it.

:License: BSD
:Author: Thorsten Weimann <thorsten.weimann (at) gmx (dot) net>
:Date: 2010-02-05
"""

import re

import cherrypy
from babel.core import Locale, UnknownLocaleError
from babel.support import Translations, LazyProxy

try:
    # Python 2.6 and above
    from collections import namedtuple
    Lang = namedtuple('Lang', 'locale trans')
except ImportError:
    # Python 2.5
    class Lang(object):
        def __init__(self, locale, trans):
            self.locale = locale
            self.trans = trans


# Cache for Translations and Locale objects
_languages = {}

# Regex taken from django project (http://www.djangoproject.com) to
# extract the HTTP-ACCEPT-LANGUAGE header
accept_language_re = re.compile(r'''
    ([A-Za-z]{1,8}(?:-[A-Za-z]{1,8})*|\*) # "en", "en-us", "x-y-z", "*"
    (?:;q=(0(?:\.\d{,3})?|1(?:.0{,3})?))? # Optional "q=1.00", "q=0.8"
    (?:\s*,\s*|$)                         # Multiple accepts per header
    ''', re.VERBOSE)


# Exception
class ImproperlyConfigured(Exception):
    """Raised if no known locale were found."""


# Public translation functions
def ugettext(message):
    """Standard translation function. You can use it in all your exposed
    methods and everywhere where the response object is available.

    :parameters:
        message : Unicode
            The message to translate.

    :returns: The translated message.
    :rtype: Unicode
    """
    return cherrypy.response.i18n.trans.ugettext(message)

def ugettext_lazy(message):
    """Like ugettext, but lazy.

    :returns: A proxy for the translation object.
    :rtype: LazyProxy
    """
    def get_translation():
        return cherrypy.response.i18n.trans.ugettext(message)
    return LazyProxy(get_translation)

def ungettext(singular, plural, num):
    """Like ugettext, but considers plural forms.

    :parameters:
        singular : Unicode
            The message to translate in singular form.
        plural : Unicode
            The message to translate in plural form.
        num : Integer
            Number to apply the plural formula on. If num is 1 or no
            translation is found, singular is returned.

    :returns: The translated message as singular or plural.
    :rtype: Unicode
    """
    return cherrypy.response.i18n.trans.ungettext(singular, plural, num)

def ungettext_lazy(singular, plural, num):
    """Like ungettext, but lazy.

    :returns: A proxy for the translation object.
    :rtype: LazyProxy
    """
    def get_translation():
        return cherrypy.response.i18n.trans.ungettext(singular, plural, num)
    return LazyProxy(get_translation)


# Function taken from django project
def parse_accept_language_header(raw):
    """Splits the content of the Accept-Language header to a list of
    languages.

    :parameters:
        raw : String
            The raw header (e.g. 'de-DE,de;q=0.9,en;q=0.8').

    :returns: List of languages sorted by priority if given.
    :rtype: List
    """
    langs = []
    pieces = accept_language_re.split(raw)
    if pieces[-1]:
        return []
    for i in range(0, len(pieces) - 1, 3):
        first, lang, priority = pieces[i:i+3]
        if first:
            return []
        priority = priority and float(priority) or 1.0
        lang = lang.replace('-', '_')
        langs.append((lang, priority))
    langs.sort(lambda x, y: -cmp(x[1], y[1]))
    return [x[0] for x in langs]


def load_translation(langs, dirname, domain):
    """Loads the first existing translations for known locale and saves the
    `Lang` object in a global cache for faster lookup on the next request.

    :parameters:
        langs : List
            List of languages as returned by `parse_accept_language_header`.
        dirname : String
            Directory of the translations (`tools.I18nTool.mo_dir`).
        domain : String
            Gettext domain of the catalog (`tools.I18nTool.domain`).

    :returns: Lang object with two attributes (Lang.trans = the translations
              object, Lang.locale = the corresponding Locale object).
    :rtype: Lang
    :raises: ImproperlyConfigured if no locale where known.
    """
    locale = None
    for lang in langs:
        short = lang[:2].lower()
        try:
            locale = Locale.parse(lang)
            if (domain, short) in _languages:
                return _languages[(domain, short)]
            trans = Translations.load(dirname, short, domain)
        except (ValueError, UnknownLocaleError):
            continue
        # If the translation was found, exit loop
        if isinstance(trans, Translations):
            break
    if locale is None:
        raise ImproperlyConfigured('Default locale not known.')
    _languages[(domain, short)] = res = Lang(locale, trans)
    return res


def get_lang(mo_dir, default, domain):
    """Main function which will be invoked during the request by `I18nTool`.
    If the SessionTool is on and has a lang key, this language get the
    highest priority. Default language get the lowest priority.
    The `Lang` object will be saved as `cherrypy.response.i18n` and the
    language string will also saved as `cherrypy.session['_lang_']` (if
    SessionTool is on).

    :parameters:
        mo_dir : String
            `tools.I18nTool.mo_dir`
        default : String
            `tools.I18nTool.default`
        domain : String
            `tools.I18nTool.domain`
    """
    raw_langs = cherrypy.request.headers.get('Accept-Language', default)
    langs = parse_accept_language_header(raw_langs)
    sessions_on = cherrypy.request.config.get('tools.sessions.on', False)
    if sessions_on and cherrypy.session.get('_lang_', ''):
        langs.insert(0, cherrypy.session.get('_lang_', '__'))
    langs.append(default)
    loc = load_translation(langs, mo_dir, domain)
    cherrypy.response.i18n = loc
    if sessions_on:
        cherrypy.session['_lang_'] = str(loc.locale)


def set_lang():
    """Sets the Content-Language response header (if not already set) to the
    language of `cherrypy.response.i18n.locale`.
    """
    if 'Content-Language' not in cherrypy.response.headers:
        cherrypy.response.headers['Content-Language'] = str(
                cherrypy.response.i18n.locale)


class I18nTool(cherrypy.Tool):
    """Tool to integrate babel translations in CherryPy."""

    def __init__(self):
        self._name = 'I18nTool'
        self._point = 'before_handler'
        self.callable = get_lang
        # Make sure, session tool (priority 50) is loaded before
        self._priority = 100

    def _setup(self):
        c = cherrypy.request.config
        if c.get('tools.staticdir.on', False) or \
           c.get('tools.staticfile.on', False):
            return
        cherrypy.Tool._setup(self)
        cherrypy.request.hooks.attach('before_finalize', set_lang)


cherrypy.tools.I18nTool = I18nTool()

Instead of parse_accept_language_header, try: [x.value for x in request.headers.elements['Accept-Language']]

fumanchu

Attachments

Hosted by WebFaction

Log in as guest/cherrypy to create/edit wiki pages