| """ |
| babel.core |
| ~~~~~~~~~~ |
| |
| Core locale representation and locale data access. |
| |
| :copyright: (c) 2013-2026 by the Babel Team. |
| :license: BSD, see LICENSE for more details. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import os |
| import pickle |
| from collections.abc import Iterable, Mapping |
| from typing import TYPE_CHECKING, Any, Literal |
|
|
| from babel import localedata |
| from babel.plural import PluralRule |
|
|
| __all__ = [ |
| 'Locale', |
| 'UnknownLocaleError', |
| 'default_locale', |
| 'get_cldr_version', |
| 'get_global', |
| 'get_locale_identifier', |
| 'negotiate_locale', |
| 'parse_locale', |
| ] |
|
|
| if TYPE_CHECKING: |
| from typing_extensions import TypeAlias |
|
|
| _GLOBAL_KEY: TypeAlias = Literal[ |
| "all_currencies", |
| "cldr", |
| "currency_fractions", |
| "language_aliases", |
| "likely_subtags", |
| "meta_zones", |
| "parent_exceptions", |
| "script_aliases", |
| "territory_aliases", |
| "territory_currencies", |
| "territory_languages", |
| "territory_zones", |
| "variant_aliases", |
| "windows_zone_mapping", |
| "zone_aliases", |
| "zone_territories", |
| ] |
|
|
| _global_data: Mapping[_GLOBAL_KEY, Mapping[str, Any]] | None |
|
|
| _global_data = None |
| _default_plural_rule = PluralRule({}) |
|
|
|
|
| def _raise_no_data_error(): |
| raise RuntimeError( |
| 'The babel data files are not available. ' |
| 'This usually happens because you are using ' |
| 'a source checkout from Babel and you did ' |
| 'not build the data files. Just make sure ' |
| 'to run "python setup.py import_cldr" before ' |
| 'installing the library.', |
| ) |
|
|
|
|
| def get_global(key: _GLOBAL_KEY) -> Mapping[str, Any]: |
| """Return the dictionary for the given key in the global data. |
| |
| The global data is stored in the ``babel/global.dat`` file and contains |
| information independent of individual locales. |
| |
| >>> get_global('zone_aliases')['UTC'] |
| 'Etc/UTC' |
| >>> get_global('zone_territories')['Europe/Berlin'] |
| 'DE' |
| |
| The keys available are: |
| |
| - ``all_currencies`` |
| - ``cldr`` (metadata) |
| - ``currency_fractions`` |
| - ``language_aliases`` |
| - ``likely_subtags`` |
| - ``parent_exceptions`` |
| - ``script_aliases`` |
| - ``territory_aliases`` |
| - ``territory_currencies`` |
| - ``territory_languages`` |
| - ``territory_zones`` |
| - ``variant_aliases`` |
| - ``windows_zone_mapping`` |
| - ``zone_aliases`` |
| - ``zone_territories`` |
| |
| .. note:: The internal structure of the data may change between versions. |
| |
| .. versionadded:: 0.9 |
| |
| :param key: the data key |
| """ |
| global _global_data |
| if _global_data is None: |
| dirname = os.path.join(os.path.dirname(__file__)) |
| filename = os.path.join(dirname, 'global.dat') |
| if not os.path.isfile(filename): |
| _raise_no_data_error() |
| with open(filename, 'rb') as fileobj: |
| _global_data = pickle.load(fileobj) |
| assert _global_data is not None |
| return _global_data.get(key, {}) |
|
|
|
|
| LOCALE_ALIASES = { |
| 'ar': 'ar_SY', 'bg': 'bg_BG', 'bs': 'bs_BA', 'ca': 'ca_ES', 'cs': 'cs_CZ', |
| 'da': 'da_DK', 'de': 'de_DE', 'el': 'el_GR', 'en': 'en_US', 'es': 'es_ES', |
| 'et': 'et_EE', 'fa': 'fa_IR', 'fi': 'fi_FI', 'fr': 'fr_FR', 'gl': 'gl_ES', |
| 'he': 'he_IL', 'hu': 'hu_HU', 'id': 'id_ID', 'is': 'is_IS', 'it': 'it_IT', |
| 'ja': 'ja_JP', 'km': 'km_KH', 'ko': 'ko_KR', 'lt': 'lt_LT', 'lv': 'lv_LV', |
| 'mk': 'mk_MK', 'nl': 'nl_NL', 'nn': 'nn_NO', 'no': 'nb_NO', 'pl': 'pl_PL', |
| 'pt': 'pt_PT', 'ro': 'ro_RO', 'ru': 'ru_RU', 'sk': 'sk_SK', 'sl': 'sl_SI', |
| 'sv': 'sv_SE', 'th': 'th_TH', 'tr': 'tr_TR', 'uk': 'uk_UA', |
| } |
|
|
|
|
| class UnknownLocaleError(Exception): |
| """Exception thrown when a locale is requested for which no locale data |
| is available. |
| """ |
|
|
| def __init__(self, identifier: str) -> None: |
| """Create the exception. |
| |
| :param identifier: the identifier string of the unsupported locale |
| """ |
| Exception.__init__(self, f"unknown locale {identifier!r}") |
|
|
| |
| self.identifier = identifier |
|
|
|
|
| class Locale: |
| """Representation of a specific locale. |
| |
| >>> locale = Locale('en', 'US') |
| >>> repr(locale) |
| "Locale('en', territory='US')" |
| >>> locale.display_name |
| 'English (United States)' |
| |
| A `Locale` object can also be instantiated from a raw locale string: |
| |
| >>> locale = Locale.parse('en-US', sep='-') |
| >>> repr(locale) |
| "Locale('en', territory='US')" |
| |
| `Locale` objects provide access to a collection of locale data, such as |
| territory and language names, number and date format patterns, and more: |
| |
| >>> locale.number_symbols['latn']['decimal'] |
| '.' |
| |
| If a locale is requested for which no locale data is available, an |
| `UnknownLocaleError` is raised: |
| |
| >>> Locale.parse('en_XX') |
| Traceback (most recent call last): |
| ... |
| UnknownLocaleError: unknown locale 'en_XX' |
| |
| For more information see :rfc:`3066`. |
| """ |
|
|
| def __init__( |
| self, |
| language: str, |
| territory: str | None = None, |
| script: str | None = None, |
| variant: str | None = None, |
| modifier: str | None = None, |
| ) -> None: |
| """Initialize the locale object from the given identifier components. |
| |
| >>> locale = Locale('en', 'US') |
| >>> locale.language |
| 'en' |
| >>> locale.territory |
| 'US' |
| |
| :param language: the language code |
| :param territory: the territory (country or region) code |
| :param script: the script code |
| :param variant: the variant code |
| :param modifier: a modifier (following the '@' symbol, sometimes called '@variant') |
| :raise `UnknownLocaleError`: if no locale data is available for the |
| requested locale |
| """ |
| |
| self.language = language |
| |
| self.territory = territory |
| |
| self.script = script |
| |
| self.variant = variant |
| |
| self.modifier = modifier |
| self.__data: localedata.LocaleDataDict | None = None |
|
|
| identifier = str(self) |
| identifier_without_modifier = identifier.partition('@')[0] |
| if localedata.exists(identifier): |
| self.__data_identifier = identifier |
| elif localedata.exists(identifier_without_modifier): |
| self.__data_identifier = identifier_without_modifier |
| else: |
| raise UnknownLocaleError(identifier) |
|
|
| @classmethod |
| def default( |
| cls, |
| category: str | None = None, |
| aliases: Mapping[str, str] = LOCALE_ALIASES, |
| ) -> Locale: |
| """Return the system default locale for the specified category. |
| |
| >>> for name in ['LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LC_MESSAGES']: |
| ... os.environ[name] = '' |
| >>> os.environ['LANG'] = 'fr_FR.UTF-8' |
| >>> Locale.default('LC_MESSAGES') |
| Locale('fr', territory='FR') |
| |
| The following fallbacks to the variable are always considered: |
| |
| - ``LANGUAGE`` |
| - ``LC_ALL`` |
| - ``LC_CTYPE`` |
| - ``LANG`` |
| |
| :param category: one of the ``LC_XXX`` environment variable names |
| :param aliases: a dictionary of aliases for locale identifiers |
| """ |
| |
| |
| locale_string = default_locale(category, aliases=aliases) |
| return cls.parse(locale_string) |
|
|
| @classmethod |
| def negotiate( |
| cls, |
| preferred: Iterable[str], |
| available: Iterable[str], |
| sep: str = '_', |
| aliases: Mapping[str, str] = LOCALE_ALIASES, |
| ) -> Locale | None: |
| """Find the best match between available and requested locale strings. |
| |
| >>> Locale.negotiate(['de_DE', 'en_US'], ['de_DE', 'de_AT']) |
| Locale('de', territory='DE') |
| >>> Locale.negotiate(['de_DE', 'en_US'], ['en', 'de']) |
| Locale('de') |
| >>> Locale.negotiate(['de_DE', 'de'], ['en_US']) |
| |
| You can specify the character used in the locale identifiers to separate |
| the different components. This separator is applied to both lists. Also, |
| case is ignored in the comparison: |
| |
| >>> Locale.negotiate(['de-DE', 'de'], ['en-us', 'de-de'], sep='-') |
| Locale('de', territory='DE') |
| |
| :param preferred: the list of locale identifiers preferred by the user |
| :param available: the list of locale identifiers available |
| :param aliases: a dictionary of aliases for locale identifiers |
| :param sep: separator for parsing; e.g. Windows tends to use '-' instead of '_'. |
| """ |
| identifier = negotiate_locale(preferred, available, sep=sep, aliases=aliases) |
| if identifier: |
| return Locale.parse(identifier, sep=sep) |
| return None |
|
|
| @classmethod |
| def parse( |
| cls, |
| identifier: Locale | str | None, |
| sep: str = '_', |
| resolve_likely_subtags: bool = True, |
| ) -> Locale: |
| """Create a `Locale` instance for the given locale identifier. |
| |
| >>> l = Locale.parse('de-DE', sep='-') |
| >>> l.display_name |
| 'Deutsch (Deutschland)' |
| |
| If the `identifier` parameter is not a string, but actually a `Locale` |
| object, that object is returned: |
| |
| >>> Locale.parse(l) |
| Locale('de', territory='DE') |
| |
| If the `identifier` parameter is neither of these, such as `None` |
| or an empty string, e.g. because a default locale identifier |
| could not be determined, a `TypeError` is raised: |
| |
| >>> Locale.parse(None) |
| Traceback (most recent call last): |
| ... |
| TypeError: ... |
| |
| This also can perform resolving of likely subtags which it does |
| by default. This is for instance useful to figure out the most |
| likely locale for a territory you can use ``'und'`` as the |
| language tag: |
| |
| >>> Locale.parse('und_AT') |
| Locale('de', territory='AT') |
| |
| Modifiers are optional, and always at the end, separated by "@": |
| |
| >>> Locale.parse('de_AT@euro') |
| Locale('de', territory='AT', modifier='euro') |
| |
| :param identifier: the locale identifier string |
| :param sep: optional component separator |
| :param resolve_likely_subtags: if this is specified then a locale will |
| have its likely subtag resolved if the |
| locale otherwise does not exist. For |
| instance ``zh_TW`` by itself is not a |
| locale that exists but Babel can |
| automatically expand it to the full |
| form of ``zh_hant_TW``. Note that this |
| expansion is only taking place if no |
| locale exists otherwise. For instance |
| there is a locale ``en`` that can exist |
| by itself. |
| :raise `ValueError`: if the string does not appear to be a valid locale |
| identifier |
| :raise `UnknownLocaleError`: if no locale data is available for the |
| requested locale |
| :raise `TypeError`: if the identifier is not a string or a `Locale` |
| :raise `ValueError`: if the identifier is not a valid string |
| """ |
| if isinstance(identifier, Locale): |
| return identifier |
|
|
| if not identifier: |
| msg = ( |
| f"Empty locale identifier value: {identifier!r}\n\n" |
| f"If you didn't explicitly pass an empty value to a Babel function, " |
| f"this could be caused by there being no suitable locale environment " |
| f"variables for the API you tried to use." |
| ) |
| if isinstance(identifier, str): |
| |
| raise ValueError(msg) |
| raise TypeError(msg) |
|
|
| if not isinstance(identifier, str): |
| raise TypeError(f"Unexpected value for identifier: {identifier!r}") |
|
|
| parts = parse_locale(identifier, sep=sep) |
| input_id = get_locale_identifier(parts) |
|
|
| def _try_load(parts): |
| try: |
| return cls(*parts) |
| except UnknownLocaleError: |
| return None |
|
|
| def _try_load_reducing(parts): |
| |
| locale = _try_load(parts) |
| if locale is not None: |
| return locale |
|
|
| |
| locale = _try_load(parts[:2]) |
| if locale is not None: |
| return locale |
|
|
| locale = _try_load(parts) |
| if locale is not None: |
| return locale |
| if not resolve_likely_subtags: |
| raise UnknownLocaleError(input_id) |
|
|
| |
| |
| |
| |
| |
| |
|
|
| if len(parts) == 5: |
| language, territory, script, variant, modifier = parts |
| else: |
| language, territory, script, variant = parts |
| modifier = None |
| language = get_global('language_aliases').get(language, language) |
| territory = get_global('territory_aliases').get(territory or '', (territory,))[0] |
| script = get_global('script_aliases').get(script or '', script) |
| variant = get_global('variant_aliases').get(variant or '', variant) |
|
|
| if territory == 'ZZ': |
| territory = None |
| if script == 'Zzzz': |
| script = None |
|
|
| parts = language, territory, script, variant, modifier |
|
|
| |
| new_id = get_locale_identifier(parts) |
| likely_subtag = get_global('likely_subtags').get(new_id) |
| if likely_subtag is not None: |
| locale = _try_load_reducing(parse_locale(likely_subtag)) |
| if locale is not None: |
| return locale |
|
|
| |
| |
| likely_subtag = get_global('likely_subtags').get(language) |
| if likely_subtag is not None: |
| parts2 = parse_locale(likely_subtag) |
| if len(parts2) == 5: |
| language2, _, script2, variant2, modifier2 = parts2 |
| else: |
| language2, _, script2, variant2 = parts2 |
| modifier2 = None |
| locale = _try_load_reducing( |
| (language2, territory, script2, variant2, modifier2), |
| ) |
| if locale is not None: |
| return locale |
|
|
| raise UnknownLocaleError(input_id) |
|
|
| def __eq__(self, other: object) -> bool: |
| for key in ('language', 'territory', 'script', 'variant', 'modifier'): |
| if not hasattr(other, key): |
| return False |
| return ( |
| self.language == getattr(other, 'language') |
| and self.territory == getattr(other, 'territory') |
| and self.script == getattr(other, 'script') |
| and self.variant == getattr(other, 'variant') |
| and self.modifier == getattr(other, 'modifier') |
| ) |
|
|
| def __ne__(self, other: object) -> bool: |
| return not self.__eq__(other) |
|
|
| def __hash__(self) -> int: |
| return hash((self.language, self.territory, self.script, self.variant, self.modifier)) |
|
|
| def __repr__(self) -> str: |
| parameters = [''] |
| for key in ('territory', 'script', 'variant', 'modifier'): |
| value = getattr(self, key) |
| if value is not None: |
| parameters.append(f"{key}={value!r}") |
| return f"Locale({self.language!r}{', '.join(parameters)})" |
|
|
| def __str__(self) -> str: |
| return get_locale_identifier( |
| (self.language, self.territory, self.script, self.variant, self.modifier), |
| ) |
|
|
| @property |
| def _data(self) -> localedata.LocaleDataDict: |
| if self.__data is None: |
| self.__data = localedata.LocaleDataDict(localedata.load(self.__data_identifier)) |
| return self.__data |
|
|
| def get_display_name(self, locale: Locale | str | None = None) -> str | None: |
| """Return the display name of the locale using the given locale. |
| |
| The display name will include the language, territory, script, and |
| variant, if those are specified. |
| |
| >>> Locale('zh', 'CN', script='Hans').get_display_name('en') |
| 'Chinese (Simplified, China)' |
| |
| Modifiers are currently passed through verbatim: |
| |
| >>> Locale('it', 'IT', modifier='euro').get_display_name('en') |
| 'Italian (Italy, euro)' |
| |
| :param locale: the locale to use |
| """ |
| if locale is None: |
| locale = self |
| locale = Locale.parse(locale) |
| retval = locale.languages.get(self.language) |
| if retval and (self.territory or self.script or self.variant): |
| details = [] |
| if self.script: |
| details.append(locale.scripts.get(self.script)) |
| if self.territory: |
| details.append(locale.territories.get(self.territory)) |
| if self.variant: |
| details.append(locale.variants.get(self.variant)) |
| if self.modifier: |
| details.append(self.modifier) |
| detail_string = ', '.join(atom for atom in details if atom) |
| if detail_string: |
| retval += f" ({detail_string})" |
| return retval |
|
|
| display_name = property( |
| get_display_name, |
| doc="""\ |
| The localized display name of the locale. |
| |
| >>> Locale('en').display_name |
| 'English' |
| >>> Locale('en', 'US').display_name |
| 'English (United States)' |
| >>> Locale('sv').display_name |
| 'svenska' |
| |
| :type: `unicode` |
| """, |
| ) |
|
|
| def get_language_name(self, locale: Locale | str | None = None) -> str | None: |
| """Return the language of this locale in the given locale. |
| |
| >>> Locale('zh', 'CN', script='Hans').get_language_name('de') |
| 'Chinesisch' |
| |
| .. versionadded:: 1.0 |
| |
| :param locale: the locale to use |
| """ |
| if locale is None: |
| locale = self |
| locale = Locale.parse(locale) |
| return locale.languages.get(self.language) |
|
|
| language_name = property( |
| get_language_name, |
| doc="""\ |
| The localized language name of the locale. |
| |
| >>> Locale('en', 'US').language_name |
| 'English' |
| """, |
| ) |
|
|
| def get_territory_name(self, locale: Locale | str | None = None) -> str | None: |
| """Return the territory name in the given locale.""" |
| if locale is None: |
| locale = self |
| locale = Locale.parse(locale) |
| return locale.territories.get(self.territory or '') |
|
|
| territory_name = property( |
| get_territory_name, |
| doc="""\ |
| The localized territory name of the locale if available. |
| |
| >>> Locale('de', 'DE').territory_name |
| 'Deutschland' |
| """, |
| ) |
|
|
| def get_script_name(self, locale: Locale | str | None = None) -> str | None: |
| """Return the script name in the given locale.""" |
| if locale is None: |
| locale = self |
| locale = Locale.parse(locale) |
| return locale.scripts.get(self.script or '') |
|
|
| script_name = property( |
| get_script_name, |
| doc="""\ |
| The localized script name of the locale if available. |
| |
| >>> Locale('sr', 'ME', script='Latn').script_name |
| 'latinica' |
| """, |
| ) |
|
|
| @property |
| def english_name(self) -> str | None: |
| """The english display name of the locale. |
| |
| >>> Locale('de').english_name |
| 'German' |
| >>> Locale('de', 'DE').english_name |
| 'German (Germany)' |
| |
| :type: `unicode`""" |
| return self.get_display_name(Locale('en')) |
|
|
| |
|
|
| @property |
| def languages(self) -> localedata.LocaleDataDict: |
| """Mapping of language codes to translated language names. |
| |
| >>> Locale('de', 'DE').languages['ja'] |
| 'Japanisch' |
| |
| See `ISO 639 <https://www.loc.gov/standards/iso639-2/>`_ for |
| more information. |
| """ |
| return self._data['languages'] |
|
|
| @property |
| def scripts(self) -> localedata.LocaleDataDict: |
| """Mapping of script codes to translated script names. |
| |
| >>> Locale('en', 'US').scripts['Hira'] |
| 'Hiragana' |
| |
| See `ISO 15924 <https://www.unicode.org/iso15924/>`_ |
| for more information. |
| """ |
| return self._data['scripts'] |
|
|
| @property |
| def territories(self) -> localedata.LocaleDataDict: |
| """Mapping of script codes to translated script names. |
| |
| >>> Locale('es', 'CO').territories['DE'] |
| 'Alemania' |
| |
| See `ISO 3166 <https://en.wikipedia.org/wiki/ISO_3166>`_ |
| for more information. |
| """ |
| return self._data['territories'] |
|
|
| @property |
| def variants(self) -> localedata.LocaleDataDict: |
| """Mapping of script codes to translated script names. |
| |
| >>> Locale('de', 'DE').variants['1901'] |
| 'Alte deutsche Rechtschreibung' |
| """ |
| return self._data['variants'] |
|
|
| |
|
|
| @property |
| def currencies(self) -> localedata.LocaleDataDict: |
| """Mapping of currency codes to translated currency names. This |
| only returns the generic form of the currency name, not the count |
| specific one. If an actual number is requested use the |
| :func:`babel.numbers.get_currency_name` function. |
| |
| >>> Locale('en').currencies['COP'] |
| 'Colombian Peso' |
| >>> Locale('de', 'DE').currencies['COP'] |
| 'Kolumbianischer Peso' |
| """ |
| return self._data['currency_names'] |
|
|
| @property |
| def currency_symbols(self) -> localedata.LocaleDataDict: |
| """Mapping of currency codes to symbols. |
| |
| >>> Locale('en', 'US').currency_symbols['USD'] |
| '$' |
| >>> Locale('es', 'CO').currency_symbols['USD'] |
| 'US$' |
| """ |
| return self._data['currency_symbols'] |
|
|
| @property |
| def number_symbols(self) -> localedata.LocaleDataDict: |
| """Symbols used in number formatting by number system. |
| |
| .. note:: The format of the value returned may change between |
| Babel versions. |
| |
| >>> Locale('fr', 'FR').number_symbols["latn"]['decimal'] |
| ',' |
| >>> Locale('fa', 'IR').number_symbols["arabext"]['decimal'] |
| '٫' |
| >>> Locale('fa', 'IR').number_symbols["latn"]['decimal'] |
| '.' |
| """ |
| return self._data['number_symbols'] |
|
|
| @property |
| def other_numbering_systems(self) -> localedata.LocaleDataDict: |
| """ |
| Mapping of other numbering systems available for the locale. |
| See: https://www.unicode.org/reports/tr35/tr35-numbers.html#otherNumberingSystems |
| |
| >>> Locale('el', 'GR').other_numbering_systems['traditional'] |
| 'grek' |
| |
| .. note:: The format of the value returned may change between |
| Babel versions. |
| """ |
| return self._data['numbering_systems'] |
|
|
| @property |
| def default_numbering_system(self) -> str: |
| """The default numbering system used by the locale. |
| >>> Locale('el', 'GR').default_numbering_system |
| 'latn' |
| """ |
| return self._data['default_numbering_system'] |
|
|
| @property |
| def decimal_formats(self) -> localedata.LocaleDataDict: |
| """Locale patterns for decimal number formatting. |
| |
| .. note:: The format of the value returned may change between |
| Babel versions. |
| |
| >>> Locale('en', 'US').decimal_formats[None] |
| <NumberPattern '#,##0.###'> |
| """ |
| return self._data['decimal_formats'] |
|
|
| @property |
| def compact_decimal_formats(self) -> localedata.LocaleDataDict: |
| """Locale patterns for compact decimal number formatting. |
| |
| .. note:: The format of the value returned may change between |
| Babel versions. |
| |
| >>> Locale('en', 'US').compact_decimal_formats["short"]["one"]["1000"] |
| <NumberPattern '0K'> |
| """ |
| return self._data['compact_decimal_formats'] |
|
|
| @property |
| def currency_formats(self) -> localedata.LocaleDataDict: |
| """Locale patterns for currency number formatting. |
| |
| .. note:: The format of the value returned may change between |
| Babel versions. |
| |
| >>> Locale('en', 'US').currency_formats['standard'] |
| <NumberPattern '\\xa4#,##0.00'> |
| >>> Locale('en', 'US').currency_formats['accounting'] |
| <NumberPattern '\\xa4#,##0.00;(\\xa4#,##0.00)'> |
| """ |
| return self._data['currency_formats'] |
|
|
| @property |
| def compact_currency_formats(self) -> localedata.LocaleDataDict: |
| """Locale patterns for compact currency number formatting. |
| |
| .. note:: The format of the value returned may change between |
| Babel versions. |
| |
| >>> Locale('en', 'US').compact_currency_formats["short"]["one"]["1000"] |
| <NumberPattern '¤0K'> |
| """ |
| return self._data['compact_currency_formats'] |
|
|
| @property |
| def percent_formats(self) -> localedata.LocaleDataDict: |
| """Locale patterns for percent number formatting. |
| |
| .. note:: The format of the value returned may change between |
| Babel versions. |
| |
| >>> Locale('en', 'US').percent_formats[None] |
| <NumberPattern '#,##0%'> |
| """ |
| return self._data['percent_formats'] |
|
|
| @property |
| def scientific_formats(self) -> localedata.LocaleDataDict: |
| """Locale patterns for scientific number formatting. |
| |
| .. note:: The format of the value returned may change between |
| Babel versions. |
| |
| >>> Locale('en', 'US').scientific_formats[None] |
| <NumberPattern '#E0'> |
| """ |
| return self._data['scientific_formats'] |
|
|
| |
|
|
| @property |
| def periods(self) -> localedata.LocaleDataDict: |
| """Locale display names for day periods (AM/PM). |
| |
| >>> Locale('en', 'US').periods['am'] |
| 'AM' |
| """ |
| try: |
| return self._data['day_periods']['stand-alone']['wide'] |
| except KeyError: |
| return localedata.LocaleDataDict({}) |
|
|
| @property |
| def day_periods(self) -> localedata.LocaleDataDict: |
| """Locale display names for various day periods (not necessarily only AM/PM). |
| |
| These are not meant to be used without the relevant `day_period_rules`. |
| """ |
| return self._data['day_periods'] |
|
|
| @property |
| def day_period_rules(self) -> localedata.LocaleDataDict: |
| """Day period rules for the locale. Used by `get_period_id`.""" |
| return self._data.get('day_period_rules', localedata.LocaleDataDict({})) |
|
|
| @property |
| def days(self) -> localedata.LocaleDataDict: |
| """Locale display names for weekdays. |
| |
| >>> Locale('de', 'DE').days['format']['wide'][3] |
| 'Donnerstag' |
| """ |
| return self._data['days'] |
|
|
| @property |
| def months(self) -> localedata.LocaleDataDict: |
| """Locale display names for months. |
| |
| >>> Locale('de', 'DE').months['format']['wide'][10] |
| 'Oktober' |
| """ |
| return self._data['months'] |
|
|
| @property |
| def quarters(self) -> localedata.LocaleDataDict: |
| """Locale display names for quarters. |
| |
| >>> Locale('de', 'DE').quarters['format']['wide'][1] |
| '1. Quartal' |
| """ |
| return self._data['quarters'] |
|
|
| @property |
| def eras(self) -> localedata.LocaleDataDict: |
| """Locale display names for eras. |
| |
| .. note:: The format of the value returned may change between |
| Babel versions. |
| |
| >>> Locale('en', 'US').eras['wide'][1] |
| 'Anno Domini' |
| >>> Locale('en', 'US').eras['abbreviated'][0] |
| 'BC' |
| """ |
| return self._data['eras'] |
|
|
| @property |
| def time_zones(self) -> localedata.LocaleDataDict: |
| """Locale display names for time zones. |
| |
| .. note:: The format of the value returned may change between |
| Babel versions. |
| |
| >>> Locale('en', 'US').time_zones['Europe/London']['long']['daylight'] |
| 'British Summer Time' |
| >>> Locale('en', 'US').time_zones['America/St_Johns']['city'] |
| 'St. John’s' |
| """ |
| return self._data['time_zones'] |
|
|
| @property |
| def meta_zones(self) -> localedata.LocaleDataDict: |
| """Locale display names for meta time zones. |
| |
| Meta time zones are basically groups of different Olson time zones that |
| have the same GMT offset and daylight savings time. |
| |
| .. note:: The format of the value returned may change between |
| Babel versions. |
| |
| >>> Locale('en', 'US').meta_zones['Europe_Central']['long']['daylight'] |
| 'Central European Summer Time' |
| |
| .. versionadded:: 0.9 |
| """ |
| return self._data['meta_zones'] |
|
|
| @property |
| def zone_formats(self) -> localedata.LocaleDataDict: |
| """Patterns related to the formatting of time zones. |
| |
| .. note:: The format of the value returned may change between |
| Babel versions. |
| |
| >>> Locale('en', 'US').zone_formats['fallback'] |
| '%(1)s (%(0)s)' |
| >>> Locale('pt', 'BR').zone_formats['region'] |
| 'Horário %s' |
| |
| .. versionadded:: 0.9 |
| """ |
| return self._data['zone_formats'] |
|
|
| @property |
| def first_week_day(self) -> int: |
| """The first day of a week, with 0 being Monday. |
| |
| >>> Locale('de', 'DE').first_week_day |
| 0 |
| >>> Locale('en', 'US').first_week_day |
| 6 |
| """ |
| return self._data['week_data']['first_day'] |
|
|
| @property |
| def weekend_start(self) -> int: |
| """The day the weekend starts, with 0 being Monday. |
| |
| >>> Locale('de', 'DE').weekend_start |
| 5 |
| """ |
| return self._data['week_data']['weekend_start'] |
|
|
| @property |
| def weekend_end(self) -> int: |
| """The day the weekend ends, with 0 being Monday. |
| |
| >>> Locale('de', 'DE').weekend_end |
| 6 |
| """ |
| return self._data['week_data']['weekend_end'] |
|
|
| @property |
| def min_week_days(self) -> int: |
| """The minimum number of days in a week so that the week is counted as |
| the first week of a year or month. |
| |
| >>> Locale('de', 'DE').min_week_days |
| 4 |
| """ |
| return self._data['week_data']['min_days'] |
|
|
| @property |
| def date_formats(self) -> localedata.LocaleDataDict: |
| """Locale patterns for date formatting. |
| |
| .. note:: The format of the value returned may change between |
| Babel versions. |
| |
| >>> Locale('en', 'US').date_formats['short'] |
| <DateTimePattern 'M/d/yy'> |
| >>> Locale('fr', 'FR').date_formats['long'] |
| <DateTimePattern 'd MMMM y'> |
| """ |
| return self._data['date_formats'] |
|
|
| @property |
| def time_formats(self) -> localedata.LocaleDataDict: |
| """Locale patterns for time formatting. |
| |
| .. note:: The format of the value returned may change between |
| Babel versions. |
| |
| >>> Locale('en', 'US').time_formats['short'] |
| <DateTimePattern 'h:mm\\u202fa'> |
| >>> Locale('fr', 'FR').time_formats['long'] |
| <DateTimePattern 'HH:mm:ss z'> |
| """ |
| return self._data['time_formats'] |
|
|
| @property |
| def datetime_formats(self) -> localedata.LocaleDataDict: |
| """Locale patterns for datetime formatting. |
| |
| .. note:: The format of the value returned may change between |
| Babel versions. |
| |
| >>> Locale('en').datetime_formats['full'] |
| '{1}, {0}' |
| >>> Locale('th').datetime_formats['medium'] |
| '{1} {0}' |
| """ |
| return self._data['datetime_formats'] |
|
|
| @property |
| def datetime_skeletons(self) -> localedata.LocaleDataDict: |
| """Locale patterns for formatting parts of a datetime. |
| |
| >>> Locale('en').datetime_skeletons['MEd'] |
| <DateTimePattern 'E, M/d'> |
| >>> Locale('fr').datetime_skeletons['MEd'] |
| <DateTimePattern 'E dd/MM'> |
| >>> Locale('fr').datetime_skeletons['H'] |
| <DateTimePattern "HH 'h'"> |
| """ |
| return self._data['datetime_skeletons'] |
|
|
| @property |
| def interval_formats(self) -> localedata.LocaleDataDict: |
| """Locale patterns for interval formatting. |
| |
| .. note:: The format of the value returned may change between |
| Babel versions. |
| |
| How to format date intervals in Finnish when the day is the |
| smallest changing component: |
| |
| >>> Locale('fi_FI').interval_formats['MEd']['d'] |
| ['E d.\\u2009–\\u2009', 'E d.M.'] |
| |
| .. seealso:: |
| |
| The primary API to use this data is :py:func:`babel.dates.format_interval`. |
| |
| |
| :rtype: dict[str, dict[str, list[str]]] |
| """ |
| return self._data['interval_formats'] |
|
|
| @property |
| def plural_form(self) -> PluralRule: |
| """Plural rules for the locale. |
| |
| >>> Locale('en').plural_form(1) |
| 'one' |
| >>> Locale('en').plural_form(0) |
| 'other' |
| >>> Locale('fr').plural_form(0) |
| 'one' |
| >>> Locale('ru').plural_form(100) |
| 'many' |
| """ |
| return self._data.get('plural_form', _default_plural_rule) |
|
|
| @property |
| def list_patterns(self) -> localedata.LocaleDataDict: |
| """Patterns for generating lists |
| |
| .. note:: The format of the value returned may change between |
| Babel versions. |
| |
| >>> Locale('en').list_patterns['standard']['start'] |
| '{0}, {1}' |
| >>> Locale('en').list_patterns['standard']['end'] |
| '{0}, and {1}' |
| >>> Locale('en_GB').list_patterns['standard']['end'] |
| '{0} and {1}' |
| """ |
| return self._data['list_patterns'] |
|
|
| @property |
| def ordinal_form(self) -> PluralRule: |
| """Plural rules for the locale. |
| |
| >>> Locale('en').ordinal_form(1) |
| 'one' |
| >>> Locale('en').ordinal_form(2) |
| 'two' |
| >>> Locale('en').ordinal_form(3) |
| 'few' |
| >>> Locale('fr').ordinal_form(2) |
| 'other' |
| >>> Locale('ru').ordinal_form(100) |
| 'other' |
| """ |
| return self._data.get('ordinal_form', _default_plural_rule) |
|
|
| @property |
| def measurement_systems(self) -> localedata.LocaleDataDict: |
| """Localized names for various measurement systems. |
| |
| >>> Locale('fr', 'FR').measurement_systems['US'] |
| 'américain' |
| >>> Locale('en', 'US').measurement_systems['US'] |
| 'US' |
| |
| """ |
| return self._data['measurement_systems'] |
|
|
| @property |
| def character_order(self) -> str: |
| """The text direction for the language. |
| |
| >>> Locale('de', 'DE').character_order |
| 'left-to-right' |
| >>> Locale('ar', 'SA').character_order |
| 'right-to-left' |
| """ |
| return self._data['character_order'] |
|
|
| @property |
| def text_direction(self) -> str: |
| """The text direction for the language in CSS short-hand form. |
| |
| >>> Locale('de', 'DE').text_direction |
| 'ltr' |
| >>> Locale('ar', 'SA').text_direction |
| 'rtl' |
| """ |
| return ''.join(word[0] for word in self.character_order.split('-')) |
|
|
| @property |
| def unit_display_names(self) -> localedata.LocaleDataDict: |
| """Display names for units of measurement. |
| |
| .. seealso:: |
| |
| You may want to use :py:func:`babel.units.get_unit_name` instead. |
| |
| .. note:: The format of the value returned may change between |
| Babel versions. |
| |
| """ |
| return self._data['unit_display_names'] |
|
|
|
|
| def default_locale( |
| category: str | tuple[str, ...] | list[str] | None = None, |
| aliases: Mapping[str, str] = LOCALE_ALIASES, |
| ) -> str | None: |
| """Returns the system default locale for a given category, based on |
| environment variables. |
| |
| >>> for name in ['LANGUAGE', 'LC_ALL', 'LC_CTYPE']: |
| ... os.environ[name] = '' |
| >>> os.environ['LANG'] = 'fr_FR.UTF-8' |
| >>> default_locale('LC_MESSAGES') |
| 'fr_FR' |
| |
| The "C" or "POSIX" pseudo-locales are treated as aliases for the |
| "en_US_POSIX" locale: |
| |
| >>> os.environ['LC_MESSAGES'] = 'POSIX' |
| >>> default_locale('LC_MESSAGES') |
| 'en_US_POSIX' |
| |
| The following fallbacks to the variable are always considered: |
| |
| - ``LANGUAGE`` |
| - ``LC_ALL`` |
| - ``LC_CTYPE`` |
| - ``LANG`` |
| |
| :param category: one or more of the ``LC_XXX`` environment variable names |
| :param aliases: a dictionary of aliases for locale identifiers |
| """ |
|
|
| varnames = ('LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LANG') |
| if category: |
| if isinstance(category, str): |
| varnames = (category, *varnames) |
| elif isinstance(category, (list, tuple)): |
| varnames = (*category, *varnames) |
| else: |
| raise TypeError(f"Invalid type for category: {category!r}") |
|
|
| for name in varnames: |
| if not name: |
| continue |
| locale = os.getenv(name) |
| if locale: |
| if name == 'LANGUAGE' and ':' in locale: |
| |
| |
| locale = locale.split(':')[0] |
| if locale.split('.')[0] in ('C', 'POSIX'): |
| locale = 'en_US_POSIX' |
| elif aliases and locale in aliases: |
| locale = aliases[locale] |
| try: |
| return get_locale_identifier(parse_locale(locale)) |
| except ValueError: |
| pass |
| return None |
|
|
|
|
| def negotiate_locale( |
| preferred: Iterable[str], |
| available: Iterable[str], |
| sep: str = '_', |
| aliases: Mapping[str, str] = LOCALE_ALIASES, |
| ) -> str | None: |
| """Find the best match between available and requested locale strings. |
| |
| >>> negotiate_locale(['de_DE', 'en_US'], ['de_DE', 'de_AT']) |
| 'de_DE' |
| >>> negotiate_locale(['de_DE', 'en_US'], ['en', 'de']) |
| 'de' |
| |
| Case is ignored by the algorithm, the result uses the case of the preferred |
| locale identifier: |
| |
| >>> negotiate_locale(['de_DE', 'en_US'], ['de_de', 'de_at']) |
| 'de_DE' |
| |
| >>> negotiate_locale(['de_DE', 'en_US'], ['de_de', 'de_at']) |
| 'de_DE' |
| |
| By default, some web browsers unfortunately do not include the territory |
| in the locale identifier for many locales, and some don't even allow the |
| user to easily add the territory. So while you may prefer using qualified |
| locale identifiers in your web-application, they would not normally match |
| the language-only locale sent by such browsers. To workaround that, this |
| function uses a default mapping of commonly used language-only locale |
| identifiers to identifiers including the territory: |
| |
| >>> negotiate_locale(['ja', 'en_US'], ['ja_JP', 'en_US']) |
| 'ja_JP' |
| |
| Some browsers even use an incorrect or outdated language code, such as "no" |
| for Norwegian, where the correct locale identifier would actually be "nb_NO" |
| (Bokmål) or "nn_NO" (Nynorsk). The aliases are intended to take care of |
| such cases, too: |
| |
| >>> negotiate_locale(['no', 'sv'], ['nb_NO', 'sv_SE']) |
| 'nb_NO' |
| |
| You can override this default mapping by passing a different `aliases` |
| dictionary to this function, or you can bypass the behavior althogher by |
| setting the `aliases` parameter to `None`. |
| |
| :param preferred: the list of locale strings preferred by the user |
| :param available: the list of locale strings available |
| :param sep: character that separates the different parts of the locale |
| strings |
| :param aliases: a dictionary of aliases for locale identifiers |
| """ |
| available = [a.lower() for a in available if a] |
| for locale in preferred: |
| ll = locale.lower() |
| if ll in available: |
| return locale |
| if aliases: |
| alias = aliases.get(ll) |
| if alias: |
| alias = alias.replace('_', sep) |
| if alias.lower() in available: |
| return alias |
| parts = locale.split(sep) |
| if len(parts) > 1 and parts[0].lower() in available: |
| return parts[0] |
| return None |
|
|
|
|
| def parse_locale( |
| identifier: str, |
| sep: str = '_', |
| ) -> ( |
| tuple[str, str | None, str | None, str | None] |
| | tuple[str, str | None, str | None, str | None, str | None] |
| ): |
| """Parse a locale identifier into a tuple of the form ``(language, |
| territory, script, variant, modifier)``. |
| |
| >>> parse_locale('zh_CN') |
| ('zh', 'CN', None, None) |
| >>> parse_locale('zh_Hans_CN') |
| ('zh', 'CN', 'Hans', None) |
| >>> parse_locale('ca_es_valencia') |
| ('ca', 'ES', None, 'VALENCIA') |
| >>> parse_locale('en_150') |
| ('en', '150', None, None) |
| >>> parse_locale('en_us_posix') |
| ('en', 'US', None, 'POSIX') |
| >>> parse_locale('it_IT@euro') |
| ('it', 'IT', None, None, 'euro') |
| >>> parse_locale('it_IT@custom') |
| ('it', 'IT', None, None, 'custom') |
| >>> parse_locale('it_IT@') |
| ('it', 'IT', None, None) |
| |
| The default component separator is "_", but a different separator can be |
| specified using the `sep` parameter. |
| |
| The optional modifier is always separated with "@" and at the end: |
| |
| >>> parse_locale('zh-CN', sep='-') |
| ('zh', 'CN', None, None) |
| >>> parse_locale('zh-CN@custom', sep='-') |
| ('zh', 'CN', None, None, 'custom') |
| |
| If the identifier cannot be parsed into a locale, a `ValueError` exception |
| is raised: |
| |
| >>> parse_locale('not_a_LOCALE_String') |
| Traceback (most recent call last): |
| ... |
| ValueError: 'not_a_LOCALE_String' is not a valid locale identifier |
| |
| Encoding information is removed from the identifier, while modifiers are |
| kept: |
| |
| >>> parse_locale('en_US.UTF-8') |
| ('en', 'US', None, None) |
| >>> parse_locale('de_DE.iso885915@euro') |
| ('de', 'DE', None, None, 'euro') |
| |
| See :rfc:`4646` for more information. |
| |
| :param identifier: the locale identifier string |
| :param sep: character that separates the different components of the locale |
| identifier |
| :raise `ValueError`: if the string does not appear to be a valid locale |
| identifier |
| """ |
| if not identifier: |
| raise ValueError("empty locale identifier") |
| identifier, _, modifier = identifier.partition('@') |
| if '.' in identifier: |
| |
| identifier = identifier.split('.', 1)[0] |
|
|
| parts = identifier.split(sep) |
| lang = parts.pop(0).lower() |
| if not lang.isalpha(): |
| raise ValueError(f"expected only letters, got {lang!r}") |
|
|
| script = territory = variant = None |
| if parts and len(parts[0]) == 4 and parts[0].isalpha(): |
| script = parts.pop(0).title() |
|
|
| if parts: |
| if len(parts[0]) == 2 and parts[0].isalpha(): |
| territory = parts.pop(0).upper() |
| elif len(parts[0]) == 3 and parts[0].isdigit(): |
| territory = parts.pop(0) |
|
|
| if parts and ( |
| len(parts[0]) == 4 |
| and parts[0][0].isdigit() |
| or len(parts[0]) >= 5 |
| and parts[0][0].isalpha() |
| ): |
| variant = parts.pop().upper() |
|
|
| if parts: |
| raise ValueError(f"{identifier!r} is not a valid locale identifier") |
|
|
| |
| if modifier: |
| return lang, territory, script, variant, modifier |
| else: |
| return lang, territory, script, variant |
|
|
|
|
| def get_locale_identifier( |
| tup: tuple[str] |
| | tuple[str, str | None] |
| | tuple[str, str | None, str | None] |
| | tuple[str, str | None, str | None, str | None] |
| | tuple[str, str | None, str | None, str | None, str | None], |
| sep: str = "_", |
| ) -> str: |
| """The reverse of :func:`parse_locale`. It creates a locale identifier out |
| of a ``(language, territory, script, variant, modifier)`` tuple. Items can be set to |
| ``None`` and trailing ``None``\\s can also be left out of the tuple. |
| |
| >>> get_locale_identifier(('de', 'DE', None, '1999', 'custom')) |
| 'de_DE_1999@custom' |
| >>> get_locale_identifier(('fi', None, None, None, 'custom')) |
| 'fi@custom' |
| |
| |
| .. versionadded:: 1.0 |
| |
| :param tup: the tuple as returned by :func:`parse_locale`. |
| :param sep: the separator for the identifier. |
| """ |
| tup = tuple(tup[:5]) |
| lang, territory, script, variant, modifier = tup + (None,) * (5 - len(tup)) |
| ret = sep.join(filter(None, (lang, script, territory, variant))) |
| return f'{ret}@{modifier}' if modifier else ret |
|
|
|
|
| def get_cldr_version() -> str: |
| """Return the Unicode CLDR version used by this Babel installation. |
| |
| Generally, you should be able to assume that the return value of this |
| function is a string representing a version number, e.g. '47'. |
| |
| >>> get_cldr_version() |
| '47' |
| |
| .. versionadded:: 2.18 |
| |
| :rtype: str |
| """ |
| return str(get_global("cldr")["version"]) |
|
|