Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/babel/core.py: 37%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

376 statements  

1""" 

2 babel.core 

3 ~~~~~~~~~~ 

4 

5 Core locale representation and locale data access. 

6 

7 :copyright: (c) 2013-2025 by the Babel Team. 

8 :license: BSD, see LICENSE for more details. 

9""" 

10 

11from __future__ import annotations 

12 

13import os 

14import pickle 

15from collections.abc import Iterable, Mapping 

16from typing import TYPE_CHECKING, Any, Literal 

17 

18from babel import localedata 

19from babel.plural import PluralRule 

20 

21__all__ = [ 

22 'Locale', 

23 'UnknownLocaleError', 

24 'default_locale', 

25 'get_global', 

26 'get_locale_identifier', 

27 'negotiate_locale', 

28 'parse_locale', 

29] 

30 

31if TYPE_CHECKING: 

32 from typing_extensions import TypeAlias 

33 

34 _GLOBAL_KEY: TypeAlias = Literal[ 

35 "all_currencies", 

36 "currency_fractions", 

37 "language_aliases", 

38 "likely_subtags", 

39 "meta_zones", 

40 "parent_exceptions", 

41 "script_aliases", 

42 "territory_aliases", 

43 "territory_currencies", 

44 "territory_languages", 

45 "territory_zones", 

46 "variant_aliases", 

47 "windows_zone_mapping", 

48 "zone_aliases", 

49 "zone_territories", 

50 ] 

51 

52 _global_data: Mapping[_GLOBAL_KEY, Mapping[str, Any]] | None 

53 

54_global_data = None 

55_default_plural_rule = PluralRule({}) 

56 

57 

58def _raise_no_data_error(): 

59 raise RuntimeError('The babel data files are not available. ' 

60 'This usually happens because you are using ' 

61 'a source checkout from Babel and you did ' 

62 'not build the data files. Just make sure ' 

63 'to run "python setup.py import_cldr" before ' 

64 'installing the library.') 

65 

66 

67def get_global(key: _GLOBAL_KEY) -> Mapping[str, Any]: 

68 """Return the dictionary for the given key in the global data. 

69 

70 The global data is stored in the ``babel/global.dat`` file and contains 

71 information independent of individual locales. 

72 

73 >>> get_global('zone_aliases')['UTC'] 

74 u'Etc/UTC' 

75 >>> get_global('zone_territories')['Europe/Berlin'] 

76 u'DE' 

77 

78 The keys available are: 

79 

80 - ``all_currencies`` 

81 - ``currency_fractions`` 

82 - ``language_aliases`` 

83 - ``likely_subtags`` 

84 - ``parent_exceptions`` 

85 - ``script_aliases`` 

86 - ``territory_aliases`` 

87 - ``territory_currencies`` 

88 - ``territory_languages`` 

89 - ``territory_zones`` 

90 - ``variant_aliases`` 

91 - ``windows_zone_mapping`` 

92 - ``zone_aliases`` 

93 - ``zone_territories`` 

94 

95 .. note:: The internal structure of the data may change between versions. 

96 

97 .. versionadded:: 0.9 

98 

99 :param key: the data key 

100 """ 

101 global _global_data 

102 if _global_data is None: 

103 dirname = os.path.join(os.path.dirname(__file__)) 

104 filename = os.path.join(dirname, 'global.dat') 

105 if not os.path.isfile(filename): 

106 _raise_no_data_error() 

107 with open(filename, 'rb') as fileobj: 

108 _global_data = pickle.load(fileobj) 

109 assert _global_data is not None 

110 return _global_data.get(key, {}) 

111 

112 

113LOCALE_ALIASES = { 

114 'ar': 'ar_SY', 'bg': 'bg_BG', 'bs': 'bs_BA', 'ca': 'ca_ES', 'cs': 'cs_CZ', 

115 'da': 'da_DK', 'de': 'de_DE', 'el': 'el_GR', 'en': 'en_US', 'es': 'es_ES', 

116 'et': 'et_EE', 'fa': 'fa_IR', 'fi': 'fi_FI', 'fr': 'fr_FR', 'gl': 'gl_ES', 

117 'he': 'he_IL', 'hu': 'hu_HU', 'id': 'id_ID', 'is': 'is_IS', 'it': 'it_IT', 

118 'ja': 'ja_JP', 'km': 'km_KH', 'ko': 'ko_KR', 'lt': 'lt_LT', 'lv': 'lv_LV', 

119 'mk': 'mk_MK', 'nl': 'nl_NL', 'nn': 'nn_NO', 'no': 'nb_NO', 'pl': 'pl_PL', 

120 'pt': 'pt_PT', 'ro': 'ro_RO', 'ru': 'ru_RU', 'sk': 'sk_SK', 'sl': 'sl_SI', 

121 'sv': 'sv_SE', 'th': 'th_TH', 'tr': 'tr_TR', 'uk': 'uk_UA', 

122} 

123 

124 

125class UnknownLocaleError(Exception): 

126 """Exception thrown when a locale is requested for which no locale data 

127 is available. 

128 """ 

129 

130 def __init__(self, identifier: str) -> None: 

131 """Create the exception. 

132 

133 :param identifier: the identifier string of the unsupported locale 

134 """ 

135 Exception.__init__(self, f"unknown locale {identifier!r}") 

136 

137 #: The identifier of the locale that could not be found. 

138 self.identifier = identifier 

139 

140 

141class Locale: 

142 """Representation of a specific locale. 

143 

144 >>> locale = Locale('en', 'US') 

145 >>> repr(locale) 

146 "Locale('en', territory='US')" 

147 >>> locale.display_name 

148 u'English (United States)' 

149 

150 A `Locale` object can also be instantiated from a raw locale string: 

151 

152 >>> locale = Locale.parse('en-US', sep='-') 

153 >>> repr(locale) 

154 "Locale('en', territory='US')" 

155 

156 `Locale` objects provide access to a collection of locale data, such as 

157 territory and language names, number and date format patterns, and more: 

158 

159 >>> locale.number_symbols['latn']['decimal'] 

160 u'.' 

161 

162 If a locale is requested for which no locale data is available, an 

163 `UnknownLocaleError` is raised: 

164 

165 >>> Locale.parse('en_XX') 

166 Traceback (most recent call last): 

167 ... 

168 UnknownLocaleError: unknown locale 'en_XX' 

169 

170 For more information see :rfc:`3066`. 

171 """ 

172 

173 def __init__( 

174 self, 

175 language: str, 

176 territory: str | None = None, 

177 script: str | None = None, 

178 variant: str | None = None, 

179 modifier: str | None = None, 

180 ) -> None: 

181 """Initialize the locale object from the given identifier components. 

182 

183 >>> locale = Locale('en', 'US') 

184 >>> locale.language 

185 'en' 

186 >>> locale.territory 

187 'US' 

188 

189 :param language: the language code 

190 :param territory: the territory (country or region) code 

191 :param script: the script code 

192 :param variant: the variant code 

193 :param modifier: a modifier (following the '@' symbol, sometimes called '@variant') 

194 :raise `UnknownLocaleError`: if no locale data is available for the 

195 requested locale 

196 """ 

197 #: the language code 

198 self.language = language 

199 #: the territory (country or region) code 

200 self.territory = territory 

201 #: the script code 

202 self.script = script 

203 #: the variant code 

204 self.variant = variant 

205 #: the modifier 

206 self.modifier = modifier 

207 self.__data: localedata.LocaleDataDict | None = None 

208 

209 identifier = str(self) 

210 identifier_without_modifier = identifier.partition('@')[0] 

211 if localedata.exists(identifier): 

212 self.__data_identifier = identifier 

213 elif localedata.exists(identifier_without_modifier): 

214 self.__data_identifier = identifier_without_modifier 

215 else: 

216 raise UnknownLocaleError(identifier) 

217 

218 @classmethod 

219 def default(cls, category: str | None = None, aliases: Mapping[str, str] = LOCALE_ALIASES) -> Locale: 

220 """Return the system default locale for the specified category. 

221 

222 >>> for name in ['LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LC_MESSAGES']: 

223 ... os.environ[name] = '' 

224 >>> os.environ['LANG'] = 'fr_FR.UTF-8' 

225 >>> Locale.default('LC_MESSAGES') 

226 Locale('fr', territory='FR') 

227 

228 The following fallbacks to the variable are always considered: 

229 

230 - ``LANGUAGE`` 

231 - ``LC_ALL`` 

232 - ``LC_CTYPE`` 

233 - ``LANG`` 

234 

235 :param category: one of the ``LC_XXX`` environment variable names 

236 :param aliases: a dictionary of aliases for locale identifiers 

237 """ 

238 # XXX: use likely subtag expansion here instead of the 

239 # aliases dictionary. 

240 locale_string = default_locale(category, aliases=aliases) 

241 return cls.parse(locale_string) 

242 

243 @classmethod 

244 def negotiate( 

245 cls, 

246 preferred: Iterable[str], 

247 available: Iterable[str], 

248 sep: str = '_', 

249 aliases: Mapping[str, str] = LOCALE_ALIASES, 

250 ) -> Locale | None: 

251 """Find the best match between available and requested locale strings. 

252 

253 >>> Locale.negotiate(['de_DE', 'en_US'], ['de_DE', 'de_AT']) 

254 Locale('de', territory='DE') 

255 >>> Locale.negotiate(['de_DE', 'en_US'], ['en', 'de']) 

256 Locale('de') 

257 >>> Locale.negotiate(['de_DE', 'de'], ['en_US']) 

258 

259 You can specify the character used in the locale identifiers to separate 

260 the different components. This separator is applied to both lists. Also, 

261 case is ignored in the comparison: 

262 

263 >>> Locale.negotiate(['de-DE', 'de'], ['en-us', 'de-de'], sep='-') 

264 Locale('de', territory='DE') 

265 

266 :param preferred: the list of locale identifiers preferred by the user 

267 :param available: the list of locale identifiers available 

268 :param aliases: a dictionary of aliases for locale identifiers 

269 :param sep: separator for parsing; e.g. Windows tends to use '-' instead of '_'. 

270 """ 

271 identifier = negotiate_locale(preferred, available, sep=sep, 

272 aliases=aliases) 

273 if identifier: 

274 return Locale.parse(identifier, sep=sep) 

275 return None 

276 

277 @classmethod 

278 def parse( 

279 cls, 

280 identifier: Locale | str | None, 

281 sep: str = '_', 

282 resolve_likely_subtags: bool = True, 

283 ) -> Locale: 

284 """Create a `Locale` instance for the given locale identifier. 

285 

286 >>> l = Locale.parse('de-DE', sep='-') 

287 >>> l.display_name 

288 u'Deutsch (Deutschland)' 

289 

290 If the `identifier` parameter is not a string, but actually a `Locale` 

291 object, that object is returned: 

292 

293 >>> Locale.parse(l) 

294 Locale('de', territory='DE') 

295 

296 If the `identifier` parameter is neither of these, such as `None` 

297 or an empty string, e.g. because a default locale identifier 

298 could not be determined, a `TypeError` is raised: 

299 

300 >>> Locale.parse(None) 

301 Traceback (most recent call last): 

302 ... 

303 TypeError: ... 

304 

305 This also can perform resolving of likely subtags which it does 

306 by default. This is for instance useful to figure out the most 

307 likely locale for a territory you can use ``'und'`` as the 

308 language tag: 

309 

310 >>> Locale.parse('und_AT') 

311 Locale('de', territory='AT') 

312 

313 Modifiers are optional, and always at the end, separated by "@": 

314 

315 >>> Locale.parse('de_AT@euro') 

316 Locale('de', territory='AT', modifier='euro') 

317 

318 :param identifier: the locale identifier string 

319 :param sep: optional component separator 

320 :param resolve_likely_subtags: if this is specified then a locale will 

321 have its likely subtag resolved if the 

322 locale otherwise does not exist. For 

323 instance ``zh_TW`` by itself is not a 

324 locale that exists but Babel can 

325 automatically expand it to the full 

326 form of ``zh_hant_TW``. Note that this 

327 expansion is only taking place if no 

328 locale exists otherwise. For instance 

329 there is a locale ``en`` that can exist 

330 by itself. 

331 :raise `ValueError`: if the string does not appear to be a valid locale 

332 identifier 

333 :raise `UnknownLocaleError`: if no locale data is available for the 

334 requested locale 

335 :raise `TypeError`: if the identifier is not a string or a `Locale` 

336 :raise `ValueError`: if the identifier is not a valid string 

337 """ 

338 if isinstance(identifier, Locale): 

339 return identifier 

340 

341 if not identifier: 

342 msg = ( 

343 f"Empty locale identifier value: {identifier!r}\n\n" 

344 f"If you didn't explicitly pass an empty value to a Babel function, " 

345 f"this could be caused by there being no suitable locale environment " 

346 f"variables for the API you tried to use.", 

347 ) 

348 if isinstance(identifier, str): 

349 raise ValueError(msg) # `parse_locale` would raise a ValueError, so let's do that here 

350 raise TypeError(msg) 

351 

352 if not isinstance(identifier, str): 

353 raise TypeError(f"Unexpected value for identifier: {identifier!r}") 

354 

355 parts = parse_locale(identifier, sep=sep) 

356 input_id = get_locale_identifier(parts) 

357 

358 def _try_load(parts): 

359 try: 

360 return cls(*parts) 

361 except UnknownLocaleError: 

362 return None 

363 

364 def _try_load_reducing(parts): 

365 # Success on first hit, return it. 

366 locale = _try_load(parts) 

367 if locale is not None: 

368 return locale 

369 

370 # Now try without script and variant 

371 locale = _try_load(parts[:2]) 

372 if locale is not None: 

373 return locale 

374 

375 locale = _try_load(parts) 

376 if locale is not None: 

377 return locale 

378 if not resolve_likely_subtags: 

379 raise UnknownLocaleError(input_id) 

380 

381 # From here onwards is some very bad likely subtag resolving. This 

382 # whole logic is not entirely correct but good enough (tm) for the 

383 # time being. This has been added so that zh_TW does not cause 

384 # errors for people when they upgrade. Later we should properly 

385 # implement ICU like fuzzy locale objects and provide a way to 

386 # maximize and minimize locale tags. 

387 

388 if len(parts) == 5: 

389 language, territory, script, variant, modifier = parts 

390 else: 

391 language, territory, script, variant = parts 

392 modifier = None 

393 language = get_global('language_aliases').get(language, language) 

394 territory = get_global('territory_aliases').get(territory or '', (territory,))[0] 

395 script = get_global('script_aliases').get(script or '', script) 

396 variant = get_global('variant_aliases').get(variant or '', variant) 

397 

398 if territory == 'ZZ': 

399 territory = None 

400 if script == 'Zzzz': 

401 script = None 

402 

403 parts = language, territory, script, variant, modifier 

404 

405 # First match: try the whole identifier 

406 new_id = get_locale_identifier(parts) 

407 likely_subtag = get_global('likely_subtags').get(new_id) 

408 if likely_subtag is not None: 

409 locale = _try_load_reducing(parse_locale(likely_subtag)) 

410 if locale is not None: 

411 return locale 

412 

413 # If we did not find anything so far, try again with a 

414 # simplified identifier that is just the language 

415 likely_subtag = get_global('likely_subtags').get(language) 

416 if likely_subtag is not None: 

417 parts2 = parse_locale(likely_subtag) 

418 if len(parts2) == 5: 

419 language2, _, script2, variant2, modifier2 = parts2 

420 else: 

421 language2, _, script2, variant2 = parts2 

422 modifier2 = None 

423 locale = _try_load_reducing((language2, territory, script2, variant2, modifier2)) 

424 if locale is not None: 

425 return locale 

426 

427 raise UnknownLocaleError(input_id) 

428 

429 def __eq__(self, other: object) -> bool: 

430 for key in ('language', 'territory', 'script', 'variant', 'modifier'): 

431 if not hasattr(other, key): 

432 return False 

433 return ( 

434 self.language == getattr(other, 'language') and # noqa: B009 

435 self.territory == getattr(other, 'territory') and # noqa: B009 

436 self.script == getattr(other, 'script') and # noqa: B009 

437 self.variant == getattr(other, 'variant') and # noqa: B009 

438 self.modifier == getattr(other, 'modifier') # noqa: B009 

439 ) 

440 

441 def __ne__(self, other: object) -> bool: 

442 return not self.__eq__(other) 

443 

444 def __hash__(self) -> int: 

445 return hash((self.language, self.territory, self.script, 

446 self.variant, self.modifier)) 

447 

448 def __repr__(self) -> str: 

449 parameters = [''] 

450 for key in ('territory', 'script', 'variant', 'modifier'): 

451 value = getattr(self, key) 

452 if value is not None: 

453 parameters.append(f"{key}={value!r}") 

454 return f"Locale({self.language!r}{', '.join(parameters)})" 

455 

456 def __str__(self) -> str: 

457 return get_locale_identifier((self.language, self.territory, 

458 self.script, self.variant, 

459 self.modifier)) 

460 

461 @property 

462 def _data(self) -> localedata.LocaleDataDict: 

463 if self.__data is None: 

464 self.__data = localedata.LocaleDataDict(localedata.load(self.__data_identifier)) 

465 return self.__data 

466 

467 def get_display_name(self, locale: Locale | str | None = None) -> str | None: 

468 """Return the display name of the locale using the given locale. 

469 

470 The display name will include the language, territory, script, and 

471 variant, if those are specified. 

472 

473 >>> Locale('zh', 'CN', script='Hans').get_display_name('en') 

474 u'Chinese (Simplified, China)' 

475 

476 Modifiers are currently passed through verbatim: 

477 

478 >>> Locale('it', 'IT', modifier='euro').get_display_name('en') 

479 u'Italian (Italy, euro)' 

480 

481 :param locale: the locale to use 

482 """ 

483 if locale is None: 

484 locale = self 

485 locale = Locale.parse(locale) 

486 retval = locale.languages.get(self.language) 

487 if retval and (self.territory or self.script or self.variant): 

488 details = [] 

489 if self.script: 

490 details.append(locale.scripts.get(self.script)) 

491 if self.territory: 

492 details.append(locale.territories.get(self.territory)) 

493 if self.variant: 

494 details.append(locale.variants.get(self.variant)) 

495 if self.modifier: 

496 details.append(self.modifier) 

497 detail_string = ', '.join(atom for atom in details if atom) 

498 if detail_string: 

499 retval += f" ({detail_string})" 

500 return retval 

501 

502 display_name = property(get_display_name, doc="""\ 

503 The localized display name of the locale. 

504 

505 >>> Locale('en').display_name 

506 u'English' 

507 >>> Locale('en', 'US').display_name 

508 u'English (United States)' 

509 >>> Locale('sv').display_name 

510 u'svenska' 

511 

512 :type: `unicode` 

513 """) 

514 

515 def get_language_name(self, locale: Locale | str | None = None) -> str | None: 

516 """Return the language of this locale in the given locale. 

517 

518 >>> Locale('zh', 'CN', script='Hans').get_language_name('de') 

519 u'Chinesisch' 

520 

521 .. versionadded:: 1.0 

522 

523 :param locale: the locale to use 

524 """ 

525 if locale is None: 

526 locale = self 

527 locale = Locale.parse(locale) 

528 return locale.languages.get(self.language) 

529 

530 language_name = property(get_language_name, doc="""\ 

531 The localized language name of the locale. 

532 

533 >>> Locale('en', 'US').language_name 

534 u'English' 

535 """) 

536 

537 def get_territory_name(self, locale: Locale | str | None = None) -> str | None: 

538 """Return the territory name in the given locale.""" 

539 if locale is None: 

540 locale = self 

541 locale = Locale.parse(locale) 

542 return locale.territories.get(self.territory or '') 

543 

544 territory_name = property(get_territory_name, doc="""\ 

545 The localized territory name of the locale if available. 

546 

547 >>> Locale('de', 'DE').territory_name 

548 u'Deutschland' 

549 """) 

550 

551 def get_script_name(self, locale: Locale | str | None = None) -> str | None: 

552 """Return the script name in the given locale.""" 

553 if locale is None: 

554 locale = self 

555 locale = Locale.parse(locale) 

556 return locale.scripts.get(self.script or '') 

557 

558 script_name = property(get_script_name, doc="""\ 

559 The localized script name of the locale if available. 

560 

561 >>> Locale('sr', 'ME', script='Latn').script_name 

562 u'latinica' 

563 """) 

564 

565 @property 

566 def english_name(self) -> str | None: 

567 """The english display name of the locale. 

568 

569 >>> Locale('de').english_name 

570 u'German' 

571 >>> Locale('de', 'DE').english_name 

572 u'German (Germany)' 

573 

574 :type: `unicode`""" 

575 return self.get_display_name(Locale('en')) 

576 

577 # { General Locale Display Names 

578 

579 @property 

580 def languages(self) -> localedata.LocaleDataDict: 

581 """Mapping of language codes to translated language names. 

582 

583 >>> Locale('de', 'DE').languages['ja'] 

584 u'Japanisch' 

585 

586 See `ISO 639 <https://www.loc.gov/standards/iso639-2/>`_ for 

587 more information. 

588 """ 

589 return self._data['languages'] 

590 

591 @property 

592 def scripts(self) -> localedata.LocaleDataDict: 

593 """Mapping of script codes to translated script names. 

594 

595 >>> Locale('en', 'US').scripts['Hira'] 

596 u'Hiragana' 

597 

598 See `ISO 15924 <https://www.unicode.org/iso15924/>`_ 

599 for more information. 

600 """ 

601 return self._data['scripts'] 

602 

603 @property 

604 def territories(self) -> localedata.LocaleDataDict: 

605 """Mapping of script codes to translated script names. 

606 

607 >>> Locale('es', 'CO').territories['DE'] 

608 u'Alemania' 

609 

610 See `ISO 3166 <https://en.wikipedia.org/wiki/ISO_3166>`_ 

611 for more information. 

612 """ 

613 return self._data['territories'] 

614 

615 @property 

616 def variants(self) -> localedata.LocaleDataDict: 

617 """Mapping of script codes to translated script names. 

618 

619 >>> Locale('de', 'DE').variants['1901'] 

620 u'Alte deutsche Rechtschreibung' 

621 """ 

622 return self._data['variants'] 

623 

624 # { Number Formatting 

625 

626 @property 

627 def currencies(self) -> localedata.LocaleDataDict: 

628 """Mapping of currency codes to translated currency names. This 

629 only returns the generic form of the currency name, not the count 

630 specific one. If an actual number is requested use the 

631 :func:`babel.numbers.get_currency_name` function. 

632 

633 >>> Locale('en').currencies['COP'] 

634 u'Colombian Peso' 

635 >>> Locale('de', 'DE').currencies['COP'] 

636 u'Kolumbianischer Peso' 

637 """ 

638 return self._data['currency_names'] 

639 

640 @property 

641 def currency_symbols(self) -> localedata.LocaleDataDict: 

642 """Mapping of currency codes to symbols. 

643 

644 >>> Locale('en', 'US').currency_symbols['USD'] 

645 u'$' 

646 >>> Locale('es', 'CO').currency_symbols['USD'] 

647 u'US$' 

648 """ 

649 return self._data['currency_symbols'] 

650 

651 @property 

652 def number_symbols(self) -> localedata.LocaleDataDict: 

653 """Symbols used in number formatting by number system. 

654 

655 .. note:: The format of the value returned may change between 

656 Babel versions. 

657 

658 >>> Locale('fr', 'FR').number_symbols["latn"]['decimal'] 

659 u',' 

660 >>> Locale('fa', 'IR').number_symbols["arabext"]['decimal'] 

661 u'٫' 

662 >>> Locale('fa', 'IR').number_symbols["latn"]['decimal'] 

663 u'.' 

664 """ 

665 return self._data['number_symbols'] 

666 

667 @property 

668 def other_numbering_systems(self) -> localedata.LocaleDataDict: 

669 """ 

670 Mapping of other numbering systems available for the locale. 

671 See: https://www.unicode.org/reports/tr35/tr35-numbers.html#otherNumberingSystems 

672 

673 >>> Locale('el', 'GR').other_numbering_systems['traditional'] 

674 u'grek' 

675 

676 .. note:: The format of the value returned may change between 

677 Babel versions. 

678 """ 

679 return self._data['numbering_systems'] 

680 

681 @property 

682 def default_numbering_system(self) -> str: 

683 """The default numbering system used by the locale. 

684 >>> Locale('el', 'GR').default_numbering_system 

685 u'latn' 

686 """ 

687 return self._data['default_numbering_system'] 

688 

689 @property 

690 def decimal_formats(self) -> localedata.LocaleDataDict: 

691 """Locale patterns for decimal number formatting. 

692 

693 .. note:: The format of the value returned may change between 

694 Babel versions. 

695 

696 >>> Locale('en', 'US').decimal_formats[None] 

697 <NumberPattern u'#,##0.###'> 

698 """ 

699 return self._data['decimal_formats'] 

700 

701 @property 

702 def compact_decimal_formats(self) -> localedata.LocaleDataDict: 

703 """Locale patterns for compact decimal number formatting. 

704 

705 .. note:: The format of the value returned may change between 

706 Babel versions. 

707 

708 >>> Locale('en', 'US').compact_decimal_formats["short"]["one"]["1000"] 

709 <NumberPattern u'0K'> 

710 """ 

711 return self._data['compact_decimal_formats'] 

712 

713 @property 

714 def currency_formats(self) -> localedata.LocaleDataDict: 

715 """Locale patterns for currency number formatting. 

716 

717 .. note:: The format of the value returned may change between 

718 Babel versions. 

719 

720 >>> Locale('en', 'US').currency_formats['standard'] 

721 <NumberPattern u'\\xa4#,##0.00'> 

722 >>> Locale('en', 'US').currency_formats['accounting'] 

723 <NumberPattern u'\\xa4#,##0.00;(\\xa4#,##0.00)'> 

724 """ 

725 return self._data['currency_formats'] 

726 

727 @property 

728 def compact_currency_formats(self) -> localedata.LocaleDataDict: 

729 """Locale patterns for compact currency number formatting. 

730 

731 .. note:: The format of the value returned may change between 

732 Babel versions. 

733 

734 >>> Locale('en', 'US').compact_currency_formats["short"]["one"]["1000"] 

735 <NumberPattern u'¤0K'> 

736 """ 

737 return self._data['compact_currency_formats'] 

738 

739 @property 

740 def percent_formats(self) -> localedata.LocaleDataDict: 

741 """Locale patterns for percent number formatting. 

742 

743 .. note:: The format of the value returned may change between 

744 Babel versions. 

745 

746 >>> Locale('en', 'US').percent_formats[None] 

747 <NumberPattern u'#,##0%'> 

748 """ 

749 return self._data['percent_formats'] 

750 

751 @property 

752 def scientific_formats(self) -> localedata.LocaleDataDict: 

753 """Locale patterns for scientific number formatting. 

754 

755 .. note:: The format of the value returned may change between 

756 Babel versions. 

757 

758 >>> Locale('en', 'US').scientific_formats[None] 

759 <NumberPattern u'#E0'> 

760 """ 

761 return self._data['scientific_formats'] 

762 

763 # { Calendar Information and Date Formatting 

764 

765 @property 

766 def periods(self) -> localedata.LocaleDataDict: 

767 """Locale display names for day periods (AM/PM). 

768 

769 >>> Locale('en', 'US').periods['am'] 

770 u'AM' 

771 """ 

772 try: 

773 return self._data['day_periods']['stand-alone']['wide'] 

774 except KeyError: 

775 return localedata.LocaleDataDict({}) # pragma: no cover 

776 

777 @property 

778 def day_periods(self) -> localedata.LocaleDataDict: 

779 """Locale display names for various day periods (not necessarily only AM/PM). 

780 

781 These are not meant to be used without the relevant `day_period_rules`. 

782 """ 

783 return self._data['day_periods'] 

784 

785 @property 

786 def day_period_rules(self) -> localedata.LocaleDataDict: 

787 """Day period rules for the locale. Used by `get_period_id`. 

788 """ 

789 return self._data.get('day_period_rules', localedata.LocaleDataDict({})) 

790 

791 @property 

792 def days(self) -> localedata.LocaleDataDict: 

793 """Locale display names for weekdays. 

794 

795 >>> Locale('de', 'DE').days['format']['wide'][3] 

796 u'Donnerstag' 

797 """ 

798 return self._data['days'] 

799 

800 @property 

801 def months(self) -> localedata.LocaleDataDict: 

802 """Locale display names for months. 

803 

804 >>> Locale('de', 'DE').months['format']['wide'][10] 

805 u'Oktober' 

806 """ 

807 return self._data['months'] 

808 

809 @property 

810 def quarters(self) -> localedata.LocaleDataDict: 

811 """Locale display names for quarters. 

812 

813 >>> Locale('de', 'DE').quarters['format']['wide'][1] 

814 u'1. Quartal' 

815 """ 

816 return self._data['quarters'] 

817 

818 @property 

819 def eras(self) -> localedata.LocaleDataDict: 

820 """Locale display names for eras. 

821 

822 .. note:: The format of the value returned may change between 

823 Babel versions. 

824 

825 >>> Locale('en', 'US').eras['wide'][1] 

826 u'Anno Domini' 

827 >>> Locale('en', 'US').eras['abbreviated'][0] 

828 u'BC' 

829 """ 

830 return self._data['eras'] 

831 

832 @property 

833 def time_zones(self) -> localedata.LocaleDataDict: 

834 """Locale display names for time zones. 

835 

836 .. note:: The format of the value returned may change between 

837 Babel versions. 

838 

839 >>> Locale('en', 'US').time_zones['Europe/London']['long']['daylight'] 

840 u'British Summer Time' 

841 >>> Locale('en', 'US').time_zones['America/St_Johns']['city'] 

842 u'St. John\u2019s' 

843 """ 

844 return self._data['time_zones'] 

845 

846 @property 

847 def meta_zones(self) -> localedata.LocaleDataDict: 

848 """Locale display names for meta time zones. 

849 

850 Meta time zones are basically groups of different Olson time zones that 

851 have the same GMT offset and daylight savings time. 

852 

853 .. note:: The format of the value returned may change between 

854 Babel versions. 

855 

856 >>> Locale('en', 'US').meta_zones['Europe_Central']['long']['daylight'] 

857 u'Central European Summer Time' 

858 

859 .. versionadded:: 0.9 

860 """ 

861 return self._data['meta_zones'] 

862 

863 @property 

864 def zone_formats(self) -> localedata.LocaleDataDict: 

865 """Patterns related to the formatting of time zones. 

866 

867 .. note:: The format of the value returned may change between 

868 Babel versions. 

869 

870 >>> Locale('en', 'US').zone_formats['fallback'] 

871 u'%(1)s (%(0)s)' 

872 >>> Locale('pt', 'BR').zone_formats['region'] 

873 u'Hor\\xe1rio %s' 

874 

875 .. versionadded:: 0.9 

876 """ 

877 return self._data['zone_formats'] 

878 

879 @property 

880 def first_week_day(self) -> int: 

881 """The first day of a week, with 0 being Monday. 

882 

883 >>> Locale('de', 'DE').first_week_day 

884 0 

885 >>> Locale('en', 'US').first_week_day 

886 6 

887 """ 

888 return self._data['week_data']['first_day'] 

889 

890 @property 

891 def weekend_start(self) -> int: 

892 """The day the weekend starts, with 0 being Monday. 

893 

894 >>> Locale('de', 'DE').weekend_start 

895 5 

896 """ 

897 return self._data['week_data']['weekend_start'] 

898 

899 @property 

900 def weekend_end(self) -> int: 

901 """The day the weekend ends, with 0 being Monday. 

902 

903 >>> Locale('de', 'DE').weekend_end 

904 6 

905 """ 

906 return self._data['week_data']['weekend_end'] 

907 

908 @property 

909 def min_week_days(self) -> int: 

910 """The minimum number of days in a week so that the week is counted as 

911 the first week of a year or month. 

912 

913 >>> Locale('de', 'DE').min_week_days 

914 4 

915 """ 

916 return self._data['week_data']['min_days'] 

917 

918 @property 

919 def date_formats(self) -> localedata.LocaleDataDict: 

920 """Locale patterns for date formatting. 

921 

922 .. note:: The format of the value returned may change between 

923 Babel versions. 

924 

925 >>> Locale('en', 'US').date_formats['short'] 

926 <DateTimePattern u'M/d/yy'> 

927 >>> Locale('fr', 'FR').date_formats['long'] 

928 <DateTimePattern u'd MMMM y'> 

929 """ 

930 return self._data['date_formats'] 

931 

932 @property 

933 def time_formats(self) -> localedata.LocaleDataDict: 

934 """Locale patterns for time formatting. 

935 

936 .. note:: The format of the value returned may change between 

937 Babel versions. 

938 

939 >>> Locale('en', 'US').time_formats['short'] 

940 <DateTimePattern u'h:mm\u202fa'> 

941 >>> Locale('fr', 'FR').time_formats['long'] 

942 <DateTimePattern u'HH:mm:ss z'> 

943 """ 

944 return self._data['time_formats'] 

945 

946 @property 

947 def datetime_formats(self) -> localedata.LocaleDataDict: 

948 """Locale patterns for datetime formatting. 

949 

950 .. note:: The format of the value returned may change between 

951 Babel versions. 

952 

953 >>> Locale('en').datetime_formats['full'] 

954 u'{1}, {0}' 

955 >>> Locale('th').datetime_formats['medium'] 

956 u'{1} {0}' 

957 """ 

958 return self._data['datetime_formats'] 

959 

960 @property 

961 def datetime_skeletons(self) -> localedata.LocaleDataDict: 

962 """Locale patterns for formatting parts of a datetime. 

963 

964 >>> Locale('en').datetime_skeletons['MEd'] 

965 <DateTimePattern u'E, M/d'> 

966 >>> Locale('fr').datetime_skeletons['MEd'] 

967 <DateTimePattern u'E dd/MM'> 

968 >>> Locale('fr').datetime_skeletons['H'] 

969 <DateTimePattern u"HH 'h'"> 

970 """ 

971 return self._data['datetime_skeletons'] 

972 

973 @property 

974 def interval_formats(self) -> localedata.LocaleDataDict: 

975 """Locale patterns for interval formatting. 

976 

977 .. note:: The format of the value returned may change between 

978 Babel versions. 

979 

980 How to format date intervals in Finnish when the day is the 

981 smallest changing component: 

982 

983 >>> Locale('fi_FI').interval_formats['MEd']['d'] 

984 [u'E d.\u2009\u2013\u2009', u'E d.M.'] 

985 

986 .. seealso:: 

987 

988 The primary API to use this data is :py:func:`babel.dates.format_interval`. 

989 

990 

991 :rtype: dict[str, dict[str, list[str]]] 

992 """ 

993 return self._data['interval_formats'] 

994 

995 @property 

996 def plural_form(self) -> PluralRule: 

997 """Plural rules for the locale. 

998 

999 >>> Locale('en').plural_form(1) 

1000 'one' 

1001 >>> Locale('en').plural_form(0) 

1002 'other' 

1003 >>> Locale('fr').plural_form(0) 

1004 'one' 

1005 >>> Locale('ru').plural_form(100) 

1006 'many' 

1007 """ 

1008 return self._data.get('plural_form', _default_plural_rule) 

1009 

1010 @property 

1011 def list_patterns(self) -> localedata.LocaleDataDict: 

1012 """Patterns for generating lists 

1013 

1014 .. note:: The format of the value returned may change between 

1015 Babel versions. 

1016 

1017 >>> Locale('en').list_patterns['standard']['start'] 

1018 u'{0}, {1}' 

1019 >>> Locale('en').list_patterns['standard']['end'] 

1020 u'{0}, and {1}' 

1021 >>> Locale('en_GB').list_patterns['standard']['end'] 

1022 u'{0} and {1}' 

1023 """ 

1024 return self._data['list_patterns'] 

1025 

1026 @property 

1027 def ordinal_form(self) -> PluralRule: 

1028 """Plural rules for the locale. 

1029 

1030 >>> Locale('en').ordinal_form(1) 

1031 'one' 

1032 >>> Locale('en').ordinal_form(2) 

1033 'two' 

1034 >>> Locale('en').ordinal_form(3) 

1035 'few' 

1036 >>> Locale('fr').ordinal_form(2) 

1037 'other' 

1038 >>> Locale('ru').ordinal_form(100) 

1039 'other' 

1040 """ 

1041 return self._data.get('ordinal_form', _default_plural_rule) 

1042 

1043 @property 

1044 def measurement_systems(self) -> localedata.LocaleDataDict: 

1045 """Localized names for various measurement systems. 

1046 

1047 >>> Locale('fr', 'FR').measurement_systems['US'] 

1048 u'am\\xe9ricain' 

1049 >>> Locale('en', 'US').measurement_systems['US'] 

1050 u'US' 

1051 

1052 """ 

1053 return self._data['measurement_systems'] 

1054 

1055 @property 

1056 def character_order(self) -> str: 

1057 """The text direction for the language. 

1058 

1059 >>> Locale('de', 'DE').character_order 

1060 'left-to-right' 

1061 >>> Locale('ar', 'SA').character_order 

1062 'right-to-left' 

1063 """ 

1064 return self._data['character_order'] 

1065 

1066 @property 

1067 def text_direction(self) -> str: 

1068 """The text direction for the language in CSS short-hand form. 

1069 

1070 >>> Locale('de', 'DE').text_direction 

1071 'ltr' 

1072 >>> Locale('ar', 'SA').text_direction 

1073 'rtl' 

1074 """ 

1075 return ''.join(word[0] for word in self.character_order.split('-')) 

1076 

1077 @property 

1078 def unit_display_names(self) -> localedata.LocaleDataDict: 

1079 """Display names for units of measurement. 

1080 

1081 .. seealso:: 

1082 

1083 You may want to use :py:func:`babel.units.get_unit_name` instead. 

1084 

1085 .. note:: The format of the value returned may change between 

1086 Babel versions. 

1087 

1088 """ 

1089 return self._data['unit_display_names'] 

1090 

1091 

1092def default_locale( 

1093 category: str | tuple[str, ...] | list[str] | None = None, 

1094 aliases: Mapping[str, str] = LOCALE_ALIASES, 

1095) -> str | None: 

1096 """Returns the system default locale for a given category, based on 

1097 environment variables. 

1098 

1099 >>> for name in ['LANGUAGE', 'LC_ALL', 'LC_CTYPE']: 

1100 ... os.environ[name] = '' 

1101 >>> os.environ['LANG'] = 'fr_FR.UTF-8' 

1102 >>> default_locale('LC_MESSAGES') 

1103 'fr_FR' 

1104 

1105 The "C" or "POSIX" pseudo-locales are treated as aliases for the 

1106 "en_US_POSIX" locale: 

1107 

1108 >>> os.environ['LC_MESSAGES'] = 'POSIX' 

1109 >>> default_locale('LC_MESSAGES') 

1110 'en_US_POSIX' 

1111 

1112 The following fallbacks to the variable are always considered: 

1113 

1114 - ``LANGUAGE`` 

1115 - ``LC_ALL`` 

1116 - ``LC_CTYPE`` 

1117 - ``LANG`` 

1118 

1119 :param category: one or more of the ``LC_XXX`` environment variable names 

1120 :param aliases: a dictionary of aliases for locale identifiers 

1121 """ 

1122 

1123 varnames = ('LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LANG') 

1124 if category: 

1125 if isinstance(category, str): 

1126 varnames = (category, *varnames) 

1127 elif isinstance(category, (list, tuple)): 

1128 varnames = (*category, *varnames) 

1129 else: 

1130 raise TypeError(f"Invalid type for category: {category!r}") 

1131 

1132 for name in varnames: 

1133 if not name: 

1134 continue 

1135 locale = os.getenv(name) 

1136 if locale: 

1137 if name == 'LANGUAGE' and ':' in locale: 

1138 # the LANGUAGE variable may contain a colon-separated list of 

1139 # language codes; we just pick the language on the list 

1140 locale = locale.split(':')[0] 

1141 if locale.split('.')[0] in ('C', 'POSIX'): 

1142 locale = 'en_US_POSIX' 

1143 elif aliases and locale in aliases: 

1144 locale = aliases[locale] 

1145 try: 

1146 return get_locale_identifier(parse_locale(locale)) 

1147 except ValueError: 

1148 pass 

1149 return None 

1150 

1151 

1152def negotiate_locale(preferred: Iterable[str], available: Iterable[str], sep: str = '_', aliases: Mapping[str, str] = LOCALE_ALIASES) -> str | None: 

1153 """Find the best match between available and requested locale strings. 

1154 

1155 >>> negotiate_locale(['de_DE', 'en_US'], ['de_DE', 'de_AT']) 

1156 'de_DE' 

1157 >>> negotiate_locale(['de_DE', 'en_US'], ['en', 'de']) 

1158 'de' 

1159 

1160 Case is ignored by the algorithm, the result uses the case of the preferred 

1161 locale identifier: 

1162 

1163 >>> negotiate_locale(['de_DE', 'en_US'], ['de_de', 'de_at']) 

1164 'de_DE' 

1165 

1166 >>> negotiate_locale(['de_DE', 'en_US'], ['de_de', 'de_at']) 

1167 'de_DE' 

1168 

1169 By default, some web browsers unfortunately do not include the territory 

1170 in the locale identifier for many locales, and some don't even allow the 

1171 user to easily add the territory. So while you may prefer using qualified 

1172 locale identifiers in your web-application, they would not normally match 

1173 the language-only locale sent by such browsers. To workaround that, this 

1174 function uses a default mapping of commonly used language-only locale 

1175 identifiers to identifiers including the territory: 

1176 

1177 >>> negotiate_locale(['ja', 'en_US'], ['ja_JP', 'en_US']) 

1178 'ja_JP' 

1179 

1180 Some browsers even use an incorrect or outdated language code, such as "no" 

1181 for Norwegian, where the correct locale identifier would actually be "nb_NO" 

1182 (Bokmål) or "nn_NO" (Nynorsk). The aliases are intended to take care of 

1183 such cases, too: 

1184 

1185 >>> negotiate_locale(['no', 'sv'], ['nb_NO', 'sv_SE']) 

1186 'nb_NO' 

1187 

1188 You can override this default mapping by passing a different `aliases` 

1189 dictionary to this function, or you can bypass the behavior althogher by 

1190 setting the `aliases` parameter to `None`. 

1191 

1192 :param preferred: the list of locale strings preferred by the user 

1193 :param available: the list of locale strings available 

1194 :param sep: character that separates the different parts of the locale 

1195 strings 

1196 :param aliases: a dictionary of aliases for locale identifiers 

1197 """ 

1198 available = [a.lower() for a in available if a] 

1199 for locale in preferred: 

1200 ll = locale.lower() 

1201 if ll in available: 

1202 return locale 

1203 if aliases: 

1204 alias = aliases.get(ll) 

1205 if alias: 

1206 alias = alias.replace('_', sep) 

1207 if alias.lower() in available: 

1208 return alias 

1209 parts = locale.split(sep) 

1210 if len(parts) > 1 and parts[0].lower() in available: 

1211 return parts[0] 

1212 return None 

1213 

1214 

1215def parse_locale( 

1216 identifier: str, 

1217 sep: str = '_', 

1218) -> tuple[str, str | None, str | None, str | None] | tuple[str, str | None, str | None, str | None, str | None]: 

1219 """Parse a locale identifier into a tuple of the form ``(language, 

1220 territory, script, variant, modifier)``. 

1221 

1222 >>> parse_locale('zh_CN') 

1223 ('zh', 'CN', None, None) 

1224 >>> parse_locale('zh_Hans_CN') 

1225 ('zh', 'CN', 'Hans', None) 

1226 >>> parse_locale('ca_es_valencia') 

1227 ('ca', 'ES', None, 'VALENCIA') 

1228 >>> parse_locale('en_150') 

1229 ('en', '150', None, None) 

1230 >>> parse_locale('en_us_posix') 

1231 ('en', 'US', None, 'POSIX') 

1232 >>> parse_locale('it_IT@euro') 

1233 ('it', 'IT', None, None, 'euro') 

1234 >>> parse_locale('it_IT@custom') 

1235 ('it', 'IT', None, None, 'custom') 

1236 >>> parse_locale('it_IT@') 

1237 ('it', 'IT', None, None) 

1238 

1239 The default component separator is "_", but a different separator can be 

1240 specified using the `sep` parameter. 

1241 

1242 The optional modifier is always separated with "@" and at the end: 

1243 

1244 >>> parse_locale('zh-CN', sep='-') 

1245 ('zh', 'CN', None, None) 

1246 >>> parse_locale('zh-CN@custom', sep='-') 

1247 ('zh', 'CN', None, None, 'custom') 

1248 

1249 If the identifier cannot be parsed into a locale, a `ValueError` exception 

1250 is raised: 

1251 

1252 >>> parse_locale('not_a_LOCALE_String') 

1253 Traceback (most recent call last): 

1254 ... 

1255 ValueError: 'not_a_LOCALE_String' is not a valid locale identifier 

1256 

1257 Encoding information is removed from the identifier, while modifiers are 

1258 kept: 

1259 

1260 >>> parse_locale('en_US.UTF-8') 

1261 ('en', 'US', None, None) 

1262 >>> parse_locale('de_DE.iso885915@euro') 

1263 ('de', 'DE', None, None, 'euro') 

1264 

1265 See :rfc:`4646` for more information. 

1266 

1267 :param identifier: the locale identifier string 

1268 :param sep: character that separates the different components of the locale 

1269 identifier 

1270 :raise `ValueError`: if the string does not appear to be a valid locale 

1271 identifier 

1272 """ 

1273 if not identifier: 

1274 raise ValueError("empty locale identifier") 

1275 identifier, _, modifier = identifier.partition('@') 

1276 if '.' in identifier: 

1277 # this is probably the charset/encoding, which we don't care about 

1278 identifier = identifier.split('.', 1)[0] 

1279 

1280 parts = identifier.split(sep) 

1281 lang = parts.pop(0).lower() 

1282 if not lang.isalpha(): 

1283 raise ValueError(f"expected only letters, got {lang!r}") 

1284 

1285 script = territory = variant = None 

1286 if parts and len(parts[0]) == 4 and parts[0].isalpha(): 

1287 script = parts.pop(0).title() 

1288 

1289 if parts: 

1290 if len(parts[0]) == 2 and parts[0].isalpha(): 

1291 territory = parts.pop(0).upper() 

1292 elif len(parts[0]) == 3 and parts[0].isdigit(): 

1293 territory = parts.pop(0) 

1294 

1295 if parts and ( 

1296 len(parts[0]) == 4 and parts[0][0].isdigit() or 

1297 len(parts[0]) >= 5 and parts[0][0].isalpha() 

1298 ): 

1299 variant = parts.pop().upper() 

1300 

1301 if parts: 

1302 raise ValueError(f"{identifier!r} is not a valid locale identifier") 

1303 

1304 # TODO(3.0): always return a 5-tuple 

1305 if modifier: 

1306 return lang, territory, script, variant, modifier 

1307 else: 

1308 return lang, territory, script, variant 

1309 

1310 

1311def get_locale_identifier( 

1312 tup: tuple[str] 

1313 | tuple[str, str | None] 

1314 | tuple[str, str | None, str | None] 

1315 | tuple[str, str | None, str | None, str | None] 

1316 | tuple[str, str | None, str | None, str | None, str | None], 

1317 sep: str = "_", 

1318) -> str: 

1319 """The reverse of :func:`parse_locale`. It creates a locale identifier out 

1320 of a ``(language, territory, script, variant, modifier)`` tuple. Items can be set to 

1321 ``None`` and trailing ``None``\\s can also be left out of the tuple. 

1322 

1323 >>> get_locale_identifier(('de', 'DE', None, '1999', 'custom')) 

1324 'de_DE_1999@custom' 

1325 >>> get_locale_identifier(('fi', None, None, None, 'custom')) 

1326 'fi@custom' 

1327 

1328 

1329 .. versionadded:: 1.0 

1330 

1331 :param tup: the tuple as returned by :func:`parse_locale`. 

1332 :param sep: the separator for the identifier. 

1333 """ 

1334 tup = tuple(tup[:5]) # type: ignore # length should be no more than 5 

1335 lang, territory, script, variant, modifier = tup + (None,) * (5 - len(tup)) 

1336 ret = sep.join(filter(None, (lang, script, territory, variant))) 

1337 return f'{ret}@{modifier}' if modifier else ret