cherrypy-foundation 1.0.0a12__tar.gz → 1.0.0a13__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cherrypy_foundation-1.0.0a12/src/cherrypy_foundation.egg-info → cherrypy_foundation-1.0.0a13}/PKG-INFO +1 -1
- cherrypy_foundation-1.0.0a13/src/cherrypy_foundation/tools/i18n.py +495 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/tests/test_i18n.py +9 -2
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13/src/cherrypy_foundation.egg-info}/PKG-INFO +1 -1
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation.egg-info/SOURCES.txt +0 -2
- cherrypy_foundation-1.0.0a12/src/cherrypy_foundation/tools/i18n.py +0 -454
- cherrypy_foundation-1.0.0a12/src/cherrypy_foundation/tools/tests/locales/en/LC_MESSAGES/messages.mo +0 -0
- cherrypy_foundation-1.0.0a12/src/cherrypy_foundation/tools/tests/locales/en/LC_MESSAGES/messages.po +0 -15
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/.gitignore +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/.gitlab-ci.yml +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/LICENSE.md +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/README.md +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/debian/TODO +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/debian/changelog +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/debian/control +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/debian/copyright +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/debian/python3-cherrypy-foundation.docs +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/debian/python3-cherrypy-foundation.links +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/debian/rules +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/debian/source/format +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/debian/source/options +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/debian/upstream/metadata +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/pyproject.toml +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/setup.cfg +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/sonar-project.properties +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/__init__.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/ColorModes.jinja +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/Datatable.css +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/Datatable.jinja +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/Datatable.js +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/Field.css +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/Field.jinja +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/Field.js +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/Fields.jinja +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/Flash.jinja +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/Icon.jinja +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/SideBySideMultiSelect.css +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/SideBySideMultiSelect.jinja +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/SideBySideMultiSelect.js +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/Typeahead.css +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/Typeahead.jinja +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/Typeahead.js +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/__init__.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/tests/__init__.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/tests/test_static.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/bootstrap-icons/bootstrap-icons.css +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/bootstrap-icons/bootstrap-icons.min.css +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.css +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.css.map +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.min.css +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.min.css.map +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.js +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.js.map +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.min.js +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.min.js.map +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/bootstrap5/js/color-modes.js +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables/css/dataTables.dataTables.css +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables/css/dataTables.dataTables.min.css +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables/images/sort_asc.png +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables/images/sort_asc_disabled.png +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables/images/sort_both.png +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables/images/sort_desc.png +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables/images/sort_desc_disabled.png +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables/js/dataTables.js +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables/js/dataTables.min.js +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/Buttons/css/buttons.dataTables.css +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/Buttons/css/buttons.dataTables.min.css +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/buttons.html5.js +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/buttons.html5.min.js +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/dataTables.buttons.js +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/dataTables.buttons.min.js +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/css/fixedHeader.dataTables.css +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/css/fixedHeader.dataTables.min.css +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/js/dataTables.fixedHeader.js +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/js/dataTables.fixedHeader.min.js +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/JSZip/jszip.js +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/JSZip/jszip.min.js +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/Responsive/css/responsive.dataTables.css +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/Responsive/css/responsive.dataTables.min.css +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/Responsive/js/dataTables.responsive.js +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/Responsive/js/dataTables.responsive.min.js +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/pdfmake/build/pdfmake.js +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/pdfmake/build/pdfmake.min.js +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/pdfmake/build/vfs_fonts.js +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/css/rowGroup.dataTables.css +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/css/rowGroup.dataTables.min.css +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/js/dataTables.rowGroup.js +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/js/dataTables.rowGroup.min.js +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/jquery/jquery.min.js +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/multi/LICENSE +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/multi/README.md +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/multi/multi.css +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/multi/multi.js +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/popper/popper.js +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/popper/popper.min.js +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/typeahead/jquery.typeahead.min.css +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/typeahead/jquery.typeahead.min.js +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/error_page.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/flash.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/form.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/logging.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/passwd.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/plugins/__init__.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/plugins/db.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/plugins/ldap.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/plugins/restapi.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/plugins/scheduler.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/plugins/smtp.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/plugins/tests/__init__.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/plugins/tests/test_db.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/plugins/tests/test_ldap.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/plugins/tests/test_scheduler.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/plugins/tests/test_scheduler_db.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/plugins/tests/test_smtp.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tests/__init__.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tests/templates/test_flash.html +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tests/templates/test_form.html +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tests/templates/test_url.html +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tests/test_error_page.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tests/test_flash.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tests/test_form.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tests/test_passwd.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tests/test_url.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/__init__.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/auth.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/auth_mfa.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/errors.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/jinja2.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/ratelimit.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/secure_headers.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/sessions_timeout.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/tests/__init__.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/tests/components/Button.jinja +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.mo +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.po +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/tests/locales/fr/LC_MESSAGES/messages.mo +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/tests/locales/fr/LC_MESSAGES/messages.po +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/tests/locales/messages.pot +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/tests/templates/test_jinja2.html +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/tests/templates/test_jinja2_i18n.html +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/tests/templates/test_jinjax.html +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/tests/test_auth.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/tests/test_auth_mfa.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/tests/test_jinja2.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/tests/test_ratelimit.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/url.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/widgets.py +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation.egg-info/dependency_links.txt +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation.egg-info/requires.txt +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation.egg-info/top_level.txt +0 -0
- {cherrypy_foundation-1.0.0a12 → cherrypy_foundation-1.0.0a13}/tox.ini +0 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
# Internationalisation tool for cherrypy
|
|
2
|
+
# Copyright (C) 2012-2025 Patrik Dufresne
|
|
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
|
+
"""
|
|
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):
|
|
25
|
+
|
|
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``
|
|
30
|
+
|
|
31
|
+
Translations are loaded using Babel and gettext, and the resolved locale is
|
|
32
|
+
available through ``i18n.get_translation()`` during request handling.
|
|
33
|
+
|
|
34
|
+
---------------------------------------------------------------------------
|
|
35
|
+
Basic usage
|
|
36
|
+
---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
Within Python code, mark translatable strings using ``ugettext`` or
|
|
39
|
+
``ungettext``:
|
|
40
|
+
|
|
41
|
+
from i18n import gettext as _, ngettext
|
|
42
|
+
|
|
43
|
+
class MyController:
|
|
44
|
+
@cherrypy.expose
|
|
45
|
+
def index(self):
|
|
46
|
+
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])
|
|
54
|
+
|
|
55
|
+
---------------------------------------------------------------------------
|
|
56
|
+
Lazy translations
|
|
57
|
+
---------------------------------------------------------------------------
|
|
58
|
+
|
|
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:
|
|
62
|
+
|
|
63
|
+
from i18n_tool import gettext_lazy
|
|
64
|
+
|
|
65
|
+
class Model:
|
|
66
|
+
name = gettext_lazy(u"Model name")
|
|
67
|
+
|
|
68
|
+
---------------------------------------------------------------------------
|
|
69
|
+
Templates
|
|
70
|
+
---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
For template rendering, i18n integrate with jinja2.
|
|
73
|
+
|
|
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') }}
|
|
80
|
+
|
|
81
|
+
---------------------------------------------------------------------------
|
|
82
|
+
Configuration
|
|
83
|
+
---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
Example CherryPy configuration:
|
|
86
|
+
|
|
87
|
+
[/]
|
|
88
|
+
tools.i18n.on = True
|
|
89
|
+
tools.i18n.default = "en_US"
|
|
90
|
+
tools.i18n.mo_dir = "/path/to/i18n"
|
|
91
|
+
tools.i18n.domain = "myapp"
|
|
92
|
+
|
|
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:
|
|
96
|
+
|
|
97
|
+
<mo_dir>/<language>/LC_MESSAGES/<domain>.mo
|
|
98
|
+
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
import logging
|
|
102
|
+
import os
|
|
103
|
+
from contextlib import contextmanager
|
|
104
|
+
from contextvars import ContextVar
|
|
105
|
+
from functools import lru_cache
|
|
106
|
+
from gettext import NullTranslations, translation
|
|
107
|
+
|
|
108
|
+
import cherrypy
|
|
109
|
+
import pytz
|
|
110
|
+
from babel import dates
|
|
111
|
+
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
|
+
|
|
119
|
+
|
|
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)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@contextmanager
|
|
130
|
+
def preferred_lang(lang):
|
|
131
|
+
"""
|
|
132
|
+
Re-define the preferred language to be used for translation within a given context.
|
|
133
|
+
|
|
134
|
+
with i18n.preferred_lang('fr'):
|
|
135
|
+
i18n.gettext('some string')
|
|
136
|
+
"""
|
|
137
|
+
if not (lang is None or isinstance(lang, str)):
|
|
138
|
+
raise ValueError(lang)
|
|
139
|
+
try:
|
|
140
|
+
# Update preferred lang and clear translation.
|
|
141
|
+
if lang:
|
|
142
|
+
token_l = _preferred_lang.set((lang,))
|
|
143
|
+
else:
|
|
144
|
+
token_l = _preferred_lang.set(tuple())
|
|
145
|
+
token_t = _translation.set(None)
|
|
146
|
+
yield
|
|
147
|
+
finally:
|
|
148
|
+
# Restore previous value
|
|
149
|
+
_preferred_lang.reset(token_l)
|
|
150
|
+
_translation.reset(token_t)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@contextmanager
|
|
154
|
+
def preferred_timezone(timezone):
|
|
155
|
+
"""
|
|
156
|
+
Re-define the preferred timezone to be used for date format within a given context.
|
|
157
|
+
|
|
158
|
+
with i18n.preferred_lang('America/Montreal'):
|
|
159
|
+
i18n.format_datetime(...)
|
|
160
|
+
"""
|
|
161
|
+
if not (timezone is None or isinstance(timezone, str)):
|
|
162
|
+
raise ValueError(timezone)
|
|
163
|
+
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)
|
|
170
|
+
yield
|
|
171
|
+
finally:
|
|
172
|
+
# Restore previous value
|
|
173
|
+
_preferred_timezone.reset(token_t)
|
|
174
|
+
_tzinfo.reset(token_z)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@lru_cache(maxsize=32)
|
|
178
|
+
def _search_translation(dirname, domain, *locales, sourcecode_lang='en'):
|
|
179
|
+
"""
|
|
180
|
+
Loads the first existing translations for known locale.
|
|
181
|
+
|
|
182
|
+
:parameters:
|
|
183
|
+
langs : List
|
|
184
|
+
List of languages as returned by `parse_accept_language_header`.
|
|
185
|
+
dirname : String
|
|
186
|
+
A single directory of the translations (`tools.i18n.mo_dir`).
|
|
187
|
+
domain : String
|
|
188
|
+
Gettext domain of the catalog (`tools.i18n.domain`).
|
|
189
|
+
|
|
190
|
+
:returns: Translations, the corresponding Locale object.
|
|
191
|
+
"""
|
|
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)
|
|
217
|
+
return t
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def get_language_name(lang_code):
|
|
221
|
+
"""
|
|
222
|
+
Translate the language code into it's language display name.
|
|
223
|
+
"""
|
|
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)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def get_timezone():
|
|
233
|
+
"""
|
|
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
|
+
|
|
242
|
+
"""
|
|
243
|
+
# When tzinfo is defined, use it
|
|
244
|
+
tzinfo = _tzinfo.get()
|
|
245
|
+
if tzinfo is not None:
|
|
246
|
+
return tzinfo
|
|
247
|
+
# 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
|
+
)
|
|
255
|
+
for timezone in preferred_timezone:
|
|
256
|
+
try:
|
|
257
|
+
tzinfo = dates.get_timezone(timezone)
|
|
258
|
+
break
|
|
259
|
+
except Exception:
|
|
260
|
+
pass
|
|
261
|
+
# If we can't find a valid timezone using the default and preferred value, fall back to server timezone.
|
|
262
|
+
if tzinfo is None:
|
|
263
|
+
tzinfo = dates.get_timezone(None)
|
|
264
|
+
_tzinfo.set(tzinfo)
|
|
265
|
+
return tzinfo
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def get_translation():
|
|
269
|
+
"""
|
|
270
|
+
Get the best translation for the current context.
|
|
271
|
+
"""
|
|
272
|
+
# When translation is defined, use it
|
|
273
|
+
translation = _translation.get()
|
|
274
|
+
if translation is not None:
|
|
275
|
+
return translation
|
|
276
|
+
|
|
277
|
+
# Otherwise, we need to search the translation.
|
|
278
|
+
# `preferred_lang` should always has a sane value within a cherrypy request because of hooks
|
|
279
|
+
# 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
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def list_available_locales():
|
|
296
|
+
"""
|
|
297
|
+
Return a list of available translations.
|
|
298
|
+
"""
|
|
299
|
+
mo_dir = _get_config('tools.i18n.mo_dir')
|
|
300
|
+
domain = _get_config('tools.i18n.domain', 'messages')
|
|
301
|
+
if not mo_dir:
|
|
302
|
+
return
|
|
303
|
+
for lang in os.listdir(mo_dir):
|
|
304
|
+
if os.path.exists(os.path.join(mo_dir, lang, 'LC_MESSAGES', f'{domain}.mo')):
|
|
305
|
+
try:
|
|
306
|
+
yield Locale.parse(lang)
|
|
307
|
+
except Exception:
|
|
308
|
+
continue
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def list_available_timezones():
|
|
312
|
+
"""
|
|
313
|
+
Return list of available timezone.
|
|
314
|
+
"""
|
|
315
|
+
# Babel only support a narrow list of timezone.
|
|
316
|
+
babel_timezone = get_global('zone_territories').keys()
|
|
317
|
+
return [t for t in pytz.all_timezones if t in babel_timezone]
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# Public translation functions
|
|
321
|
+
def gettext(message):
|
|
322
|
+
"""Standard translation function. You can use it in all your exposed
|
|
323
|
+
methods and everywhere where the response object is available.
|
|
324
|
+
|
|
325
|
+
:parameters:
|
|
326
|
+
message : Unicode
|
|
327
|
+
The message to translate.
|
|
328
|
+
|
|
329
|
+
:returns: The translated message.
|
|
330
|
+
:rtype: Unicode
|
|
331
|
+
"""
|
|
332
|
+
return get_translation().gettext(message)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
ugettext = gettext
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def ngettext(singular, plural, num):
|
|
339
|
+
"""Like ugettext, but considers plural forms.
|
|
340
|
+
|
|
341
|
+
:parameters:
|
|
342
|
+
singular : Unicode
|
|
343
|
+
The message to translate in singular form.
|
|
344
|
+
plural : Unicode
|
|
345
|
+
The message to translate in plural form.
|
|
346
|
+
num : Integer
|
|
347
|
+
Number to apply the plural formula on. If num is 1 or no
|
|
348
|
+
translation is found, singular is returned.
|
|
349
|
+
|
|
350
|
+
:returns: The translated message as singular or plural.
|
|
351
|
+
:rtype: Unicode
|
|
352
|
+
"""
|
|
353
|
+
return get_translation().ngettext(singular, plural, num)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
ungettext = ngettext
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def gettext_lazy(message):
|
|
360
|
+
"""Like gettext, but lazy.
|
|
361
|
+
|
|
362
|
+
:returns: A proxy for the translation object.
|
|
363
|
+
:rtype: LazyProxy
|
|
364
|
+
"""
|
|
365
|
+
|
|
366
|
+
def func():
|
|
367
|
+
return get_translation().gettext(message)
|
|
368
|
+
|
|
369
|
+
return LazyProxy(func, enable_cache=False)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def format_datetime(datetime=None, format='medium', tzinfo=None):
|
|
373
|
+
"""
|
|
374
|
+
Wrapper around babel format_datetime to use current locale and current timezone.
|
|
375
|
+
"""
|
|
376
|
+
return dates.format_datetime(
|
|
377
|
+
datetime=datetime,
|
|
378
|
+
format=format,
|
|
379
|
+
locale=get_translation().locale,
|
|
380
|
+
tzinfo=tzinfo or get_timezone(),
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def format_date(datetime=None, format='medium', tzinfo=None):
|
|
385
|
+
"""
|
|
386
|
+
Wrapper around babel format_date to provide a default locale.
|
|
387
|
+
"""
|
|
388
|
+
# To enforce the timezone and locale, make use of format_datetime for dates.
|
|
389
|
+
return dates.format_datetime(
|
|
390
|
+
datetime=datetime,
|
|
391
|
+
format=dates.get_date_format(format),
|
|
392
|
+
locale=get_translation().locale,
|
|
393
|
+
tzinfo=tzinfo or get_timezone(),
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def get_timezone_name(tzinfo, width='long'):
|
|
398
|
+
return dates.get_timezone_name(tzinfo, width=width, locale=get_translation().locale)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _load_default(mo_dir, domain, default, **kwargs):
|
|
402
|
+
"""
|
|
403
|
+
Initialize the language using the default value from the configuration.
|
|
404
|
+
"""
|
|
405
|
+
# Clear current translation
|
|
406
|
+
_preferred_lang.set(tuple())
|
|
407
|
+
_preferred_timezone.set(tuple())
|
|
408
|
+
cherrypy.request._i18n_lang_func = kwargs.get('lang', kwargs.get('func', False))
|
|
409
|
+
cherrypy.request._i18n_tzinfo_func = kwargs.get('tzinfo', False)
|
|
410
|
+
# Clear current translation
|
|
411
|
+
_translation.set(None)
|
|
412
|
+
# Clear current timezone
|
|
413
|
+
_tzinfo.set(None)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _load_accept_language(**kwargs):
|
|
417
|
+
"""
|
|
418
|
+
When running within a request, load the preferred language from Accept-Language header.
|
|
419
|
+
"""
|
|
420
|
+
if cherrypy.request.headers.elements('Accept-Language'):
|
|
421
|
+
# Sort language by quality
|
|
422
|
+
languages = sorted(cherrypy.request.headers.elements('Accept-Language'), key=lambda x: x.qvalue, reverse=True)
|
|
423
|
+
_preferred_lang.set(tuple(lang.value.replace('-', '_') for lang in languages))
|
|
424
|
+
# Clear current translation
|
|
425
|
+
_translation.set(None)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _load_func_language(**kwargs):
|
|
429
|
+
"""
|
|
430
|
+
When running a request where a current user is found, load preferred language from user preferences.
|
|
431
|
+
"""
|
|
432
|
+
func = getattr(cherrypy.request, '_i18n_lang_func', False)
|
|
433
|
+
if not func:
|
|
434
|
+
return
|
|
435
|
+
try:
|
|
436
|
+
lang = func()
|
|
437
|
+
except Exception:
|
|
438
|
+
return
|
|
439
|
+
if not lang:
|
|
440
|
+
return
|
|
441
|
+
# Add custom lang to preferred_lang
|
|
442
|
+
_preferred_lang.set((lang,))
|
|
443
|
+
# Clear current translation
|
|
444
|
+
_translation.set(None)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _load_func_tzinfo(**kwargs):
|
|
448
|
+
"""
|
|
449
|
+
When running a request, load the preferred timezone information from user preferences.
|
|
450
|
+
"""
|
|
451
|
+
func = getattr(cherrypy.request, '_i18n_tzinfo_func', False)
|
|
452
|
+
if not func:
|
|
453
|
+
return
|
|
454
|
+
try:
|
|
455
|
+
tzinfo = func()
|
|
456
|
+
except Exception:
|
|
457
|
+
return
|
|
458
|
+
if not tzinfo:
|
|
459
|
+
return
|
|
460
|
+
# Add custom lang to preferred_lang
|
|
461
|
+
_preferred_timezone.set((tzinfo,))
|
|
462
|
+
# Clear current translation
|
|
463
|
+
_tzinfo.set(None)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _set_content_language(**kwargs):
|
|
467
|
+
"""
|
|
468
|
+
Sets the Content-Language response header (if not already set) to the
|
|
469
|
+
language of `cherrypy.response.i18n.locale`.
|
|
470
|
+
"""
|
|
471
|
+
if 'Content-Language' not in cherrypy.response.headers:
|
|
472
|
+
# Only define the content language if the handler uses i18n module.
|
|
473
|
+
translation = _translation.get()
|
|
474
|
+
if translation:
|
|
475
|
+
locale = translation.locale
|
|
476
|
+
language_tag = f"{locale.language}-{locale.territory}" if locale.territory else locale.language
|
|
477
|
+
cherrypy.response.headers['Content-Language'] = language_tag
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
class I18nTool(cherrypy.Tool):
|
|
481
|
+
"""Tool to integrate babel translations in CherryPy."""
|
|
482
|
+
|
|
483
|
+
def __init__(self):
|
|
484
|
+
super().__init__('before_handler', _load_default, 'i18n')
|
|
485
|
+
|
|
486
|
+
def _setup(self):
|
|
487
|
+
cherrypy.Tool._setup(self)
|
|
488
|
+
# Attach additional hooks as different priority to update preferred lang with more accurate preferences.
|
|
489
|
+
cherrypy.request.hooks.attach('before_handler', _load_accept_language, priority=60)
|
|
490
|
+
cherrypy.request.hooks.attach('before_handler', _load_func_language, priority=75)
|
|
491
|
+
cherrypy.request.hooks.attach('before_handler', _load_func_tzinfo, priority=75)
|
|
492
|
+
cherrypy.request.hooks.attach('before_finalize', _set_content_language)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
cherrypy.tools.i18n = I18nTool()
|
|
@@ -38,7 +38,6 @@ class TestI18n(unittest.TestCase):
|
|
|
38
38
|
def test_search_translation_en(self):
|
|
39
39
|
# Load default translation return translation
|
|
40
40
|
t = i18n._search_translation(self.mo_dir, 'messages', 'en')
|
|
41
|
-
self.assertIsInstance(t, gettext.GNUTranslations)
|
|
42
41
|
self.assertEqual("en", t.locale.language)
|
|
43
42
|
# Test translation object
|
|
44
43
|
self.assertEqual(TEXT_EN, t.gettext(TEXT_EN))
|
|
@@ -54,7 +53,8 @@ class TestI18n(unittest.TestCase):
|
|
|
54
53
|
def test_search_translation_invalid(self):
|
|
55
54
|
# Load invalid translation return None
|
|
56
55
|
t = i18n._search_translation(self.mo_dir, 'messages', 'tr')
|
|
57
|
-
|
|
56
|
+
# Return Null translations.
|
|
57
|
+
self.assertIn('NullTranslations', str(t.__class__))
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
class Root:
|
|
@@ -117,6 +117,13 @@ class TestI18nWebCase(AbstractI18nTest):
|
|
|
117
117
|
self.assertHeaderItemValue("Content-Language", "fr-CA")
|
|
118
118
|
self.assertInBody(TEXT_FR)
|
|
119
119
|
|
|
120
|
+
def test_language_en_US_POSIX(self):
|
|
121
|
+
# When calling with locale variant
|
|
122
|
+
self.getPage("/", headers=[("Accept-Language", "en-US-POSIX")])
|
|
123
|
+
self.assertStatus('200 OK')
|
|
124
|
+
# Tehn page return en-US
|
|
125
|
+
self.assertHeaderItemValue("Content-Language", "en-US")
|
|
126
|
+
|
|
120
127
|
def test_with_preferred_lang(self):
|
|
121
128
|
# Given a default lang 'en'
|
|
122
129
|
date = datetime.fromtimestamp(1680111611, timezone.utc)
|
|
@@ -141,8 +141,6 @@ src/cherrypy_foundation/tools/tests/components/Button.jinja
|
|
|
141
141
|
src/cherrypy_foundation/tools/tests/locales/messages.pot
|
|
142
142
|
src/cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.mo
|
|
143
143
|
src/cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.po
|
|
144
|
-
src/cherrypy_foundation/tools/tests/locales/en/LC_MESSAGES/messages.mo
|
|
145
|
-
src/cherrypy_foundation/tools/tests/locales/en/LC_MESSAGES/messages.po
|
|
146
144
|
src/cherrypy_foundation/tools/tests/locales/fr/LC_MESSAGES/messages.mo
|
|
147
145
|
src/cherrypy_foundation/tools/tests/locales/fr/LC_MESSAGES/messages.po
|
|
148
146
|
src/cherrypy_foundation/tools/tests/templates/test_jinja2.html
|