autocat3/i18n_tool.py

342 lines
11 KiB
Python

# -*- mode: python; indent-tabs-mode: nil; -*- coding: utf-8 -*-
"""Internationalization and Localization for CherryPy
**Tested with CherryPy 3.1.2**
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 = _('Translateable string')
s2 = ungettext('There is one string.',
'There are more strings.', 2)
return '<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('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-08
"""
from __future__ import unicode_literals
import gettext
import cherrypy
import six
from babel.core import Locale, UnknownLocaleError
from babel.support import Translations, LazyProxy
# Cache for Translations objects
_trans_cache = {}
class Struct (object):
""" Empty class to pin attributes on later. """
pass
if six.PY2:
# 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
"""
if message:
return cherrypy.response.i18n.trans.ugettext(message)
return ''
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)
else: # PY3
# 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
"""
if message:
return cherrypy.response.i18n.trans.gettext(message)
return ''
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.gettext(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.ngettext(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.ngettext(singular, plural, num)
return LazyProxy(get_translation)
def load_translation(languages, dirname, domain, default):
"""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.
"""
res = Struct ()
res.trans = gettext.NullTranslations ()
try:
# use the preferred locale for date formatting
# even if we have no translation for it
res.locale = Locale.parse (languages [0])
except (IndexError, ValueError, UnknownLocaleError):
res.locale = Locale.parse (default)
for language in languages:
try:
#cherrypy.log ("trying %s" % str (language),
# context = 'REQUEST', severity = logging.DEBUG)
locale = str (Locale.parse (language))
# cached ?
if (domain, locale) in _trans_cache:
res.trans = _trans_cache[(domain, locale)]
return res
# not cached
trans = Translations.load (dirname, locale, domain)
if isinstance (trans, Translations):
res.trans = _trans_cache [(domain, locale)] = trans
break
except (ValueError, UnknownLocaleError):
continue
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`
"""
# try explicit lang param, then session
lang = cherrypy.request.params.get ('lang', None)
if not lang:
try:
lang = cherrypy.session['_lang_']
except (AttributeError, KeyError):
pass
if lang:
lang = lang.replace ('-', '_')
langs = (lang, )
else:
langs = cherrypy.request.headers.elements ('Accept-Language') or []
langs = [x.value.replace ('-', '_') for x in langs]
loc = load_translation (langs, mo_dir, domain, default)
cherrypy.response.i18n = loc
try:
cherrypy.session['_lang_'] = str (loc.locale)
except AttributeError:
pass
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:
if hasattr (cherrypy.response, 'i18n'):
cherrypy.response.headers['Content-Language'] = str (
cherrypy.response.i18n.locale)
class I18nTool (cherrypy.Tool):
"""Tool to integrate babel translations in CherryPy."""
def __init__ (self):
# cherrypy.Tool.__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)
dirname = c.get ('tools.I18nTool.mo_dir', 'i18n')
default = c.get ('tools.I18nTool.default', 'en')[:2].lower ()
domain = c.get ('tools.I18nTool.domain', 'messages')
trans = Translations.load (dirname, default, domain)
if isinstance (trans, Translations):
_trans_cache [(domain, default)] = trans
else:
_trans_cache [(domain, default)] = gettext.NullTranslations ()
cherrypy.request.hooks.attach ('before_finalize', set_lang)