| 1 |
|
|---|
| 2 |
|
|---|
| 3 |
"""Internationalization and Localization for CherryPy |
|---|
| 4 |
|
|---|
| 5 |
**Tested with CherryPy 3.1.2** |
|---|
| 6 |
|
|---|
| 7 |
This tool provides locales and loads translations based on the |
|---|
| 8 |
HTTP-ACCEPT-LANGUAGE header. If no header is send or the given language |
|---|
| 9 |
is not supported by the application, it falls back to |
|---|
| 10 |
`tools.I18nTool.default`. Set `default` to the native language used in your |
|---|
| 11 |
code for strings, so you must not provide a .mo file for it. |
|---|
| 12 |
|
|---|
| 13 |
The tool uses `babel<http://babel.edgewall.org>`_ for localization and |
|---|
| 14 |
handling translations. Within your Python code you can use four functions |
|---|
| 15 |
defined in this module and the loaded locale provided as |
|---|
| 16 |
`cherrypy.response.i18n.locale`. |
|---|
| 17 |
|
|---|
| 18 |
Example:: |
|---|
| 19 |
|
|---|
| 20 |
from i18n_tool import ugettext as _, ungettext |
|---|
| 21 |
|
|---|
| 22 |
class MyController(object): |
|---|
| 23 |
@cherrypy.expose |
|---|
| 24 |
def index(self): |
|---|
| 25 |
loc = cherrypy.response.i18n.locale |
|---|
| 26 |
s1 = _(u'Translateable string') |
|---|
| 27 |
s2 = ungettext(u'There is one string.', |
|---|
| 28 |
u'There are more strings.', 2) |
|---|
| 29 |
return u'<br />'.join([s1, s2, loc.display_name]) |
|---|
| 30 |
|
|---|
| 31 |
If you have code (e.g. database models) that is executed before the response |
|---|
| 32 |
object is available, use the *_lazy functions to mark the strings |
|---|
| 33 |
translateable. They will be translated later on, when the text is used (and |
|---|
| 34 |
hopefully the response object is available then). |
|---|
| 35 |
|
|---|
| 36 |
Example:: |
|---|
| 37 |
|
|---|
| 38 |
from i18n_tool import ugettext_lazy |
|---|
| 39 |
|
|---|
| 40 |
class Model: |
|---|
| 41 |
def __init__(self): |
|---|
| 42 |
name = ugettext_lazy(u'Name of the model') |
|---|
| 43 |
|
|---|
| 44 |
For your templates read the documentation of your template engine how to |
|---|
| 45 |
integrate babel with it. I think `Genshi<http://genshi.edgewall.org>`_ and |
|---|
| 46 |
`Jinja 2<http://jinja.pocoo.org`_ support it out of the box. |
|---|
| 47 |
|
|---|
| 48 |
|
|---|
| 49 |
Settings for the CherryPy configuration:: |
|---|
| 50 |
|
|---|
| 51 |
[/] |
|---|
| 52 |
tools.I18nTool.on = True |
|---|
| 53 |
tools.I18nTool.default = Your language with territory (e.g. 'en_US') |
|---|
| 54 |
tools.I18nTool.mo_dir = Directory holding the locale directories |
|---|
| 55 |
tools.I18nTool.domain = Your gettext domain (e.g. application name) |
|---|
| 56 |
|
|---|
| 57 |
The mo_dir must contain subdirectories named with the language prefix |
|---|
| 58 |
for all translations, containing a LC_MESSAGES dir with the compiled |
|---|
| 59 |
catalog file in it. |
|---|
| 60 |
|
|---|
| 61 |
Example:: |
|---|
| 62 |
|
|---|
| 63 |
[/] |
|---|
| 64 |
tools.I18nTool.on = True |
|---|
| 65 |
tools.I18nTool.default = 'en_US' |
|---|
| 66 |
tools.I18nTool.mo_dir = '/home/user/web/myapp/i18n' |
|---|
| 67 |
tools.I18nTool.domain = 'myapp' |
|---|
| 68 |
|
|---|
| 69 |
Now the tool will look for a file called myapp.mo in |
|---|
| 70 |
/home/user/web/myapp/i18n/en/LC_MESSACES/ |
|---|
| 71 |
or generic: <mo_dir>/<language>/LC_MESSAGES/<domain>.mo |
|---|
| 72 |
|
|---|
| 73 |
That's it. |
|---|
| 74 |
|
|---|
| 75 |
:License: BSD |
|---|
| 76 |
:Author: Thorsten Weimann <thorsten.weimann (at) gmx (dot) net> |
|---|
| 77 |
:Date: 2010-02-08 |
|---|
| 78 |
""" |
|---|
| 79 |
|
|---|
| 80 |
import re |
|---|
| 81 |
|
|---|
| 82 |
import cherrypy |
|---|
| 83 |
from babel.core import Locale, UnknownLocaleError |
|---|
| 84 |
from babel.support import Translations, LazyProxy |
|---|
| 85 |
|
|---|
| 86 |
try: |
|---|
| 87 |
|
|---|
| 88 |
from collections import namedtuple |
|---|
| 89 |
Lang = namedtuple('Lang', 'locale trans') |
|---|
| 90 |
except ImportError: |
|---|
| 91 |
|
|---|
| 92 |
class Lang(object): |
|---|
| 93 |
def __init__(self, locale, trans): |
|---|
| 94 |
self.locale = locale |
|---|
| 95 |
self.trans = trans |
|---|
| 96 |
|
|---|
| 97 |
|
|---|
| 98 |
|
|---|
| 99 |
_languages = {} |
|---|
| 100 |
|
|---|
| 101 |
|
|---|
| 102 |
|
|---|
| 103 |
class ImproperlyConfigured(Exception): |
|---|
| 104 |
"""Raised if no known locale were found.""" |
|---|
| 105 |
|
|---|
| 106 |
|
|---|
| 107 |
|
|---|
| 108 |
def ugettext(message): |
|---|
| 109 |
"""Standard translation function. You can use it in all your exposed |
|---|
| 110 |
methods and everywhere where the response object is available. |
|---|
| 111 |
|
|---|
| 112 |
:parameters: |
|---|
| 113 |
message : Unicode |
|---|
| 114 |
The message to translate. |
|---|
| 115 |
|
|---|
| 116 |
:returns: The translated message. |
|---|
| 117 |
:rtype: Unicode |
|---|
| 118 |
""" |
|---|
| 119 |
return cherrypy.response.i18n.trans.ugettext(message) |
|---|
| 120 |
|
|---|
| 121 |
def ugettext_lazy(message): |
|---|
| 122 |
"""Like ugettext, but lazy. |
|---|
| 123 |
|
|---|
| 124 |
:returns: A proxy for the translation object. |
|---|
| 125 |
:rtype: LazyProxy |
|---|
| 126 |
""" |
|---|
| 127 |
def get_translation(): |
|---|
| 128 |
return cherrypy.response.i18n.trans.ugettext(message) |
|---|
| 129 |
return LazyProxy(get_translation) |
|---|
| 130 |
|
|---|
| 131 |
def ungettext(singular, plural, num): |
|---|
| 132 |
"""Like ugettext, but considers plural forms. |
|---|
| 133 |
|
|---|
| 134 |
:parameters: |
|---|
| 135 |
singular : Unicode |
|---|
| 136 |
The message to translate in singular form. |
|---|
| 137 |
plural : Unicode |
|---|
| 138 |
The message to translate in plural form. |
|---|
| 139 |
num : Integer |
|---|
| 140 |
Number to apply the plural formula on. If num is 1 or no |
|---|
| 141 |
translation is found, singular is returned. |
|---|
| 142 |
|
|---|
| 143 |
:returns: The translated message as singular or plural. |
|---|
| 144 |
:rtype: Unicode |
|---|
| 145 |
""" |
|---|
| 146 |
return cherrypy.response.i18n.trans.ungettext(singular, plural, num) |
|---|
| 147 |
|
|---|
| 148 |
def ungettext_lazy(singular, plural, num): |
|---|
| 149 |
"""Like ungettext, but lazy. |
|---|
| 150 |
|
|---|
| 151 |
:returns: A proxy for the translation object. |
|---|
| 152 |
:rtype: LazyProxy |
|---|
| 153 |
""" |
|---|
| 154 |
def get_translation(): |
|---|
| 155 |
return cherrypy.response.i18n.trans.ungettext(singular, plural, num) |
|---|
| 156 |
return LazyProxy(get_translation) |
|---|
| 157 |
|
|---|
| 158 |
|
|---|
| 159 |
def load_translation(langs, dirname, domain): |
|---|
| 160 |
"""Loads the first existing translations for known locale and saves the |
|---|
| 161 |
`Lang` object in a global cache for faster lookup on the next request. |
|---|
| 162 |
|
|---|
| 163 |
:parameters: |
|---|
| 164 |
langs : List |
|---|
| 165 |
List of languages as returned by `parse_accept_language_header`. |
|---|
| 166 |
dirname : String |
|---|
| 167 |
Directory of the translations (`tools.I18nTool.mo_dir`). |
|---|
| 168 |
domain : String |
|---|
| 169 |
Gettext domain of the catalog (`tools.I18nTool.domain`). |
|---|
| 170 |
|
|---|
| 171 |
:returns: Lang object with two attributes (Lang.trans = the translations |
|---|
| 172 |
object, Lang.locale = the corresponding Locale object). |
|---|
| 173 |
:rtype: Lang |
|---|
| 174 |
:raises: ImproperlyConfigured if no locale where known. |
|---|
| 175 |
""" |
|---|
| 176 |
locale = None |
|---|
| 177 |
for lang in langs: |
|---|
| 178 |
short = lang[:2].lower() |
|---|
| 179 |
try: |
|---|
| 180 |
locale = Locale.parse(lang) |
|---|
| 181 |
if (domain, short) in _languages: |
|---|
| 182 |
return _languages[(domain, short)] |
|---|
| 183 |
trans = Translations.load(dirname, short, domain) |
|---|
| 184 |
except (ValueError, UnknownLocaleError): |
|---|
| 185 |
continue |
|---|
| 186 |
|
|---|
| 187 |
if isinstance(trans, Translations): |
|---|
| 188 |
break |
|---|
| 189 |
if locale is None: |
|---|
| 190 |
raise ImproperlyConfigured('Default locale not known.') |
|---|
| 191 |
_languages[(domain, short)] = res = Lang(locale, trans) |
|---|
| 192 |
return res |
|---|
| 193 |
|
|---|
| 194 |
|
|---|
| 195 |
def get_lang(mo_dir, default, domain): |
|---|
| 196 |
"""Main function which will be invoked during the request by `I18nTool`. |
|---|
| 197 |
If the SessionTool is on and has a lang key, this language get the |
|---|
| 198 |
highest priority. Default language get the lowest priority. |
|---|
| 199 |
The `Lang` object will be saved as `cherrypy.response.i18n` and the |
|---|
| 200 |
language string will also saved as `cherrypy.session['_lang_']` (if |
|---|
| 201 |
SessionTool is on). |
|---|
| 202 |
|
|---|
| 203 |
:parameters: |
|---|
| 204 |
mo_dir : String |
|---|
| 205 |
`tools.I18nTool.mo_dir` |
|---|
| 206 |
default : String |
|---|
| 207 |
`tools.I18nTool.default` |
|---|
| 208 |
domain : String |
|---|
| 209 |
`tools.I18nTool.domain` |
|---|
| 210 |
""" |
|---|
| 211 |
langs = [x.value.replace('-', '_') for x in |
|---|
| 212 |
cherrypy.request.headers.elements('Accept-Language')] |
|---|
| 213 |
sessions_on = cherrypy.request.config.get('tools.sessions.on', False) |
|---|
| 214 |
if sessions_on and cherrypy.session.get('_lang_', ''): |
|---|
| 215 |
langs.insert(0, cherrypy.session.get('_lang_', '__')) |
|---|
| 216 |
langs.append(default) |
|---|
| 217 |
loc = load_translation(langs, mo_dir, domain) |
|---|
| 218 |
cherrypy.response.i18n = loc |
|---|
| 219 |
if sessions_on: |
|---|
| 220 |
cherrypy.session['_lang_'] = str(loc.locale) |
|---|
| 221 |
|
|---|
| 222 |
|
|---|
| 223 |
def set_lang(): |
|---|
| 224 |
"""Sets the Content-Language response header (if not already set) to the |
|---|
| 225 |
language of `cherrypy.response.i18n.locale`. |
|---|
| 226 |
""" |
|---|
| 227 |
if 'Content-Language' not in cherrypy.response.headers: |
|---|
| 228 |
cherrypy.response.headers['Content-Language'] = str( |
|---|
| 229 |
cherrypy.response.i18n.locale) |
|---|
| 230 |
|
|---|
| 231 |
|
|---|
| 232 |
class I18nTool(cherrypy.Tool): |
|---|
| 233 |
"""Tool to integrate babel translations in CherryPy.""" |
|---|
| 234 |
|
|---|
| 235 |
def __init__(self): |
|---|
| 236 |
self._name = 'I18nTool' |
|---|
| 237 |
self._point = 'before_handler' |
|---|
| 238 |
self.callable = get_lang |
|---|
| 239 |
|
|---|
| 240 |
self._priority = 100 |
|---|
| 241 |
|
|---|
| 242 |
def _setup(self): |
|---|
| 243 |
c = cherrypy.request.config |
|---|
| 244 |
if c.get('tools.staticdir.on', False) or \ |
|---|
| 245 |
c.get('tools.staticfile.on', False): |
|---|
| 246 |
return |
|---|
| 247 |
cherrypy.Tool._setup(self) |
|---|
| 248 |
cherrypy.request.hooks.attach('before_finalize', set_lang) |
|---|
| 249 |
|
|---|
| 250 |
|
|---|
| 251 |
cherrypy.tools.I18nTool = I18nTool() |
|---|
| 252 |
|
|---|