codekingpro/portable-devtools
114k
1"""2 babel.core3 ~~~~~~~~~~4 5 Core locale representation and locale data access.6 7 :copyright: (c) 2013-2024 by the Babel Team.8 :license: BSD, see LICENSE for more details.9"""10 11from __future__ import annotations12 13import os14import pickle15from collections.abc import Iterable, Mapping16from typing import TYPE_CHECKING, Any17 18from babel import localedata19from babel.plural import PluralRule20 21__all__ = ['UnknownLocaleError', 'Locale', 'default_locale', 'negotiate_locale',22 'parse_locale']23 24if TYPE_CHECKING:25 from typing_extensions import Literal, TypeAlias26 27 _GLOBAL_KEY: TypeAlias = Literal[28 "all_currencies",29 "currency_fractions",30 "language_aliases",31 "likely_subtags",32 "meta_zones",33 "parent_exceptions",34 "script_aliases",35 "territory_aliases",36 "territory_currencies",37 "territory_languages",38 "territory_zones",39 "variant_aliases",40 "windows_zone_mapping",41 "zone_aliases",42 "zone_territories",43 ]44 45 _global_data: Mapping[_GLOBAL_KEY, Mapping[str, Any]] | None46 47_global_data = None48_default_plural_rule = PluralRule({})49 50 51def _raise_no_data_error():52 raise RuntimeError('The babel data files are not available. '53 'This usually happens because you are using '54 'a source checkout from Babel and you did '55 'not build the data files. Just make sure '56 'to run "python setup.py import_cldr" before '57 'installing the library.')58 59 60def get_global(key: _GLOBAL_KEY) -> Mapping[str, Any]:61 """Return the dictionary for the given key in the global data.62 63 The global data is stored in the ``babel/global.dat`` file and contains64 information independent of individual locales.65 66 >>> get_global('zone_aliases')['UTC']67 u'Etc/UTC'68 >>> get_global('zone_territories')['Europe/Berlin']69 u'DE'70 71 The keys available are:72 73 - ``all_currencies``74 - ``currency_fractions``75 - ``language_aliases``76 - ``likely_subtags``77 - ``parent_exceptions``78 - ``script_aliases``79 - ``territory_aliases``80 - ``territory_currencies``81 - ``territory_languages``82 - ``territory_zones``83 - ``variant_aliases``84 - ``windows_zone_mapping``85 - ``zone_aliases``86 - ``zone_territories``87 88 .. note:: The internal structure of the data may change between versions.89 90 .. versionadded:: 0.991 92 :param key: the data key93 """94 global _global_data95 if _global_data is None:96 dirname = os.path.join(os.path.dirname(__file__))97 filename = os.path.join(dirname, 'global.dat')98 if not os.path.isfile(filename):99 _raise_no_data_error()100 with open(filename, 'rb') as fileobj:101 _global_data = pickle.load(fileobj)102 assert _global_data is not None103 return _global_data.get(key, {})104 105 106LOCALE_ALIASES = {107 'ar': 'ar_SY', 'bg': 'bg_BG', 'bs': 'bs_BA', 'ca': 'ca_ES', 'cs': 'cs_CZ',108 'da': 'da_DK', 'de': 'de_DE', 'el': 'el_GR', 'en': 'en_US', 'es': 'es_ES',109 'et': 'et_EE', 'fa': 'fa_IR', 'fi': 'fi_FI', 'fr': 'fr_FR', 'gl': 'gl_ES',110 'he': 'he_IL', 'hu': 'hu_HU', 'id': 'id_ID', 'is': 'is_IS', 'it': 'it_IT',111 'ja': 'ja_JP', 'km': 'km_KH', 'ko': 'ko_KR', 'lt': 'lt_LT', 'lv': 'lv_LV',112 'mk': 'mk_MK', 'nl': 'nl_NL', 'nn': 'nn_NO', 'no': 'nb_NO', 'pl': 'pl_PL',113 'pt': 'pt_PT', 'ro': 'ro_RO', 'ru': 'ru_RU', 'sk': 'sk_SK', 'sl': 'sl_SI',114 'sv': 'sv_SE', 'th': 'th_TH', 'tr': 'tr_TR', 'uk': 'uk_UA',115}116 117 118class UnknownLocaleError(Exception):119 """Exception thrown when a locale is requested for which no locale data120 is available.121 """122 123 def __init__(self, identifier: str) -> None:124 """Create the exception.125 126 :param identifier: the identifier string of the unsupported locale127 """128 Exception.__init__(self, f"unknown locale {identifier!r}")129 130 #: The identifier of the locale that could not be found.131 self.identifier = identifier132 133 134class Locale:135 """Representation of a specific locale.136 137 >>> locale = Locale('en', 'US')138 >>> repr(locale)139 "Locale('en', territory='US')"140 >>> locale.display_name141 u'English (United States)'142 143 A `Locale` object can also be instantiated from a raw locale string:144 145 >>> locale = Locale.parse('en-US', sep='-')146 >>> repr(locale)147 "Locale('en', territory='US')"148 149 `Locale` objects provide access to a collection of locale data, such as150 territory and language names, number and date format patterns, and more:151 152 >>> locale.number_symbols['latn']['decimal']153 u'.'154 155 If a locale is requested for which no locale data is available, an156 `UnknownLocaleError` is raised:157 158 >>> Locale.parse('en_XX')159 Traceback (most recent call last):160 ...161 UnknownLocaleError: unknown locale 'en_XX'162 163 For more information see :rfc:`3066`.164 """165 166 def __init__(167 self,168 language: str,169 territory: str | None = None,170 script: str | None = None,171 variant: str | None = None,172 modifier: str | None = None,173 ) -> None:174 """Initialize the locale object from the given identifier components.175 176 >>> locale = Locale('en', 'US')177 >>> locale.language178 'en'179 >>> locale.territory180 'US'181 182 :param language: the language code183 :param territory: the territory (country or region) code184 :param script: the script code185 :param variant: the variant code186 :param modifier: a modifier (following the '@' symbol, sometimes called '@variant')187 :raise `UnknownLocaleError`: if no locale data is available for the188 requested locale189 """190 #: the language code191 self.language = language192 #: the territory (country or region) code193 self.territory = territory194 #: the script code195 self.script = script196 #: the variant code197 self.variant = variant198 #: the modifier199 self.modifier = modifier200 self.__data: localedata.LocaleDataDict | None = None201 202 identifier = str(self)203 identifier_without_modifier = identifier.partition('@')[0]204 if not localedata.exists(identifier_without_modifier):205 raise UnknownLocaleError(identifier)206 207 @classmethod208 def default(cls, category: str | None = None, aliases: Mapping[str, str] = LOCALE_ALIASES) -> Locale:209 """Return the system default locale for the specified category.210 211 >>> for name in ['LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LC_MESSAGES']:212 ... os.environ[name] = ''213 >>> os.environ['LANG'] = 'fr_FR.UTF-8'214 >>> Locale.default('LC_MESSAGES')215 Locale('fr', territory='FR')216 217 The following fallbacks to the variable are always considered:218 219 - ``LANGUAGE``220 - ``LC_ALL``221 - ``LC_CTYPE``222 - ``LANG``223 224 :param category: one of the ``LC_XXX`` environment variable names225 :param aliases: a dictionary of aliases for locale identifiers226 """227 # XXX: use likely subtag expansion here instead of the228 # aliases dictionary.229 locale_string = default_locale(category, aliases=aliases)230 return cls.parse(locale_string)231 232 @classmethod233 def negotiate(234 cls,235 preferred: Iterable[str],236 available: Iterable[str],237 sep: str = '_',238 aliases: Mapping[str, str] = LOCALE_ALIASES,239 ) -> Locale | None:240 """Find the best match between available and requested locale strings.241 242 >>> Locale.negotiate(['de_DE', 'en_US'], ['de_DE', 'de_AT'])243 Locale('de', territory='DE')244 >>> Locale.negotiate(['de_DE', 'en_US'], ['en', 'de'])245 Locale('de')246 >>> Locale.negotiate(['de_DE', 'de'], ['en_US'])247 248 You can specify the character used in the locale identifiers to separate249 the different components. This separator is applied to both lists. Also,250 case is ignored in the comparison:251 252 >>> Locale.negotiate(['de-DE', 'de'], ['en-us', 'de-de'], sep='-')253 Locale('de', territory='DE')254 255 :param preferred: the list of locale identifiers preferred by the user256 :param available: the list of locale identifiers available257 :param aliases: a dictionary of aliases for locale identifiers258 """259 identifier = negotiate_locale(preferred, available, sep=sep,260 aliases=aliases)261 if identifier:262 return Locale.parse(identifier, sep=sep)263 return None264 265 @classmethod266 def parse(267 cls,268 identifier: str | Locale | None,269 sep: str = '_',270 resolve_likely_subtags: bool = True,271 ) -> Locale:272 """Create a `Locale` instance for the given locale identifier.273 274 >>> l = Locale.parse('de-DE', sep='-')275 >>> l.display_name276 u'Deutsch (Deutschland)'277 278 If the `identifier` parameter is not a string, but actually a `Locale`279 object, that object is returned:280 281 >>> Locale.parse(l)282 Locale('de', territory='DE')283 284 If the `identifier` parameter is neither of these, such as `None`285 e.g. because a default locale identifier could not be determined,286 a `TypeError` is raised:287 288 >>> Locale.parse(None)289 Traceback (most recent call last):290 ...291 TypeError: ...292 293 This also can perform resolving of likely subtags which it does294 by default. This is for instance useful to figure out the most295 likely locale for a territory you can use ``'und'`` as the296 language tag:297 298 >>> Locale.parse('und_AT')299 Locale('de', territory='AT')300 301 Modifiers are optional, and always at the end, separated by "@":302 303 >>> Locale.parse('de_AT@euro')304 Locale('de', territory='AT', modifier='euro')305 306 :param identifier: the locale identifier string307 :param sep: optional component separator308 :param resolve_likely_subtags: if this is specified then a locale will309 have its likely subtag resolved if the310 locale otherwise does not exist. For311 instance ``zh_TW`` by itself is not a312 locale that exists but Babel can313 automatically expand it to the full314 form of ``zh_hant_TW``. Note that this315 expansion is only taking place if no316 locale exists otherwise. For instance317 there is a locale ``en`` that can exist318 by itself.319 :raise `ValueError`: if the string does not appear to be a valid locale320 identifier321 :raise `UnknownLocaleError`: if no locale data is available for the322 requested locale323 :raise `TypeError`: if the identifier is not a string or a `Locale`324 """325 if isinstance(identifier, Locale):326 return identifier327 elif not isinstance(identifier, str):328 raise TypeError(f"Unexpected value for identifier: {identifier!r}")329 330 parts = parse_locale(identifier, sep=sep)331 input_id = get_locale_identifier(parts)332 333 def _try_load(parts):334 try:335 return cls(*parts)336 except UnknownLocaleError:337 return None338 339 def _try_load_reducing(parts):340 # Success on first hit, return it.341 locale = _try_load(parts)342 if locale is not None:343 return locale344 345 # Now try without script and variant346 locale = _try_load(parts[:2])347 if locale is not None:348 return locale349 350 locale = _try_load(parts)351 if locale is not None:352 return locale353 if not resolve_likely_subtags:354 raise UnknownLocaleError(input_id)355 356 # From here onwards is some very bad likely subtag resolving. This357 # whole logic is not entirely correct but good enough (tm) for the358 # time being. This has been added so that zh_TW does not cause359 # errors for people when they upgrade. Later we should properly360 # implement ICU like fuzzy locale objects and provide a way to361 # maximize and minimize locale tags.362 363 if len(parts) == 5:364 language, territory, script, variant, modifier = parts365 else:366 language, territory, script, variant = parts367 modifier = None368 language = get_global('language_aliases').get(language, language)369 territory = get_global('territory_aliases').get(territory or '', (territory,))[0]370 script = get_global('script_aliases').get(script or '', script)371 variant = get_global('variant_aliases').get(variant or '', variant)372 373 if territory == 'ZZ':374 territory = None375 if script == 'Zzzz':376 script = None377 378 parts = language, territory, script, variant, modifier379 380 # First match: try the whole identifier381 new_id = get_locale_identifier(parts)382 likely_subtag = get_global('likely_subtags').get(new_id)383 if likely_subtag is not None:384 locale = _try_load_reducing(parse_locale(likely_subtag))385 if locale is not None:386 return locale387 388 # If we did not find anything so far, try again with a389 # simplified identifier that is just the language390 likely_subtag = get_global('likely_subtags').get(language)391 if likely_subtag is not None:392 parts2 = parse_locale(likely_subtag)393 if len(parts2) == 5:394 language2, _, script2, variant2, modifier2 = parts2395 else:396 language2, _, script2, variant2 = parts2397 modifier2 = None398 locale = _try_load_reducing((language2, territory, script2, variant2, modifier2))399 if locale is not None:400 return locale401 402 raise UnknownLocaleError(input_id)403 404 def __eq__(self, other: object) -> bool:405 for key in ('language', 'territory', 'script', 'variant', 'modifier'):406 if not hasattr(other, key):407 return False408 return (409 self.language == getattr(other, 'language') and # noqa: B009410 self.territory == getattr(other, 'territory') and # noqa: B009411 self.script == getattr(other, 'script') and # noqa: B009412 self.variant == getattr(other, 'variant') and # noqa: B009413 self.modifier == getattr(other, 'modifier') # noqa: B009414 )415 416 def __ne__(self, other: object) -> bool:417 return not self.__eq__(other)418 419 def __hash__(self) -> int:420 return hash((self.language, self.territory, self.script,421 self.variant, self.modifier))422 423 def __repr__(self) -> str:424 parameters = ['']425 for key in ('territory', 'script', 'variant', 'modifier'):426 value = getattr(self, key)427 if value is not None:428 parameters.append(f"{key}={value!r}")429 return f"Locale({self.language!r}{', '.join(parameters)})"430 431 def __str__(self) -> str:432 return get_locale_identifier((self.language, self.territory,433 self.script, self.variant,434 self.modifier))435 436 @property437 def _data(self) -> localedata.LocaleDataDict:438 if self.__data is None:439 self.__data = localedata.LocaleDataDict(localedata.load(str(self)))440 return self.__data441 442 def get_display_name(self, locale: Locale | str | None = None) -> str | None:443 """Return the display name of the locale using the given locale.444 445 The display name will include the language, territory, script, and446 variant, if those are specified.447 448 >>> Locale('zh', 'CN', script='Hans').get_display_name('en')449 u'Chinese (Simplified, China)'450 451 Modifiers are currently passed through verbatim:452 453 >>> Locale('it', 'IT', modifier='euro').get_display_name('en')454 u'Italian (Italy, euro)'455 456 :param locale: the locale to use457 """458 if locale is None:459 locale = self460 locale = Locale.parse(locale)461 retval = locale.languages.get(self.language)462 if retval and (self.territory or self.script or self.variant):463 details = []464 if self.script:465 details.append(locale.scripts.get(self.script))466 if self.territory:467 details.append(locale.territories.get(self.territory))468 if self.variant:469 details.append(locale.variants.get(self.variant))470 if self.modifier:471 details.append(self.modifier)472 detail_string = ', '.join(atom for atom in details if atom)473 if detail_string:474 retval += f" ({detail_string})"475 return retval476 477 display_name = property(get_display_name, doc="""\478 The localized display name of the locale.479 480 >>> Locale('en').display_name481 u'English'482 >>> Locale('en', 'US').display_name483 u'English (United States)'484 >>> Locale('sv').display_name485 u'svenska'486 487 :type: `unicode`488 """)489 490 def get_language_name(self, locale: Locale | str | None = None) -> str | None:491 """Return the language of this locale in the given locale.492 493 >>> Locale('zh', 'CN', script='Hans').get_language_name('de')494 u'Chinesisch'495 496 .. versionadded:: 1.0497 498 :param locale: the locale to use499 """500 if locale is None:501 locale = self502 locale = Locale.parse(locale)503 return locale.languages.get(self.language)504 505 language_name = property(get_language_name, doc="""\506 The localized language name of the locale.507 508 >>> Locale('en', 'US').language_name509 u'English'510 """)511 512 def get_territory_name(self, locale: Locale | str | None = None) -> str | None:513 """Return the territory name in the given locale."""514 if locale is None:515 locale = self516 locale = Locale.parse(locale)517 return locale.territories.get(self.territory or '')518 519 territory_name = property(get_territory_name, doc="""\520 The localized territory name of the locale if available.521 522 >>> Locale('de', 'DE').territory_name523 u'Deutschland'524 """)525 526 def get_script_name(self, locale: Locale | str | None = None) -> str | None:527 """Return the script name in the given locale."""528 if locale is None:529 locale = self530 locale = Locale.parse(locale)531 return locale.scripts.get(self.script or '')532 533 script_name = property(get_script_name, doc="""\534 The localized script name of the locale if available.535 536 >>> Locale('sr', 'ME', script='Latn').script_name537 u'latinica'538 """)539 540 @property541 def english_name(self) -> str | None:542 """The english display name of the locale.543 544 >>> Locale('de').english_name545 u'German'546 >>> Locale('de', 'DE').english_name547 u'German (Germany)'548 549 :type: `unicode`"""550 return self.get_display_name(Locale('en'))551 552 # { General Locale Display Names553 554 @property555 def languages(self) -> localedata.LocaleDataDict:556 """Mapping of language codes to translated language names.557 558 >>> Locale('de', 'DE').languages['ja']559 u'Japanisch'560 561 See `ISO 639 <http://www.loc.gov/standards/iso639-2/>`_ for562 more information.563 """564 return self._data['languages']565 566 @property567 def scripts(self) -> localedata.LocaleDataDict:568 """Mapping of script codes to translated script names.569 570 >>> Locale('en', 'US').scripts['Hira']571 u'Hiragana'572 573 See `ISO 15924 <http://www.evertype.com/standards/iso15924/>`_574 for more information.575 """576 return self._data['scripts']577 578 @property579 def territories(self) -> localedata.LocaleDataDict:580 """Mapping of script codes to translated script names.581 582 >>> Locale('es', 'CO').territories['DE']583 u'Alemania'584 585 See `ISO 3166 <http://www.iso.org/iso/en/prods-services/iso3166ma/>`_586 for more information.587 """588 return self._data['territories']589 590 @property591 def variants(self) -> localedata.LocaleDataDict:592 """Mapping of script codes to translated script names.593 594 >>> Locale('de', 'DE').variants['1901']595 u'Alte deutsche Rechtschreibung'596 """597 return self._data['variants']598 599 # { Number Formatting600 601 @property602 def currencies(self) -> localedata.LocaleDataDict:603 """Mapping of currency codes to translated currency names. This604 only returns the generic form of the currency name, not the count605 specific one. If an actual number is requested use the606 :func:`babel.numbers.get_currency_name` function.607 608 >>> Locale('en').currencies['COP']609 u'Colombian Peso'610 >>> Locale('de', 'DE').currencies['COP']611 u'Kolumbianischer Peso'612 """613 return self._data['currency_names']614 615 @property616 def currency_symbols(self) -> localedata.LocaleDataDict:617 """Mapping of currency codes to symbols.618 619 >>> Locale('en', 'US').currency_symbols['USD']620 u'$'621 >>> Locale('es', 'CO').currency_symbols['USD']622 u'US$'623 """624 return self._data['currency_symbols']625 626 @property627 def number_symbols(self) -> localedata.LocaleDataDict:628 """Symbols used in number formatting by number system.629 630 .. note:: The format of the value returned may change between631 Babel versions.632 633 >>> Locale('fr', 'FR').number_symbols["latn"]['decimal']634 u','635 >>> Locale('fa', 'IR').number_symbols["arabext"]['decimal']636 u'٫'637 >>> Locale('fa', 'IR').number_symbols["latn"]['decimal']638 u'.'639 """640 return self._data['number_symbols']641 642 @property643 def other_numbering_systems(self) -> localedata.LocaleDataDict:644 """645 Mapping of other numbering systems available for the locale.646 See: https://www.unicode.org/reports/tr35/tr35-numbers.html#otherNumberingSystems647 648 >>> Locale('el', 'GR').other_numbering_systems['traditional']649 u'grek'650 651 .. note:: The format of the value returned may change between652 Babel versions.653 """654 return self._data['numbering_systems']655 656 @property657 def default_numbering_system(self) -> str:658 """The default numbering system used by the locale.659 >>> Locale('el', 'GR').default_numbering_system660 u'latn'661 """662 return self._data['default_numbering_system']663 664 @property665 def decimal_formats(self) -> localedata.LocaleDataDict:666 """Locale patterns for decimal number formatting.667 668 .. note:: The format of the value returned may change between669 Babel versions.670 671 >>> Locale('en', 'US').decimal_formats[None]672 <NumberPattern u'#,##0.###'>673 """674 return self._data['decimal_formats']675 676 @property677 def compact_decimal_formats(self) -> localedata.LocaleDataDict:678 """Locale patterns for compact decimal number formatting.679 680 .. note:: The format of the value returned may change between681 Babel versions.682 683 >>> Locale('en', 'US').compact_decimal_formats["short"]["one"]["1000"]684 <NumberPattern u'0K'>685 """686 return self._data['compact_decimal_formats']687 688 @property689 def currency_formats(self) -> localedata.LocaleDataDict:690 """Locale patterns for currency number formatting.691 692 .. note:: The format of the value returned may change between693 Babel versions.694 695 >>> Locale('en', 'US').currency_formats['standard']696 <NumberPattern u'\\xa4#,##0.00'>697 >>> Locale('en', 'US').currency_formats['accounting']698 <NumberPattern u'\\xa4#,##0.00;(\\xa4#,##0.00)'>699 """700 return self._data['currency_formats']701 702 @property703 def compact_currency_formats(self) -> localedata.LocaleDataDict:704 """Locale patterns for compact currency number formatting.705 706 .. note:: The format of the value returned may change between707 Babel versions.708 709 >>> Locale('en', 'US').compact_currency_formats["short"]["one"]["1000"]710 <NumberPattern u'¤0K'>711 """712 return self._data['compact_currency_formats']713 714 @property715 def percent_formats(self) -> localedata.LocaleDataDict:716 """Locale patterns for percent number formatting.717 718 .. note:: The format of the value returned may change between719 Babel versions.720 721 >>> Locale('en', 'US').percent_formats[None]722 <NumberPattern u'#,##0%'>723 """724 return self._data['percent_formats']725 726 @property727 def scientific_formats(self) -> localedata.LocaleDataDict:728 """Locale patterns for scientific number formatting.729 730 .. note:: The format of the value returned may change between731 Babel versions.732 733 >>> Locale('en', 'US').scientific_formats[None]734 <NumberPattern u'#E0'>735 """736 return self._data['scientific_formats']737 738 # { Calendar Information and Date Formatting739 740 @property741 def periods(self) -> localedata.LocaleDataDict:742 """Locale display names for day periods (AM/PM).743 744 >>> Locale('en', 'US').periods['am']745 u'AM'746 """747 try:748 return self._data['day_periods']['stand-alone']['wide']749 except KeyError:750 return localedata.LocaleDataDict({}) # pragma: no cover751 752 @property753 def day_periods(self) -> localedata.LocaleDataDict:754 """Locale display names for various day periods (not necessarily only AM/PM).755 756 These are not meant to be used without the relevant `day_period_rules`.757 """758 return self._data['day_periods']759 760 @property761 def day_period_rules(self) -> localedata.LocaleDataDict:762 """Day period rules for the locale. Used by `get_period_id`.763 """764 return self._data.get('day_period_rules', localedata.LocaleDataDict({}))765 766 @property767 def days(self) -> localedata.LocaleDataDict:768 """Locale display names for weekdays.769 770 >>> Locale('de', 'DE').days['format']['wide'][3]771 u'Donnerstag'772 """773 return self._data['days']774 775 @property776 def months(self) -> localedata.LocaleDataDict:777 """Locale display names for months.778 779 >>> Locale('de', 'DE').months['format']['wide'][10]780 u'Oktober'781 """782 return self._data['months']783 784 @property785 def quarters(self) -> localedata.LocaleDataDict:786 """Locale display names for quarters.787 788 >>> Locale('de', 'DE').quarters['format']['wide'][1]789 u'1. Quartal'790 """791 return self._data['quarters']792 793 @property794 def eras(self) -> localedata.LocaleDataDict:795 """Locale display names for eras.796 797 .. note:: The format of the value returned may change between798 Babel versions.799 800 >>> Locale('en', 'US').eras['wide'][1]801 u'Anno Domini'802 >>> Locale('en', 'US').eras['abbreviated'][0]803 u'BC'804 """805 return self._data['eras']806 807 @property808 def time_zones(self) -> localedata.LocaleDataDict:809 """Locale display names for time zones.810 811 .. note:: The format of the value returned may change between812 Babel versions.813 814 >>> Locale('en', 'US').time_zones['Europe/London']['long']['daylight']815 u'British Summer Time'816 >>> Locale('en', 'US').time_zones['America/St_Johns']['city']817 u'St. John\u2019s'818 """819 return self._data['time_zones']820 821 @property822 def meta_zones(self) -> localedata.LocaleDataDict:823 """Locale display names for meta time zones.824 825 Meta time zones are basically groups of different Olson time zones that826 have the same GMT offset and daylight savings time.827 828 .. note:: The format of the value returned may change between829 Babel versions.830 831 >>> Locale('en', 'US').meta_zones['Europe_Central']['long']['daylight']832 u'Central European Summer Time'833 834 .. versionadded:: 0.9835 """836 return self._data['meta_zones']837 838 @property839 def zone_formats(self) -> localedata.LocaleDataDict:840 """Patterns related to the formatting of time zones.841 842 .. note:: The format of the value returned may change between843 Babel versions.844 845 >>> Locale('en', 'US').zone_formats['fallback']846 u'%(1)s (%(0)s)'847 >>> Locale('pt', 'BR').zone_formats['region']848 u'Hor\\xe1rio %s'849 850 .. versionadded:: 0.9851 """852 return self._data['zone_formats']853 854 @property855 def first_week_day(self) -> int:856 """The first day of a week, with 0 being Monday.857 858 >>> Locale('de', 'DE').first_week_day859 0860 >>> Locale('en', 'US').first_week_day861 6862 """863 return self._data['week_data']['first_day']864 865 @property866 def weekend_start(self) -> int:867 """The day the weekend starts, with 0 being Monday.868 869 >>> Locale('de', 'DE').weekend_start870 5871 """872 return self._data['week_data']['weekend_start']873 874 @property875 def weekend_end(self) -> int:876 """The day the weekend ends, with 0 being Monday.877 878 >>> Locale('de', 'DE').weekend_end879 6880 """881 return self._data['week_data']['weekend_end']882 883 @property884 def min_week_days(self) -> int:885 """The minimum number of days in a week so that the week is counted as886 the first week of a year or month.887 888 >>> Locale('de', 'DE').min_week_days889 4890 """891 return self._data['week_data']['min_days']892 893 @property894 def date_formats(self) -> localedata.LocaleDataDict:895 """Locale patterns for date formatting.896 897 .. note:: The format of the value returned may change between898 Babel versions.899 900 >>> Locale('en', 'US').date_formats['short']901 <DateTimePattern u'M/d/yy'>902 >>> Locale('fr', 'FR').date_formats['long']903 <DateTimePattern u'd MMMM y'>904 """905 return self._data['date_formats']906 907 @property908 def time_formats(self) -> localedata.LocaleDataDict:909 """Locale patterns for time formatting.910 911 .. note:: The format of the value returned may change between912 Babel versions.913 914 >>> Locale('en', 'US').time_formats['short']915 <DateTimePattern u'h:mm\u202fa'>916 >>> Locale('fr', 'FR').time_formats['long']917 <DateTimePattern u'HH:mm:ss z'>918 """919 return self._data['time_formats']920 921 @property922 def datetime_formats(self) -> localedata.LocaleDataDict:923 """Locale patterns for datetime formatting.924 925 .. note:: The format of the value returned may change between926 Babel versions.927 928 >>> Locale('en').datetime_formats['full']929 u'{1}, {0}'930 >>> Locale('th').datetime_formats['medium']931 u'{1} {0}'932 """933 return self._data['datetime_formats']934 935 @property936 def datetime_skeletons(self) -> localedata.LocaleDataDict:937 """Locale patterns for formatting parts of a datetime.938 939 >>> Locale('en').datetime_skeletons['MEd']940 <DateTimePattern u'E, M/d'>941 >>> Locale('fr').datetime_skeletons['MEd']942 <DateTimePattern u'E dd/MM'>943 >>> Locale('fr').datetime_skeletons['H']944 <DateTimePattern u"HH 'h'">945 """946 return self._data['datetime_skeletons']947 948 @property949 def interval_formats(self) -> localedata.LocaleDataDict:950 """Locale patterns for interval formatting.951 952 .. note:: The format of the value returned may change between953 Babel versions.954 955 How to format date intervals in Finnish when the day is the956 smallest changing component:957 958 >>> Locale('fi_FI').interval_formats['MEd']['d']959 [u'E d.\u2009\u2013\u2009', u'E d.M.']960 961 .. seealso::962 963 The primary API to use this data is :py:func:`babel.dates.format_interval`.964 965 966 :rtype: dict[str, dict[str, list[str]]]967 """968 return self._data['interval_formats']969 970 @property971 def plural_form(self) -> PluralRule:972 """Plural rules for the locale.973 974 >>> Locale('en').plural_form(1)975 'one'976 >>> Locale('en').plural_form(0)977 'other'978 >>> Locale('fr').plural_form(0)979 'one'980 >>> Locale('ru').plural_form(100)981 'many'982 """983 return self._data.get('plural_form', _default_plural_rule)984 985 @property986 def list_patterns(self) -> localedata.LocaleDataDict:987 """Patterns for generating lists988 989 .. note:: The format of the value returned may change between990 Babel versions.991 992 >>> Locale('en').list_patterns['standard']['start']993 u'{0}, {1}'994 >>> Locale('en').list_patterns['standard']['end']995 u'{0}, and {1}'996 >>> Locale('en_GB').list_patterns['standard']['end']997 u'{0} and {1}'998 """999 return self._data['list_patterns']1000 1001 @property1002 def ordinal_form(self) -> PluralRule:1003 """Plural rules for the locale.1004 1005 >>> Locale('en').ordinal_form(1)1006 'one'1007 >>> Locale('en').ordinal_form(2)1008 'two'1009 >>> Locale('en').ordinal_form(3)1010 'few'1011 >>> Locale('fr').ordinal_form(2)1012 'other'1013 >>> Locale('ru').ordinal_form(100)1014 'other'1015 """1016 return self._data.get('ordinal_form', _default_plural_rule)1017 1018 @property1019 def measurement_systems(self) -> localedata.LocaleDataDict:1020 """Localized names for various measurement systems.1021 1022 >>> Locale('fr', 'FR').measurement_systems['US']1023 u'am\\xe9ricain'1024 >>> Locale('en', 'US').measurement_systems['US']1025 u'US'1026 1027 """1028 return self._data['measurement_systems']1029 1030 @property1031 def character_order(self) -> str:1032 """The text direction for the language.1033 1034 >>> Locale('de', 'DE').character_order1035 'left-to-right'1036 >>> Locale('ar', 'SA').character_order1037 'right-to-left'1038 """1039 return self._data['character_order']1040 1041 @property1042 def text_direction(self) -> str:1043 """The text direction for the language in CSS short-hand form.1044 1045 >>> Locale('de', 'DE').text_direction1046 'ltr'1047 >>> Locale('ar', 'SA').text_direction1048 'rtl'1049 """1050 return ''.join(word[0] for word in self.character_order.split('-'))1051 1052 @property1053 def unit_display_names(self) -> localedata.LocaleDataDict:1054 """Display names for units of measurement.1055 1056 .. seealso::1057 1058 You may want to use :py:func:`babel.units.get_unit_name` instead.1059 1060 .. note:: The format of the value returned may change between1061 Babel versions.1062 1063 """1064 return self._data['unit_display_names']1065 1066 1067def default_locale(category: str | None = None, aliases: Mapping[str, str] = LOCALE_ALIASES) -> str | None:1068 """Returns the system default locale for a given category, based on1069 environment variables.1070 1071 >>> for name in ['LANGUAGE', 'LC_ALL', 'LC_CTYPE']:1072 ... os.environ[name] = ''1073 >>> os.environ['LANG'] = 'fr_FR.UTF-8'1074 >>> default_locale('LC_MESSAGES')1075 'fr_FR'1076 1077 The "C" or "POSIX" pseudo-locales are treated as aliases for the1078 "en_US_POSIX" locale:1079 1080 >>> os.environ['LC_MESSAGES'] = 'POSIX'1081 >>> default_locale('LC_MESSAGES')1082 'en_US_POSIX'1083 1084 The following fallbacks to the variable are always considered:1085 1086 - ``LANGUAGE``1087 - ``LC_ALL``1088 - ``LC_CTYPE``1089 - ``LANG``1090 1091 :param category: one of the ``LC_XXX`` environment variable names1092 :param aliases: a dictionary of aliases for locale identifiers1093 """1094 varnames = (category, 'LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LANG')1095 for name in filter(None, varnames):1096 locale = os.getenv(name)1097 if locale:1098 if name == 'LANGUAGE' and ':' in locale:1099 # the LANGUAGE variable may contain a colon-separated list of1100 # language codes; we just pick the language on the list1101 locale = locale.split(':')[0]1102 if locale.split('.')[0] in ('C', 'POSIX'):1103 locale = 'en_US_POSIX'1104 elif aliases and locale in aliases:1105 locale = aliases[locale]1106 try:1107 return get_locale_identifier(parse_locale(locale))1108 except ValueError:1109 pass1110 return None1111 1112 1113def negotiate_locale(preferred: Iterable[str], available: Iterable[str], sep: str = '_', aliases: Mapping[str, str] = LOCALE_ALIASES) -> str | None:1114 """Find the best match between available and requested locale strings.1115 1116 >>> negotiate_locale(['de_DE', 'en_US'], ['de_DE', 'de_AT'])1117 'de_DE'1118 >>> negotiate_locale(['de_DE', 'en_US'], ['en', 'de'])1119 'de'1120 1121 Case is ignored by the algorithm, the result uses the case of the preferred1122 locale identifier:1123 1124 >>> negotiate_locale(['de_DE', 'en_US'], ['de_de', 'de_at'])1125 'de_DE'1126 1127 >>> negotiate_locale(['de_DE', 'en_US'], ['de_de', 'de_at'])1128 'de_DE'1129 1130 By default, some web browsers unfortunately do not include the territory1131 in the locale identifier for many locales, and some don't even allow the1132 user to easily add the territory. So while you may prefer using qualified1133 locale identifiers in your web-application, they would not normally match1134 the language-only locale sent by such browsers. To workaround that, this1135 function uses a default mapping of commonly used language-only locale1136 identifiers to identifiers including the territory:1137 1138 >>> negotiate_locale(['ja', 'en_US'], ['ja_JP', 'en_US'])1139 'ja_JP'1140 1141 Some browsers even use an incorrect or outdated language code, such as "no"1142 for Norwegian, where the correct locale identifier would actually be "nb_NO"1143 (Bokmål) or "nn_NO" (Nynorsk). The aliases are intended to take care of1144 such cases, too:1145 1146 >>> negotiate_locale(['no', 'sv'], ['nb_NO', 'sv_SE'])1147 'nb_NO'1148 1149 You can override this default mapping by passing a different `aliases`1150 dictionary to this function, or you can bypass the behavior althogher by1151 setting the `aliases` parameter to `None`.1152 1153 :param preferred: the list of locale strings preferred by the user1154 :param available: the list of locale strings available1155 :param sep: character that separates the different parts of the locale1156 strings1157 :param aliases: a dictionary of aliases for locale identifiers1158 """1159 available = [a.lower() for a in available if a]1160 for locale in preferred:1161 ll = locale.lower()1162 if ll in available:1163 return locale1164 if aliases:1165 alias = aliases.get(ll)1166 if alias:1167 alias = alias.replace('_', sep)1168 if alias.lower() in available:1169 return alias1170 parts = locale.split(sep)1171 if len(parts) > 1 and parts[0].lower() in available:1172 return parts[0]1173 return None1174 1175 1176def parse_locale(1177 identifier: str,1178 sep: str = '_',1179) -> tuple[str, str | None, str | None, str | None] | tuple[str, str | None, str | None, str | None, str | None]:1180 """Parse a locale identifier into a tuple of the form ``(language,1181 territory, script, variant, modifier)``.1182 1183 >>> parse_locale('zh_CN')1184 ('zh', 'CN', None, None)1185 >>> parse_locale('zh_Hans_CN')1186 ('zh', 'CN', 'Hans', None)1187 >>> parse_locale('ca_es_valencia')1188 ('ca', 'ES', None, 'VALENCIA')1189 >>> parse_locale('en_150')1190 ('en', '150', None, None)1191 >>> parse_locale('en_us_posix')1192 ('en', 'US', None, 'POSIX')1193 >>> parse_locale('it_IT@euro')1194 ('it', 'IT', None, None, 'euro')1195 >>> parse_locale('it_IT@custom')1196 ('it', 'IT', None, None, 'custom')1197 >>> parse_locale('it_IT@')1198 ('it', 'IT', None, None)1199 1200 The default component separator is "_", but a different separator can be