cherrypy-foundation 1.0.0__py3-none-any.whl → 1.0.0a1__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 +6 -16
  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 +17 -20
  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 +9 -35
  16. cherrypy_foundation/plugins/ldap.py +38 -46
  17. cherrypy_foundation/plugins/restapi.py +1 -1
  18. cherrypy_foundation/plugins/scheduler.py +84 -208
  19. cherrypy_foundation/plugins/smtp.py +46 -78
  20. cherrypy_foundation/plugins/tests/test_db.py +4 -4
  21. cherrypy_foundation/plugins/tests/test_ldap.py +3 -76
  22. cherrypy_foundation/plugins/tests/test_scheduler.py +50 -58
  23. cherrypy_foundation/plugins/tests/test_smtp.py +7 -40
  24. cherrypy_foundation/tests/__init__.py +0 -72
  25. cherrypy_foundation/tests/test_error_page.py +1 -7
  26. cherrypy_foundation/tests/test_passwd.py +2 -2
  27. cherrypy_foundation/tools/auth.py +38 -59
  28. cherrypy_foundation/tools/auth_mfa.py +88 -89
  29. cherrypy_foundation/tools/errors.py +27 -0
  30. cherrypy_foundation/tools/i18n.py +153 -246
  31. cherrypy_foundation/tools/jinja2.py +13 -29
  32. cherrypy_foundation/tools/ratelimit.py +27 -37
  33. cherrypy_foundation/tools/secure_headers.py +5 -1
  34. cherrypy_foundation/tools/sessions_timeout.py +21 -23
  35. cherrypy_foundation/tools/tests/locales/en/LC_MESSAGES/messages.mo +0 -0
  36. cherrypy_foundation/tools/tests/locales/{de → en}/LC_MESSAGES/messages.po +2 -2
  37. cherrypy_foundation/tools/tests/test_auth.py +4 -21
  38. cherrypy_foundation/tools/tests/test_i18n.py +6 -81
  39. cherrypy_foundation/tools/tests/test_ratelimit.py +2 -2
  40. cherrypy_foundation/url.py +25 -25
  41. cherrypy_foundation/widgets.py +2 -2
  42. cherrypy_foundation-1.0.0a1.dist-info/METADATA +42 -0
  43. {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a1.dist-info}/RECORD +46 -65
  44. {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a1.dist-info}/WHEEL +1 -1
  45. cherrypy_foundation/components/Flash.jinja +0 -13
  46. cherrypy_foundation/components/LocaleSelection.jinja +0 -13
  47. cherrypy_foundation/components/LocaleSelection.js +0 -26
  48. cherrypy_foundation/plugins/tests/test_scheduler_db.py +0 -107
  49. cherrypy_foundation/sessions.py +0 -93
  50. cherrypy_foundation/tests/templates/test_flash.html +0 -9
  51. cherrypy_foundation/tests/templates/test_form.html +0 -16
  52. cherrypy_foundation/tests/templates/test_url.html +0 -15
  53. cherrypy_foundation/tests/test_flash.py +0 -61
  54. cherrypy_foundation/tests/test_form.py +0 -148
  55. cherrypy_foundation/tests/test_logging.py +0 -78
  56. cherrypy_foundation/tests/test_sessions.py +0 -89
  57. cherrypy_foundation/tests/test_url.py +0 -161
  58. cherrypy_foundation/tools/tests/components/Button.jinja +0 -2
  59. cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.mo +0 -0
  60. cherrypy_foundation/tools/tests/templates/test_jinja2.html +0 -11
  61. cherrypy_foundation/tools/tests/templates/test_jinjax.html +0 -9
  62. cherrypy_foundation/tools/tests/templates/test_jinjax_i18n.html +0 -22
  63. cherrypy_foundation/tools/tests/test_auth_mfa.py +0 -369
  64. cherrypy_foundation/tools/tests/test_jinja2.py +0 -153
  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.0a1.dist-info}/licenses/LICENSE.md +0 -0
  68. {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a1.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,116 +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
113
-
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)
118
-
102
+ from babel.support import LazyProxy, NullTranslations, Translations
119
103
 
120
- def _get_config(key, default=None):
121
- """
122
- Lookup configuration from request, if available. Fallback to global config.
123
- """
124
- if getattr(cherrypy, 'request') and getattr(cherrypy.request, 'config') and key in cherrypy.request.config:
125
- return cherrypy.request.config[key]
126
- return cherrypy.config.get(key, default)
104
+ # Store current translation and preferred_lang
105
+ _current = threading.local()
127
106
 
128
107
 
129
108
  @contextmanager
@@ -134,20 +113,21 @@ def preferred_lang(lang):
134
113
  with i18n.preferred_lang('fr'):
135
114
  i18n.gettext('some string')
136
115
  """
137
- if not (lang is None or isinstance(lang, str)):
138
- raise ValueError(lang)
116
+ assert lang is None or isinstance(lang, str)
117
+ prev_lang = getattr(_current, 'preferred_lang', [])
118
+ prev_trans = getattr(_current, 'translation', None)
139
119
  try:
140
- # Update preferred lang and clear translation.
120
+ # Update prefered lang and clear translation.
141
121
  if lang:
142
- token_l = _preferred_lang.set((lang,))
143
- else:
144
- token_l = _preferred_lang.set(tuple())
145
- token_t = _translation.set(None)
122
+ _current.preferred_lang = [lang]
123
+ elif hasattr(_current, 'preferred_lang'):
124
+ del _current.preferred_lang
125
+ _current.translation = None
146
126
  yield
147
127
  finally:
148
128
  # Restore previous value
149
- _preferred_lang.reset(token_l)
150
- _translation.reset(token_t)
129
+ _current.preferred_lang = prev_lang
130
+ _current.translation = prev_trans
151
131
 
152
132
 
153
133
  @contextmanager
@@ -158,24 +138,22 @@ def preferred_timezone(timezone):
158
138
  with i18n.preferred_lang('America/Montreal'):
159
139
  i18n.format_datetime(...)
160
140
  """
161
- if not (timezone is None or isinstance(timezone, str)):
162
- raise ValueError(timezone)
141
+ assert timezone is None or isinstance(timezone, str)
142
+ prev_timezone = getattr(_current, 'preferred_timezone', [])
143
+ prev_tzinfo = getattr(_current, 'tzinfo', None)
163
144
  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)
145
+ # Update prefered lang and clear translation.
146
+ _current.preferred_timezone = [timezone] + prev_timezone
147
+ _current.tzinfo = None
170
148
  yield
171
149
  finally:
172
150
  # Restore previous value
173
- _preferred_timezone.reset(token_t)
174
- _tzinfo.reset(token_z)
151
+ _current.preferred_timezone = prev_timezone
152
+ _current.tzinfo = prev_tzinfo
175
153
 
176
154
 
177
- @lru_cache(maxsize=32)
178
- def _search_translation(dirname, domain, *locales, sourcecode_lang='en'):
155
+ @lru_cache(maxsize=10)
156
+ def _search_translation(dirname, domain, *langs):
179
157
  """
180
158
  Loads the first existing translations for known locale.
181
159
 
@@ -183,37 +161,22 @@ def _search_translation(dirname, domain, *locales, sourcecode_lang='en'):
183
161
  langs : List
184
162
  List of languages as returned by `parse_accept_language_header`.
185
163
  dirname : String
186
- A single directory of the translations (`tools.i18n.mo_dir`).
164
+ Directory of the translations (`tools.I18nTool.mo_dir`).
165
+ Might be a list of directories.
187
166
  domain : String
188
- Gettext domain of the catalog (`tools.i18n.domain`).
167
+ Gettext domain of the catalog (`tools.I18nTool.domain`).
189
168
 
190
169
  :returns: Translations, the corresponding Locale object.
191
170
  """
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)
171
+ if not isinstance(langs, (list, tuple)):
172
+ langs = [langs]
173
+ t = Translations.load(dirname, langs, domain)
174
+ # Ignore null translation
175
+ if t.__class__ is NullTranslations:
176
+ return None
177
+ # Get Locale from file name
178
+ lang = t.files[0].split('/')[-3]
179
+ t.locale = Locale.parse(lang)
217
180
  return t
218
181
 
219
182
 
@@ -221,37 +184,23 @@ def get_language_name(lang_code):
221
184
  """
222
185
  Translate the language code into it's language display name.
223
186
  """
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)
187
+ locale = Locale.parse(lang_code)
188
+ trans = get_translation()
189
+ return locale.get_language_name(trans.locale)
230
190
 
231
191
 
232
192
  def get_timezone():
233
193
  """
234
194
  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
195
  """
243
196
  # When tzinfo is defined, use it
244
- tzinfo = _tzinfo.get()
197
+ tzinfo = getattr(_current, 'tzinfo', None)
245
198
  if tzinfo is not None:
246
199
  return tzinfo
247
200
  # 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
- )
201
+ tzinfo = None
202
+ default_timezone = cherrypy.config.get('tools.i18n.default_timezone')
203
+ preferred_timezone = getattr(_current, 'preferred_timezone', [default_timezone])
255
204
  for timezone in preferred_timezone:
256
205
  try:
257
206
  tzinfo = dates.get_timezone(timezone)
@@ -261,8 +210,8 @@ def get_timezone():
261
210
  # If we can't find a valid timezone using the default and preferred value, fall back to server timezone.
262
211
  if tzinfo is None:
263
212
  tzinfo = dates.get_timezone(None)
264
- _tzinfo.set(tzinfo)
265
- return tzinfo
213
+ _current.tzinfo = tzinfo
214
+ return _current.tzinfo
266
215
 
267
216
 
268
217
  def get_translation():
@@ -270,48 +219,37 @@ def get_translation():
270
219
  Get the best translation for the current context.
271
220
  """
272
221
  # When translation is defined, use it
273
- translation = _translation.get()
222
+ translation = getattr(_current, 'translation', None)
274
223
  if translation is not None:
275
224
  return translation
276
225
 
277
226
  # Otherwise, we need to search the translation.
278
227
  # `preferred_lang` should always has a sane value within a cherrypy request because of hooks
279
228
  # But we also need to support calls outside cherrypy.
280
- sourcecode_lang = _get_config('tools.i18n.sourcecode_lang', 'en')
281
- 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
- )
288
- 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
229
+ default = cherrypy.config.get('tools.i18n.default')
230
+ preferred_lang = getattr(_current, 'preferred_lang', [default])
231
+ mo_dir = cherrypy.config.get('tools.i18n.mo_dir')
232
+ domain = cherrypy.config.get('tools.i18n.domain')
233
+ trans = _search_translation(mo_dir, domain, *preferred_lang)
234
+ if trans is None:
235
+ trans = NullTranslations()
236
+ trans.locale = Locale('en')
237
+ _current.translation = trans
238
+ return _current.translation
293
239
 
294
240
 
295
241
  def list_available_locales():
296
242
  """
297
243
  Return a list of available translations.
298
244
  """
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')
245
+ mo_dir = cherrypy.config.get('tools.i18n.mo_dir', False)
246
+ domain = cherrypy.config.get('tools.i18n.domain')
306
247
  if not mo_dir:
307
248
  return
308
249
  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
250
+ trans = _search_translation(mo_dir, domain, lang)
251
+ if trans is not None:
252
+ yield trans.locale
315
253
 
316
254
 
317
255
  def list_available_timezones():
@@ -324,7 +262,7 @@ def list_available_timezones():
324
262
 
325
263
 
326
264
  # Public translation functions
327
- def gettext(message):
265
+ def ugettext(message):
328
266
  """Standard translation function. You can use it in all your exposed
329
267
  methods and everywhere where the response object is available.
330
268
 
@@ -335,13 +273,13 @@ def gettext(message):
335
273
  :returns: The translated message.
336
274
  :rtype: Unicode
337
275
  """
338
- return get_translation().gettext(message)
276
+ return get_translation().ugettext(message)
339
277
 
340
278
 
341
- ugettext = gettext
279
+ gettext = ugettext
342
280
 
343
281
 
344
- def ngettext(singular, plural, num):
282
+ def ungettext(singular, plural, num):
345
283
  """Like ugettext, but considers plural forms.
346
284
 
347
285
  :parameters:
@@ -356,29 +294,34 @@ def ngettext(singular, plural, num):
356
294
  :returns: The translated message as singular or plural.
357
295
  :rtype: Unicode
358
296
  """
359
- return get_translation().ngettext(singular, plural, num)
297
+ return get_translation().ungettext(singular, plural, num)
360
298
 
361
299
 
362
- ungettext = ngettext
300
+ ngettext = ungettext
363
301
 
364
302
 
365
303
  def gettext_lazy(message):
366
- """Like gettext, but lazy.
304
+ """Like ugettext, but lazy.
367
305
 
368
306
  :returns: A proxy for the translation object.
369
307
  :rtype: LazyProxy
370
308
  """
371
309
 
372
310
  def func():
373
- return get_translation().gettext(message)
311
+ return get_translation().ugettext(message)
374
312
 
375
313
  return LazyProxy(func, enable_cache=False)
376
314
 
377
315
 
378
316
  def format_datetime(datetime=None, format='medium', tzinfo=None):
379
317
  """
380
- Wrapper around babel format_datetime to use current locale and current timezone.
318
+ Wraper arround babel format_datetime to provide a default locale.
319
+ The timezone used to format the date is determine with the following priorities:
320
+ * value of tzinfo
321
+ * value of get_timezone()
322
+ * default server time.
381
323
  """
324
+ # When formating date or english, let use en_GB as it's to most complete translation.
382
325
  return dates.format_datetime(
383
326
  datetime=datetime,
384
327
  format=format,
@@ -389,19 +332,14 @@ def format_datetime(datetime=None, format='medium', tzinfo=None):
389
332
 
390
333
  def format_date(datetime=None, format='medium', tzinfo=None):
391
334
  """
392
- Wrapper around babel format_date to provide a default locale.
335
+ Wraper arround babel format_date to provide a default locale.
393
336
  """
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
- )
337
+ return format_datetime(datetime=datetime, format=dates.get_date_format(format), tzinfo=tzinfo)
401
338
 
402
339
 
403
- def get_timezone_name(tzinfo, width='long'):
404
- return dates.get_timezone_name(tzinfo, width=width, locale=get_translation().locale)
340
+ def get_timezone_name(tzinfo):
341
+ locale = Locale(get_translation().locale.language)
342
+ return dates.get_timezone_name(tzinfo, width='long', locale=locale)
405
343
 
406
344
 
407
345
  def _load_default(mo_dir, domain, default, **kwargs):
@@ -409,60 +347,35 @@ def _load_default(mo_dir, domain, default, **kwargs):
409
347
  Initialize the language using the default value from the configuration.
410
348
  """
411
349
  # Clear current translation
412
- _preferred_lang.set(tuple())
413
- _preferred_timezone.set(tuple())
350
+ _current.preferred_lang = [default]
351
+ _current.preferred_timezone = [kwargs['default_timezone']] if 'default_timezone' in kwargs else []
352
+ cherrypy.request._i18n_lang_func = kwargs.get('lang', kwargs.get('func', False))
353
+ cherrypy.request._i18n_tzinfo_func = kwargs.get('tzinfo', False)
414
354
  # Clear current translation
415
- _translation.set(None)
355
+ _current.translation = None
416
356
  # Clear current timezone
417
- _tzinfo.set(None)
357
+ _current.tzinfo = None
418
358
 
419
359
 
420
360
  def _load_accept_language(**kwargs):
421
361
  """
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):
433
- """
434
- Load preferred language from a request cookie.
435
-
436
- Expected cookie value formats:
437
- - en
438
- - en_US
439
- - en-US
362
+ When running within a request, load the prefered language from Accept-Language
440
363
  """
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
364
 
456
- # Set preferred language and clear cached translation
457
- _preferred_lang.set((value,))
458
- _translation.set(None)
365
+ if cherrypy.request.headers.elements('Accept-Language'):
366
+ count = 0
367
+ for x in cherrypy.request.headers.elements('Accept-Language'):
368
+ _current.preferred_lang.insert(count, x.value.replace('-', '_'))
369
+ count += 1
370
+ # Clear current translation
371
+ _current.translation = None
459
372
 
460
373
 
461
374
  def _load_func_language(**kwargs):
462
375
  """
463
- When running a request where a current user is found, load preferred language from user preferences.
376
+ When running a request where a current user is found, load prefered language from user preferences.
464
377
  """
465
- func = _get_config('tools.i18n.lang', default=_get_config('tools.i18n.func'))
378
+ func = getattr(cherrypy.request, '_i18n_lang_func', False)
466
379
  if not func:
467
380
  return
468
381
  try:
@@ -472,16 +385,16 @@ def _load_func_language(**kwargs):
472
385
  if not lang:
473
386
  return
474
387
  # Add custom lang to preferred_lang
475
- _preferred_lang.set((lang,))
388
+ _current.preferred_lang.insert(0, lang)
476
389
  # Clear current translation
477
- _translation.set(None)
390
+ _current.translation = None
478
391
 
479
392
 
480
393
  def _load_func_tzinfo(**kwargs):
481
394
  """
482
- When running a request, load the preferred timezone information from user preferences.
395
+ When running a request, load the prefered timezone information from user preferences.
483
396
  """
484
- func = _get_config('tools.i18n.tzinfo')
397
+ func = getattr(cherrypy.request, '_i18n_tzinfo_func', False)
485
398
  if not func:
486
399
  return
487
400
  try:
@@ -491,9 +404,9 @@ def _load_func_tzinfo(**kwargs):
491
404
  if not tzinfo:
492
405
  return
493
406
  # Add custom lang to preferred_lang
494
- _preferred_timezone.set((tzinfo,))
407
+ _current.preferred_timezone.insert(0, tzinfo)
495
408
  # Clear current translation
496
- _tzinfo.set(None)
409
+ _current.tzinfo = None
497
410
 
498
411
 
499
412
  def _set_content_language(**kwargs):
@@ -502,12 +415,7 @@ def _set_content_language(**kwargs):
502
415
  language of `cherrypy.response.i18n.locale`.
503
416
  """
504
417
  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
418
+ cherrypy.response.headers['Content-Language'] = str(get_translation().locale)
511
419
 
512
420
 
513
421
  class I18nTool(cherrypy.Tool):
@@ -520,7 +428,6 @@ class I18nTool(cherrypy.Tool):
520
428
  cherrypy.Tool._setup(self)
521
429
  # Attach additional hooks as different priority to update preferred lang with more accurate preferences.
522
430
  cherrypy.request.hooks.attach('before_handler', _load_accept_language, priority=60)
523
- cherrypy.request.hooks.attach('before_handler', _load_cookie_language, priority=70)
524
431
  cherrypy.request.hooks.attach('before_handler', _load_func_language, priority=75)
525
432
  cherrypy.request.hooks.attach('before_handler', _load_func_tzinfo, priority=75)
526
433
  cherrypy.request.hooks.attach('before_finalize', _set_content_language)