cherrypy-foundation 1.0.0a11__py3-none-any.whl → 1.0.0a13__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/plugins/ldap.py +1 -1
- cherrypy_foundation/plugins/tests/test_ldap.py +75 -2
- cherrypy_foundation/tools/i18n.py +193 -152
- cherrypy_foundation/tools/tests/test_i18n.py +9 -2
- {cherrypy_foundation-1.0.0a11.dist-info → cherrypy_foundation-1.0.0a13.dist-info}/METADATA +1 -1
- {cherrypy_foundation-1.0.0a11.dist-info → cherrypy_foundation-1.0.0a13.dist-info}/RECORD +9 -11
- cherrypy_foundation/tools/tests/locales/en/LC_MESSAGES/messages.mo +0 -0
- cherrypy_foundation/tools/tests/locales/en/LC_MESSAGES/messages.po +0 -15
- {cherrypy_foundation-1.0.0a11.dist-info → cherrypy_foundation-1.0.0a13.dist-info}/WHEEL +0 -0
- {cherrypy_foundation-1.0.0a11.dist-info → cherrypy_foundation-1.0.0a13.dist-info}/licenses/LICENSE.md +0 -0
- {cherrypy_foundation-1.0.0a11.dist-info → cherrypy_foundation-1.0.0a13.dist-info}/top_level.txt +0 -0
|
@@ -19,13 +19,13 @@ Created on Oct 17, 2015
|
|
|
19
19
|
@author: Patrik Dufresne <patrik@ikus-soft.com>
|
|
20
20
|
"""
|
|
21
21
|
import os
|
|
22
|
-
from unittest import mock, skipUnless
|
|
22
|
+
from unittest import TestCase, mock, skipUnless
|
|
23
23
|
|
|
24
24
|
import cherrypy
|
|
25
25
|
import ldap3
|
|
26
26
|
from cherrypy.test import helper
|
|
27
27
|
|
|
28
|
-
from .. import
|
|
28
|
+
from ..ldap import all_attribute, first_attribute # noqa
|
|
29
29
|
|
|
30
30
|
original_connection = ldap3.Connection
|
|
31
31
|
|
|
@@ -35,6 +35,79 @@ def mock_ldap_connection(*args, **kwargs):
|
|
|
35
35
|
return original_connection(*args, client_strategy=ldap3.MOCK_ASYNC, **kwargs)
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
class LdapFirstAttributeTest(TestCase):
|
|
39
|
+
|
|
40
|
+
def test_no_keys_returns_default(self):
|
|
41
|
+
attributes = {"cn": ["John Doe"]}
|
|
42
|
+
self.assertIsNone(first_attribute(attributes, None))
|
|
43
|
+
self.assertEqual(first_attribute(attributes, [], default="fallback"), "fallback")
|
|
44
|
+
|
|
45
|
+
def test_single_key_with_scalar_value(self):
|
|
46
|
+
attributes = {"uid": "jdoe"}
|
|
47
|
+
self.assertEqual(first_attribute(attributes, "uid"), "jdoe")
|
|
48
|
+
|
|
49
|
+
def test_single_key_with_list_value(self):
|
|
50
|
+
attributes = {"mail": ["john@example.com", "alt@example.com"]}
|
|
51
|
+
self.assertEqual(first_attribute(attributes, "mail"), "john@example.com")
|
|
52
|
+
|
|
53
|
+
def test_empty_list_value_is_skipped(self):
|
|
54
|
+
attributes = {
|
|
55
|
+
"mail": [],
|
|
56
|
+
"uid": ["jdoe"],
|
|
57
|
+
}
|
|
58
|
+
self.assertEqual(first_attribute(attributes, ["mail", "uid"]), "jdoe")
|
|
59
|
+
|
|
60
|
+
def test_missing_first_key_uses_next_key(self):
|
|
61
|
+
attributes = {"cn": ["John Doe"]}
|
|
62
|
+
self.assertEqual(first_attribute(attributes, ["sn", "cn"]), "John Doe")
|
|
63
|
+
|
|
64
|
+
def test_all_keys_missing_returns_default(self):
|
|
65
|
+
attributes = {"cn": ["John Doe"]}
|
|
66
|
+
self.assertEqual(first_attribute(attributes, ["sn", "uid"], default="unknown"), "unknown")
|
|
67
|
+
|
|
68
|
+
def test_key_not_found_without_default_returns_none(self):
|
|
69
|
+
attributes = {"cn": ["John Doe"]}
|
|
70
|
+
self.assertIsNone(first_attribute(attributes, "uid"))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class LdapAllAttributeTest(TestCase):
|
|
74
|
+
|
|
75
|
+
def test_no_keys_returns_default(self):
|
|
76
|
+
attributes = {"cn": ["John Doe"]}
|
|
77
|
+
self.assertIsNone(all_attribute(attributes, None))
|
|
78
|
+
self.assertEqual(all_attribute(attributes, [], default="fallback"), "fallback")
|
|
79
|
+
|
|
80
|
+
def test_single_key_with_scalar_value(self):
|
|
81
|
+
attributes = {"uid": "jdoe"}
|
|
82
|
+
self.assertEqual(all_attribute(attributes, "uid"), ["jdoe"])
|
|
83
|
+
|
|
84
|
+
def test_single_key_with_list_value(self):
|
|
85
|
+
attributes = {"mail": ["john@example.com", "alt@example.com"]}
|
|
86
|
+
self.assertEqual(all_attribute(attributes, "mail"), ["john@example.com"])
|
|
87
|
+
|
|
88
|
+
def test_multiple_keys_collect_values(self):
|
|
89
|
+
attributes = {
|
|
90
|
+
"uid": "jdoe",
|
|
91
|
+
"cn": ["John Doe"],
|
|
92
|
+
}
|
|
93
|
+
self.assertEqual(all_attribute(attributes, ["uid", "cn"]), ["jdoe", "John Doe"])
|
|
94
|
+
|
|
95
|
+
def test_missing_keys_are_skipped(self):
|
|
96
|
+
attributes = {"cn": ["John Doe"]}
|
|
97
|
+
self.assertEqual(all_attribute(attributes, ["sn", "cn", "uid"]), ["John Doe"])
|
|
98
|
+
|
|
99
|
+
def test_all_keys_missing_returns_default(self):
|
|
100
|
+
attributes = {"cn": ["John Doe"]}
|
|
101
|
+
self.assertEqual(all_attribute(attributes, ["sn", "uid"], default=[]), [])
|
|
102
|
+
|
|
103
|
+
def test_empty_list_value_is_appended_as_empty_list(self):
|
|
104
|
+
attributes = {
|
|
105
|
+
"mail": [],
|
|
106
|
+
"uid": "jdoe",
|
|
107
|
+
}
|
|
108
|
+
self.assertEqual(all_attribute(attributes, ["mail", "uid"]), [[], "jdoe"])
|
|
109
|
+
|
|
110
|
+
|
|
38
111
|
class LdapPluginTest(helper.CPWebCase):
|
|
39
112
|
|
|
40
113
|
@classmethod
|
|
@@ -14,95 +14,107 @@
|
|
|
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
|
-
"""
|
|
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):
|
|
18
25
|
|
|
19
|
-
|
|
26
|
+
1. Language explicitly set with ``with i18n.preferred_lang():``
|
|
27
|
+
2. User-defined callback (``tools.i18n.func``)
|
|
28
|
+
3. HTTP ``Accept-Language`` request header
|
|
29
|
+
4. Default language configured via ``tools.i18n.default``
|
|
20
30
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
3. HTTP Accept-Language headers
|
|
24
|
-
4. `tools.i18n.default` value
|
|
31
|
+
Translations are loaded using Babel and gettext, and the resolved locale is
|
|
32
|
+
available through ``i18n.get_translation()`` during request handling.
|
|
25
33
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
---------------------------------------------------------------------------
|
|
35
|
+
Basic usage
|
|
36
|
+
---------------------------------------------------------------------------
|
|
29
37
|
|
|
30
|
-
|
|
38
|
+
Within Python code, mark translatable strings using ``ugettext`` or
|
|
39
|
+
``ungettext``:
|
|
31
40
|
|
|
32
|
-
from i18n import
|
|
41
|
+
from i18n import gettext as _, ngettext
|
|
33
42
|
|
|
34
|
-
class MyController
|
|
43
|
+
class MyController:
|
|
35
44
|
@cherrypy.expose
|
|
36
45
|
def index(self):
|
|
37
46
|
locale = cherrypy.response.i18n.locale
|
|
38
|
-
s1 = _(u
|
|
39
|
-
s2 =
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
s1 = _(u"Translatable string")
|
|
48
|
+
s2 = ngettext(
|
|
49
|
+
u"There is one item.",
|
|
50
|
+
u"There are multiple items.",
|
|
51
|
+
2
|
|
52
|
+
)
|
|
53
|
+
return "<br />".join([s1, s2, locale.display_name])
|
|
42
54
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
hopefully the response object is available then).
|
|
55
|
+
---------------------------------------------------------------------------
|
|
56
|
+
Lazy translations
|
|
57
|
+
---------------------------------------------------------------------------
|
|
47
58
|
|
|
48
|
-
|
|
59
|
+
If code is executed before a CherryPy response object is available
|
|
60
|
+
(e.g. model definitions or module-level constants), use the ``*_lazy``
|
|
61
|
+
helpers. These defer translation until the value is actually rendered:
|
|
49
62
|
|
|
50
|
-
from i18n_tool import
|
|
63
|
+
from i18n_tool import gettext_lazy
|
|
51
64
|
|
|
52
65
|
class Model:
|
|
53
|
-
|
|
54
|
-
name = ugettext_lazy(u'Name of the model')
|
|
66
|
+
name = gettext_lazy(u"Model name")
|
|
55
67
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
68
|
+
---------------------------------------------------------------------------
|
|
69
|
+
Templates
|
|
70
|
+
---------------------------------------------------------------------------
|
|
59
71
|
|
|
72
|
+
For template rendering, i18n integrate with jinja2.
|
|
60
73
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
tools.i18n.domain = Your gettext domain (e.g. application name)
|
|
74
|
+
{% trans %}Text to translate{% endtrans %}
|
|
75
|
+
{{ _('Text to translate') }}
|
|
76
|
+
{{ get_translation().gettext('Text to translate') }}
|
|
77
|
+
{{ get_translation().locale }}
|
|
78
|
+
{{ var | format_datetime(format='full') }}
|
|
79
|
+
{{ var | format_date(format='full') }}
|
|
68
80
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
81
|
+
---------------------------------------------------------------------------
|
|
82
|
+
Configuration
|
|
83
|
+
---------------------------------------------------------------------------
|
|
72
84
|
|
|
73
|
-
Example
|
|
85
|
+
Example CherryPy configuration:
|
|
74
86
|
|
|
75
87
|
[/]
|
|
76
88
|
tools.i18n.on = True
|
|
77
|
-
tools.i18n.default =
|
|
78
|
-
tools.i18n.mo_dir =
|
|
79
|
-
tools.i18n.domain =
|
|
89
|
+
tools.i18n.default = "en_US"
|
|
90
|
+
tools.i18n.mo_dir = "/path/to/i18n"
|
|
91
|
+
tools.i18n.domain = "myapp"
|
|
80
92
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
93
|
+
The ``mo_dir`` directory must contain subdirectories named after language
|
|
94
|
+
codes (e.g. ``en``, ``fr_CA``), each containing an ``LC_MESSAGES`` directory
|
|
95
|
+
with the compiled ``.mo`` file:
|
|
84
96
|
|
|
85
|
-
|
|
97
|
+
<mo_dir>/<language>/LC_MESSAGES/<domain>.mo
|
|
86
98
|
|
|
87
|
-
:License: BSD
|
|
88
|
-
:Author: Thorsten Weimann <thorsten.weimann (at) gmx (dot) net>
|
|
89
|
-
:Date: 2010-02-08
|
|
90
99
|
"""
|
|
91
100
|
|
|
92
|
-
|
|
101
|
+
import logging
|
|
93
102
|
import os
|
|
94
|
-
import threading
|
|
95
103
|
from contextlib import contextmanager
|
|
104
|
+
from contextvars import ContextVar
|
|
96
105
|
from functools import lru_cache
|
|
106
|
+
from gettext import NullTranslations, translation
|
|
97
107
|
|
|
98
108
|
import cherrypy
|
|
99
109
|
import pytz
|
|
100
110
|
from babel import dates
|
|
101
111
|
from babel.core import Locale, get_global
|
|
102
|
-
from babel.support import LazyProxy,
|
|
112
|
+
from babel.support import LazyProxy, Translations
|
|
103
113
|
|
|
104
|
-
|
|
105
|
-
|
|
114
|
+
_preferred_lang = ContextVar('preferred_lang', default=())
|
|
115
|
+
_preferred_timezone = ContextVar('preferred_timezone', default=())
|
|
116
|
+
_translation = ContextVar('translation', default=None)
|
|
117
|
+
_tzinfo = ContextVar('tzinfo', default=None)
|
|
106
118
|
|
|
107
119
|
|
|
108
120
|
def _get_config(key, default=None):
|
|
@@ -122,21 +134,20 @@ def preferred_lang(lang):
|
|
|
122
134
|
with i18n.preferred_lang('fr'):
|
|
123
135
|
i18n.gettext('some string')
|
|
124
136
|
"""
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
prev_trans = getattr(_current, 'translation', None)
|
|
137
|
+
if not (lang is None or isinstance(lang, str)):
|
|
138
|
+
raise ValueError(lang)
|
|
128
139
|
try:
|
|
129
|
-
# Update
|
|
140
|
+
# Update preferred lang and clear translation.
|
|
130
141
|
if lang:
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
142
|
+
token_l = _preferred_lang.set((lang,))
|
|
143
|
+
else:
|
|
144
|
+
token_l = _preferred_lang.set(tuple())
|
|
145
|
+
token_t = _translation.set(None)
|
|
135
146
|
yield
|
|
136
147
|
finally:
|
|
137
148
|
# Restore previous value
|
|
138
|
-
|
|
139
|
-
|
|
149
|
+
_preferred_lang.reset(token_l)
|
|
150
|
+
_translation.reset(token_t)
|
|
140
151
|
|
|
141
152
|
|
|
142
153
|
@contextmanager
|
|
@@ -147,22 +158,24 @@ def preferred_timezone(timezone):
|
|
|
147
158
|
with i18n.preferred_lang('America/Montreal'):
|
|
148
159
|
i18n.format_datetime(...)
|
|
149
160
|
"""
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
prev_tzinfo = getattr(_current, 'tzinfo', None)
|
|
161
|
+
if not (timezone is None or isinstance(timezone, str)):
|
|
162
|
+
raise ValueError(timezone)
|
|
153
163
|
try:
|
|
154
|
-
# Update
|
|
155
|
-
|
|
156
|
-
|
|
164
|
+
# Update preferred timezone and clear tzinfo.
|
|
165
|
+
if timezone:
|
|
166
|
+
token_t = _preferred_timezone.set((timezone,))
|
|
167
|
+
else:
|
|
168
|
+
token_t = _preferred_timezone.set(tuple())
|
|
169
|
+
token_z = _tzinfo.set(None)
|
|
157
170
|
yield
|
|
158
171
|
finally:
|
|
159
172
|
# Restore previous value
|
|
160
|
-
|
|
161
|
-
|
|
173
|
+
_preferred_timezone.reset(token_t)
|
|
174
|
+
_tzinfo.reset(token_z)
|
|
162
175
|
|
|
163
176
|
|
|
164
|
-
@lru_cache(maxsize=
|
|
165
|
-
def _search_translation(dirname, domain, *locales):
|
|
177
|
+
@lru_cache(maxsize=32)
|
|
178
|
+
def _search_translation(dirname, domain, *locales, sourcecode_lang='en'):
|
|
166
179
|
"""
|
|
167
180
|
Loads the first existing translations for known locale.
|
|
168
181
|
|
|
@@ -170,29 +183,37 @@ def _search_translation(dirname, domain, *locales):
|
|
|
170
183
|
langs : List
|
|
171
184
|
List of languages as returned by `parse_accept_language_header`.
|
|
172
185
|
dirname : String
|
|
173
|
-
|
|
174
|
-
Might be a list of directories.
|
|
186
|
+
A single directory of the translations (`tools.i18n.mo_dir`).
|
|
175
187
|
domain : String
|
|
176
|
-
Gettext domain of the catalog (`tools.
|
|
188
|
+
Gettext domain of the catalog (`tools.i18n.domain`).
|
|
177
189
|
|
|
178
190
|
:returns: Translations, the corresponding Locale object.
|
|
179
191
|
"""
|
|
180
192
|
if not isinstance(locales, (list, tuple)):
|
|
181
|
-
locales =
|
|
182
|
-
|
|
183
|
-
#
|
|
184
|
-
if t.__class__ is NullTranslations:
|
|
185
|
-
return None
|
|
186
|
-
# Assign prefered local to this translation to know the current locale.
|
|
187
|
-
trans_locale = Locale.parse(t.files[0].split('/')[-3])
|
|
193
|
+
locales = tuple(locales)
|
|
194
|
+
|
|
195
|
+
# Loop on each locales to find the best matching translation.
|
|
188
196
|
for locale in locales:
|
|
189
197
|
try:
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
198
|
+
# Use `gettext.translation()` instead of `gettext.find()` to chain translation fr_CA -> fr -> src.
|
|
199
|
+
t = translation(domain=domain, localedir=dirname, languages=[locale], fallback=True, class_=Translations)
|
|
200
|
+
except Exception:
|
|
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)
|
|
196
217
|
return t
|
|
197
218
|
|
|
198
219
|
|
|
@@ -200,23 +221,37 @@ def get_language_name(lang_code):
|
|
|
200
221
|
"""
|
|
201
222
|
Translate the language code into it's language display name.
|
|
202
223
|
"""
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
224
|
+
try:
|
|
225
|
+
locale = Locale.parse(lang_code)
|
|
226
|
+
except Exception:
|
|
227
|
+
return lang_code
|
|
228
|
+
translation = get_translation()
|
|
229
|
+
return locale.get_language_name(translation.locale)
|
|
206
230
|
|
|
207
231
|
|
|
208
232
|
def get_timezone():
|
|
209
233
|
"""
|
|
210
234
|
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
|
+
|
|
211
242
|
"""
|
|
212
243
|
# When tzinfo is defined, use it
|
|
213
|
-
tzinfo =
|
|
244
|
+
tzinfo = _tzinfo.get()
|
|
214
245
|
if tzinfo is not None:
|
|
215
246
|
return tzinfo
|
|
216
247
|
# Otherwise search for a valid timezone.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
248
|
+
default = _get_config('tools.i18n.default_timezone')
|
|
249
|
+
preferred_timezone = _preferred_timezone.get()
|
|
250
|
+
if default and default not in preferred_timezone:
|
|
251
|
+
preferred_timezone = (
|
|
252
|
+
*preferred_timezone,
|
|
253
|
+
default,
|
|
254
|
+
)
|
|
220
255
|
for timezone in preferred_timezone:
|
|
221
256
|
try:
|
|
222
257
|
tzinfo = dates.get_timezone(timezone)
|
|
@@ -226,8 +261,8 @@ def get_timezone():
|
|
|
226
261
|
# If we can't find a valid timezone using the default and preferred value, fall back to server timezone.
|
|
227
262
|
if tzinfo is None:
|
|
228
263
|
tzinfo = dates.get_timezone(None)
|
|
229
|
-
|
|
230
|
-
return
|
|
264
|
+
_tzinfo.set(tzinfo)
|
|
265
|
+
return tzinfo
|
|
231
266
|
|
|
232
267
|
|
|
233
268
|
def get_translation():
|
|
@@ -235,37 +270,42 @@ def get_translation():
|
|
|
235
270
|
Get the best translation for the current context.
|
|
236
271
|
"""
|
|
237
272
|
# When translation is defined, use it
|
|
238
|
-
translation =
|
|
273
|
+
translation = _translation.get()
|
|
239
274
|
if translation is not None:
|
|
240
275
|
return translation
|
|
241
276
|
|
|
242
277
|
# Otherwise, we need to search the translation.
|
|
243
278
|
# `preferred_lang` should always has a sane value within a cherrypy request because of hooks
|
|
244
279
|
# But we also need to support calls outside cherrypy.
|
|
280
|
+
sourcecode_lang = _get_config('tools.i18n.sourcecode_lang', 'en')
|
|
245
281
|
default = _get_config('tools.i18n.default')
|
|
246
|
-
preferred_lang =
|
|
282
|
+
preferred_lang = _preferred_lang.get()
|
|
283
|
+
if default and default not in preferred_lang:
|
|
284
|
+
preferred_lang = (
|
|
285
|
+
*preferred_lang,
|
|
286
|
+
default,
|
|
287
|
+
)
|
|
247
288
|
mo_dir = _get_config('tools.i18n.mo_dir')
|
|
248
|
-
domain = _get_config('tools.i18n.domain')
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
trans.locale = Locale('en')
|
|
253
|
-
_current.translation = trans
|
|
254
|
-
return _current.translation
|
|
289
|
+
domain = _get_config('tools.i18n.domain', 'messages')
|
|
290
|
+
translation = _search_translation(mo_dir, domain, *preferred_lang, sourcecode_lang=sourcecode_lang)
|
|
291
|
+
_translation.set(translation)
|
|
292
|
+
return translation
|
|
255
293
|
|
|
256
294
|
|
|
257
295
|
def list_available_locales():
|
|
258
296
|
"""
|
|
259
297
|
Return a list of available translations.
|
|
260
298
|
"""
|
|
261
|
-
mo_dir = _get_config('tools.i18n.mo_dir'
|
|
262
|
-
domain = _get_config('tools.i18n.domain')
|
|
299
|
+
mo_dir = _get_config('tools.i18n.mo_dir')
|
|
300
|
+
domain = _get_config('tools.i18n.domain', 'messages')
|
|
263
301
|
if not mo_dir:
|
|
264
302
|
return
|
|
265
303
|
for lang in os.listdir(mo_dir):
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
304
|
+
if os.path.exists(os.path.join(mo_dir, lang, 'LC_MESSAGES', f'{domain}.mo')):
|
|
305
|
+
try:
|
|
306
|
+
yield Locale.parse(lang)
|
|
307
|
+
except Exception:
|
|
308
|
+
continue
|
|
269
309
|
|
|
270
310
|
|
|
271
311
|
def list_available_timezones():
|
|
@@ -278,7 +318,7 @@ def list_available_timezones():
|
|
|
278
318
|
|
|
279
319
|
|
|
280
320
|
# Public translation functions
|
|
281
|
-
def
|
|
321
|
+
def gettext(message):
|
|
282
322
|
"""Standard translation function. You can use it in all your exposed
|
|
283
323
|
methods and everywhere where the response object is available.
|
|
284
324
|
|
|
@@ -289,13 +329,13 @@ def ugettext(message):
|
|
|
289
329
|
:returns: The translated message.
|
|
290
330
|
:rtype: Unicode
|
|
291
331
|
"""
|
|
292
|
-
return get_translation().
|
|
332
|
+
return get_translation().gettext(message)
|
|
293
333
|
|
|
294
334
|
|
|
295
|
-
|
|
335
|
+
ugettext = gettext
|
|
296
336
|
|
|
297
337
|
|
|
298
|
-
def
|
|
338
|
+
def ngettext(singular, plural, num):
|
|
299
339
|
"""Like ugettext, but considers plural forms.
|
|
300
340
|
|
|
301
341
|
:parameters:
|
|
@@ -310,34 +350,29 @@ def ungettext(singular, plural, num):
|
|
|
310
350
|
:returns: The translated message as singular or plural.
|
|
311
351
|
:rtype: Unicode
|
|
312
352
|
"""
|
|
313
|
-
return get_translation().
|
|
353
|
+
return get_translation().ngettext(singular, plural, num)
|
|
314
354
|
|
|
315
355
|
|
|
316
|
-
|
|
356
|
+
ungettext = ngettext
|
|
317
357
|
|
|
318
358
|
|
|
319
359
|
def gettext_lazy(message):
|
|
320
|
-
"""Like
|
|
360
|
+
"""Like gettext, but lazy.
|
|
321
361
|
|
|
322
362
|
:returns: A proxy for the translation object.
|
|
323
363
|
:rtype: LazyProxy
|
|
324
364
|
"""
|
|
325
365
|
|
|
326
366
|
def func():
|
|
327
|
-
return get_translation().
|
|
367
|
+
return get_translation().gettext(message)
|
|
328
368
|
|
|
329
369
|
return LazyProxy(func, enable_cache=False)
|
|
330
370
|
|
|
331
371
|
|
|
332
372
|
def format_datetime(datetime=None, format='medium', tzinfo=None):
|
|
333
373
|
"""
|
|
334
|
-
|
|
335
|
-
The timezone used to format the date is determine with the following priorities:
|
|
336
|
-
* value of tzinfo
|
|
337
|
-
* value of get_timezone()
|
|
338
|
-
* default server time.
|
|
374
|
+
Wrapper around babel format_datetime to use current locale and current timezone.
|
|
339
375
|
"""
|
|
340
|
-
# When formating date or english, let use en_GB as it's to most complete translation.
|
|
341
376
|
return dates.format_datetime(
|
|
342
377
|
datetime=datetime,
|
|
343
378
|
format=format,
|
|
@@ -348,14 +383,19 @@ def format_datetime(datetime=None, format='medium', tzinfo=None):
|
|
|
348
383
|
|
|
349
384
|
def format_date(datetime=None, format='medium', tzinfo=None):
|
|
350
385
|
"""
|
|
351
|
-
|
|
386
|
+
Wrapper around babel format_date to provide a default locale.
|
|
352
387
|
"""
|
|
353
|
-
|
|
388
|
+
# To enforce the timezone and locale, make use of format_datetime for dates.
|
|
389
|
+
return dates.format_datetime(
|
|
390
|
+
datetime=datetime,
|
|
391
|
+
format=dates.get_date_format(format),
|
|
392
|
+
locale=get_translation().locale,
|
|
393
|
+
tzinfo=tzinfo or get_timezone(),
|
|
394
|
+
)
|
|
354
395
|
|
|
355
396
|
|
|
356
|
-
def get_timezone_name(tzinfo):
|
|
357
|
-
|
|
358
|
-
return dates.get_timezone_name(tzinfo, width='long', locale=locale)
|
|
397
|
+
def get_timezone_name(tzinfo, width='long'):
|
|
398
|
+
return dates.get_timezone_name(tzinfo, width=width, locale=get_translation().locale)
|
|
359
399
|
|
|
360
400
|
|
|
361
401
|
def _load_default(mo_dir, domain, default, **kwargs):
|
|
@@ -363,33 +403,31 @@ def _load_default(mo_dir, domain, default, **kwargs):
|
|
|
363
403
|
Initialize the language using the default value from the configuration.
|
|
364
404
|
"""
|
|
365
405
|
# Clear current translation
|
|
366
|
-
|
|
367
|
-
|
|
406
|
+
_preferred_lang.set(tuple())
|
|
407
|
+
_preferred_timezone.set(tuple())
|
|
368
408
|
cherrypy.request._i18n_lang_func = kwargs.get('lang', kwargs.get('func', False))
|
|
369
409
|
cherrypy.request._i18n_tzinfo_func = kwargs.get('tzinfo', False)
|
|
370
410
|
# Clear current translation
|
|
371
|
-
|
|
411
|
+
_translation.set(None)
|
|
372
412
|
# Clear current timezone
|
|
373
|
-
|
|
413
|
+
_tzinfo.set(None)
|
|
374
414
|
|
|
375
415
|
|
|
376
416
|
def _load_accept_language(**kwargs):
|
|
377
417
|
"""
|
|
378
|
-
When running within a request, load the
|
|
418
|
+
When running within a request, load the preferred language from Accept-Language header.
|
|
379
419
|
"""
|
|
380
|
-
|
|
381
420
|
if cherrypy.request.headers.elements('Accept-Language'):
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
_current.translation = None
|
|
421
|
+
# Sort language by quality
|
|
422
|
+
languages = sorted(cherrypy.request.headers.elements('Accept-Language'), key=lambda x: x.qvalue, reverse=True)
|
|
423
|
+
_preferred_lang.set(tuple(lang.value.replace('-', '_') for lang in languages))
|
|
424
|
+
# Clear current translation
|
|
425
|
+
_translation.set(None)
|
|
388
426
|
|
|
389
427
|
|
|
390
428
|
def _load_func_language(**kwargs):
|
|
391
429
|
"""
|
|
392
|
-
When running a request where a current user is found, load
|
|
430
|
+
When running a request where a current user is found, load preferred language from user preferences.
|
|
393
431
|
"""
|
|
394
432
|
func = getattr(cherrypy.request, '_i18n_lang_func', False)
|
|
395
433
|
if not func:
|
|
@@ -401,14 +439,14 @@ def _load_func_language(**kwargs):
|
|
|
401
439
|
if not lang:
|
|
402
440
|
return
|
|
403
441
|
# Add custom lang to preferred_lang
|
|
404
|
-
|
|
442
|
+
_preferred_lang.set((lang,))
|
|
405
443
|
# Clear current translation
|
|
406
|
-
|
|
444
|
+
_translation.set(None)
|
|
407
445
|
|
|
408
446
|
|
|
409
447
|
def _load_func_tzinfo(**kwargs):
|
|
410
448
|
"""
|
|
411
|
-
When running a request, load the
|
|
449
|
+
When running a request, load the preferred timezone information from user preferences.
|
|
412
450
|
"""
|
|
413
451
|
func = getattr(cherrypy.request, '_i18n_tzinfo_func', False)
|
|
414
452
|
if not func:
|
|
@@ -420,9 +458,9 @@ def _load_func_tzinfo(**kwargs):
|
|
|
420
458
|
if not tzinfo:
|
|
421
459
|
return
|
|
422
460
|
# Add custom lang to preferred_lang
|
|
423
|
-
|
|
461
|
+
_preferred_timezone.set((tzinfo,))
|
|
424
462
|
# Clear current translation
|
|
425
|
-
|
|
463
|
+
_tzinfo.set(None)
|
|
426
464
|
|
|
427
465
|
|
|
428
466
|
def _set_content_language(**kwargs):
|
|
@@ -431,9 +469,12 @@ def _set_content_language(**kwargs):
|
|
|
431
469
|
language of `cherrypy.response.i18n.locale`.
|
|
432
470
|
"""
|
|
433
471
|
if 'Content-Language' not in cherrypy.response.headers:
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
472
|
+
# Only define the content language if the handler uses i18n module.
|
|
473
|
+
translation = _translation.get()
|
|
474
|
+
if translation:
|
|
475
|
+
locale = translation.locale
|
|
476
|
+
language_tag = f"{locale.language}-{locale.territory}" if locale.territory else locale.language
|
|
477
|
+
cherrypy.response.headers['Content-Language'] = language_tag
|
|
437
478
|
|
|
438
479
|
|
|
439
480
|
class I18nTool(cherrypy.Tool):
|
|
@@ -38,7 +38,6 @@ class TestI18n(unittest.TestCase):
|
|
|
38
38
|
def test_search_translation_en(self):
|
|
39
39
|
# Load default translation return translation
|
|
40
40
|
t = i18n._search_translation(self.mo_dir, 'messages', 'en')
|
|
41
|
-
self.assertIsInstance(t, gettext.GNUTranslations)
|
|
42
41
|
self.assertEqual("en", t.locale.language)
|
|
43
42
|
# Test translation object
|
|
44
43
|
self.assertEqual(TEXT_EN, t.gettext(TEXT_EN))
|
|
@@ -54,7 +53,8 @@ class TestI18n(unittest.TestCase):
|
|
|
54
53
|
def test_search_translation_invalid(self):
|
|
55
54
|
# Load invalid translation return None
|
|
56
55
|
t = i18n._search_translation(self.mo_dir, 'messages', 'tr')
|
|
57
|
-
|
|
56
|
+
# Return Null translations.
|
|
57
|
+
self.assertIn('NullTranslations', str(t.__class__))
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
class Root:
|
|
@@ -117,6 +117,13 @@ class TestI18nWebCase(AbstractI18nTest):
|
|
|
117
117
|
self.assertHeaderItemValue("Content-Language", "fr-CA")
|
|
118
118
|
self.assertInBody(TEXT_FR)
|
|
119
119
|
|
|
120
|
+
def test_language_en_US_POSIX(self):
|
|
121
|
+
# When calling with locale variant
|
|
122
|
+
self.getPage("/", headers=[("Accept-Language", "en-US-POSIX")])
|
|
123
|
+
self.assertStatus('200 OK')
|
|
124
|
+
# Tehn page return en-US
|
|
125
|
+
self.assertHeaderItemValue("Content-Language", "en-US")
|
|
126
|
+
|
|
120
127
|
def test_with_preferred_lang(self):
|
|
121
128
|
# Given a default lang 'en'
|
|
122
129
|
date = datetime.fromtimestamp(1680111611, timezone.utc)
|
|
@@ -81,13 +81,13 @@ cherrypy_foundation/components/vendor/typeahead/jquery.typeahead.min.css,sha256=
|
|
|
81
81
|
cherrypy_foundation/components/vendor/typeahead/jquery.typeahead.min.js,sha256=eFBtgouYdW587kYINWsjkN-UR8GVWAG_fQe1fpJfjOw,47828
|
|
82
82
|
cherrypy_foundation/plugins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
83
83
|
cherrypy_foundation/plugins/db.py,sha256=46S-2kR9BHVYRN9cxWwYfzY2WkpEVBCpx1ubsJolYfA,10108
|
|
84
|
-
cherrypy_foundation/plugins/ldap.py,sha256=
|
|
84
|
+
cherrypy_foundation/plugins/ldap.py,sha256=PxZp-2tHy73CegiIV5pKnfWe0WOCqNRIYQR8aIh1UEs,9913
|
|
85
85
|
cherrypy_foundation/plugins/restapi.py,sha256=S5GIxHL0M3Wcs33fyVOb0RfX1FXbp2fqUxJ_ufqBrD4,2843
|
|
86
86
|
cherrypy_foundation/plugins/scheduler.py,sha256=McTR72GGTuW7UZKco_Czmp09PfqJ-aHXJxh7h9BFZvA,10777
|
|
87
87
|
cherrypy_foundation/plugins/smtp.py,sha256=yTCUj2XzrJEJ84CIbYcrCUAP6Mg59aZfu6OZDGQyJbQ,7479
|
|
88
88
|
cherrypy_foundation/plugins/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
89
89
|
cherrypy_foundation/plugins/tests/test_db.py,sha256=PELi5ojj8VqcXyPbpaig2Ac4ZqlR_gEPGUYrWJPANWg,3921
|
|
90
|
-
cherrypy_foundation/plugins/tests/test_ldap.py,sha256=
|
|
90
|
+
cherrypy_foundation/plugins/tests/test_ldap.py,sha256=j0QUIxqeEBgN4VLvczUlw0xMM_ldPWVHCceAJzU71Jg,17269
|
|
91
91
|
cherrypy_foundation/plugins/tests/test_scheduler.py,sha256=I-ZuQhMvCCvqFDwukwsyz_UkdJJ8JSLTkAanUo24GCE,3564
|
|
92
92
|
cherrypy_foundation/plugins/tests/test_scheduler_db.py,sha256=-iZnLgb3qsJzs0p7JAw6TQswRtExCmHAtYXqRNPZn7U,3101
|
|
93
93
|
cherrypy_foundation/plugins/tests/test_smtp.py,sha256=qs5yezIpSXkBmLmFlqckfPW7NmntHZxQjDSkdQG_dNE,4183
|
|
@@ -104,7 +104,7 @@ cherrypy_foundation/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
|
|
|
104
104
|
cherrypy_foundation/tools/auth.py,sha256=lTSajxCiReMzm-Fl-xhTByi4yFnInEWOoNsmUMnHQhs,9761
|
|
105
105
|
cherrypy_foundation/tools/auth_mfa.py,sha256=VaLvBz9wo6jTx-2mCGqFXPxl-z14f8UMWvd6_xeXd40,9212
|
|
106
106
|
cherrypy_foundation/tools/errors.py,sha256=ELpAj0N9kIxC22QW5xDQJz60zMpCwgm-Twu2WpELM1A,1005
|
|
107
|
-
cherrypy_foundation/tools/i18n.py,sha256=
|
|
107
|
+
cherrypy_foundation/tools/i18n.py,sha256=GXNh1SIWXrs3EScHkw9lqsvUC8PUeIv514HyTrc78y0,16229
|
|
108
108
|
cherrypy_foundation/tools/jinja2.py,sha256=nppYnk2ASDsyfNHF9m83W4foiN3MhcwDJvo5baEgnGU,5520
|
|
109
109
|
cherrypy_foundation/tools/ratelimit.py,sha256=pT7vZRmjltNeuiQpdyXOmnpG9BcXjLaj-AXJ0e2x_zw,8300
|
|
110
110
|
cherrypy_foundation/tools/secure_headers.py,sha256=Yh-iA_Js4MUsx5nq4ilbc-iWy90ZC0oMb3TJJD_UwYo,3921
|
|
@@ -112,22 +112,20 @@ cherrypy_foundation/tools/sessions_timeout.py,sha256=6iBWJntPMk_Qt94fBSfBISf1IXI
|
|
|
112
112
|
cherrypy_foundation/tools/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
113
113
|
cherrypy_foundation/tools/tests/test_auth.py,sha256=oeM5t38M8DUC9dYn59dcf00jdGY6Ry0jZWhQd_PYQUk,2847
|
|
114
114
|
cherrypy_foundation/tools/tests/test_auth_mfa.py,sha256=911hnBbdg5CKb613uIBrlggoTAyBU9SoL7Sxd-tIKS0,15008
|
|
115
|
-
cherrypy_foundation/tools/tests/test_i18n.py,sha256=
|
|
115
|
+
cherrypy_foundation/tools/tests/test_i18n.py,sha256=hIpdKSwA9bwePnABjUah7ptTWKW1tWKzwkj8n1nEs0s,9390
|
|
116
116
|
cherrypy_foundation/tools/tests/test_jinja2.py,sha256=_dkRJpjB0ybDV6YO0uEFFO8LAcWgVu3VBB8_vWthQ48,4296
|
|
117
117
|
cherrypy_foundation/tools/tests/test_ratelimit.py,sha256=rrqybwMbh1GFlF2-Ut57zHPAc1uqX88aqea6VS_6p5E,3449
|
|
118
118
|
cherrypy_foundation/tools/tests/components/Button.jinja,sha256=uSLp1GpEIgZNXK_GWglu0E_a1c3jHpDLI66MRfMqGhE,95
|
|
119
119
|
cherrypy_foundation/tools/tests/locales/messages.pot,sha256=5K9piTRL7H5MxDXFIWJsCccSJRA0HwfCQQU8b8VYo30,40
|
|
120
120
|
cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.mo,sha256=bsJTVL4OefevkxeHDS3VcW3egP6Yq18LFXwjSyoqIng,336
|
|
121
121
|
cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.po,sha256=glzYY96jmCaGyxYtMqmNF-1q-OYWXBIkMysvbXO4L6E,351
|
|
122
|
-
cherrypy_foundation/tools/tests/locales/en/LC_MESSAGES/messages.mo,sha256=cdyG2Js1TIU6eenDX1ICH8uP45yvl0OLN0-SMUXTBa4,259
|
|
123
|
-
cherrypy_foundation/tools/tests/locales/en/LC_MESSAGES/messages.po,sha256=JCpiRLLHUSYQhzta8ZYjfB50NmpwPGNCTNwo2Glww14,322
|
|
124
122
|
cherrypy_foundation/tools/tests/locales/fr/LC_MESSAGES/messages.mo,sha256=u3_kl_nqZ3FNaSyKVQKmu4KJzN3xOxxJNVmcdhw37jA,327
|
|
125
123
|
cherrypy_foundation/tools/tests/locales/fr/LC_MESSAGES/messages.po,sha256=6_Sk9Igqm7dtpyyS701p5qc4DvOJE7TRT0ajRZctFAQ,342
|
|
126
124
|
cherrypy_foundation/tools/tests/templates/test_jinja2.html,sha256=v9AHxksbBvzE7sesPqE61HMhsvU4juXt3E0ZQo-zXVQ,190
|
|
127
125
|
cherrypy_foundation/tools/tests/templates/test_jinja2_i18n.html,sha256=98S51dgG7Vb4rvMZNZvomw1D9pBiM4g6pdlxAgvrxXA,373
|
|
128
126
|
cherrypy_foundation/tools/tests/templates/test_jinjax.html,sha256=NT19UaUzm8FRKOIc6H6HNGPDJU6KATnakd8zf3BCeAs,153
|
|
129
|
-
cherrypy_foundation-1.0.
|
|
130
|
-
cherrypy_foundation-1.0.
|
|
131
|
-
cherrypy_foundation-1.0.
|
|
132
|
-
cherrypy_foundation-1.0.
|
|
133
|
-
cherrypy_foundation-1.0.
|
|
127
|
+
cherrypy_foundation-1.0.0a13.dist-info/licenses/LICENSE.md,sha256=trSLYs5qlaow_bBwsLTRKpmTXsXzFksM_YUCMqrgAJQ,35149
|
|
128
|
+
cherrypy_foundation-1.0.0a13.dist-info/METADATA,sha256=KYmjHwbA7u8dgIzKxb5ILWHMpC9blo3A46l2CBfDa4U,2023
|
|
129
|
+
cherrypy_foundation-1.0.0a13.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
130
|
+
cherrypy_foundation-1.0.0a13.dist-info/top_level.txt,sha256=B1vQPTLYhpKJ6W0JkRCWyAf8RPcnwJWdYxixv75-4ew,20
|
|
131
|
+
cherrypy_foundation-1.0.0a13.dist-info/RECORD,,
|
|
Binary file
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
msgid ""
|
|
2
|
-
msgstr ""
|
|
3
|
-
"Project-Id-Version: \n"
|
|
4
|
-
"POT-Creation-Date: \n"
|
|
5
|
-
"PO-Revision-Date: \n"
|
|
6
|
-
"Last-Translator: \n"
|
|
7
|
-
"Language-Team: \n"
|
|
8
|
-
"Language: en\n"
|
|
9
|
-
"MIME-Version: 1.0\n"
|
|
10
|
-
"Content-Type: text/plain; charset=UTF-8\n"
|
|
11
|
-
"Content-Transfer-Encoding: 8bit\n"
|
|
12
|
-
"X-Generator: Poedit 3.6\n"
|
|
13
|
-
|
|
14
|
-
msgid "Some text to translate"
|
|
15
|
-
msgstr ""
|
|
File without changes
|
|
File without changes
|
{cherrypy_foundation-1.0.0a11.dist-info → cherrypy_foundation-1.0.0a13.dist-info}/top_level.txt
RENAMED
|
File without changes
|