cherrypy-foundation 1.0.0a12__py3-none-any.whl → 1.0.0a14__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 (23) hide show
  1. cherrypy_foundation/components/ColorModes.jinja +5 -4
  2. cherrypy_foundation/components/LocaleSelection.jinja +13 -0
  3. cherrypy_foundation/components/LocaleSelection.js +26 -0
  4. cherrypy_foundation/tests/__init__.py +72 -0
  5. cherrypy_foundation/tests/templates/test_flash.html +2 -1
  6. cherrypy_foundation/tests/templates/test_form.html +2 -1
  7. cherrypy_foundation/tests/templates/test_url.html +1 -1
  8. cherrypy_foundation/tools/i18n.py +231 -156
  9. cherrypy_foundation/tools/jinja2.py +14 -1
  10. cherrypy_foundation/tools/tests/templates/test_jinja2.html +2 -1
  11. cherrypy_foundation/tools/tests/templates/test_jinjax.html +3 -2
  12. cherrypy_foundation/tools/tests/templates/test_jinjax_i18n.html +22 -0
  13. cherrypy_foundation/tools/tests/test_i18n.py +34 -2
  14. cherrypy_foundation/tools/tests/test_jinja2.py +33 -3
  15. cherrypy_foundation/url.py +2 -3
  16. {cherrypy_foundation-1.0.0a12.dist-info → cherrypy_foundation-1.0.0a14.dist-info}/METADATA +1 -1
  17. {cherrypy_foundation-1.0.0a12.dist-info → cherrypy_foundation-1.0.0a14.dist-info}/RECORD +20 -20
  18. cherrypy_foundation/tools/tests/locales/en/LC_MESSAGES/messages.mo +0 -0
  19. cherrypy_foundation/tools/tests/locales/en/LC_MESSAGES/messages.po +0 -15
  20. cherrypy_foundation/tools/tests/templates/test_jinja2_i18n.html +0 -11
  21. {cherrypy_foundation-1.0.0a12.dist-info → cherrypy_foundation-1.0.0a14.dist-info}/WHEEL +0 -0
  22. {cherrypy_foundation-1.0.0a12.dist-info → cherrypy_foundation-1.0.0a14.dist-info}/licenses/LICENSE.md +0 -0
  23. {cherrypy_foundation-1.0.0a12.dist-info → cherrypy_foundation-1.0.0a14.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,4 @@
1
+ {# def header="Toggle theme", light_label="Light", dark_label="Dark", auto_label="Auto" #}
1
2
  {#css vendor/bootstrap5/css/bootstrap.min.css #}
2
3
  {#js vendor/popper/popper.min.js, vendor/bootstrap5/js/bootstrap.min.js, vendor/bootstrap5/js/color-modes.js #}
3
4
  <svg class="d-none" xmlns="http://www.w3.org/2000/svg">
@@ -21,7 +22,7 @@
21
22
  </svg>
22
23
  <li>
23
24
  <span id="bd-theme" class="dropdown-item disabled">
24
- <span id="bd-theme-text">{% trans %}Toggle theme{% endtrans %}</span>
25
+ <span id="bd-theme-text">{{ header }}</span>
25
26
  <svg aria-hidden="true"
26
27
  class="theme-icon-active visually-hidden"
27
28
  width="16"
@@ -40,7 +41,7 @@
40
41
  <use href="#sun-fill">
41
42
  </use>
42
43
  </svg>
43
- {% trans %}Light{% endtrans %}
44
+ {{ light_label }}
44
45
  </button>
45
46
  </li>
46
47
  <li>
@@ -52,7 +53,7 @@
52
53
  <use href="#moon-stars-fill">
53
54
  </use>
54
55
  </svg>
55
- {% trans %}Dark{% endtrans %}
56
+ {{ dark_label }}
56
57
  </button>
57
58
  </li>
58
59
  <li>
@@ -64,6 +65,6 @@
64
65
  <use href="#circle-half">
65
66
  </use>
66
67
  </svg>
67
- {% trans %}Auto{% endtrans %}
68
+ {{ auto_label }}
68
69
  </button>
69
70
  </li>
@@ -0,0 +1,13 @@
1
+ {# def header="Language selection" #}
2
+ <li>
3
+ <h6 class="dropdown-header">{{ header }}</h6>
4
+ </li>
5
+ {% set cur_locale = get_translation().locale %}
6
+ {% for locale in list_available_locales() %}
7
+ <li>
8
+ <button aria-pressed="{{ 'true' if cur_locale == locale else 'false' }}"
9
+ class="dropdown-item btn-locale{{ ' active' if cur_locale == locale else '' }}"
10
+ data-locale="{{ locale.language }}"
11
+ type="button">{{ locale.display_name.capitalize() }}</button>
12
+ </li>
13
+ {% endfor %}
@@ -0,0 +1,26 @@
1
+ /*
2
+ * Handle locale selection.
3
+ */
4
+ document.addEventListener('DOMContentLoaded', function () {
5
+ const LOCALE_COOKIE_NAME = 'locale';
6
+ const ONE_YEAR = 60 * 60 * 24 * 365;
7
+
8
+ function setLocaleCookie(locale) {
9
+ document.cookie = `${LOCALE_COOKIE_NAME}=${locale}; path=/; max-age=${ONE_YEAR}; SameSite=Lax`;
10
+ }
11
+
12
+ function onLanguageClick(event) {
13
+ const locale = event.target.dataset.locale;
14
+ if (!locale) return;
15
+
16
+ setLocaleCookie(locale);
17
+
18
+ // Reload page so backend can use the new language
19
+ window.location.reload();
20
+ }
21
+
22
+ document.querySelectorAll('.btn-locale').forEach(btn => {
23
+ btn.addEventListener('click', onLanguageClick);
24
+ });
25
+
26
+ });
@@ -0,0 +1,72 @@
1
+ # CherryPy foundation
2
+ # Copyright (C) 2026 IKUS Software
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+
17
+ import os
18
+ import tempfile
19
+ import unittest
20
+ from contextlib import contextmanager
21
+
22
+ from selenium import webdriver
23
+
24
+
25
+ class SeleniumUnitTest:
26
+
27
+ @property
28
+ def _session_id(self):
29
+ if hasattr(self, 'cookies') and self.cookies:
30
+ for unused, value in self.cookies:
31
+ for part in value.split(';'):
32
+ key, unused, value = part.partition('=')
33
+ if key == 'session_id':
34
+ return value
35
+
36
+ @contextmanager
37
+ def selenium(self, headless=True, implicitly_wait=3):
38
+ """
39
+ Decorator to load selenium for a test.
40
+ """
41
+ # Skip selenium test is display is not available.
42
+ if not os.environ.get('DISPLAY', False):
43
+ raise unittest.SkipTest("selenium require a display")
44
+ # Start selenium driver
45
+ options = webdriver.ChromeOptions()
46
+ if headless:
47
+ options.add_argument('--headless')
48
+ options.add_argument('--disable-gpu')
49
+ options.add_argument('--window-size=1280,800')
50
+ options.add_argument('--no-sandbox')
51
+ options.add_argument('--disable-dev-shm-usage')
52
+ options.add_argument('--lang=en-US')
53
+ driver = webdriver.Chrome(options=options)
54
+ try:
55
+ # If logged in, reuse the same session id.
56
+ if self._session_id:
57
+ driver.get(f'{self.baseurl}/login/')
58
+ driver.add_cookie({"name": "session_id", "value": self.session_id})
59
+ # Configure download folder
60
+ download = os.path.join(os.path.expanduser('~'), 'Downloads')
61
+ os.makedirs(download, exist_ok=True)
62
+ self._selenium_download_dir = tempfile.mkdtemp(dir=download, prefix='selenium-download-')
63
+ driver.execute_cdp_cmd(
64
+ 'Page.setDownloadBehavior', {'behavior': 'allow', 'downloadPath': self._selenium_download_dir}
65
+ )
66
+ # Set default wait.
67
+ driver.implicitly_wait(implicitly_wait)
68
+ yield driver
69
+ finally:
70
+ # Code to release resource, e.g.:
71
+ driver.close()
72
+ driver = None
@@ -1,4 +1,5 @@
1
- <html>
1
+ <!DOCTYPE html>
2
+ <html lang="en">
2
3
  <head>
3
4
  <title>test-flash</title>
4
5
  </head>
@@ -1,4 +1,5 @@
1
- <html>
1
+ <!DOCTYPE html>
2
+ <html >
2
3
  <head>
3
4
  <title>test-form</title>
4
5
  </head>
@@ -1,4 +1,4 @@
1
- <html>
1
+ <html lang="en">
2
2
  <head>
3
3
  <title>test-url</title>
4
4
  </head>
@@ -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
- """Internationalization and Localization for CherryPy
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
- This tool provides locales and loads translations in the following order:
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
- 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
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
- 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()`
34
+ ---------------------------------------------------------------------------
35
+ Basic usage
36
+ ---------------------------------------------------------------------------
29
37
 
30
- Example::
38
+ Within Python code, mark translatable strings using ``ugettext`` or
39
+ ``ungettext``:
31
40
 
32
- from i18n import ugettext as _, ungettext
41
+ from i18n import gettext as _, ngettext
33
42
 
34
- class MyController(object):
43
+ class MyController:
35
44
  @cherrypy.expose
36
45
  def index(self):
37
46
  locale = cherrypy.response.i18n.locale
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])
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
- 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).
55
+ ---------------------------------------------------------------------------
56
+ Lazy translations
57
+ ---------------------------------------------------------------------------
47
58
 
48
- Example::
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 ugettext_lazy
63
+ from i18n_tool import gettext_lazy
51
64
 
52
65
  class Model:
53
- def __init__(self):
54
- name = ugettext_lazy(u'Name of the model')
66
+ name = gettext_lazy(u"Model name")
55
67
 
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.
68
+ ---------------------------------------------------------------------------
69
+ Templates
70
+ ---------------------------------------------------------------------------
59
71
 
72
+ For template rendering, i18n integrate with jinja2.
60
73
 
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)
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
- 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.
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 = 'en_US'
78
- tools.i18n.mo_dir = '/home/user/web/myapp/i18n'
79
- tools.i18n.domain = 'myapp'
89
+ tools.i18n.default = "en_US"
90
+ tools.i18n.mo_dir = "/path/to/i18n"
91
+ tools.i18n.domain = "myapp"
80
92
 
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
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
- That's it.
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, NullTranslations, Translations
112
+ from babel.support import LazyProxy, Translations
103
113
 
104
- # Store current translation and preferred_lang
105
- _current = threading.local()
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
- assert lang is None or isinstance(lang, str)
126
- prev_lang = getattr(_current, 'preferred_lang', [])
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 prefered lang and clear translation.
140
+ # Update preferred lang and clear translation.
130
141
  if lang:
131
- _current.preferred_lang = [lang]
132
- elif hasattr(_current, 'preferred_lang'):
133
- del _current.preferred_lang
134
- _current.translation = None
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
- _current.preferred_lang = prev_lang
139
- _current.translation = prev_trans
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
- assert timezone is None or isinstance(timezone, str)
151
- prev_timezone = getattr(_current, 'preferred_timezone', [])
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 prefered lang and clear translation.
155
- _current.preferred_timezone = [timezone] + prev_timezone
156
- _current.tzinfo = None
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
- _current.preferred_timezone = prev_timezone
161
- _current.tzinfo = prev_tzinfo
173
+ _preferred_timezone.reset(token_t)
174
+ _tzinfo.reset(token_z)
162
175
 
163
176
 
164
- @lru_cache(maxsize=10)
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
- Directory of the translations (`tools.I18nTool.mo_dir`).
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.I18nTool.domain`).
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 = [locales]
182
- t = Translations.load(dirname, locales, domain)
183
- # Ignore null translation
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
- locale = Locale.parse(locale)
191
- if trans_locale == locale or trans_locale.language == locale.language:
192
- t.locale = locale
193
- break
194
- except ValueError:
195
- continue # Invalid locale identifier
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
- locale = Locale.parse(lang_code)
204
- trans = get_translation()
205
- return locale.get_language_name(trans.locale)
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 = getattr(_current, 'tzinfo', None)
244
+ tzinfo = _tzinfo.get()
214
245
  if tzinfo is not None:
215
246
  return tzinfo
216
247
  # Otherwise search for a valid timezone.
217
- tzinfo = None
218
- default_timezone = _get_config('tools.i18n.default_timezone')
219
- preferred_timezone = getattr(_current, 'preferred_timezone', [default_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
+ )
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
- _current.tzinfo = tzinfo
230
- return _current.tzinfo
264
+ _tzinfo.set(tzinfo)
265
+ return tzinfo
231
266
 
232
267
 
233
268
  def get_translation():
@@ -235,37 +270,48 @@ 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 = getattr(_current, 'translation', None)
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 = getattr(_current, 'preferred_lang', [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
+ )
247
288
  mo_dir = _get_config('tools.i18n.mo_dir')
248
- domain = _get_config('tools.i18n.domain')
249
- trans = _search_translation(mo_dir, domain, *preferred_lang)
250
- if trans is None:
251
- trans = NullTranslations()
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', False)
262
- domain = _get_config('tools.i18n.domain')
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')
263
306
  if not mo_dir:
264
307
  return
265
308
  for lang in os.listdir(mo_dir):
266
- trans = _search_translation(mo_dir, domain, lang)
267
- if trans is not None:
268
- yield trans.locale
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
269
315
 
270
316
 
271
317
  def list_available_timezones():
@@ -278,7 +324,7 @@ def list_available_timezones():
278
324
 
279
325
 
280
326
  # Public translation functions
281
- def ugettext(message):
327
+ def gettext(message):
282
328
  """Standard translation function. You can use it in all your exposed
283
329
  methods and everywhere where the response object is available.
284
330
 
@@ -289,13 +335,13 @@ def ugettext(message):
289
335
  :returns: The translated message.
290
336
  :rtype: Unicode
291
337
  """
292
- return get_translation().ugettext(message)
338
+ return get_translation().gettext(message)
293
339
 
294
340
 
295
- gettext = ugettext
341
+ ugettext = gettext
296
342
 
297
343
 
298
- def ungettext(singular, plural, num):
344
+ def ngettext(singular, plural, num):
299
345
  """Like ugettext, but considers plural forms.
300
346
 
301
347
  :parameters:
@@ -310,34 +356,29 @@ def ungettext(singular, plural, num):
310
356
  :returns: The translated message as singular or plural.
311
357
  :rtype: Unicode
312
358
  """
313
- return get_translation().ungettext(singular, plural, num)
359
+ return get_translation().ngettext(singular, plural, num)
314
360
 
315
361
 
316
- ngettext = ungettext
362
+ ungettext = ngettext
317
363
 
318
364
 
319
365
  def gettext_lazy(message):
320
- """Like ugettext, but lazy.
366
+ """Like gettext, but lazy.
321
367
 
322
368
  :returns: A proxy for the translation object.
323
369
  :rtype: LazyProxy
324
370
  """
325
371
 
326
372
  def func():
327
- return get_translation().ugettext(message)
373
+ return get_translation().gettext(message)
328
374
 
329
375
  return LazyProxy(func, enable_cache=False)
330
376
 
331
377
 
332
378
  def format_datetime(datetime=None, format='medium', tzinfo=None):
333
379
  """
334
- Wraper arround babel format_datetime to provide a default locale.
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.
380
+ Wrapper around babel format_datetime to use current locale and current timezone.
339
381
  """
340
- # When formating date or english, let use en_GB as it's to most complete translation.
341
382
  return dates.format_datetime(
342
383
  datetime=datetime,
343
384
  format=format,
@@ -348,14 +389,19 @@ def format_datetime(datetime=None, format='medium', tzinfo=None):
348
389
 
349
390
  def format_date(datetime=None, format='medium', tzinfo=None):
350
391
  """
351
- Wraper arround babel format_date to provide a default locale.
392
+ Wrapper around babel format_date to provide a default locale.
352
393
  """
353
- return format_datetime(datetime=datetime, format=dates.get_date_format(format), tzinfo=tzinfo)
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
+ )
354
401
 
355
402
 
356
- def get_timezone_name(tzinfo):
357
- locale = Locale(get_translation().locale.language)
358
- return dates.get_timezone_name(tzinfo, width='long', locale=locale)
403
+ def get_timezone_name(tzinfo, width='long'):
404
+ return dates.get_timezone_name(tzinfo, width=width, locale=get_translation().locale)
359
405
 
360
406
 
361
407
  def _load_default(mo_dir, domain, default, **kwargs):
@@ -363,35 +409,60 @@ def _load_default(mo_dir, domain, default, **kwargs):
363
409
  Initialize the language using the default value from the configuration.
364
410
  """
365
411
  # Clear current translation
366
- _current.preferred_lang = [default]
367
- _current.preferred_timezone = [kwargs['default_timezone']] if 'default_timezone' in kwargs else []
368
- cherrypy.request._i18n_lang_func = kwargs.get('lang', kwargs.get('func', False))
369
- cherrypy.request._i18n_tzinfo_func = kwargs.get('tzinfo', False)
412
+ _preferred_lang.set(tuple())
413
+ _preferred_timezone.set(tuple())
370
414
  # Clear current translation
371
- _current.translation = None
415
+ _translation.set(None)
372
416
  # Clear current timezone
373
- _current.tzinfo = None
417
+ _tzinfo.set(None)
374
418
 
375
419
 
376
420
  def _load_accept_language(**kwargs):
377
421
  """
378
- When running within a request, load the prefered language from Accept-Language
422
+ When running within a request, load the preferred language from Accept-Language header.
379
423
  """
380
-
381
424
  if cherrypy.request.headers.elements('Accept-Language'):
382
- count = 0
383
- for x in cherrypy.request.headers.elements('Accept-Language'):
384
- _current.preferred_lang.insert(count, x.value.replace('-', '_'))
385
- count += 1
386
- # Clear current translation
387
- _current.translation = None
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
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)
388
459
 
389
460
 
390
461
  def _load_func_language(**kwargs):
391
462
  """
392
- When running a request where a current user is found, load prefered language from user preferences.
463
+ When running a request where a current user is found, load preferred language from user preferences.
393
464
  """
394
- func = getattr(cherrypy.request, '_i18n_lang_func', False)
465
+ func = _get_config('tools.i18n.lang', default=_get_config('tools.i18n.func'))
395
466
  if not func:
396
467
  return
397
468
  try:
@@ -401,16 +472,16 @@ def _load_func_language(**kwargs):
401
472
  if not lang:
402
473
  return
403
474
  # Add custom lang to preferred_lang
404
- _current.preferred_lang.insert(0, lang)
475
+ _preferred_lang.set((lang,))
405
476
  # Clear current translation
406
- _current.translation = None
477
+ _translation.set(None)
407
478
 
408
479
 
409
480
  def _load_func_tzinfo(**kwargs):
410
481
  """
411
- When running a request, load the prefered timezone information from user preferences.
482
+ When running a request, load the preferred timezone information from user preferences.
412
483
  """
413
- func = getattr(cherrypy.request, '_i18n_tzinfo_func', False)
484
+ func = _get_config('tools.i18n.tzinfo')
414
485
  if not func:
415
486
  return
416
487
  try:
@@ -420,9 +491,9 @@ def _load_func_tzinfo(**kwargs):
420
491
  if not tzinfo:
421
492
  return
422
493
  # Add custom lang to preferred_lang
423
- _current.preferred_timezone.insert(0, tzinfo)
494
+ _preferred_timezone.set((tzinfo,))
424
495
  # Clear current translation
425
- _current.tzinfo = None
496
+ _tzinfo.set(None)
426
497
 
427
498
 
428
499
  def _set_content_language(**kwargs):
@@ -431,9 +502,12 @@ def _set_content_language(**kwargs):
431
502
  language of `cherrypy.response.i18n.locale`.
432
503
  """
433
504
  if 'Content-Language' not in cherrypy.response.headers:
434
- locale = get_translation().locale
435
- language_tag = f"{locale.language}-{locale.territory}" if locale.territory else locale.language
436
- cherrypy.response.headers['Content-Language'] = language_tag
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
437
511
 
438
512
 
439
513
  class I18nTool(cherrypy.Tool):
@@ -446,6 +520,7 @@ class I18nTool(cherrypy.Tool):
446
520
  cherrypy.Tool._setup(self)
447
521
  # Attach additional hooks as different priority to update preferred lang with more accurate preferences.
448
522
  cherrypy.request.hooks.attach('before_handler', _load_accept_language, priority=60)
523
+ cherrypy.request.hooks.attach('before_handler', _load_cookie_language, priority=70)
449
524
  cherrypy.request.hooks.attach('before_handler', _load_func_language, priority=75)
450
525
  cherrypy.request.hooks.attach('before_handler', _load_func_tzinfo, priority=75)
451
526
  cherrypy.request.hooks.attach('before_finalize', _set_content_language)
@@ -80,7 +80,17 @@ class Jinja2Tool(cherrypy.Tool):
80
80
 
81
81
  # Enable translation if available
82
82
  if hasattr(cherrypy.tools, 'i18n'):
83
- from .i18n import format_date, format_datetime, get_language_name, get_translation, ugettext, ungettext
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
+ )
84
94
 
85
95
  env.add_extension('jinja2.ext.i18n')
86
96
  env.install_gettext_callables(ugettext, ungettext, newstyle=True)
@@ -88,6 +98,9 @@ class Jinja2Tool(cherrypy.Tool):
88
98
  env.filters['format_datetime'] = format_datetime
89
99
  env.globals['get_language_name'] = get_language_name
90
100
  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
91
104
 
92
105
  # Update globals, filters and tests
93
106
  env.globals.update(globals)
@@ -1,4 +1,5 @@
1
- <html>
1
+ <!DOCTYPE html>
2
+ <html lang="en">
2
3
  <head>
3
4
  <title>test-jinja2</title>
4
5
  </head>
@@ -1,6 +1,7 @@
1
- <html>
1
+ <!DOCTYPE html>
2
+ <html lang="en">
2
3
  <head>
3
- <title>test-jinja2</title>
4
+ <title>test-jinjax</title>
4
5
  </head>
5
6
  <body>
6
7
  <Button link="http://example.com" label="foo" />
@@ -0,0 +1,22 @@
1
+ <!DOCTYPE html>
2
+ <html lang="{{ get_translation().locale }}">
3
+ <head>
4
+ <title>test-jinja2</title>
5
+ {{ catalog.render_assets() }}
6
+ </head>
7
+ <body>
8
+ <!-- Selector -->
9
+ <div class="dropdown">
10
+ <button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
11
+ Dropdown button
12
+ </button>
13
+ <ul class="dropdown-menu">
14
+ <LocaleSelection />
15
+ </ul>
16
+ </div>
17
+ {{ get_language_name(get_translation().locale) }}<br/>
18
+ {% trans %}Some text to translate{% endtrans %}<br/>
19
+ {{ my_datetime | format_datetime(format='full') }}<br/>
20
+ {{ my_date | format_date(format='full') }}
21
+ </body>
22
+ </html>
@@ -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
- self.assertIsNone(t)
56
+ # Return Null translations.
57
+ self.assertIn('NullTranslations', str(t.__class__))
58
58
 
59
59
 
60
60
  class Root:
@@ -76,6 +76,7 @@ class AbstractI18nTest(helper.CPWebCase):
76
76
  'tools.i18n.default': cls.default_lang,
77
77
  'tools.i18n.mo_dir': importlib.resources.files(__package__) / 'locales',
78
78
  'tools.i18n.domain': 'messages',
79
+ 'tools.i18n.cookie_name': 'locale',
79
80
  }
80
81
  )
81
82
  cherrypy.tree.mount(Root(), '/')
@@ -117,6 +118,20 @@ class TestI18nWebCase(AbstractI18nTest):
117
118
  self.assertHeaderItemValue("Content-Language", "fr-CA")
118
119
  self.assertInBody(TEXT_FR)
119
120
 
121
+ def test_language_en_US_POSIX(self):
122
+ # When calling with locale variant
123
+ self.getPage("/", headers=[("Accept-Language", "en-US-POSIX")])
124
+ self.assertStatus('200 OK')
125
+ # Tehn page return en-US
126
+ self.assertHeaderItemValue("Content-Language", "en-US")
127
+
128
+ def test_cookie_fr(self):
129
+ # When calling with locale variant
130
+ self.getPage("/", headers=[("Accept-Language", "en-US"), ("Cookie", "locale=fr")])
131
+ self.assertStatus('200 OK')
132
+ # Tehn page return en-US
133
+ self.assertHeaderItemValue("Content-Language", "fr")
134
+
120
135
  def test_with_preferred_lang(self):
121
136
  # Given a default lang 'en'
122
137
  date = datetime.fromtimestamp(1680111611, timezone.utc)
@@ -168,6 +183,23 @@ class TestI18nWebCase(AbstractI18nTest):
168
183
  with i18n.preferred_lang('de_CH'):
169
184
  self.assertEqual('29. März 2023, 19:40:11 MESZ', i18n.format_datetime(date, format='long'))
170
185
 
186
+ def test_list_available_locales(self):
187
+ self.assertEqual(['de', 'en', 'fr'], sorted([str(l) for l in i18n.list_available_locales()]))
188
+
189
+ def test_list_available_timezones(self):
190
+ timezones = i18n.list_available_timezones()
191
+ self.assertIn('America/Toronto', timezones)
192
+
193
+ def test_get_timezone_name(self):
194
+ with i18n.preferred_lang('en'):
195
+ self.assertEqual('Eastern Time', i18n.get_timezone_name('America/Toronto'))
196
+ self.assertEqual('ET', i18n.get_timezone_name('America/Toronto', width='short'))
197
+ self.assertEqual('Eastern Time', i18n.get_timezone_name('America/Toronto', width='long'))
198
+ with i18n.preferred_lang('fr'):
199
+ self.assertEqual('heure de l’Est nord-américain', i18n.get_timezone_name('America/Toronto'))
200
+ self.assertEqual('HE', i18n.get_timezone_name('America/Toronto', width='short'))
201
+ self.assertEqual('heure de l’Est nord-américain', i18n.get_timezone_name('America/Toronto', width='long'))
202
+
171
203
 
172
204
  class TestI18nDefaultLangWebCase(AbstractI18nTest):
173
205
  default_lang = 'FR'
@@ -21,6 +21,10 @@ import cherrypy
21
21
  from cherrypy.test import helper
22
22
  from parameterized import parameterized
23
23
 
24
+ from cherrypy_foundation.components import StaticMiddleware
25
+ from cherrypy_foundation.tests import SeleniumUnitTest
26
+ from cherrypy_foundation.url import url_for
27
+
24
28
  from .. import i18n # noqa
25
29
  from .. import jinja2 # noqa
26
30
 
@@ -38,9 +42,18 @@ def extra_processor():
38
42
  return {'var2': 'bar'}
39
43
 
40
44
 
41
- @cherrypy.tools.jinja2(env=env, extra_processor=extra_processor)
45
+ @cherrypy.tools.i18n(on=False)
46
+ @cherrypy.tools.sessions(on=False)
47
+ class Static:
48
+
49
+ components = StaticMiddleware()
50
+
51
+
52
+ @cherrypy.tools.jinja2(on=False, env=env, extra_processor=extra_processor)
42
53
  class Root:
43
54
 
55
+ static = Static()
56
+
44
57
  @cherrypy.expose
45
58
  @cherrypy.tools.jinja2(template='test_jinja2.html')
46
59
  def index(self):
@@ -52,12 +65,13 @@ class Root:
52
65
  return {'var1': 'test-jinjax'}
53
66
 
54
67
  @cherrypy.expose
55
- @cherrypy.tools.jinja2(template='test_jinja2_i18n.html')
68
+ @cherrypy.tools.jinja2(template='test_jinjax_i18n.html')
56
69
  @cherrypy.tools.i18n(
57
70
  default='fr',
58
71
  default_timezone='America/Toronto',
59
72
  mo_dir=importlib.resources.files(__package__) / 'locales',
60
73
  domain='messages',
74
+ cookie_name='locale', # For LocaleSelection
61
75
  )
62
76
  def localized(self):
63
77
  return {
@@ -66,7 +80,7 @@ class Root:
66
80
  }
67
81
 
68
82
 
69
- class Jinja2Test(helper.CPWebCase):
83
+ class Jinja2Test(helper.CPWebCase, SeleniumUnitTest):
70
84
  default_lang = None
71
85
  interactive = False
72
86
 
@@ -95,6 +109,7 @@ class Jinja2Test(helper.CPWebCase):
95
109
  # Then the page is render dynamically using page context
96
110
  self.assertInBody('<a class="btn btn-primary" href="http://example.com">foo</a>')
97
111
 
112
+ @skipUnless(HAS_JINJAX, reason='Required jinjax')
98
113
  @parameterized.expand(
99
114
  [
100
115
  ('server_default', {}, 'fr'),
@@ -121,3 +136,18 @@ class Jinja2Test(helper.CPWebCase):
121
136
  self.assertInBody('Some text to translate')
122
137
  self.assertInBody('Wednesday, November 26, 2025, 6:16:00\u202fAM Eastern Standard Time')
123
138
  self.assertInBody('Monday, December 22, 2025')
139
+
140
+ @skipUnless(HAS_JINJAX, reason='Required jinjax')
141
+ def test_get_page_i18n_selenium(self):
142
+ # Given a localized page render with jinja2
143
+ with self.selenium() as driver:
144
+ # When querying the page
145
+ driver.get(url_for('localized'))
146
+ # Then page load without error in english (enforced chronium lang)
147
+ self.assertFalse(driver.get_log('browser'))
148
+ self.assertEqual('en_US', driver.find_element('css selector', 'html').get_attribute('lang'))
149
+ # When user select a language
150
+ btn = driver.find_element('css selector', 'button[data-locale=fr]')
151
+ btn.click()
152
+ # Then page is reloaded with in French.
153
+ self.assertEqual('fr', driver.find_element('css selector', 'html').get_attribute('lang'))
@@ -60,8 +60,7 @@ def url_for(*args, _relative=None, _base=None, **kwargs):
60
60
  elif not path.startswith('.'):
61
61
  path = urljoin('/', path)
62
62
  # Outside a request, use cherrypy.tools.proxy config
63
- if not cherrypy.request.app:
64
- if _base is None:
65
- _base = cherrypy.config.get('tools.proxy.base', None)
63
+ if not cherrypy.request.app and _base is None:
64
+ _base = cherrypy.config.get('tools.proxy.base', None)
66
65
  # Use cherrypy to build the URL
67
66
  return cherrypy.url(path=path, qs=qs, relative=_relative, base=_base)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cherrypy-foundation
3
- Version: 1.0.0a12
3
+ Version: 1.0.0a14
4
4
  Summary: CherryPy Foundation
5
5
  Author-email: Patrik Dufresne <patrik@ikus-soft.com>
6
6
  License: GPLv3
@@ -4,9 +4,9 @@ cherrypy_foundation/flash.py,sha256=fFRbutUX6c1lVHqjehmO9y98dJgmfNCjhd76t2mth2s,
4
4
  cherrypy_foundation/form.py,sha256=8c9dO0o47sK3CBosTkGXoVRtzNQwY0aw0vNZfTqmhvo,3994
5
5
  cherrypy_foundation/logging.py,sha256=YIOK5ZAZLCv52YDdP66yBYpEX1C336JnI3wnrTKl1Lw,3468
6
6
  cherrypy_foundation/passwd.py,sha256=ZGdrBNKtLP75l01W6VEd8cIjSQ3guJ_YVPEfbSew7T0,2144
7
- cherrypy_foundation/url.py,sha256=l11A4MYTwTAPrGYXygkpibx_eYAYfVbbq-IrlePgg4U,2866
7
+ cherrypy_foundation/url.py,sha256=IjDADMIZTZbQ5mECSsoz_MsyJvA85amV-93J_E3hbb4,2854
8
8
  cherrypy_foundation/widgets.py,sha256=0B5Y2V6x5Ufl6ExR3tc0Olrzj7N4TAAOtqGq_MUxBG0,1549
9
- cherrypy_foundation/components/ColorModes.jinja,sha256=8MzkeZsra1wtIdiaQKc7UQUbMfRMUlmM6e9X7V1vfq0,3501
9
+ cherrypy_foundation/components/ColorModes.jinja,sha256=Ai2fy1qHFwEgutvyvvGjKJmffcBdNb7wmY20DJqZ8R4,3528
10
10
  cherrypy_foundation/components/Datatable.css,sha256=7wSwgdA61vYCdEuQ0bp2o0oSvu5mGLN1c6ovCUSe718,947
11
11
  cherrypy_foundation/components/Datatable.jinja,sha256=K-24vpN5TO8Jk2COvuC5uBaCj3PFLd6ubr28JHFq3KM,3375
12
12
  cherrypy_foundation/components/Datatable.js,sha256=jiV78bJPNMTLcjrX03vaYcsEdRbI6ctzSvCo880H8Hw,12072
@@ -16,6 +16,8 @@ cherrypy_foundation/components/Field.js,sha256=SFixZ62WlLq7SSCEazMAGhSnc9EnQ1wg6
16
16
  cherrypy_foundation/components/Fields.jinja,sha256=_8Or6DOlciKjRao-sdBpLs--bx5V-K80X3hDEA1VKCQ,165
17
17
  cherrypy_foundation/components/Flash.jinja,sha256=COy44drQsXpbZajjJS4w_9NMPYFdMfUNdfhd7SbFdYA,442
18
18
  cherrypy_foundation/components/Icon.jinja,sha256=Z1RGYBg5xlDEoUy3glqb_k_LEjkJHeCxQXqDEvWzEF4,135
19
+ cherrypy_foundation/components/LocaleSelection.jinja,sha256=NHhQGI6CCJl4LWl-eteu_AIg1_qc1MusuA1fKcrjluQ,509
20
+ cherrypy_foundation/components/LocaleSelection.js,sha256=-iTZD5ejg1R9F3tZASTH7dkgd19upke-pzOhsZ9K_LA,670
19
21
  cherrypy_foundation/components/SideBySideMultiSelect.css,sha256=_poMY9O8rvDsOh01pQLf9qtg1Gm4eCM2HsM_ekC5zkk,503
20
22
  cherrypy_foundation/components/SideBySideMultiSelect.jinja,sha256=sud1WP-6JzuP7ZLRr-JQqvgMRWZRlXvxUJfguFr_klk,478
21
23
  cherrypy_foundation/components/SideBySideMultiSelect.js,sha256=5YMz1pgkXeWC_SRRfDbQI3X-c4PuxiTIpbWJt9sY7Rc,197
@@ -91,43 +93,41 @@ cherrypy_foundation/plugins/tests/test_ldap.py,sha256=j0QUIxqeEBgN4VLvczUlw0xMM_
91
93
  cherrypy_foundation/plugins/tests/test_scheduler.py,sha256=I-ZuQhMvCCvqFDwukwsyz_UkdJJ8JSLTkAanUo24GCE,3564
92
94
  cherrypy_foundation/plugins/tests/test_scheduler_db.py,sha256=-iZnLgb3qsJzs0p7JAw6TQswRtExCmHAtYXqRNPZn7U,3101
93
95
  cherrypy_foundation/plugins/tests/test_smtp.py,sha256=qs5yezIpSXkBmLmFlqckfPW7NmntHZxQjDSkdQG_dNE,4183
94
- cherrypy_foundation/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
96
+ cherrypy_foundation/tests/__init__.py,sha256=VIBhEMPDIaffF7lYzA5KevcVNzALz4kykBBaO-gdKHY,2818
95
97
  cherrypy_foundation/tests/test_error_page.py,sha256=8yLK8OGbJIdUjilFIHMNBZadLKHrXnD6KSmQ3Da4LaQ,2399
96
98
  cherrypy_foundation/tests/test_flash.py,sha256=JqZDAgazlNnP3HcPFmFOWbPeDDMzc6z5fHNe-pBTin0,1976
97
99
  cherrypy_foundation/tests/test_form.py,sha256=sWsPWyXwAVkCeP5t0qIHc0Oi32Zi3kztoQ_wlDR9STc,4326
98
100
  cherrypy_foundation/tests/test_passwd.py,sha256=gC5O4yhHyU1YRYuDc0pG0T_5zvrG2qrr6P822iyK3Rg,1956
99
101
  cherrypy_foundation/tests/test_url.py,sha256=W-RTKQuxYS2KXxCYTTtnKcxfdP9F6Fp3QKY_sBTnBmE,6434
100
- cherrypy_foundation/tests/templates/test_flash.html,sha256=MyOPsHQX97TB30sbKrbG3tagKDyvV8OVKp-UOrw7REc,187
101
- cherrypy_foundation/tests/templates/test_form.html,sha256=sm-n2cYvih2vbDE4Y8kkERSoulnKAbwoefbzBggMMnA,189
102
- cherrypy_foundation/tests/templates/test_url.html,sha256=Rb6NokHEduMHAXO8P6EduMMHXuNzJGN5Of2OF4fSWns,502
102
+ cherrypy_foundation/tests/templates/test_flash.html,sha256=b1S4I9v0n-Y1yoTUh2ZKNysR1NMrqv8ldvqONtmInzw,213
103
+ cherrypy_foundation/tests/templates/test_form.html,sha256=ywaLKgWqvBdbdfnBgYmi1ihA3xK8X5YtjomA6atnJUg,206
104
+ cherrypy_foundation/tests/templates/test_url.html,sha256=427G6AnA6zUfjPoLxsVHy3U2e_XxG4ntpZX3DIjO18Q,512
103
105
  cherrypy_foundation/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
104
106
  cherrypy_foundation/tools/auth.py,sha256=lTSajxCiReMzm-Fl-xhTByi4yFnInEWOoNsmUMnHQhs,9761
105
107
  cherrypy_foundation/tools/auth_mfa.py,sha256=VaLvBz9wo6jTx-2mCGqFXPxl-z14f8UMWvd6_xeXd40,9212
106
108
  cherrypy_foundation/tools/errors.py,sha256=ELpAj0N9kIxC22QW5xDQJz60zMpCwgm-Twu2WpELM1A,1005
107
- cherrypy_foundation/tools/i18n.py,sha256=VQJs7hIv-BM9_FIU6-pwRp65eeroHoQLU7QrV5V_rbI,14641
108
- cherrypy_foundation/tools/jinja2.py,sha256=nppYnk2ASDsyfNHF9m83W4foiN3MhcwDJvo5baEgnGU,5520
109
+ cherrypy_foundation/tools/i18n.py,sha256=pP3QrGCHiNHVDpjdKNFKErOCm3eRYZL6W_nlz9j8eD8,17128
110
+ cherrypy_foundation/tools/jinja2.py,sha256=xrHfk814GjlWhfqJcXTA6I2PQ3YqsSz4eHcCh9sG05U,5969
109
111
  cherrypy_foundation/tools/ratelimit.py,sha256=pT7vZRmjltNeuiQpdyXOmnpG9BcXjLaj-AXJ0e2x_zw,8300
110
112
  cherrypy_foundation/tools/secure_headers.py,sha256=Yh-iA_Js4MUsx5nq4ilbc-iWy90ZC0oMb3TJJD_UwYo,3921
111
113
  cherrypy_foundation/tools/sessions_timeout.py,sha256=6iBWJntPMk_Qt94fBSfBISf1IXInSh-1XrxLbKXFV-g,7408
112
114
  cherrypy_foundation/tools/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
113
115
  cherrypy_foundation/tools/tests/test_auth.py,sha256=oeM5t38M8DUC9dYn59dcf00jdGY6Ry0jZWhQd_PYQUk,2847
114
116
  cherrypy_foundation/tools/tests/test_auth_mfa.py,sha256=911hnBbdg5CKb613uIBrlggoTAyBU9SoL7Sxd-tIKS0,15008
115
- cherrypy_foundation/tools/tests/test_i18n.py,sha256=T9j9fF7AwH_H4TJnC4CbAOS1a1D2Z4xPxaOmiP0By34,9091
116
- cherrypy_foundation/tools/tests/test_jinja2.py,sha256=_dkRJpjB0ybDV6YO0uEFFO8LAcWgVu3VBB8_vWthQ48,4296
117
+ cherrypy_foundation/tools/tests/test_i18n.py,sha256=9rK4_1HVTk2dtTh9egIymx4SP8EO8_igBQkCD4YiIys,10758
118
+ cherrypy_foundation/tools/tests/test_jinja2.py,sha256=gLwaGeQn1Wt7Ximc_5ESHSm499vCeyqhEhPYUPJnamk,5545
117
119
  cherrypy_foundation/tools/tests/test_ratelimit.py,sha256=rrqybwMbh1GFlF2-Ut57zHPAc1uqX88aqea6VS_6p5E,3449
118
120
  cherrypy_foundation/tools/tests/components/Button.jinja,sha256=uSLp1GpEIgZNXK_GWglu0E_a1c3jHpDLI66MRfMqGhE,95
119
121
  cherrypy_foundation/tools/tests/locales/messages.pot,sha256=5K9piTRL7H5MxDXFIWJsCccSJRA0HwfCQQU8b8VYo30,40
120
122
  cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.mo,sha256=bsJTVL4OefevkxeHDS3VcW3egP6Yq18LFXwjSyoqIng,336
121
123
  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
124
  cherrypy_foundation/tools/tests/locales/fr/LC_MESSAGES/messages.mo,sha256=u3_kl_nqZ3FNaSyKVQKmu4KJzN3xOxxJNVmcdhw37jA,327
125
125
  cherrypy_foundation/tools/tests/locales/fr/LC_MESSAGES/messages.po,sha256=6_Sk9Igqm7dtpyyS701p5qc4DvOJE7TRT0ajRZctFAQ,342
126
- cherrypy_foundation/tools/tests/templates/test_jinja2.html,sha256=v9AHxksbBvzE7sesPqE61HMhsvU4juXt3E0ZQo-zXVQ,190
127
- cherrypy_foundation/tools/tests/templates/test_jinja2_i18n.html,sha256=98S51dgG7Vb4rvMZNZvomw1D9pBiM4g6pdlxAgvrxXA,373
128
- cherrypy_foundation/tools/tests/templates/test_jinjax.html,sha256=NT19UaUzm8FRKOIc6H6HNGPDJU6KATnakd8zf3BCeAs,153
129
- cherrypy_foundation-1.0.0a12.dist-info/licenses/LICENSE.md,sha256=trSLYs5qlaow_bBwsLTRKpmTXsXzFksM_YUCMqrgAJQ,35149
130
- cherrypy_foundation-1.0.0a12.dist-info/METADATA,sha256=bVj-ivyQaks7eDS4cxGDbq0I1OhKXejr5Rq9EUo81xc,2023
131
- cherrypy_foundation-1.0.0a12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
132
- cherrypy_foundation-1.0.0a12.dist-info/top_level.txt,sha256=B1vQPTLYhpKJ6W0JkRCWyAf8RPcnwJWdYxixv75-4ew,20
133
- cherrypy_foundation-1.0.0a12.dist-info/RECORD,,
126
+ cherrypy_foundation/tools/tests/templates/test_jinja2.html,sha256=s1bHmy-lyf0YW0t-LOx3ugILV2kqFoBNYxziWgrZbo0,216
127
+ cherrypy_foundation/tools/tests/templates/test_jinjax.html,sha256=NImzIW0mUHxilFd61PSoxFC-yu1nayEVwv-5zlgD9yo,179
128
+ cherrypy_foundation/tools/tests/templates/test_jinjax_i18n.html,sha256=yre8j7HBjpTQZHpM0PuB3ASGD3O4vKkJ-y72Fm6STgY,771
129
+ cherrypy_foundation-1.0.0a14.dist-info/licenses/LICENSE.md,sha256=trSLYs5qlaow_bBwsLTRKpmTXsXzFksM_YUCMqrgAJQ,35149
130
+ cherrypy_foundation-1.0.0a14.dist-info/METADATA,sha256=4Iv0mZTcV0ApGQk9UOB0tiTopgR_J7FG31KifV9nl0E,2023
131
+ cherrypy_foundation-1.0.0a14.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
132
+ cherrypy_foundation-1.0.0a14.dist-info/top_level.txt,sha256=B1vQPTLYhpKJ6W0JkRCWyAf8RPcnwJWdYxixv75-4ew,20
133
+ cherrypy_foundation-1.0.0a14.dist-info/RECORD,,
@@ -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 ""
@@ -1,11 +0,0 @@
1
- <html lang="{{ get_translation().locale }}">
2
- <head>
3
- <title>test-jinja2</title>
4
- </head>
5
- <body>
6
- {{ get_language_name(get_translation().locale) }}<br/>
7
- {% trans %}Some text to translate{% endtrans %}<br/>
8
- {{ my_datetime | format_datetime(format='full') }}<br/>
9
- {{ my_date | format_date(format='full') }}
10
- </body>
11
- </html>