cherrypy-foundation 1.0.0__py3-none-any.whl → 1.0.0a2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cherrypy_foundation/components/ColorModes.jinja +4 -5
- cherrypy_foundation/components/Datatable.jinja +2 -2
- cherrypy_foundation/components/Datatable.js +2 -2
- cherrypy_foundation/components/Field.jinja +2 -4
- cherrypy_foundation/components/Fields.jinja +2 -0
- cherrypy_foundation/components/Typeahead.css +1 -6
- cherrypy_foundation/components/Typeahead.jinja +2 -2
- cherrypy_foundation/components/__init__.py +2 -2
- cherrypy_foundation/components/tests/test_static.py +1 -1
- cherrypy_foundation/error_page.py +3 -3
- cherrypy_foundation/flash.py +15 -17
- cherrypy_foundation/form.py +2 -2
- cherrypy_foundation/logging.py +2 -2
- cherrypy_foundation/passwd.py +2 -2
- cherrypy_foundation/plugins/db.py +1 -1
- cherrypy_foundation/plugins/ldap.py +25 -27
- cherrypy_foundation/plugins/restapi.py +1 -1
- cherrypy_foundation/plugins/scheduler.py +3 -14
- cherrypy_foundation/plugins/smtp.py +2 -8
- cherrypy_foundation/plugins/tests/test_db.py +2 -2
- cherrypy_foundation/plugins/tests/test_ldap.py +3 -76
- cherrypy_foundation/plugins/tests/test_scheduler.py +1 -1
- cherrypy_foundation/plugins/tests/test_smtp.py +1 -31
- cherrypy_foundation/tests/__init__.py +0 -72
- cherrypy_foundation/tests/templates/test_form.html +1 -7
- cherrypy_foundation/tests/test_error_page.py +1 -7
- cherrypy_foundation/tests/test_form.py +11 -40
- cherrypy_foundation/tests/test_passwd.py +2 -2
- cherrypy_foundation/tools/auth.py +27 -31
- cherrypy_foundation/tools/auth_mfa.py +83 -87
- cherrypy_foundation/tools/errors.py +27 -0
- cherrypy_foundation/tools/i18n.py +151 -235
- cherrypy_foundation/tools/jinja2.py +2 -15
- cherrypy_foundation/tools/ratelimit.py +18 -32
- cherrypy_foundation/tools/secure_headers.py +1 -1
- cherrypy_foundation/tools/sessions_timeout.py +21 -23
- cherrypy_foundation/tools/tests/locales/en/LC_MESSAGES/messages.mo +0 -0
- cherrypy_foundation/tools/tests/locales/{de → en}/LC_MESSAGES/messages.po +2 -2
- cherrypy_foundation/tools/tests/templates/test_jinja2.html +1 -2
- cherrypy_foundation/tools/tests/templates/test_jinja2_i18n.html +11 -0
- cherrypy_foundation/tools/tests/templates/test_jinjax.html +2 -3
- cherrypy_foundation/tools/tests/test_auth.py +3 -20
- cherrypy_foundation/tools/tests/test_auth_mfa.py +4 -6
- cherrypy_foundation/tools/tests/test_i18n.py +6 -81
- cherrypy_foundation/tools/tests/test_jinja2.py +5 -35
- cherrypy_foundation/tools/tests/test_ratelimit.py +2 -2
- cherrypy_foundation/url.py +25 -25
- cherrypy_foundation/widgets.py +2 -2
- cherrypy_foundation-1.0.0a2.dist-info/METADATA +42 -0
- {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a2.dist-info}/RECORD +53 -64
- {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a2.dist-info}/WHEEL +1 -1
- cherrypy_foundation/components/Flash.jinja +0 -13
- cherrypy_foundation/components/LocaleSelection.jinja +0 -13
- cherrypy_foundation/components/LocaleSelection.js +0 -26
- cherrypy_foundation/plugins/tests/test_scheduler_db.py +0 -107
- cherrypy_foundation/sessions.py +0 -93
- cherrypy_foundation/tests/templates/test_flash.html +0 -9
- cherrypy_foundation/tests/templates/test_url.html +0 -15
- cherrypy_foundation/tests/test_flash.py +0 -61
- cherrypy_foundation/tests/test_logging.py +0 -78
- cherrypy_foundation/tests/test_sessions.py +0 -89
- cherrypy_foundation/tests/test_url.py +0 -161
- cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.mo +0 -0
- cherrypy_foundation/tools/tests/templates/test_jinjax_i18n.html +0 -22
- cherrypy_foundation/tools/tests/test_secure_headers.py +0 -200
- cherrypy_foundation-1.0.0.dist-info/METADATA +0 -71
- {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a2.dist-info}/licenses/LICENSE.md +0 -0
- {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a2.dist-info}/top_level.txt +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Internationalisation tool for cherrypy
|
|
2
|
-
# Copyright (C) 2012-
|
|
2
|
+
# Copyright (C) 2012-2025 Patrik Dufresne
|
|
3
3
|
#
|
|
4
4
|
# This program is free software: you can redistribute it and/or modify
|
|
5
5
|
# it under the terms of the GNU General Public License as published by
|
|
@@ -14,107 +14,95 @@
|
|
|
14
14
|
# You should have received a copy of the GNU General Public License
|
|
15
15
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
16
|
|
|
17
|
-
"""
|
|
18
|
-
Internationalization (i18n) and Localization (l10n) support for CherryPy.
|
|
19
|
-
|
|
20
|
-
This module provides a CherryPy tool that integrates GNU gettext and Babel
|
|
21
|
-
to handle language selection, translations, locale-aware formatting, and
|
|
22
|
-
timezone handling on a per-request basis.
|
|
23
|
-
|
|
24
|
-
The active language is resolved in the following order (highest priority first):
|
|
17
|
+
"""Internationalization and Localization for CherryPy
|
|
25
18
|
|
|
26
|
-
|
|
27
|
-
2. User-defined callback (``tools.i18n.func``)
|
|
28
|
-
3. HTTP ``Accept-Language`` request header
|
|
29
|
-
4. Default language configured via ``tools.i18n.default``
|
|
19
|
+
This tool provides locales and loads translations in the following order:
|
|
30
20
|
|
|
31
|
-
|
|
32
|
-
|
|
21
|
+
1. `with i18n.preferred_lang()`
|
|
22
|
+
2. `tools.i18n.func` callback function (optional)
|
|
23
|
+
3. HTTP Accept-Language headers
|
|
24
|
+
4. `tools.i18n.default` value
|
|
33
25
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
26
|
+
The tool uses `babel` <http://babel.edgewall.org> for localization and
|
|
27
|
+
handling translations. Within your Python code you can use four functions
|
|
28
|
+
defined in this module and the loaded locale provided as `i18n.get_translation()`
|
|
37
29
|
|
|
38
|
-
|
|
39
|
-
``ungettext``:
|
|
30
|
+
Example::
|
|
40
31
|
|
|
41
|
-
from i18n import
|
|
32
|
+
from i18n import ugettext as _, ungettext
|
|
42
33
|
|
|
43
|
-
class MyController:
|
|
34
|
+
class MyController(object):
|
|
44
35
|
@cherrypy.expose
|
|
45
36
|
def index(self):
|
|
46
37
|
locale = cherrypy.response.i18n.locale
|
|
47
|
-
s1 = _(u
|
|
48
|
-
s2 =
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
2
|
|
52
|
-
)
|
|
53
|
-
return "<br />".join([s1, s2, locale.display_name])
|
|
38
|
+
s1 = _(u'Translateable string')
|
|
39
|
+
s2 = ungettext(u'There is one string.',
|
|
40
|
+
u'There are more strings.', 2)
|
|
41
|
+
return u'<br />'.join([s1, s2, locale.display_name])
|
|
54
42
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
43
|
+
If you have code (e.g. database models) that is executed before the response
|
|
44
|
+
object is available, use the *_lazy functions to mark the strings
|
|
45
|
+
translateable. They will be translated later on, when the text is used (and
|
|
46
|
+
hopefully the response object is available then).
|
|
58
47
|
|
|
59
|
-
|
|
60
|
-
(e.g. model definitions or module-level constants), use the ``*_lazy``
|
|
61
|
-
helpers. These defer translation until the value is actually rendered:
|
|
48
|
+
Example::
|
|
62
49
|
|
|
63
|
-
from i18n_tool import
|
|
50
|
+
from i18n_tool import ugettext_lazy
|
|
64
51
|
|
|
65
52
|
class Model:
|
|
66
|
-
|
|
53
|
+
def __init__(self):
|
|
54
|
+
name = ugettext_lazy(u'Name of the model')
|
|
67
55
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
56
|
+
For your templates read the documentation of your template engine how to
|
|
57
|
+
integrate babel with it. I think `Genshi<http://genshi.edgewall.org>`_ and
|
|
58
|
+
`Jinja 2<http://jinja.pocoo.org`_ support it out of the box.
|
|
71
59
|
|
|
72
|
-
For template rendering, i18n integrate with jinja2.
|
|
73
60
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
61
|
+
Settings for the CherryPy configuration::
|
|
62
|
+
|
|
63
|
+
[/]
|
|
64
|
+
tools.i18n.on = True
|
|
65
|
+
tools.i18n.default = Your language with territory (e.g. 'en_US')
|
|
66
|
+
tools.i18n.mo_dir = Directory holding the locale directories
|
|
67
|
+
tools.i18n.domain = Your gettext domain (e.g. application name)
|
|
80
68
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
69
|
+
The mo_dir must contain subdirectories named with the language prefix
|
|
70
|
+
for all translations, containing a LC_MESSAGES dir with the compiled
|
|
71
|
+
catalog file in it.
|
|
84
72
|
|
|
85
|
-
Example
|
|
73
|
+
Example::
|
|
86
74
|
|
|
87
75
|
[/]
|
|
88
76
|
tools.i18n.on = True
|
|
89
|
-
tools.i18n.default =
|
|
90
|
-
tools.i18n.mo_dir =
|
|
91
|
-
tools.i18n.domain =
|
|
77
|
+
tools.i18n.default = 'en_US'
|
|
78
|
+
tools.i18n.mo_dir = '/home/user/web/myapp/i18n'
|
|
79
|
+
tools.i18n.domain = 'myapp'
|
|
92
80
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
81
|
+
Now the tool will look for a file called myapp.mo in
|
|
82
|
+
/home/user/web/myapp/i18n/en/LC_MESSACES/
|
|
83
|
+
or generic: <mo_dir>/<language>/LC_MESSAGES/<domain>.mo
|
|
96
84
|
|
|
97
|
-
|
|
85
|
+
That's it.
|
|
98
86
|
|
|
87
|
+
:License: BSD
|
|
88
|
+
:Author: Thorsten Weimann <thorsten.weimann (at) gmx (dot) net>
|
|
89
|
+
:Date: 2010-02-08
|
|
99
90
|
"""
|
|
100
91
|
|
|
101
|
-
|
|
92
|
+
|
|
102
93
|
import os
|
|
94
|
+
import threading
|
|
103
95
|
from contextlib import contextmanager
|
|
104
|
-
from contextvars import ContextVar
|
|
105
96
|
from functools import lru_cache
|
|
106
|
-
from gettext import NullTranslations, translation
|
|
107
97
|
|
|
108
98
|
import cherrypy
|
|
109
99
|
import pytz
|
|
110
100
|
from babel import dates
|
|
111
101
|
from babel.core import Locale, get_global
|
|
112
|
-
from babel.support import LazyProxy, Translations
|
|
102
|
+
from babel.support import LazyProxy, NullTranslations, Translations
|
|
113
103
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
_translation = ContextVar('translation', default=None)
|
|
117
|
-
_tzinfo = ContextVar('tzinfo', default=None)
|
|
104
|
+
# Store current translation and preferred_lang
|
|
105
|
+
_current = threading.local()
|
|
118
106
|
|
|
119
107
|
|
|
120
108
|
def _get_config(key, default=None):
|
|
@@ -134,20 +122,21 @@ def preferred_lang(lang):
|
|
|
134
122
|
with i18n.preferred_lang('fr'):
|
|
135
123
|
i18n.gettext('some string')
|
|
136
124
|
"""
|
|
137
|
-
|
|
138
|
-
|
|
125
|
+
assert lang is None or isinstance(lang, str)
|
|
126
|
+
prev_lang = getattr(_current, 'preferred_lang', [])
|
|
127
|
+
prev_trans = getattr(_current, 'translation', None)
|
|
139
128
|
try:
|
|
140
|
-
# Update
|
|
129
|
+
# Update prefered lang and clear translation.
|
|
141
130
|
if lang:
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
131
|
+
_current.preferred_lang = [lang]
|
|
132
|
+
elif hasattr(_current, 'preferred_lang'):
|
|
133
|
+
del _current.preferred_lang
|
|
134
|
+
_current.translation = None
|
|
146
135
|
yield
|
|
147
136
|
finally:
|
|
148
137
|
# Restore previous value
|
|
149
|
-
|
|
150
|
-
|
|
138
|
+
_current.preferred_lang = prev_lang
|
|
139
|
+
_current.translation = prev_trans
|
|
151
140
|
|
|
152
141
|
|
|
153
142
|
@contextmanager
|
|
@@ -158,24 +147,22 @@ def preferred_timezone(timezone):
|
|
|
158
147
|
with i18n.preferred_lang('America/Montreal'):
|
|
159
148
|
i18n.format_datetime(...)
|
|
160
149
|
"""
|
|
161
|
-
|
|
162
|
-
|
|
150
|
+
assert timezone is None or isinstance(timezone, str)
|
|
151
|
+
prev_timezone = getattr(_current, 'preferred_timezone', [])
|
|
152
|
+
prev_tzinfo = getattr(_current, 'tzinfo', None)
|
|
163
153
|
try:
|
|
164
|
-
# Update
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
else:
|
|
168
|
-
token_t = _preferred_timezone.set(tuple())
|
|
169
|
-
token_z = _tzinfo.set(None)
|
|
154
|
+
# Update prefered lang and clear translation.
|
|
155
|
+
_current.preferred_timezone = [timezone] + prev_timezone
|
|
156
|
+
_current.tzinfo = None
|
|
170
157
|
yield
|
|
171
158
|
finally:
|
|
172
159
|
# Restore previous value
|
|
173
|
-
|
|
174
|
-
|
|
160
|
+
_current.preferred_timezone = prev_timezone
|
|
161
|
+
_current.tzinfo = prev_tzinfo
|
|
175
162
|
|
|
176
163
|
|
|
177
|
-
@lru_cache(maxsize=
|
|
178
|
-
def _search_translation(dirname, domain, *
|
|
164
|
+
@lru_cache(maxsize=10)
|
|
165
|
+
def _search_translation(dirname, domain, *langs):
|
|
179
166
|
"""
|
|
180
167
|
Loads the first existing translations for known locale.
|
|
181
168
|
|
|
@@ -183,37 +170,22 @@ def _search_translation(dirname, domain, *locales, sourcecode_lang='en'):
|
|
|
183
170
|
langs : List
|
|
184
171
|
List of languages as returned by `parse_accept_language_header`.
|
|
185
172
|
dirname : String
|
|
186
|
-
|
|
173
|
+
Directory of the translations (`tools.I18nTool.mo_dir`).
|
|
174
|
+
Might be a list of directories.
|
|
187
175
|
domain : String
|
|
188
|
-
Gettext domain of the catalog (`tools.
|
|
176
|
+
Gettext domain of the catalog (`tools.I18nTool.domain`).
|
|
189
177
|
|
|
190
178
|
:returns: Translations, the corresponding Locale object.
|
|
191
179
|
"""
|
|
192
|
-
if not isinstance(
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
#
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
# If exception occur while loading the translation file. The file is probably corrupted.
|
|
202
|
-
cherrypy.log(
|
|
203
|
-
f'failed to load gettext catalog domain={domain} localedir={dirname} locale={locale}',
|
|
204
|
-
context='I18N',
|
|
205
|
-
severity=logging.WARNING,
|
|
206
|
-
traceback=True,
|
|
207
|
-
)
|
|
208
|
-
continue
|
|
209
|
-
if t.__class__ is NullTranslations and not locale.startswith(sourcecode_lang):
|
|
210
|
-
# Continue searching if translation is not found.
|
|
211
|
-
continue
|
|
212
|
-
t.locale = Locale.parse(locale)
|
|
213
|
-
return t
|
|
214
|
-
# If translation file not found, return default
|
|
215
|
-
t = NullTranslations()
|
|
216
|
-
t.locale = Locale(sourcecode_lang)
|
|
180
|
+
if not isinstance(langs, (list, tuple)):
|
|
181
|
+
langs = [langs]
|
|
182
|
+
t = Translations.load(dirname, langs, domain)
|
|
183
|
+
# Ignore null translation
|
|
184
|
+
if t.__class__ is NullTranslations:
|
|
185
|
+
return None
|
|
186
|
+
# Get Locale from file name
|
|
187
|
+
lang = t.files[0].split('/')[-3]
|
|
188
|
+
t.locale = Locale.parse(lang)
|
|
217
189
|
return t
|
|
218
190
|
|
|
219
191
|
|
|
@@ -221,37 +193,23 @@ def get_language_name(lang_code):
|
|
|
221
193
|
"""
|
|
222
194
|
Translate the language code into it's language display name.
|
|
223
195
|
"""
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
return lang_code
|
|
228
|
-
translation = get_translation()
|
|
229
|
-
return locale.get_language_name(translation.locale)
|
|
196
|
+
locale = Locale.parse(lang_code)
|
|
197
|
+
trans = get_translation()
|
|
198
|
+
return locale.get_language_name(trans.locale)
|
|
230
199
|
|
|
231
200
|
|
|
232
201
|
def get_timezone():
|
|
233
202
|
"""
|
|
234
203
|
Get the best timezone information for the current context.
|
|
235
|
-
|
|
236
|
-
The timezone returned is determined with the following priorities:
|
|
237
|
-
|
|
238
|
-
* value of preferred_timezone()
|
|
239
|
-
* tools.i18n.default_timezone
|
|
240
|
-
* default server time.
|
|
241
|
-
|
|
242
204
|
"""
|
|
243
205
|
# When tzinfo is defined, use it
|
|
244
|
-
tzinfo =
|
|
206
|
+
tzinfo = getattr(_current, 'tzinfo', None)
|
|
245
207
|
if tzinfo is not None:
|
|
246
208
|
return tzinfo
|
|
247
209
|
# Otherwise search for a valid timezone.
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
preferred_timezone = (
|
|
252
|
-
*preferred_timezone,
|
|
253
|
-
default,
|
|
254
|
-
)
|
|
210
|
+
tzinfo = None
|
|
211
|
+
default_timezone = _get_config('tools.i18n.default_timezone')
|
|
212
|
+
preferred_timezone = getattr(_current, 'preferred_timezone', [default_timezone])
|
|
255
213
|
for timezone in preferred_timezone:
|
|
256
214
|
try:
|
|
257
215
|
tzinfo = dates.get_timezone(timezone)
|
|
@@ -261,8 +219,8 @@ def get_timezone():
|
|
|
261
219
|
# If we can't find a valid timezone using the default and preferred value, fall back to server timezone.
|
|
262
220
|
if tzinfo is None:
|
|
263
221
|
tzinfo = dates.get_timezone(None)
|
|
264
|
-
|
|
265
|
-
return tzinfo
|
|
222
|
+
_current.tzinfo = tzinfo
|
|
223
|
+
return _current.tzinfo
|
|
266
224
|
|
|
267
225
|
|
|
268
226
|
def get_translation():
|
|
@@ -270,48 +228,37 @@ def get_translation():
|
|
|
270
228
|
Get the best translation for the current context.
|
|
271
229
|
"""
|
|
272
230
|
# When translation is defined, use it
|
|
273
|
-
translation =
|
|
231
|
+
translation = getattr(_current, 'translation', None)
|
|
274
232
|
if translation is not None:
|
|
275
233
|
return translation
|
|
276
234
|
|
|
277
235
|
# Otherwise, we need to search the translation.
|
|
278
236
|
# `preferred_lang` should always has a sane value within a cherrypy request because of hooks
|
|
279
237
|
# But we also need to support calls outside cherrypy.
|
|
280
|
-
sourcecode_lang = _get_config('tools.i18n.sourcecode_lang', 'en')
|
|
281
238
|
default = _get_config('tools.i18n.default')
|
|
282
|
-
preferred_lang =
|
|
283
|
-
if default and default not in preferred_lang:
|
|
284
|
-
preferred_lang = (
|
|
285
|
-
*preferred_lang,
|
|
286
|
-
default,
|
|
287
|
-
)
|
|
239
|
+
preferred_lang = getattr(_current, 'preferred_lang', [default])
|
|
288
240
|
mo_dir = _get_config('tools.i18n.mo_dir')
|
|
289
|
-
domain = _get_config('tools.i18n.domain'
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
241
|
+
domain = _get_config('tools.i18n.domain')
|
|
242
|
+
trans = _search_translation(mo_dir, domain, *preferred_lang)
|
|
243
|
+
if trans is None:
|
|
244
|
+
trans = NullTranslations()
|
|
245
|
+
trans.locale = Locale('en')
|
|
246
|
+
_current.translation = trans
|
|
247
|
+
return _current.translation
|
|
293
248
|
|
|
294
249
|
|
|
295
250
|
def list_available_locales():
|
|
296
251
|
"""
|
|
297
252
|
Return a list of available translations.
|
|
298
253
|
"""
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
sourcecode_lang = _get_config('tools.i18n.sourcecode_lang', 'en')
|
|
302
|
-
return_value.append(Locale.parse(sourcecode_lang))
|
|
303
|
-
# Then scan language directory for more translation.
|
|
304
|
-
mo_dir = _get_config('tools.i18n.mo_dir')
|
|
305
|
-
domain = _get_config('tools.i18n.domain', 'messages')
|
|
254
|
+
mo_dir = _get_config('tools.i18n.mo_dir', False)
|
|
255
|
+
domain = _get_config('tools.i18n.domain')
|
|
306
256
|
if not mo_dir:
|
|
307
257
|
return
|
|
308
258
|
for lang in os.listdir(mo_dir):
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
except Exception:
|
|
313
|
-
continue
|
|
314
|
-
return return_value
|
|
259
|
+
trans = _search_translation(mo_dir, domain, lang)
|
|
260
|
+
if trans is not None:
|
|
261
|
+
yield trans.locale
|
|
315
262
|
|
|
316
263
|
|
|
317
264
|
def list_available_timezones():
|
|
@@ -324,7 +271,7 @@ def list_available_timezones():
|
|
|
324
271
|
|
|
325
272
|
|
|
326
273
|
# Public translation functions
|
|
327
|
-
def
|
|
274
|
+
def ugettext(message):
|
|
328
275
|
"""Standard translation function. You can use it in all your exposed
|
|
329
276
|
methods and everywhere where the response object is available.
|
|
330
277
|
|
|
@@ -335,13 +282,13 @@ def gettext(message):
|
|
|
335
282
|
:returns: The translated message.
|
|
336
283
|
:rtype: Unicode
|
|
337
284
|
"""
|
|
338
|
-
return get_translation().
|
|
285
|
+
return get_translation().ugettext(message)
|
|
339
286
|
|
|
340
287
|
|
|
341
|
-
|
|
288
|
+
gettext = ugettext
|
|
342
289
|
|
|
343
290
|
|
|
344
|
-
def
|
|
291
|
+
def ungettext(singular, plural, num):
|
|
345
292
|
"""Like ugettext, but considers plural forms.
|
|
346
293
|
|
|
347
294
|
:parameters:
|
|
@@ -356,29 +303,34 @@ def ngettext(singular, plural, num):
|
|
|
356
303
|
:returns: The translated message as singular or plural.
|
|
357
304
|
:rtype: Unicode
|
|
358
305
|
"""
|
|
359
|
-
return get_translation().
|
|
306
|
+
return get_translation().ungettext(singular, plural, num)
|
|
360
307
|
|
|
361
308
|
|
|
362
|
-
|
|
309
|
+
ngettext = ungettext
|
|
363
310
|
|
|
364
311
|
|
|
365
312
|
def gettext_lazy(message):
|
|
366
|
-
"""Like
|
|
313
|
+
"""Like ugettext, but lazy.
|
|
367
314
|
|
|
368
315
|
:returns: A proxy for the translation object.
|
|
369
316
|
:rtype: LazyProxy
|
|
370
317
|
"""
|
|
371
318
|
|
|
372
319
|
def func():
|
|
373
|
-
return get_translation().
|
|
320
|
+
return get_translation().ugettext(message)
|
|
374
321
|
|
|
375
322
|
return LazyProxy(func, enable_cache=False)
|
|
376
323
|
|
|
377
324
|
|
|
378
325
|
def format_datetime(datetime=None, format='medium', tzinfo=None):
|
|
379
326
|
"""
|
|
380
|
-
|
|
327
|
+
Wraper arround babel format_datetime to provide a default locale.
|
|
328
|
+
The timezone used to format the date is determine with the following priorities:
|
|
329
|
+
* value of tzinfo
|
|
330
|
+
* value of get_timezone()
|
|
331
|
+
* default server time.
|
|
381
332
|
"""
|
|
333
|
+
# When formating date or english, let use en_GB as it's to most complete translation.
|
|
382
334
|
return dates.format_datetime(
|
|
383
335
|
datetime=datetime,
|
|
384
336
|
format=format,
|
|
@@ -389,19 +341,14 @@ def format_datetime(datetime=None, format='medium', tzinfo=None):
|
|
|
389
341
|
|
|
390
342
|
def format_date(datetime=None, format='medium', tzinfo=None):
|
|
391
343
|
"""
|
|
392
|
-
|
|
344
|
+
Wraper arround babel format_date to provide a default locale.
|
|
393
345
|
"""
|
|
394
|
-
|
|
395
|
-
return dates.format_datetime(
|
|
396
|
-
datetime=datetime,
|
|
397
|
-
format=dates.get_date_format(format),
|
|
398
|
-
locale=get_translation().locale,
|
|
399
|
-
tzinfo=tzinfo or get_timezone(),
|
|
400
|
-
)
|
|
346
|
+
return format_datetime(datetime=datetime, format=dates.get_date_format(format), tzinfo=tzinfo)
|
|
401
347
|
|
|
402
348
|
|
|
403
|
-
def get_timezone_name(tzinfo
|
|
404
|
-
|
|
349
|
+
def get_timezone_name(tzinfo):
|
|
350
|
+
locale = Locale(get_translation().locale.language)
|
|
351
|
+
return dates.get_timezone_name(tzinfo, width='long', locale=locale)
|
|
405
352
|
|
|
406
353
|
|
|
407
354
|
def _load_default(mo_dir, domain, default, **kwargs):
|
|
@@ -409,60 +356,35 @@ def _load_default(mo_dir, domain, default, **kwargs):
|
|
|
409
356
|
Initialize the language using the default value from the configuration.
|
|
410
357
|
"""
|
|
411
358
|
# Clear current translation
|
|
412
|
-
|
|
413
|
-
|
|
359
|
+
_current.preferred_lang = [default]
|
|
360
|
+
_current.preferred_timezone = [kwargs['default_timezone']] if 'default_timezone' in kwargs else []
|
|
361
|
+
cherrypy.request._i18n_lang_func = kwargs.get('lang', kwargs.get('func', False))
|
|
362
|
+
cherrypy.request._i18n_tzinfo_func = kwargs.get('tzinfo', False)
|
|
414
363
|
# Clear current translation
|
|
415
|
-
|
|
364
|
+
_current.translation = None
|
|
416
365
|
# Clear current timezone
|
|
417
|
-
|
|
366
|
+
_current.tzinfo = None
|
|
418
367
|
|
|
419
368
|
|
|
420
369
|
def _load_accept_language(**kwargs):
|
|
421
370
|
"""
|
|
422
|
-
When running within a request, load the
|
|
423
|
-
"""
|
|
424
|
-
if cherrypy.request.headers.elements('Accept-Language'):
|
|
425
|
-
# Sort language by quality
|
|
426
|
-
languages = sorted(cherrypy.request.headers.elements('Accept-Language'), key=lambda x: x.qvalue, reverse=True)
|
|
427
|
-
_preferred_lang.set(tuple(lang.value.replace('-', '_') for lang in languages))
|
|
428
|
-
# Clear current translation
|
|
429
|
-
_translation.set(None)
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
def _load_cookie_language(**kwargs):
|
|
371
|
+
When running within a request, load the prefered language from Accept-Language
|
|
433
372
|
"""
|
|
434
|
-
Load preferred language from a request cookie.
|
|
435
373
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
#
|
|
442
|
-
|
|
443
|
-
if not cookie_name:
|
|
444
|
-
return
|
|
445
|
-
|
|
446
|
-
# Check if value defined in cookie.
|
|
447
|
-
cookie = cherrypy.request.cookie
|
|
448
|
-
if cookie_name not in cookie:
|
|
449
|
-
return
|
|
450
|
-
|
|
451
|
-
try:
|
|
452
|
-
value = cookie[cookie_name].value.replace('-', '_')
|
|
453
|
-
except Exception:
|
|
454
|
-
return
|
|
455
|
-
|
|
456
|
-
# Set preferred language and clear cached translation
|
|
457
|
-
_preferred_lang.set((value,))
|
|
458
|
-
_translation.set(None)
|
|
374
|
+
if cherrypy.request.headers.elements('Accept-Language'):
|
|
375
|
+
count = 0
|
|
376
|
+
for x in cherrypy.request.headers.elements('Accept-Language'):
|
|
377
|
+
_current.preferred_lang.insert(count, x.value.replace('-', '_'))
|
|
378
|
+
count += 1
|
|
379
|
+
# Clear current translation
|
|
380
|
+
_current.translation = None
|
|
459
381
|
|
|
460
382
|
|
|
461
383
|
def _load_func_language(**kwargs):
|
|
462
384
|
"""
|
|
463
|
-
When running a request where a current user is found, load
|
|
385
|
+
When running a request where a current user is found, load prefered language from user preferences.
|
|
464
386
|
"""
|
|
465
|
-
func =
|
|
387
|
+
func = getattr(cherrypy.request, '_i18n_lang_func', False)
|
|
466
388
|
if not func:
|
|
467
389
|
return
|
|
468
390
|
try:
|
|
@@ -472,16 +394,16 @@ def _load_func_language(**kwargs):
|
|
|
472
394
|
if not lang:
|
|
473
395
|
return
|
|
474
396
|
# Add custom lang to preferred_lang
|
|
475
|
-
|
|
397
|
+
_current.preferred_lang.insert(0, lang)
|
|
476
398
|
# Clear current translation
|
|
477
|
-
|
|
399
|
+
_current.translation = None
|
|
478
400
|
|
|
479
401
|
|
|
480
402
|
def _load_func_tzinfo(**kwargs):
|
|
481
403
|
"""
|
|
482
|
-
When running a request, load the
|
|
404
|
+
When running a request, load the prefered timezone information from user preferences.
|
|
483
405
|
"""
|
|
484
|
-
func =
|
|
406
|
+
func = getattr(cherrypy.request, '_i18n_tzinfo_func', False)
|
|
485
407
|
if not func:
|
|
486
408
|
return
|
|
487
409
|
try:
|
|
@@ -491,9 +413,9 @@ def _load_func_tzinfo(**kwargs):
|
|
|
491
413
|
if not tzinfo:
|
|
492
414
|
return
|
|
493
415
|
# Add custom lang to preferred_lang
|
|
494
|
-
|
|
416
|
+
_current.preferred_timezone.insert(0, tzinfo)
|
|
495
417
|
# Clear current translation
|
|
496
|
-
|
|
418
|
+
_current.tzinfo = None
|
|
497
419
|
|
|
498
420
|
|
|
499
421
|
def _set_content_language(**kwargs):
|
|
@@ -502,12 +424,7 @@ def _set_content_language(**kwargs):
|
|
|
502
424
|
language of `cherrypy.response.i18n.locale`.
|
|
503
425
|
"""
|
|
504
426
|
if 'Content-Language' not in cherrypy.response.headers:
|
|
505
|
-
|
|
506
|
-
translation = _translation.get()
|
|
507
|
-
if translation:
|
|
508
|
-
locale = translation.locale
|
|
509
|
-
language_tag = f"{locale.language}-{locale.territory}" if locale.territory else locale.language
|
|
510
|
-
cherrypy.response.headers['Content-Language'] = language_tag
|
|
427
|
+
cherrypy.response.headers['Content-Language'] = str(get_translation().locale)
|
|
511
428
|
|
|
512
429
|
|
|
513
430
|
class I18nTool(cherrypy.Tool):
|
|
@@ -520,7 +437,6 @@ class I18nTool(cherrypy.Tool):
|
|
|
520
437
|
cherrypy.Tool._setup(self)
|
|
521
438
|
# Attach additional hooks as different priority to update preferred lang with more accurate preferences.
|
|
522
439
|
cherrypy.request.hooks.attach('before_handler', _load_accept_language, priority=60)
|
|
523
|
-
cherrypy.request.hooks.attach('before_handler', _load_cookie_language, priority=70)
|
|
524
440
|
cherrypy.request.hooks.attach('before_handler', _load_func_language, priority=75)
|
|
525
441
|
cherrypy.request.hooks.attach('before_handler', _load_func_tzinfo, priority=75)
|
|
526
442
|
cherrypy.request.hooks.attach('before_finalize', _set_content_language)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Jinja2 tools for cherrypy
|
|
2
|
-
# Copyright (C) 2021-
|
|
2
|
+
# Copyright (C) 2021-2025 Patrik Dufresne
|
|
3
3
|
#
|
|
4
4
|
# This program is free software: you can redistribute it and/or modify
|
|
5
5
|
# it under the terms of the GNU General Public License as published by
|
|
@@ -80,17 +80,7 @@ class Jinja2Tool(cherrypy.Tool):
|
|
|
80
80
|
|
|
81
81
|
# Enable translation if available
|
|
82
82
|
if hasattr(cherrypy.tools, 'i18n'):
|
|
83
|
-
from .i18n import
|
|
84
|
-
format_date,
|
|
85
|
-
format_datetime,
|
|
86
|
-
get_language_name,
|
|
87
|
-
get_timezone_name,
|
|
88
|
-
get_translation,
|
|
89
|
-
list_available_locales,
|
|
90
|
-
list_available_timezones,
|
|
91
|
-
ugettext,
|
|
92
|
-
ungettext,
|
|
93
|
-
)
|
|
83
|
+
from .i18n import format_date, format_datetime, get_language_name, get_translation, ugettext, ungettext
|
|
94
84
|
|
|
95
85
|
env.add_extension('jinja2.ext.i18n')
|
|
96
86
|
env.install_gettext_callables(ugettext, ungettext, newstyle=True)
|
|
@@ -98,9 +88,6 @@ class Jinja2Tool(cherrypy.Tool):
|
|
|
98
88
|
env.filters['format_datetime'] = format_datetime
|
|
99
89
|
env.globals['get_language_name'] = get_language_name
|
|
100
90
|
env.globals['get_translation'] = get_translation
|
|
101
|
-
env.globals['list_available_locales'] = list_available_locales
|
|
102
|
-
env.globals['list_available_timezones'] = list_available_timezones
|
|
103
|
-
env.globals['get_timezone_name'] = get_timezone_name
|
|
104
91
|
|
|
105
92
|
# Update globals, filters and tests
|
|
106
93
|
env.globals.update(globals)
|