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

351 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:39 +0000

1""" 

2 babel.core 

3 ~~~~~~~~~~ 

4 

5 Core locale representation and locale data access. 

6 

7 :copyright: (c) 2013-2023 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 

17 

18from babel import localedata 

19from babel.plural import PluralRule 

20 

21__all__ = ['UnknownLocaleError', 'Locale', 'default_locale', 'negotiate_locale', 

22 'parse_locale'] 

23 

24if TYPE_CHECKING: 

25 from typing_extensions import Literal, TypeAlias 

26 

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]] | None 

46 

47_global_data = None 

48_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 contains 

64 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.9 

91 

92 :param key: the data key 

93 """ 

94 global _global_data 

95 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 None 

103 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 data 

120 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 locale 

127 """ 

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

129 

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

131 self.identifier = identifier 

132 

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_name 

141 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 as 

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

151 

152 >>> locale.number_symbols['decimal'] 

153 u'.' 

154 

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

156 `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.language 

178 'en' 

179 >>> locale.territory 

180 'US' 

181 

182 :param language: the language code 

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

184 :param script: the script code 

185 :param variant: the variant code 

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

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

188 requested locale 

189 """ 

190 #: the language code 

191 self.language = language 

192 #: the territory (country or region) code 

193 self.territory = territory 

194 #: the script code 

195 self.script = script 

196 #: the variant code 

197 self.variant = variant 

198 #: the modifier 

199 self.modifier = modifier 

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

201 

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 @classmethod 

208 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 names 

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

226 """ 

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

228 # aliases dictionary. 

229 locale_string = default_locale(category, aliases=aliases) 

230 return cls.parse(locale_string) 

231 

232 @classmethod 

233 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 separate 

249 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 user 

256 :param available: the list of locale identifiers available 

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

258 """ 

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

260 aliases=aliases) 

261 if identifier: 

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

263 return None 

264 

265 @classmethod 

266 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_name 

276 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 does 

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

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

296 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 string 

307 :param sep: optional component separator 

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

309 have its likely subtag resolved if the 

310 locale otherwise does not exist. For 

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

312 locale that exists but Babel can 

313 automatically expand it to the full 

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

315 expansion is only taking place if no 

316 locale exists otherwise. For instance 

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

318 by itself. 

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

320 identifier 

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

322 requested locale 

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

324 """ 

325 if isinstance(identifier, Locale): 

326 return identifier 

327 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 None 

338 

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 locale 

344 

345 # Now try without script and variant 

346 locale = _try_load(parts[:2]) 

347 if locale is not None: 

348 return locale 

349 

350 locale = _try_load(parts) 

351 if locale is not None: 

352 return locale 

353 if not resolve_likely_subtags: 

354 raise UnknownLocaleError(input_id) 

355 

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

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

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

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

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

361 # maximize and minimize locale tags. 

362 

363 if len(parts) == 5: 

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

365 else: 

366 language, territory, script, variant = parts 

367 modifier = None 

368 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 = None 

375 if script == 'Zzzz': 

376 script = None 

377 

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

379 

380 # First match: try the whole identifier 

381 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 locale 

387 

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

389 # simplified identifier that is just the language 

390 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 = parts2 

395 else: 

396 language2, _, script2, variant2 = parts2 

397 modifier2 = None 

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

399 if locale is not None: 

400 return locale 

401 

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 False 

408 return ( 

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

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

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

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

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

414 ) 

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 @property 

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

438 if self.__data is None: 

439 self.__data = localedata.LocaleDataDict(localedata.load(str(self))) 

440 return self.__data 

441 

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, and 

446 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 use 

457 """ 

458 if locale is None: 

459 locale = self 

460 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 retval 

476 

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

478 The localized display name of the locale. 

479 

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

481 u'English' 

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

483 u'English (United States)' 

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

485 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.0 

497 

498 :param locale: the locale to use 

499 """ 

500 if locale is None: 

501 locale = self 

502 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_name 

509 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 = self 

516 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_name 

523 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 = self 

530 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_name 

537 u'latinica' 

538 """) 

539 

540 @property 

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

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

543 

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

545 u'German' 

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

547 u'German (Germany)' 

548 

549 :type: `unicode`""" 

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

551 

552 # { General Locale Display Names 

553 

554 @property 

555 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/>`_ for 

562 more information. 

563 """ 

564 return self._data['languages'] 

565 

566 @property 

567 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 @property 

579 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 @property 

591 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 Formatting 

600 

601 @property 

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

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

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

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

606 :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 @property 

616 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 @property 

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

628 """Symbols used in number formatting. 

629 

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

631 Babel versions. 

632 

633 >>> Locale('fr', 'FR').number_symbols['decimal'] 

634 u',' 

635 """ 

636 return self._data['number_symbols'] 

637 

638 @property 

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

640 """Locale patterns for decimal number formatting. 

641 

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

643 Babel versions. 

644 

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

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

647 """ 

648 return self._data['decimal_formats'] 

649 

650 @property 

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

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

653 

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

655 Babel versions. 

656 

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

658 <NumberPattern u'0K'> 

659 """ 

660 return self._data['compact_decimal_formats'] 

661 

662 @property 

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

664 """Locale patterns for currency number formatting. 

665 

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

667 Babel versions. 

668 

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

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

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

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

673 """ 

674 return self._data['currency_formats'] 

675 

676 @property 

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

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

679 

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

681 Babel versions. 

682 

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

684 <NumberPattern u'¤0K'> 

685 """ 

686 return self._data['compact_currency_formats'] 

687 

688 @property 

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

690 """Locale patterns for percent number formatting. 

691 

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

693 Babel versions. 

694 

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

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

697 """ 

698 return self._data['percent_formats'] 

699 

700 @property 

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

702 """Locale patterns for scientific number formatting. 

703 

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

705 Babel versions. 

706 

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

708 <NumberPattern u'#E0'> 

709 """ 

710 return self._data['scientific_formats'] 

711 

712 # { Calendar Information and Date Formatting 

713 

714 @property 

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

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

717 

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

719 u'AM' 

720 """ 

721 try: 

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

723 except KeyError: 

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

725 

726 @property 

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

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

729 

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

731 """ 

732 return self._data['day_periods'] 

733 

734 @property 

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

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

737 """ 

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

739 

740 @property 

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

742 """Locale display names for weekdays. 

743 

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

745 u'Donnerstag' 

746 """ 

747 return self._data['days'] 

748 

749 @property 

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

751 """Locale display names for months. 

752 

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

754 u'Oktober' 

755 """ 

756 return self._data['months'] 

757 

758 @property 

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

760 """Locale display names for quarters. 

761 

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

763 u'1. Quartal' 

764 """ 

765 return self._data['quarters'] 

766 

767 @property 

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

769 """Locale display names for eras. 

770 

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

772 Babel versions. 

773 

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

775 u'Anno Domini' 

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

777 u'BC' 

778 """ 

779 return self._data['eras'] 

780 

781 @property 

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

783 """Locale display names for time zones. 

784 

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

786 Babel versions. 

787 

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

789 u'British Summer Time' 

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

791 u'St. John\u2019s' 

792 """ 

793 return self._data['time_zones'] 

794 

795 @property 

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

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

798 

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

800 have the same GMT offset and daylight savings time. 

801 

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

803 Babel versions. 

804 

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

806 u'Central European Summer Time' 

807 

808 .. versionadded:: 0.9 

809 """ 

810 return self._data['meta_zones'] 

811 

812 @property 

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

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

815 

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

817 Babel versions. 

818 

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

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

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

822 u'Hor\\xe1rio %s' 

823 

824 .. versionadded:: 0.9 

825 """ 

826 return self._data['zone_formats'] 

827 

828 @property 

829 def first_week_day(self) -> int: 

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

831 

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

833 0 

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

835 6 

836 """ 

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

838 

839 @property 

840 def weekend_start(self) -> int: 

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

842 

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

844 5 

845 """ 

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

847 

848 @property 

849 def weekend_end(self) -> int: 

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

851 

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

853 6 

854 """ 

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

856 

857 @property 

858 def min_week_days(self) -> int: 

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

860 the first week of a year or month. 

861 

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

863 4 

864 """ 

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

866 

867 @property 

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

869 """Locale patterns for date formatting. 

870 

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

872 Babel versions. 

873 

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

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

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

877 <DateTimePattern u'd MMMM y'> 

878 """ 

879 return self._data['date_formats'] 

880 

881 @property 

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

883 """Locale patterns for time formatting. 

884 

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

886 Babel versions. 

887 

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

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

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

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

892 """ 

893 return self._data['time_formats'] 

894 

895 @property 

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

897 """Locale patterns for datetime formatting. 

898 

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

900 Babel versions. 

901 

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

903 u'{1}, {0}' 

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

905 u'{1} {0}' 

906 """ 

907 return self._data['datetime_formats'] 

908 

909 @property 

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

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

912 

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

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

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

916 <DateTimePattern u'E dd/MM'> 

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

918 <DateTimePattern u"HH 'h'"> 

919 """ 

920 return self._data['datetime_skeletons'] 

921 

922 @property 

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

924 """Locale patterns for interval formatting. 

925 

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

927 Babel versions. 

928 

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

930 smallest changing component: 

931 

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

933 [u'E d. \u2013 ', u'E d.M.'] 

934 

935 .. seealso:: 

936 

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

938 

939 

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

941 """ 

942 return self._data['interval_formats'] 

943 

944 @property 

945 def plural_form(self) -> PluralRule: 

946 """Plural rules for the locale. 

947 

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

949 'one' 

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

951 'other' 

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

953 'one' 

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

955 'many' 

956 """ 

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

958 

959 @property 

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

961 """Patterns for generating lists 

962 

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

964 Babel versions. 

965 

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

967 u'{0}, {1}' 

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

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

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

971 u'{0} and {1}' 

972 """ 

973 return self._data['list_patterns'] 

974 

975 @property 

976 def ordinal_form(self) -> PluralRule: 

977 """Plural rules for the locale. 

978 

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

980 'one' 

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

982 'two' 

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

984 'few' 

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

986 'other' 

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

988 'other' 

989 """ 

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

991 

992 @property 

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

994 """Localized names for various measurement systems. 

995 

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

997 u'am\\xe9ricain' 

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

999 u'US' 

1000 

1001 """ 

1002 return self._data['measurement_systems'] 

1003 

1004 @property 

1005 def character_order(self) -> str: 

1006 """The text direction for the language. 

1007 

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

1009 'left-to-right' 

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

1011 'right-to-left' 

1012 """ 

1013 return self._data['character_order'] 

1014 

1015 @property 

1016 def text_direction(self) -> str: 

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

1018 

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

1020 'ltr' 

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

1022 'rtl' 

1023 """ 

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

1025 

1026 @property 

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

1028 """Display names for units of measurement. 

1029 

1030 .. seealso:: 

1031 

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

1033 

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

1035 Babel versions. 

1036 

1037 """ 

1038 return self._data['unit_display_names'] 

1039 

1040 

1041def default_locale(category: str | None = None, aliases: Mapping[str, str] = LOCALE_ALIASES) -> str | None: 

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

1043 environment variables. 

1044 

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

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

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

1048 >>> default_locale('LC_MESSAGES') 

1049 'fr_FR' 

1050 

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

1052 "en_US_POSIX" locale: 

1053 

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

1055 >>> default_locale('LC_MESSAGES') 

1056 'en_US_POSIX' 

1057 

1058 The following fallbacks to the variable are always considered: 

1059 

1060 - ``LANGUAGE`` 

1061 - ``LC_ALL`` 

1062 - ``LC_CTYPE`` 

1063 - ``LANG`` 

1064 

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

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

1067 """ 

1068 varnames = (category, 'LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LANG') 

1069 for name in filter(None, varnames): 

1070 locale = os.getenv(name) 

1071 if locale: 

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

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

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

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

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

1077 locale = 'en_US_POSIX' 

1078 elif aliases and locale in aliases: 

1079 locale = aliases[locale] 

1080 try: 

1081 return get_locale_identifier(parse_locale(locale)) 

1082 except ValueError: 

1083 pass 

1084 return None 

1085 

1086 

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

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

1089 

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

1091 'de_DE' 

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

1093 'de' 

1094 

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

1096 locale identifier: 

1097 

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

1099 'de_DE' 

1100 

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

1102 'de_DE' 

1103 

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

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

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

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

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

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

1110 identifiers to identifiers including the territory: 

1111 

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

1113 'ja_JP' 

1114 

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

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

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

1118 such cases, too: 

1119 

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

1121 'nb_NO' 

1122 

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

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

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

1126 

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

1128 :param available: the list of locale strings available 

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

1130 strings 

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

1132 """ 

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

1134 for locale in preferred: 

1135 ll = locale.lower() 

1136 if ll in available: 

1137 return locale 

1138 if aliases: 

1139 alias = aliases.get(ll) 

1140 if alias: 

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

1142 if alias.lower() in available: 

1143 return alias 

1144 parts = locale.split(sep) 

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

1146 return parts[0] 

1147 return None 

1148 

1149 

1150def parse_locale( 

1151 identifier: str, 

1152 sep: str = '_' 

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

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

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

1156 

1157 >>> parse_locale('zh_CN') 

1158 ('zh', 'CN', None, None) 

1159 >>> parse_locale('zh_Hans_CN') 

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

1161 >>> parse_locale('ca_es_valencia') 

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

1163 >>> parse_locale('en_150') 

1164 ('en', '150', None, None) 

1165 >>> parse_locale('en_us_posix') 

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

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

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

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

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

1171 >>> parse_locale('it_IT@') 

1172 ('it', 'IT', None, None) 

1173 

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

1175 specified using the `sep` parameter. 

1176 

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

1178 

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

1180 ('zh', 'CN', None, None) 

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

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

1183 

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

1185 is raised: 

1186 

1187 >>> parse_locale('not_a_LOCALE_String') 

1188 Traceback (most recent call last): 

1189 ... 

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

1191 

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

1193 kept: 

1194 

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

1196 ('en', 'US', None, None) 

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

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

1199 

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

1201 

1202 :param identifier: the locale identifier string 

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

1204 identifier 

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

1206 identifier 

1207 """ 

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

1209 if '.' in identifier: 

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

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

1212 

1213 parts = identifier.split(sep) 

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

1215 if not lang.isalpha(): 

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

1217 

1218 script = territory = variant = None 

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

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

1221 

1222 if parts: 

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

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

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

1226 territory = parts.pop(0) 

1227 

1228 if parts and ( 

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

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

1231 ): 

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

1233 

1234 if parts: 

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

1236 

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

1238 if modifier: 

1239 return lang, territory, script, variant, modifier 

1240 else: 

1241 return lang, territory, script, variant 

1242 

1243 

1244def get_locale_identifier( 

1245 tup: tuple[str] 

1246 | tuple[str, str | None] 

1247 | tuple[str, str | None, str | None] 

1248 | tuple[str, str | None, str | None, str | None] 

1249 | tuple[str, str | None, str | None, str | None, str | None], 

1250 sep: str = "_", 

1251) -> str: 

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

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

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

1255 

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

1257 'de_DE_1999@custom' 

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

1259 'fi@custom' 

1260 

1261 

1262 .. versionadded:: 1.0 

1263 

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

1265 :param sep: the separator for the identifier. 

1266 """ 

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

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

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

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