├── .coveragerc ├── .github ├── ISSUE_TEMPLATE.md └── workflows │ └── ci.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── AUTHORS ├── CHANGES.rst ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── babel ├── __init__.py ├── core.py ├── dates.py ├── languages.py ├── lists.py ├── locale-data │ ├── .gitignore │ └── LICENSE.unicode ├── localedata.py ├── localtime │ ├── __init__.py │ ├── _fallback.py │ ├── _helpers.py │ ├── _unix.py │ └── _win32.py ├── messages │ ├── __init__.py │ ├── _compat.py │ ├── catalog.py │ ├── checkers.py │ ├── extract.py │ ├── frontend.py │ ├── jslexer.py │ ├── mofile.py │ ├── plurals.py │ ├── pofile.py │ └── setuptools_frontend.py ├── numbers.py ├── plural.py ├── py.typed ├── support.py ├── units.py └── util.py ├── cldr └── .gitignore ├── conftest.py ├── contrib └── babel.js ├── docs ├── Makefile ├── _static │ ├── logo.pdf │ ├── logo.png │ └── logo_small.png ├── _templates │ ├── sidebar-about.html │ ├── sidebar-links.html │ └── sidebar-logo.html ├── _themes │ ├── LICENSE │ ├── README │ └── babel │ │ ├── layout.html │ │ ├── relations.html │ │ ├── static │ │ ├── babel.css_t │ │ └── small_babel.css │ │ └── theme.conf ├── api │ ├── core.rst │ ├── dates.rst │ ├── index.rst │ ├── languages.rst │ ├── lists.rst │ ├── messages │ │ ├── catalog.rst │ │ ├── extract.rst │ │ ├── index.rst │ │ ├── mofile.rst │ │ └── pofile.rst │ ├── numbers.rst │ ├── plural.rst │ ├── support.rst │ └── units.rst ├── changelog.rst ├── cmdline.rst ├── conf.py ├── dates.rst ├── dev.rst ├── index.rst ├── installation.rst ├── intro.rst ├── license.rst ├── locale.rst ├── make.bat ├── messages.rst ├── numbers.rst ├── requirements.txt ├── setup.rst └── support.rst ├── misc └── icu4c-tools │ ├── .gitignore │ ├── Makefile │ ├── README.md │ └── icu4c_date_format.cpp ├── pyproject.toml ├── scripts ├── download_import_cldr.py ├── dump_data.py ├── dump_global.py ├── generate_authors.py └── import_cldr.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── conftest.py ├── interop │ ├── __init__.py │ ├── jinja2_data │ │ ├── hello.html │ │ └── mapping.cfg │ └── test_jinja2_interop.py ├── messages │ ├── __init__.py │ ├── consts.py │ ├── data │ │ ├── mapping.cfg │ │ ├── project │ │ │ ├── __init__.py │ │ │ ├── _hidden_by_default │ │ │ │ └── hidden_file.py │ │ │ ├── file1.py │ │ │ ├── file2.py │ │ │ ├── i18n │ │ │ │ ├── de │ │ │ │ │ └── LC_MESSAGES │ │ │ │ │ │ ├── messages.mo │ │ │ │ │ │ └── messages.po │ │ │ │ ├── de_DE │ │ │ │ │ └── LC_MESSAGES │ │ │ │ │ │ ├── bar.po │ │ │ │ │ │ ├── foo.po │ │ │ │ │ │ └── messages.po │ │ │ │ ├── fi_BUGGY │ │ │ │ │ └── LC_MESSAGES │ │ │ │ │ │ └── messages.po │ │ │ │ ├── messages.pot │ │ │ │ ├── messages_non_fuzzy.pot │ │ │ │ └── ru_RU │ │ │ │ │ └── LC_MESSAGES │ │ │ │ │ └── messages.po │ │ │ └── ignored │ │ │ │ ├── a_test_file.txt │ │ │ │ ├── an_example.txt │ │ │ │ └── this_wont_normally_be_here.py │ │ ├── setup.cfg │ │ └── setup.py │ ├── test_catalog.py │ ├── test_checkers.py │ ├── test_extract.py │ ├── test_frontend.py │ ├── test_js_extract.py │ ├── test_jslexer.py │ ├── test_mofile.py │ ├── test_plurals.py │ ├── test_pofile.py │ ├── test_setuptools_frontend.py │ ├── test_toml_config.py │ ├── toml-test-cases │ │ ├── bad.extractor.toml │ │ ├── bad.extractors-not-a-dict.toml │ │ ├── bad.just-a-mapping.toml │ │ ├── bad.mapping-not-a-dict.toml │ │ ├── bad.mappings-not-a-list.toml │ │ ├── bad.missing-extraction-method.toml │ │ ├── bad.multiple-mappings-not-a-list.toml │ │ ├── bad.non-string-extraction-method.toml │ │ ├── bad.pattern-type-2.toml │ │ ├── bad.pattern-type.toml │ │ ├── bad.pyproject-without-tool-babel.toml │ │ └── bad.standalone-with-babel-prefix.toml │ └── utils.py ├── test_core.py ├── test_date_intervals.py ├── test_dates.py ├── test_day_periods.py ├── test_languages.py ├── test_lists.py ├── test_localedata.py ├── test_localtime.py ├── test_numbers.py ├── test_plural.py ├── test_smoke.py ├── test_support_format.py ├── test_support_lazy_proxy.py ├── test_support_translations.py ├── test_units.py └── test_util.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | exclude_lines = 3 | NotImplemented 4 | pragma: no cover 5 | warnings.warn 6 | if TYPE_CHECKING: 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Overview Description 2 | 3 | ## Steps to Reproduce 4 | 5 | 1. 6 | 2. 7 | 3. 8 | 9 | ## Actual Results 10 | 11 | ## Expected Results 12 | 13 | ## Reproducibility 14 | 15 | ## Additional Information 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - '*-maint' 8 | tags: 9 | - 'v*' 10 | pull_request: 11 | branches: 12 | - master 13 | - '*-maint' 14 | 15 | jobs: 16 | lint: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: pre-commit/action@v3.0.1 21 | env: 22 | RUFF_OUTPUT_FORMAT: github 23 | test: 24 | runs-on: ${{ matrix.os }} 25 | strategy: 26 | matrix: 27 | os: 28 | - "ubuntu-24.04" 29 | - "windows-2022" 30 | - "macos-14" 31 | python-version: 32 | - "3.8" 33 | - "3.9" 34 | - "3.10" 35 | - "3.11" 36 | - "3.12" 37 | - "3.13" 38 | - "pypy3.10" 39 | env: 40 | BABEL_CLDR_NO_DOWNLOAD_PROGRESS: "1" 41 | BABEL_CLDR_QUIET: "1" 42 | steps: 43 | - uses: actions/checkout@v4 44 | - uses: actions/cache@v4 45 | with: 46 | path: cldr 47 | key: cldr-${{ hashFiles('scripts/*cldr*') }} 48 | - name: Set up Python ${{ matrix.python-version }} 49 | uses: actions/setup-python@v5 50 | with: 51 | python-version: ${{ matrix.python-version }} 52 | allow-prereleases: true 53 | cache: "pip" 54 | cache-dependency-path: "**/setup.py" 55 | - name: Install dependencies 56 | run: | 57 | python -m pip install --upgrade pip setuptools wheel 58 | python -m pip install 'tox~=4.0' 'tox-gh-actions~=3.0' 59 | - name: Run test via Tox 60 | run: tox --skip-missing-interpreters 61 | env: 62 | COVERAGE_XML_PATH: ${{ runner.temp }} 63 | BABEL_TOX_EXTRA_DEPS: pytest-github-actions-annotate-failures 64 | - uses: codecov/codecov-action@v5 65 | with: 66 | directory: ${{ runner.temp }} 67 | flags: ${{ matrix.os }}-${{ matrix.python-version }} 68 | token: ${{ secrets.CODECOV_TOKEN }} 69 | verbose: true 70 | build: 71 | runs-on: ubuntu-24.04 72 | needs: lint 73 | steps: 74 | - uses: actions/checkout@v4 75 | - uses: actions/setup-python@v5 76 | with: 77 | python-version: "3.13" 78 | cache: "pip" 79 | cache-dependency-path: "**/setup.py" 80 | - run: pip install build -e . 81 | - run: make import-cldr 82 | - run: python -m build 83 | - uses: actions/upload-artifact@v4 84 | with: 85 | name: dist 86 | path: dist 87 | publish: 88 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 89 | needs: 90 | - build 91 | runs-on: ubuntu-latest 92 | environment: 93 | name: release 94 | url: https://pypi.org/p/babel/ 95 | permissions: 96 | id-token: write 97 | steps: 98 | - uses: actions/download-artifact@v4 99 | with: 100 | name: dist 101 | path: dist/ 102 | - name: Publish package distributions to PyPI 103 | uses: pypa/gh-action-pypi-publish@release/v1 104 | with: 105 | verbose: true 106 | print-hash: true 107 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/__pycache__ 2 | *.egg 3 | *.egg-info 4 | *.pyc 5 | *.pyo 6 | *.so 7 | *.swp 8 | *~ 9 | .*cache 10 | .DS_Store 11 | .coverage 12 | .idea 13 | .tox 14 | /venv* 15 | babel/global.dat 16 | babel/global.dat.json 17 | build 18 | dist 19 | docs/_build 20 | test-env 21 | tests/messages/data/project/i18n/en_US 22 | tests/messages/data/project/i18n/fi_BUGGY/LC_MESSAGES/*.mo 23 | tests/messages/data/project/i18n/long_messages.pot 24 | tests/messages/data/project/i18n/temp* 25 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: v0.9.1 4 | hooks: 5 | - id: ruff 6 | args: 7 | - --fix 8 | - repo: https://github.com/pre-commit/pre-commit-hooks 9 | rev: v5.0.0 10 | hooks: 11 | - id: check-added-large-files 12 | - id: check-docstring-first 13 | exclude: (docs/conf.py) 14 | - id: check-json 15 | - id: check-yaml 16 | - id: debug-statements 17 | exclude: (tests/messages/data/) 18 | - id: end-of-file-fixer 19 | exclude: (tests/messages/data/) 20 | - id: name-tests-test 21 | args: [ '--django' ] 22 | exclude: (tests/messages/data/|.*(consts|utils).py) 23 | - id: requirements-txt-fixer 24 | - id: trailing-whitespace 25 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 2 | 3 | version: 2 4 | 5 | build: 6 | os: ubuntu-22.04 7 | tools: 8 | python: "3.11" 9 | jobs: 10 | pre_build: 11 | # Replace any Babel version something may have pulled in 12 | # with the copy we're working on. We'll also need to build 13 | # the data files at that point, or date formatting _within_ 14 | # Sphinx will fail. 15 | - pip install -e . 16 | - make import-cldr 17 | sphinx: 18 | configuration: docs/conf.py 19 | 20 | formats: 21 | - epub 22 | - pdf 23 | 24 | python: 25 | install: 26 | - requirements: docs/requirements.txt 27 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | 2 | Babel is written and maintained by the Babel team and various contributors: 3 | 4 | - Aarni Koskela 5 | - Christopher Lenz 6 | - Armin Ronacher 7 | - Alex Morega 8 | - Lasse Schuirmann 9 | - Felix Schwarz 10 | - Pedro Algarvio 11 | - Jeroen Ruigrok van der Werven 12 | - Philip Jenvey 13 | - benselme 14 | - Isaac Jurado 15 | - Tomas R. 16 | - Tobias Bieniek 17 | - Erick Wilder 18 | - Jonah Lawrence 19 | - Michael Birtwell 20 | - Jonas Borgström 21 | - Kevin Deldycke 22 | - Ville Skyttä 23 | - Jon Dufresne 24 | - Hugo van Kemenade 25 | - Jun Omae 26 | - Heungsub Lee 27 | - Jakob Schnitzer 28 | - Sachin Paliwal 29 | - Alex Willmer 30 | - Daniel Neuhäuser 31 | - Miro Hrončok 32 | - Cédric Krier 33 | - Luke Plant 34 | - Jennifer Wang 35 | - Lukas Balaga 36 | - sudheesh001 37 | - Jean Abou Samra 38 | - Niklas Hambüchen 39 | - Changaco 40 | - Xavier Fernandez 41 | - KO. Mattsson 42 | - Sébastien Diemer 43 | - alexbodn@gmail.com 44 | - saurabhiiit 45 | - srisankethu 46 | - Erik Romijn 47 | - Lukas B 48 | - Ryan J Ollos 49 | - Arturas Moskvinas 50 | - Leonardo Pistone 51 | - Hyunjun Kim 52 | - wandrew004 53 | - James McKinney 54 | - Tomáš Hrnčiar 55 | - Gabe Sherman 56 | - mattdiaz007 57 | - Dylan Kiss 58 | - Daniel Roschka 59 | - buhtz 60 | - Bohdan Malomuzh 61 | - Leonid 62 | - Ronan Amicel 63 | - Christian Clauss 64 | - Best Olunusi 65 | - Teo 66 | - Ivan Koldakov 67 | - Rico Hermans 68 | - Daniel 69 | - Oleh Prypin 70 | - Petr Viktorin 71 | - Jean Abou-Samra 72 | - Joe Portela 73 | - Marc-Etienne Vargenau 74 | - Michał Górny 75 | - Alex Waygood 76 | - Maciej Olko 77 | - martin f. krafft 78 | - DS/Charlie 79 | - lilinjie 80 | - Johannes Wilm 81 | - Eric L 82 | - Przemyslaw Wegrzyn 83 | - Lukas Kahwe Smith 84 | - Lukas Juhrich 85 | - Nikita Sobolev 86 | - Raphael Nestler 87 | - Frank Harrison 88 | - Nehal J Wani 89 | - Mohamed Morsy 90 | - Krzysztof Jagiełło 91 | - Morgan Wahl 92 | - farhan5900 93 | - Sigurd Ljødal 94 | - Andrii Oriekhov 95 | - rachele-collin 96 | - Lukas Winkler 97 | - Juliette Monsel 98 | - Álvaro Mondéjar Rubio 99 | - ruro 100 | - Alessio Bogon 101 | - Nikiforov Konstantin 102 | - Abdullah Javed Nesar 103 | - Brad Martin 104 | - Tyler Kennedy 105 | - CyanNani123 106 | - sebleblanc 107 | - He Chen 108 | - Steve (Gadget) Barnes 109 | - Romuald Brunet 110 | - Mario Frasca 111 | - BT-sschmid 112 | - Alberto Mardegan 113 | - mondeja 114 | - NotAFile 115 | - Julien Palard 116 | - Brian Cappello 117 | - Serban Constantin 118 | - Bryn Truscott 119 | - Chris 120 | - Charly C 121 | - PTrottier 122 | - xmo-odoo 123 | - StevenJ 124 | - Jungmo Ku 125 | - Simeon Visser 126 | - Narendra Vardi 127 | - Stefane Fermigier 128 | - Narayan Acharya 129 | - François Magimel 130 | - Wolfgang Doll 131 | - Roy Williams 132 | - Marc-André Dufresne 133 | - Abhishek Tiwari 134 | - David Baumgold 135 | - Alex Kuzmenko 136 | - Georg Schölly 137 | - ldwoolley 138 | - Rodrigo Ramírez Norambuena 139 | - Jakub Wilk 140 | - Roman Rader 141 | - Max Shenfield 142 | - Nicolas Grilly 143 | - Kenny Root 144 | - Adam Chainz 145 | - Sébastien Fievet 146 | - Anthony Sottile 147 | - Yuriy Shatrov 148 | - iamshubh22 149 | - Sven Anderson 150 | - Eoin Nugent 151 | - Roman Imankulov 152 | - David Stanek 153 | - Roy Wellington Ⅳ 154 | - Florian Schulze 155 | - Todd M. Guerra 156 | - Joseph Breihan 157 | - Craig Loftus 158 | - The Gitter Badger 159 | - Régis Behmo 160 | - Julen Ruiz Aizpuru 161 | - astaric 162 | - Felix Yan 163 | - Philip_Tzou 164 | - Jesús Espino 165 | - Jeremy Weinstein 166 | - James Page 167 | - masklinn 168 | - Sjoerd Langkemper 169 | - Matt Iversen 170 | - Alexander A. Dyshev 171 | - Dirkjan Ochtman 172 | - Nick Retallack 173 | - Thomas Waldmann 174 | - xen 175 | 176 | Babel was previously developed under the Copyright of Edgewall Software. The 177 | following copyright notice holds true for releases before 2013: "Copyright (c) 178 | 2007 - 2011 by Edgewall Software" 179 | 180 | In addition to the regular contributions Babel includes a fork of Lennart 181 | Regebro's tzlocal that originally was licensed under the CC0 license. The 182 | original copyright of that project is "Copyright 2013 by Lennart Regebro". 183 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Babel Contribution Guidelines 2 | 3 | Welcome to Babel! These guidelines will give you a short overview over how we 4 | handle issues and PRs in this repository. Note that they are preliminary and 5 | still need proper phrasing - if you'd like to help - be sure to make a PR. 6 | 7 | Please know that we do appreciate all contributions - bug reports as well as 8 | Pull Requests. 9 | 10 | ## Setting up a development environment and running tests 11 | 12 | After you've cloned the repository, 13 | 14 | 1. Set up a Python virtualenv (the methods vary depending on tooling and operating system) 15 | and activate it. 16 | 2. Install Babel in editable mode with development dependencies: `pip install -e .[dev]` 17 | 3. Run `make import-cldr` to import the CLDR database. 18 | This will download the CLDR database and convert it to a format that Babel can use. 19 | 4. Run `make test` to run the tests. You can also run e.g. `pytest --cov babel .` to 20 | run the tests with coverage reporting enabled. 21 | 22 | You can also use [Tox][tox] to run the tests in separate virtualenvs 23 | for all supported Python versions; a `tox.ini` configuration (which is what the CI process 24 | uses) is included in the repository. 25 | 26 | ## On pull requests 27 | 28 | ### PR Merge Criteria 29 | 30 | For a PR to be merged, the following statements must hold true: 31 | 32 | - All CI services pass. (Windows build, linux build, sufficient test coverage.) 33 | - All commits must have been reviewed and approved by a babel maintainer who is 34 | not the author of the PR. Commits shall comply to the "Good Commits" standards 35 | outlined below. 36 | 37 | To begin contributing have a look at the open [easy issues](https://github.com/python-babel/babel/issues?q=is%3Aopen+is%3Aissue+label%3Adifficulty%2Flow) 38 | which could be fixed. 39 | 40 | ### Correcting PRs 41 | 42 | Rebasing PRs is preferred over merging master into the source branches again 43 | and again cluttering our history. If a reviewer has suggestions, the commit 44 | shall be amended so the history is not cluttered by "fixup commits". 45 | 46 | ### Writing Good Commits 47 | 48 | Please see 49 | https://api.coala.io/en/latest/Developers/Writing_Good_Commits.html 50 | for guidelines on how to write good commits and proper commit messages. 51 | 52 | [tox]: https://tox.wiki/en/latest/ 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2025 by the Babel Team, see AUTHORS for more information. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in 11 | the documentation and/or other materials provided with the 12 | distribution. 13 | 3. Neither the name of the copyright holder nor the names of its 14 | contributors may be used to endorse or promote products derived 15 | from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include Makefile CHANGES.rst LICENSE AUTHORS 2 | include conftest.py tox.ini 3 | include babel/global.dat 4 | include babel/locale-data/*.dat 5 | include babel/locale-data/LICENSE* 6 | recursive-include docs * 7 | recursive-exclude docs/_build * 8 | include scripts/* 9 | recursive-include tests * 10 | recursive-exclude tests *.pyc 11 | recursive-exclude tests *.pyo 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: import-cldr 2 | python ${PYTHON_TEST_FLAGS} -m pytest ${PYTEST_FLAGS} 3 | 4 | clean: clean-cldr clean-pyc 5 | 6 | import-cldr: 7 | python scripts/download_import_cldr.py 8 | 9 | clean-cldr: 10 | rm -f babel/locale-data/*.dat 11 | rm -f babel/global.dat 12 | 13 | clean-pyc: 14 | find . -name '*.pyc' -exec rm {} \; 15 | find . -name '__pycache__' -type d | xargs rm -rf 16 | 17 | develop: 18 | pip install --editable . 19 | 20 | tox-test: 21 | tox 22 | 23 | .PHONY: test develop tox-test clean-pyc clean-cldr import-cldr clean standalone-test 24 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | About Babel 2 | =========== 3 | 4 | Babel is a Python library that provides an integrated collection of 5 | utilities that assist with internationalizing and localizing Python 6 | applications (in particular web-based applications.) 7 | 8 | Details can be found in the HTML files in the ``docs`` folder. 9 | 10 | For more information please visit the Babel web site: 11 | 12 | https://babel.pocoo.org/ 13 | 14 | Join the chat at https://gitter.im/python-babel/babel 15 | 16 | Contributing to Babel 17 | ===================== 18 | 19 | If you want to contribute code to Babel, please take a look at our 20 | `CONTRIBUTING.md `__. 21 | 22 | If you know your way around Babels codebase a bit and like to help 23 | further, we would appreciate any help in reviewing pull requests. Please 24 | contact us at https://gitter.im/python-babel/babel if you're interested! 25 | -------------------------------------------------------------------------------- /babel/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | babel 3 | ~~~~~ 4 | 5 | Integrated collection of utilities that assist in internationalizing and 6 | localizing applications. 7 | 8 | This package is basically composed of two major parts: 9 | 10 | * tools to build and work with ``gettext`` message catalogs 11 | * a Python interface to the CLDR (Common Locale Data Repository), providing 12 | access to various locale display names, localized number and date 13 | formatting, etc. 14 | 15 | :copyright: (c) 2013-2025 by the Babel Team. 16 | :license: BSD, see LICENSE for more details. 17 | """ 18 | 19 | from babel.core import ( 20 | Locale, 21 | UnknownLocaleError, 22 | default_locale, 23 | get_locale_identifier, 24 | negotiate_locale, 25 | parse_locale, 26 | ) 27 | 28 | __version__ = '2.17.0' 29 | 30 | __all__ = [ 31 | 'Locale', 32 | 'UnknownLocaleError', 33 | '__version__', 34 | 'default_locale', 35 | 'get_locale_identifier', 36 | 'negotiate_locale', 37 | 'parse_locale', 38 | ] 39 | -------------------------------------------------------------------------------- /babel/languages.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from babel.core import get_global 4 | 5 | 6 | def get_official_languages(territory: str, regional: bool = False, de_facto: bool = False) -> tuple[str, ...]: 7 | """ 8 | Get the official language(s) for the given territory. 9 | 10 | The language codes, if any are known, are returned in order of descending popularity. 11 | 12 | If the `regional` flag is set, then languages which are regionally official are also returned. 13 | 14 | If the `de_facto` flag is set, then languages which are "de facto" official are also returned. 15 | 16 | .. warning:: Note that the data is as up to date as the current version of the CLDR used 17 | by Babel. If you need scientifically accurate information, use another source! 18 | 19 | :param territory: Territory code 20 | :type territory: str 21 | :param regional: Whether to return regionally official languages too 22 | :type regional: bool 23 | :param de_facto: Whether to return de-facto official languages too 24 | :type de_facto: bool 25 | :return: Tuple of language codes 26 | :rtype: tuple[str] 27 | """ 28 | 29 | territory = str(territory).upper() 30 | allowed_stati = {"official"} 31 | if regional: 32 | allowed_stati.add("official_regional") 33 | if de_facto: 34 | allowed_stati.add("de_facto_official") 35 | 36 | languages = get_global("territory_languages").get(territory, {}) 37 | pairs = [ 38 | (info['population_percent'], language) 39 | for language, info in languages.items() 40 | if info.get('official_status') in allowed_stati 41 | ] 42 | pairs.sort(reverse=True) 43 | return tuple(lang for _, lang in pairs) 44 | 45 | 46 | def get_territory_language_info(territory: str) -> dict[str, dict[str, float | str | None]]: 47 | """ 48 | Get a dictionary of language information for a territory. 49 | 50 | The dictionary is keyed by language code; the values are dicts with more information. 51 | 52 | The following keys are currently known for the values: 53 | 54 | * `population_percent`: The percentage of the territory's population speaking the 55 | language. 56 | * `official_status`: An optional string describing the officiality status of the language. 57 | Known values are "official", "official_regional" and "de_facto_official". 58 | 59 | .. warning:: Note that the data is as up to date as the current version of the CLDR used 60 | by Babel. If you need scientifically accurate information, use another source! 61 | 62 | .. note:: Note that the format of the dict returned may change between Babel versions. 63 | 64 | See https://www.unicode.org/cldr/charts/latest/supplemental/territory_language_information.html 65 | 66 | :param territory: Territory code 67 | :type territory: str 68 | :return: Language information dictionary 69 | :rtype: dict[str, dict] 70 | """ 71 | territory = str(territory).upper() 72 | return get_global("territory_languages").get(territory, {}).copy() 73 | -------------------------------------------------------------------------------- /babel/lists.py: -------------------------------------------------------------------------------- 1 | """ 2 | babel.lists 3 | ~~~~~~~~~~~ 4 | 5 | Locale dependent formatting of lists. 6 | 7 | The default locale for the functions in this module is determined by the 8 | following environment variables, in that order: 9 | 10 | * ``LC_ALL``, and 11 | * ``LANG`` 12 | 13 | :copyright: (c) 2015-2025 by the Babel Team. 14 | :license: BSD, see LICENSE for more details. 15 | """ 16 | from __future__ import annotations 17 | 18 | import warnings 19 | from collections.abc import Sequence 20 | from typing import Literal 21 | 22 | from babel.core import Locale, default_locale 23 | 24 | _DEFAULT_LOCALE = default_locale() # TODO(3.0): Remove this. 25 | 26 | 27 | def __getattr__(name): 28 | if name == "DEFAULT_LOCALE": 29 | warnings.warn( 30 | "The babel.lists.DEFAULT_LOCALE constant is deprecated and will be removed.", 31 | DeprecationWarning, 32 | stacklevel=2, 33 | ) 34 | return _DEFAULT_LOCALE 35 | raise AttributeError(f"module {__name__!r} has no attribute {name!r}") 36 | 37 | 38 | def format_list( 39 | lst: Sequence[str], 40 | style: Literal['standard', 'standard-short', 'or', 'or-short', 'unit', 'unit-short', 'unit-narrow'] = 'standard', 41 | locale: Locale | str | None = None, 42 | ) -> str: 43 | """ 44 | Format the items in `lst` as a list. 45 | 46 | >>> format_list(['apples', 'oranges', 'pears'], locale='en') 47 | u'apples, oranges, and pears' 48 | >>> format_list(['apples', 'oranges', 'pears'], locale='zh') 49 | u'apples\u3001oranges\u548cpears' 50 | >>> format_list(['omena', 'peruna', 'aplari'], style='or', locale='fi') 51 | u'omena, peruna tai aplari' 52 | 53 | Not all styles are necessarily available in all locales. 54 | The function will attempt to fall back to replacement styles according to the rules 55 | set forth in the CLDR root XML file, and raise a ValueError if no suitable replacement 56 | can be found. 57 | 58 | The following text is verbatim from the Unicode TR35-49 spec [1]. 59 | 60 | * standard: 61 | A typical 'and' list for arbitrary placeholders. 62 | eg. "January, February, and March" 63 | * standard-short: 64 | A short version of an 'and' list, suitable for use with short or abbreviated placeholder values. 65 | eg. "Jan., Feb., and Mar." 66 | * or: 67 | A typical 'or' list for arbitrary placeholders. 68 | eg. "January, February, or March" 69 | * or-short: 70 | A short version of an 'or' list. 71 | eg. "Jan., Feb., or Mar." 72 | * unit: 73 | A list suitable for wide units. 74 | eg. "3 feet, 7 inches" 75 | * unit-short: 76 | A list suitable for short units 77 | eg. "3 ft, 7 in" 78 | * unit-narrow: 79 | A list suitable for narrow units, where space on the screen is very limited. 80 | eg. "3′ 7″" 81 | 82 | [1]: https://www.unicode.org/reports/tr35/tr35-49/tr35-general.html#ListPatterns 83 | 84 | :param lst: a sequence of items to format in to a list 85 | :param style: the style to format the list with. See above for description. 86 | :param locale: the locale. Defaults to the system locale. 87 | """ 88 | locale = Locale.parse(locale or _DEFAULT_LOCALE) 89 | if not lst: 90 | return '' 91 | if len(lst) == 1: 92 | return lst[0] 93 | 94 | patterns = _resolve_list_style(locale, style) 95 | 96 | if len(lst) == 2 and '2' in patterns: 97 | return patterns['2'].format(*lst) 98 | 99 | result = patterns['start'].format(lst[0], lst[1]) 100 | for elem in lst[2:-1]: 101 | result = patterns['middle'].format(result, elem) 102 | result = patterns['end'].format(result, lst[-1]) 103 | 104 | return result 105 | 106 | 107 | # Based on CLDR 45's root.xml file's ``es. 108 | # The root file defines both `standard` and `or`, 109 | # so they're always available. 110 | # TODO: It would likely be better to use the 111 | # babel.localedata.Alias mechanism for this, 112 | # but I'm not quite sure how it's supposed to 113 | # work with inheritance and data in the root. 114 | _style_fallbacks = { 115 | "or-narrow": ["or-short", "or"], 116 | "or-short": ["or"], 117 | "standard-narrow": ["standard-short", "standard"], 118 | "standard-short": ["standard"], 119 | "unit": ["unit-short", "standard"], 120 | "unit-narrow": ["unit-short", "unit", "standard"], 121 | "unit-short": ["standard"], 122 | } 123 | 124 | 125 | def _resolve_list_style(locale: Locale, style: str): 126 | for style in (style, *(_style_fallbacks.get(style, []))): # noqa: B020 127 | if style in locale.list_patterns: 128 | return locale.list_patterns[style] 129 | raise ValueError( 130 | f"Locale {locale} does not support list formatting style {style!r} " 131 | f"(supported are {sorted(locale.list_patterns)})", 132 | ) 133 | -------------------------------------------------------------------------------- /babel/locale-data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | -------------------------------------------------------------------------------- /babel/locale-data/LICENSE.unicode: -------------------------------------------------------------------------------- 1 | UNICODE LICENSE V3 2 | 3 | COPYRIGHT AND PERMISSION NOTICE 4 | 5 | Copyright © 2004-2025 Unicode, Inc. 6 | 7 | NOTICE TO USER: Carefully read the following legal agreement. BY 8 | DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR 9 | SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE 10 | TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT 11 | DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a 14 | copy of data files and any associated documentation (the "Data Files") or 15 | software and any associated documentation (the "Software") to deal in the 16 | Data Files or Software without restriction, including without limitation 17 | the rights to use, copy, modify, merge, publish, distribute, and/or sell 18 | copies of the Data Files or Software, and to permit persons to whom the 19 | Data Files or Software are furnished to do so, provided that either (a) 20 | this copyright and permission notice appear with all copies of the Data 21 | Files or Software, or (b) this copyright and permission notice appear in 22 | associated Documentation. 23 | 24 | THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY 25 | KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 26 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF 27 | THIRD PARTY RIGHTS. 28 | 29 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE 30 | BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, 31 | OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, 32 | WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, 33 | ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA 34 | FILES OR SOFTWARE. 35 | 36 | Except as contained in this notice, the name of a copyright holder shall 37 | not be used in advertising or otherwise to promote the sale, use or other 38 | dealings in these Data Files or Software without prior written 39 | authorization of the copyright holder. 40 | 41 | SPDX-License-Identifier: Unicode-3.0 42 | -------------------------------------------------------------------------------- /babel/localtime/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | babel.localtime 3 | ~~~~~~~~~~~~~~~ 4 | 5 | Babel specific fork of tzlocal to determine the local timezone 6 | of the system. 7 | 8 | :copyright: (c) 2013-2025 by the Babel Team. 9 | :license: BSD, see LICENSE for more details. 10 | """ 11 | 12 | import datetime 13 | import sys 14 | 15 | if sys.platform == 'win32': 16 | from babel.localtime._win32 import _get_localzone 17 | else: 18 | from babel.localtime._unix import _get_localzone 19 | 20 | 21 | # TODO(3.0): the offset constants are not part of the public API 22 | # and should be removed 23 | from babel.localtime._fallback import ( 24 | DSTDIFF, # noqa: F401 25 | DSTOFFSET, # noqa: F401 26 | STDOFFSET, # noqa: F401 27 | ZERO, # noqa: F401 28 | _FallbackLocalTimezone, 29 | ) 30 | 31 | 32 | def get_localzone() -> datetime.tzinfo: 33 | """Returns the current underlying local timezone object. 34 | Generally this function does not need to be used, it's a 35 | better idea to use the :data:`LOCALTZ` singleton instead. 36 | """ 37 | return _get_localzone() 38 | 39 | 40 | try: 41 | LOCALTZ = get_localzone() 42 | except LookupError: 43 | LOCALTZ = _FallbackLocalTimezone() 44 | -------------------------------------------------------------------------------- /babel/localtime/_fallback.py: -------------------------------------------------------------------------------- 1 | """ 2 | babel.localtime._fallback 3 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 4 | 5 | Emulated fallback local timezone when all else fails. 6 | 7 | :copyright: (c) 2013-2025 by the Babel Team. 8 | :license: BSD, see LICENSE for more details. 9 | """ 10 | 11 | import datetime 12 | import time 13 | 14 | STDOFFSET = datetime.timedelta(seconds=-time.timezone) 15 | DSTOFFSET = datetime.timedelta(seconds=-time.altzone) if time.daylight else STDOFFSET 16 | 17 | DSTDIFF = DSTOFFSET - STDOFFSET 18 | ZERO = datetime.timedelta(0) 19 | 20 | 21 | class _FallbackLocalTimezone(datetime.tzinfo): 22 | 23 | def utcoffset(self, dt: datetime.datetime) -> datetime.timedelta: 24 | if self._isdst(dt): 25 | return DSTOFFSET 26 | else: 27 | return STDOFFSET 28 | 29 | def dst(self, dt: datetime.datetime) -> datetime.timedelta: 30 | if self._isdst(dt): 31 | return DSTDIFF 32 | else: 33 | return ZERO 34 | 35 | def tzname(self, dt: datetime.datetime) -> str: 36 | return time.tzname[self._isdst(dt)] 37 | 38 | def _isdst(self, dt: datetime.datetime) -> bool: 39 | tt = (dt.year, dt.month, dt.day, 40 | dt.hour, dt.minute, dt.second, 41 | dt.weekday(), 0, -1) 42 | stamp = time.mktime(tt) 43 | tt = time.localtime(stamp) 44 | return tt.tm_isdst > 0 45 | -------------------------------------------------------------------------------- /babel/localtime/_helpers.py: -------------------------------------------------------------------------------- 1 | try: 2 | import pytz 3 | except ModuleNotFoundError: 4 | pytz = None 5 | 6 | try: 7 | import zoneinfo 8 | except ModuleNotFoundError: 9 | zoneinfo = None 10 | 11 | 12 | def _get_tzinfo(tzenv: str): 13 | """Get the tzinfo from `zoneinfo` or `pytz` 14 | 15 | :param tzenv: timezone in the form of Continent/City 16 | :return: tzinfo object or None if not found 17 | """ 18 | if pytz: 19 | try: 20 | return pytz.timezone(tzenv) 21 | except pytz.UnknownTimeZoneError: 22 | pass 23 | else: 24 | try: 25 | return zoneinfo.ZoneInfo(tzenv) 26 | except ValueError as ve: 27 | # This is somewhat hacky, but since _validate_tzfile_path() doesn't 28 | # raise a specific error type, we'll need to check the message to be 29 | # one we know to be from that function. 30 | # If so, we pretend it meant that the TZ didn't exist, for the benefit 31 | # of `babel.localtime` catching the `LookupError` raised by 32 | # `_get_tzinfo_or_raise()`. 33 | # See https://github.com/python-babel/babel/issues/1092 34 | if str(ve).startswith("ZoneInfo keys "): 35 | return None 36 | except zoneinfo.ZoneInfoNotFoundError: 37 | pass 38 | 39 | return None 40 | 41 | 42 | def _get_tzinfo_or_raise(tzenv: str): 43 | tzinfo = _get_tzinfo(tzenv) 44 | if tzinfo is None: 45 | raise LookupError( 46 | f"Can not find timezone {tzenv}. \n" 47 | "Timezone names are generally in the form `Continent/City`.", 48 | ) 49 | return tzinfo 50 | 51 | 52 | def _get_tzinfo_from_file(tzfilename: str): 53 | with open(tzfilename, 'rb') as tzfile: 54 | if pytz: 55 | return pytz.tzfile.build_tzinfo('local', tzfile) 56 | else: 57 | return zoneinfo.ZoneInfo.from_file(tzfile) 58 | -------------------------------------------------------------------------------- /babel/localtime/_unix.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import re 4 | 5 | from babel.localtime._helpers import ( 6 | _get_tzinfo, 7 | _get_tzinfo_from_file, 8 | _get_tzinfo_or_raise, 9 | ) 10 | 11 | 12 | def _tz_from_env(tzenv: str) -> datetime.tzinfo: 13 | if tzenv[0] == ':': 14 | tzenv = tzenv[1:] 15 | 16 | # TZ specifies a file 17 | if os.path.exists(tzenv): 18 | return _get_tzinfo_from_file(tzenv) 19 | 20 | # TZ specifies a zoneinfo zone. 21 | return _get_tzinfo_or_raise(tzenv) 22 | 23 | 24 | def _get_localzone(_root: str = '/') -> datetime.tzinfo: 25 | """Tries to find the local timezone configuration. 26 | This method prefers finding the timezone name and passing that to 27 | zoneinfo or pytz, over passing in the localtime file, as in the later 28 | case the zoneinfo name is unknown. 29 | The parameter _root makes the function look for files like /etc/localtime 30 | beneath the _root directory. This is primarily used by the tests. 31 | In normal usage you call the function without parameters. 32 | """ 33 | 34 | tzenv = os.environ.get('TZ') 35 | if tzenv: 36 | return _tz_from_env(tzenv) 37 | 38 | # This is actually a pretty reliable way to test for the local time 39 | # zone on operating systems like OS X. On OS X especially this is the 40 | # only one that actually works. 41 | try: 42 | link_dst = os.readlink('/etc/localtime') 43 | except OSError: 44 | pass 45 | else: 46 | pos = link_dst.find('/zoneinfo/') 47 | if pos >= 0: 48 | # On occasion, the `/etc/localtime` symlink has a double slash, e.g. 49 | # "/usr/share/zoneinfo//UTC", which would make `zoneinfo.ZoneInfo` 50 | # complain (no absolute paths allowed), and we'd end up returning 51 | # `None` (as a fix for #1092). 52 | # Instead, let's just "fix" the double slash symlink by stripping 53 | # leading slashes before passing the assumed zone name forward. 54 | zone_name = link_dst[pos + 10:].lstrip("/") 55 | tzinfo = _get_tzinfo(zone_name) 56 | if tzinfo is not None: 57 | return tzinfo 58 | 59 | # Now look for distribution specific configuration files 60 | # that contain the timezone name. 61 | tzpath = os.path.join(_root, 'etc/timezone') 62 | if os.path.exists(tzpath): 63 | with open(tzpath, 'rb') as tzfile: 64 | data = tzfile.read() 65 | 66 | # Issue #3 in tzlocal was that /etc/timezone was a zoneinfo file. 67 | # That's a misconfiguration, but we need to handle it gracefully: 68 | if data[:5] != b'TZif2': 69 | etctz = data.strip().decode() 70 | # Get rid of host definitions and comments: 71 | if ' ' in etctz: 72 | etctz, dummy = etctz.split(' ', 1) 73 | if '#' in etctz: 74 | etctz, dummy = etctz.split('#', 1) 75 | 76 | return _get_tzinfo_or_raise(etctz.replace(' ', '_')) 77 | 78 | # CentOS has a ZONE setting in /etc/sysconfig/clock, 79 | # OpenSUSE has a TIMEZONE setting in /etc/sysconfig/clock and 80 | # Gentoo has a TIMEZONE setting in /etc/conf.d/clock 81 | # We look through these files for a timezone: 82 | timezone_re = re.compile(r'\s*(TIME)?ZONE\s*=\s*"(?P.+)"') 83 | 84 | for filename in ('etc/sysconfig/clock', 'etc/conf.d/clock'): 85 | tzpath = os.path.join(_root, filename) 86 | if not os.path.exists(tzpath): 87 | continue 88 | with open(tzpath) as tzfile: 89 | for line in tzfile: 90 | match = timezone_re.match(line) 91 | if match is not None: 92 | # We found a timezone 93 | etctz = match.group("etctz") 94 | return _get_tzinfo_or_raise(etctz.replace(' ', '_')) 95 | 96 | # No explicit setting existed. Use localtime 97 | for filename in ('etc/localtime', 'usr/local/etc/localtime'): 98 | tzpath = os.path.join(_root, filename) 99 | 100 | if not os.path.exists(tzpath): 101 | continue 102 | return _get_tzinfo_from_file(tzpath) 103 | 104 | raise LookupError('Can not find any timezone configuration') 105 | -------------------------------------------------------------------------------- /babel/localtime/_win32.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | try: 4 | import winreg 5 | except ImportError: 6 | winreg = None 7 | 8 | import datetime 9 | from typing import Any, Dict, cast 10 | 11 | from babel.core import get_global 12 | from babel.localtime._helpers import _get_tzinfo_or_raise 13 | 14 | # When building the cldr data on windows this module gets imported. 15 | # Because at that point there is no global.dat yet this call will 16 | # fail. We want to catch it down in that case then and just assume 17 | # the mapping was empty. 18 | try: 19 | tz_names: dict[str, str] = cast(Dict[str, str], get_global('windows_zone_mapping')) 20 | except RuntimeError: 21 | tz_names = {} 22 | 23 | 24 | def valuestodict(key) -> dict[str, Any]: 25 | """Convert a registry key's values to a dictionary.""" 26 | dict = {} 27 | size = winreg.QueryInfoKey(key)[1] 28 | for i in range(size): 29 | data = winreg.EnumValue(key, i) 30 | dict[data[0]] = data[1] 31 | return dict 32 | 33 | 34 | def get_localzone_name() -> str: 35 | # Windows is special. It has unique time zone names (in several 36 | # meanings of the word) available, but unfortunately, they can be 37 | # translated to the language of the operating system, so we need to 38 | # do a backwards lookup, by going through all time zones and see which 39 | # one matches. 40 | handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) 41 | 42 | TZLOCALKEYNAME = r'SYSTEM\CurrentControlSet\Control\TimeZoneInformation' 43 | localtz = winreg.OpenKey(handle, TZLOCALKEYNAME) 44 | keyvalues = valuestodict(localtz) 45 | localtz.Close() 46 | if 'TimeZoneKeyName' in keyvalues: 47 | # Windows 7 (and Vista?) 48 | 49 | # For some reason this returns a string with loads of NUL bytes at 50 | # least on some systems. I don't know if this is a bug somewhere, I 51 | # just work around it. 52 | tzkeyname = keyvalues['TimeZoneKeyName'].split('\x00', 1)[0] 53 | else: 54 | # Windows 2000 or XP 55 | 56 | # This is the localized name: 57 | tzwin = keyvalues['StandardName'] 58 | 59 | # Open the list of timezones to look up the real name: 60 | TZKEYNAME = r'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones' 61 | tzkey = winreg.OpenKey(handle, TZKEYNAME) 62 | 63 | # Now, match this value to Time Zone information 64 | tzkeyname = None 65 | for i in range(winreg.QueryInfoKey(tzkey)[0]): 66 | subkey = winreg.EnumKey(tzkey, i) 67 | sub = winreg.OpenKey(tzkey, subkey) 68 | data = valuestodict(sub) 69 | sub.Close() 70 | if data.get('Std', None) == tzwin: 71 | tzkeyname = subkey 72 | break 73 | 74 | tzkey.Close() 75 | handle.Close() 76 | 77 | if tzkeyname is None: 78 | raise LookupError('Can not find Windows timezone configuration') 79 | 80 | timezone = tz_names.get(tzkeyname) 81 | if timezone is None: 82 | # Nope, that didn't work. Try adding 'Standard Time', 83 | # it seems to work a lot of times: 84 | timezone = tz_names.get(f"{tzkeyname} Standard Time") 85 | 86 | # Return what we have. 87 | if timezone is None: 88 | raise LookupError(f"Can not find timezone {tzkeyname}") 89 | 90 | return timezone 91 | 92 | 93 | def _get_localzone() -> datetime.tzinfo: 94 | if winreg is None: 95 | raise LookupError( 96 | 'Runtime support not available') 97 | 98 | return _get_tzinfo_or_raise(get_localzone_name()) 99 | -------------------------------------------------------------------------------- /babel/messages/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | babel.messages 3 | ~~~~~~~~~~~~~~ 4 | 5 | Support for ``gettext`` message catalogs. 6 | 7 | :copyright: (c) 2013-2025 by the Babel Team. 8 | :license: BSD, see LICENSE for more details. 9 | """ 10 | 11 | from babel.messages.catalog import ( 12 | Catalog, 13 | Message, 14 | TranslationError, 15 | ) 16 | 17 | __all__ = [ 18 | "Catalog", 19 | "Message", 20 | "TranslationError", 21 | ] 22 | -------------------------------------------------------------------------------- /babel/messages/_compat.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from functools import partial 3 | 4 | 5 | def find_entrypoints(group_name: str): 6 | """ 7 | Find entrypoints of a given group using either `importlib.metadata` or the 8 | older `pkg_resources` mechanism. 9 | 10 | Yields tuples of the entrypoint name and a callable function that will 11 | load the actual entrypoint. 12 | """ 13 | if sys.version_info >= (3, 10): 14 | # "Changed in version 3.10: importlib.metadata is no longer provisional." 15 | try: 16 | from importlib.metadata import entry_points 17 | except ImportError: 18 | pass 19 | else: 20 | eps = entry_points(group=group_name) 21 | # Only do this if this implementation of `importlib.metadata` is 22 | # modern enough to not return a dict. 23 | if not isinstance(eps, dict): 24 | for entry_point in eps: 25 | yield (entry_point.name, entry_point.load) 26 | return 27 | 28 | try: 29 | from pkg_resources import working_set 30 | except ImportError: 31 | pass 32 | else: 33 | for entry_point in working_set.iter_entry_points(group_name): 34 | yield (entry_point.name, partial(entry_point.load, require=True)) 35 | -------------------------------------------------------------------------------- /babel/messages/checkers.py: -------------------------------------------------------------------------------- 1 | """ 2 | babel.messages.checkers 3 | ~~~~~~~~~~~~~~~~~~~~~~~ 4 | 5 | Various routines that help with validation of translations. 6 | 7 | :since: version 0.9 8 | 9 | :copyright: (c) 2013-2025 by the Babel Team. 10 | :license: BSD, see LICENSE for more details. 11 | """ 12 | from __future__ import annotations 13 | 14 | from collections.abc import Callable 15 | 16 | from babel.messages.catalog import PYTHON_FORMAT, Catalog, Message, TranslationError 17 | 18 | #: list of format chars that are compatible to each other 19 | _string_format_compatibilities = [ 20 | {'i', 'd', 'u'}, 21 | {'x', 'X'}, 22 | {'f', 'F', 'g', 'G'}, 23 | ] 24 | 25 | 26 | def num_plurals(catalog: Catalog | None, message: Message) -> None: 27 | """Verify the number of plurals in the translation.""" 28 | if not message.pluralizable: 29 | if not isinstance(message.string, str): 30 | raise TranslationError("Found plural forms for non-pluralizable " 31 | "message") 32 | return 33 | 34 | # skip further tests if no catalog is provided. 35 | elif catalog is None: 36 | return 37 | 38 | msgstrs = message.string 39 | if not isinstance(msgstrs, (list, tuple)): 40 | msgstrs = (msgstrs,) 41 | if len(msgstrs) != catalog.num_plurals: 42 | raise TranslationError("Wrong number of plural forms (expected %d)" % 43 | catalog.num_plurals) 44 | 45 | 46 | def python_format(catalog: Catalog | None, message: Message) -> None: 47 | """Verify the format string placeholders in the translation.""" 48 | if 'python-format' not in message.flags: 49 | return 50 | msgids = message.id 51 | if not isinstance(msgids, (list, tuple)): 52 | msgids = (msgids,) 53 | msgstrs = message.string 54 | if not isinstance(msgstrs, (list, tuple)): 55 | msgstrs = (msgstrs,) 56 | 57 | if msgstrs[0]: 58 | _validate_format(msgids[0], msgstrs[0]) 59 | if message.pluralizable: 60 | for msgstr in msgstrs[1:]: 61 | if msgstr: 62 | _validate_format(msgids[1], msgstr) 63 | 64 | 65 | def _validate_format(format: str, alternative: str) -> None: 66 | """Test format string `alternative` against `format`. `format` can be the 67 | msgid of a message and `alternative` one of the `msgstr`\\s. The two 68 | arguments are not interchangeable as `alternative` may contain less 69 | placeholders if `format` uses named placeholders. 70 | 71 | If the string formatting of `alternative` is compatible to `format` the 72 | function returns `None`, otherwise a `TranslationError` is raised. 73 | 74 | Examples for compatible format strings: 75 | 76 | >>> _validate_format('Hello %s!', 'Hallo %s!') 77 | >>> _validate_format('Hello %i!', 'Hallo %d!') 78 | 79 | Example for an incompatible format strings: 80 | 81 | >>> _validate_format('Hello %(name)s!', 'Hallo %s!') 82 | Traceback (most recent call last): 83 | ... 84 | TranslationError: the format strings are of different kinds 85 | 86 | This function is used by the `python_format` checker. 87 | 88 | :param format: The original format string 89 | :param alternative: The alternative format string that should be checked 90 | against format 91 | :raises TranslationError: on formatting errors 92 | """ 93 | 94 | def _parse(string: str) -> list[tuple[str, str]]: 95 | result: list[tuple[str, str]] = [] 96 | for match in PYTHON_FORMAT.finditer(string): 97 | name, format, typechar = match.groups() 98 | if typechar == '%' and name is None: 99 | continue 100 | result.append((name, str(typechar))) 101 | return result 102 | 103 | def _compatible(a: str, b: str) -> bool: 104 | if a == b: 105 | return True 106 | for set in _string_format_compatibilities: 107 | if a in set and b in set: 108 | return True 109 | return False 110 | 111 | def _check_positional(results: list[tuple[str, str]]) -> bool: 112 | positional = None 113 | for name, _char in results: 114 | if positional is None: 115 | positional = name is None 116 | else: 117 | if (name is None) != positional: 118 | raise TranslationError('format string mixes positional ' 119 | 'and named placeholders') 120 | return bool(positional) 121 | 122 | a = _parse(format) 123 | b = _parse(alternative) 124 | 125 | if not a: 126 | return 127 | 128 | # now check if both strings are positional or named 129 | a_positional = _check_positional(a) 130 | b_positional = _check_positional(b) 131 | if a_positional and not b_positional and not b: 132 | raise TranslationError('placeholders are incompatible') 133 | elif a_positional != b_positional: 134 | raise TranslationError('the format strings are of different kinds') 135 | 136 | # if we are operating on positional strings both must have the 137 | # same number of format chars and those must be compatible 138 | if a_positional: 139 | if len(a) != len(b): 140 | raise TranslationError('positional format placeholders are ' 141 | 'unbalanced') 142 | for idx, ((_, first), (_, second)) in enumerate(zip(a, b)): 143 | if not _compatible(first, second): 144 | raise TranslationError('incompatible format for placeholder ' 145 | '%d: %r and %r are not compatible' % 146 | (idx + 1, first, second)) 147 | 148 | # otherwise the second string must not have names the first one 149 | # doesn't have and the types of those included must be compatible 150 | else: 151 | type_map = dict(a) 152 | for name, typechar in b: 153 | if name not in type_map: 154 | raise TranslationError(f'unknown named placeholder {name!r}') 155 | elif not _compatible(typechar, type_map[name]): 156 | raise TranslationError( 157 | f'incompatible format for placeholder {name!r}: ' 158 | f'{typechar!r} and {type_map[name]!r} are not compatible', 159 | ) 160 | 161 | 162 | def _find_checkers() -> list[Callable[[Catalog | None, Message], object]]: 163 | from babel.messages._compat import find_entrypoints 164 | checkers: list[Callable[[Catalog | None, Message], object]] = [] 165 | checkers.extend(load() for (name, load) in find_entrypoints('babel.checkers')) 166 | if len(checkers) == 0: 167 | # if entrypoints are not available or no usable egg-info was found 168 | # (see #230), just resort to hard-coded checkers 169 | return [num_plurals, python_format] 170 | return checkers 171 | 172 | 173 | checkers: list[Callable[[Catalog | None, Message], object]] = _find_checkers() 174 | -------------------------------------------------------------------------------- /babel/messages/jslexer.py: -------------------------------------------------------------------------------- 1 | """ 2 | babel.messages.jslexer 3 | ~~~~~~~~~~~~~~~~~~~~~~ 4 | 5 | A simple JavaScript 1.5 lexer which is used for the JavaScript 6 | extractor. 7 | 8 | :copyright: (c) 2013-2025 by the Babel Team. 9 | :license: BSD, see LICENSE for more details. 10 | """ 11 | from __future__ import annotations 12 | 13 | import re 14 | from collections.abc import Generator 15 | from typing import NamedTuple 16 | 17 | operators: list[str] = sorted([ 18 | '+', '-', '*', '%', '!=', '==', '<', '>', '<=', '>=', '=', 19 | '+=', '-=', '*=', '%=', '<<', '>>', '>>>', '<<=', '>>=', 20 | '>>>=', '&', '&=', '|', '|=', '&&', '||', '^', '^=', '(', ')', 21 | '[', ']', '{', '}', '!', '--', '++', '~', ',', ';', '.', ':', 22 | ], key=len, reverse=True) 23 | 24 | escapes: dict[str, str] = {'b': '\b', 'f': '\f', 'n': '\n', 'r': '\r', 't': '\t'} 25 | 26 | name_re = re.compile(r'[\w$_][\w\d$_]*', re.UNICODE) 27 | dotted_name_re = re.compile(r'[\w$_][\w\d$_.]*[\w\d$_.]', re.UNICODE) 28 | division_re = re.compile(r'/=?') 29 | regex_re = re.compile(r'/(?:[^/\\]*(?:\\.[^/\\]*)*)/[a-zA-Z]*', re.DOTALL) 30 | line_re = re.compile(r'(\r\n|\n|\r)') 31 | line_join_re = re.compile(r'\\' + line_re.pattern) 32 | uni_escape_re = re.compile(r'[a-fA-F0-9]{1,4}') 33 | hex_escape_re = re.compile(r'[a-fA-F0-9]{1,2}') 34 | 35 | 36 | class Token(NamedTuple): 37 | type: str 38 | value: str 39 | lineno: int 40 | 41 | 42 | _rules: list[tuple[str | None, re.Pattern[str]]] = [ 43 | (None, re.compile(r'\s+', re.UNICODE)), 44 | (None, re.compile(r'