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.
Files changed (68) hide show
  1. cherrypy_foundation/components/ColorModes.jinja +4 -5
  2. cherrypy_foundation/components/Datatable.jinja +2 -2
  3. cherrypy_foundation/components/Datatable.js +2 -2
  4. cherrypy_foundation/components/Field.jinja +2 -4
  5. cherrypy_foundation/components/Fields.jinja +2 -0
  6. cherrypy_foundation/components/Typeahead.css +1 -6
  7. cherrypy_foundation/components/Typeahead.jinja +2 -2
  8. cherrypy_foundation/components/__init__.py +2 -2
  9. cherrypy_foundation/components/tests/test_static.py +1 -1
  10. cherrypy_foundation/error_page.py +3 -3
  11. cherrypy_foundation/flash.py +15 -17
  12. cherrypy_foundation/form.py +2 -2
  13. cherrypy_foundation/logging.py +2 -2
  14. cherrypy_foundation/passwd.py +2 -2
  15. cherrypy_foundation/plugins/db.py +1 -1
  16. cherrypy_foundation/plugins/ldap.py +25 -27
  17. cherrypy_foundation/plugins/restapi.py +1 -1
  18. cherrypy_foundation/plugins/scheduler.py +3 -14
  19. cherrypy_foundation/plugins/smtp.py +2 -8
  20. cherrypy_foundation/plugins/tests/test_db.py +2 -2
  21. cherrypy_foundation/plugins/tests/test_ldap.py +3 -76
  22. cherrypy_foundation/plugins/tests/test_scheduler.py +1 -1
  23. cherrypy_foundation/plugins/tests/test_smtp.py +1 -31
  24. cherrypy_foundation/tests/__init__.py +0 -72
  25. cherrypy_foundation/tests/templates/test_form.html +1 -7
  26. cherrypy_foundation/tests/test_error_page.py +1 -7
  27. cherrypy_foundation/tests/test_form.py +11 -40
  28. cherrypy_foundation/tests/test_passwd.py +2 -2
  29. cherrypy_foundation/tools/auth.py +27 -31
  30. cherrypy_foundation/tools/auth_mfa.py +83 -87
  31. cherrypy_foundation/tools/errors.py +27 -0
  32. cherrypy_foundation/tools/i18n.py +151 -235
  33. cherrypy_foundation/tools/jinja2.py +2 -15
  34. cherrypy_foundation/tools/ratelimit.py +18 -32
  35. cherrypy_foundation/tools/secure_headers.py +1 -1
  36. cherrypy_foundation/tools/sessions_timeout.py +21 -23
  37. cherrypy_foundation/tools/tests/locales/en/LC_MESSAGES/messages.mo +0 -0
  38. cherrypy_foundation/tools/tests/locales/{de → en}/LC_MESSAGES/messages.po +2 -2
  39. cherrypy_foundation/tools/tests/templates/test_jinja2.html +1 -2
  40. cherrypy_foundation/tools/tests/templates/test_jinja2_i18n.html +11 -0
  41. cherrypy_foundation/tools/tests/templates/test_jinjax.html +2 -3
  42. cherrypy_foundation/tools/tests/test_auth.py +3 -20
  43. cherrypy_foundation/tools/tests/test_auth_mfa.py +4 -6
  44. cherrypy_foundation/tools/tests/test_i18n.py +6 -81
  45. cherrypy_foundation/tools/tests/test_jinja2.py +5 -35
  46. cherrypy_foundation/tools/tests/test_ratelimit.py +2 -2
  47. cherrypy_foundation/url.py +25 -25
  48. cherrypy_foundation/widgets.py +2 -2
  49. cherrypy_foundation-1.0.0a2.dist-info/METADATA +42 -0
  50. {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a2.dist-info}/RECORD +53 -64
  51. {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a2.dist-info}/WHEEL +1 -1
  52. cherrypy_foundation/components/Flash.jinja +0 -13
  53. cherrypy_foundation/components/LocaleSelection.jinja +0 -13
  54. cherrypy_foundation/components/LocaleSelection.js +0 -26
  55. cherrypy_foundation/plugins/tests/test_scheduler_db.py +0 -107
  56. cherrypy_foundation/sessions.py +0 -93
  57. cherrypy_foundation/tests/templates/test_flash.html +0 -9
  58. cherrypy_foundation/tests/templates/test_url.html +0 -15
  59. cherrypy_foundation/tests/test_flash.py +0 -61
  60. cherrypy_foundation/tests/test_logging.py +0 -78
  61. cherrypy_foundation/tests/test_sessions.py +0 -89
  62. cherrypy_foundation/tests/test_url.py +0 -161
  63. cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.mo +0 -0
  64. cherrypy_foundation/tools/tests/templates/test_jinjax_i18n.html +0 -22
  65. cherrypy_foundation/tools/tests/test_secure_headers.py +0 -200
  66. cherrypy_foundation-1.0.0.dist-info/METADATA +0 -71
  67. {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a2.dist-info}/licenses/LICENSE.md +0 -0
  68. {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-2026 IKUS Software
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
- 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``
19
+ This tool provides locales and loads translations in the following order:
30
20
 
31
- Translations are loaded using Babel and gettext, and the resolved locale is
32
- available through ``i18n.get_translation()`` during request handling.
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
- Basic usage
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
- Within Python code, mark translatable strings using ``ugettext`` or
39
- ``ungettext``:
30
+ Example::
40
31
 
41
- from i18n import gettext as _, ngettext
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"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])
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
- Lazy translations
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
- 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:
48
+ Example::
62
49
 
63
- from i18n_tool import gettext_lazy
50
+ from i18n_tool import ugettext_lazy
64
51
 
65
52
  class Model:
66
- name = gettext_lazy(u"Model name")
53
+ def __init__(self):
54
+ name = ugettext_lazy(u'Name of the model')
67
55
 
68
- ---------------------------------------------------------------------------
69
- Templates
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
- {% 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') }}
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
- Configuration
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 CherryPy configuration:
73
+ Example::
86
74
 
87
75
  [/]
88
76
  tools.i18n.on = True
89
- tools.i18n.default = "en_US"
90
- tools.i18n.mo_dir = "/path/to/i18n"
91
- tools.i18n.domain = "myapp"
77
+ tools.i18n.default = 'en_US'
78
+ tools.i18n.mo_dir = '/home/user/web/myapp/i18n'
79
+ tools.i18n.domain = 'myapp'
92
80
 
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:
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
- <mo_dir>/<language>/LC_MESSAGES/<domain>.mo
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
- import logging
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
- _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)
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
- if not (lang is None or isinstance(lang, str)):
138
- raise ValueError(lang)
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 preferred lang and clear translation.
129
+ # Update prefered lang and clear translation.
141
130
  if lang:
142
- token_l = _preferred_lang.set((lang,))
143
- else:
144
- token_l = _preferred_lang.set(tuple())
145
- token_t = _translation.set(None)
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
- _preferred_lang.reset(token_l)
150
- _translation.reset(token_t)
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
- if not (timezone is None or isinstance(timezone, str)):
162
- raise ValueError(timezone)
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 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)
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
- _preferred_timezone.reset(token_t)
174
- _tzinfo.reset(token_z)
160
+ _current.preferred_timezone = prev_timezone
161
+ _current.tzinfo = prev_tzinfo
175
162
 
176
163
 
177
- @lru_cache(maxsize=32)
178
- def _search_translation(dirname, domain, *locales, sourcecode_lang='en'):
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
- A single directory of the translations (`tools.i18n.mo_dir`).
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.i18n.domain`).
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(locales, (list, tuple)):
193
- locales = tuple(locales)
194
-
195
- # Loop on each locales to find the best matching translation.
196
- for locale in locales:
197
- try:
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)
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
- 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)
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 = _tzinfo.get()
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
- 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
- )
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
- _tzinfo.set(tzinfo)
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 = _translation.get()
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 = _preferred_lang.get()
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', 'messages')
290
- translation = _search_translation(mo_dir, domain, *preferred_lang, sourcecode_lang=sourcecode_lang)
291
- _translation.set(translation)
292
- return translation
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
- return_value = []
300
- # Always return the source code locale.
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
- if os.path.exists(os.path.join(mo_dir, lang, 'LC_MESSAGES', f'{domain}.mo')):
310
- try:
311
- return_value.append(Locale.parse(lang))
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 gettext(message):
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().gettext(message)
285
+ return get_translation().ugettext(message)
339
286
 
340
287
 
341
- ugettext = gettext
288
+ gettext = ugettext
342
289
 
343
290
 
344
- def ngettext(singular, plural, num):
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().ngettext(singular, plural, num)
306
+ return get_translation().ungettext(singular, plural, num)
360
307
 
361
308
 
362
- ungettext = ngettext
309
+ ngettext = ungettext
363
310
 
364
311
 
365
312
  def gettext_lazy(message):
366
- """Like gettext, but lazy.
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().gettext(message)
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
- Wrapper around babel format_datetime to use current locale and current timezone.
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
- Wrapper around babel format_date to provide a default locale.
344
+ Wraper arround babel format_date to provide a default locale.
393
345
  """
394
- # To enforce the timezone and locale, make use of format_datetime for dates.
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, width='long'):
404
- return dates.get_timezone_name(tzinfo, width=width, locale=get_translation().locale)
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
- _preferred_lang.set(tuple())
413
- _preferred_timezone.set(tuple())
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
- _translation.set(None)
364
+ _current.translation = None
416
365
  # Clear current timezone
417
- _tzinfo.set(None)
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 preferred language from Accept-Language header.
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
- Expected cookie value formats:
437
- - en
438
- - en_US
439
- - en-US
440
- """
441
- # Skip this step if cookie name is not defined.
442
- cookie_name = _get_config('tools.i18n.cookie_name')
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 preferred language from user preferences.
385
+ When running a request where a current user is found, load prefered language from user preferences.
464
386
  """
465
- func = _get_config('tools.i18n.lang', default=_get_config('tools.i18n.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
- _preferred_lang.set((lang,))
397
+ _current.preferred_lang.insert(0, lang)
476
398
  # Clear current translation
477
- _translation.set(None)
399
+ _current.translation = None
478
400
 
479
401
 
480
402
  def _load_func_tzinfo(**kwargs):
481
403
  """
482
- When running a request, load the preferred timezone information from user preferences.
404
+ When running a request, load the prefered timezone information from user preferences.
483
405
  """
484
- func = _get_config('tools.i18n.tzinfo')
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
- _preferred_timezone.set((tzinfo,))
416
+ _current.preferred_timezone.insert(0, tzinfo)
495
417
  # Clear current translation
496
- _tzinfo.set(None)
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
- # Only define the content language if the handler uses i18n module.
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-2026 IKUS Software
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)