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

392 statements  

1""" 

2babel.core 

3~~~~~~~~~~ 

4 

5Core locale representation and locale data access. 

6 

7:copyright: (c) 2013-2026 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_cldr_version', 

26 'get_global', 

27 'get_locale_identifier', 

28 'negotiate_locale', 

29 'parse_locale', 

30] 

31 

32if TYPE_CHECKING: 

33 from typing_extensions import TypeAlias 

34 

35 _GLOBAL_KEY: TypeAlias = Literal[ 

36 "all_currencies", 

37 "cldr", 

38 "currency_fractions", 

39 "language_aliases", 

40 "likely_subtags", 

41 "meta_zones", 

42 "parent_exceptions", 

43 "script_aliases", 

44 "territory_aliases", 

45 "territory_currencies", 

46 "territory_languages", 

47 "territory_zones", 

48 "variant_aliases", 

49 "windows_zone_mapping", 

50 "zone_aliases", 

51 "zone_territories", 

52 ] 

53 

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

55 

56_global_data = None 

57_default_plural_rule = PluralRule({}) 

58 

59 

60def _raise_no_data_error(): 

61 raise RuntimeError( 

62 'The babel data files are not available. ' 

63 'This usually happens because you are using ' 

64 'a source checkout from Babel and you did ' 

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

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

67 'installing the library.', 

68 ) 

69 

70 

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

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

73 

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

75 information independent of individual locales. 

76 

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

78 'Etc/UTC' 

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

80 'DE' 

81 

82 The keys available are: 

83 

84 - ``all_currencies`` 

85 - ``cldr`` (metadata) 

86 - ``currency_fractions`` 

87 - ``language_aliases`` 

88 - ``likely_subtags`` 

89 - ``parent_exceptions`` 

90 - ``script_aliases`` 

91 - ``territory_aliases`` 

92 - ``territory_currencies`` 

93 - ``territory_languages`` 

94 - ``territory_zones`` 

95 - ``variant_aliases`` 

96 - ``windows_zone_mapping`` 

97 - ``zone_aliases`` 

98 - ``zone_territories`` 

99 

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

101 

102 .. versionadded:: 0.9 

103 

104 :param key: the data key 

105 """ 

106 global _global_data 

107 if _global_data is None: 

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

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

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

111 _raise_no_data_error() 

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

113 _global_data = pickle.load(fileobj) 

114 assert _global_data is not None 

115 return _global_data.get(key, {}) 

116 

117 

118LOCALE_ALIASES = { 

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

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

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

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

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

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

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

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

127} # fmt: skip 

128 

129 

130class UnknownLocaleError(Exception): 

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

132 is available. 

133 """ 

134 

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

136 """Create the exception. 

137 

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

139 """ 

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

141 

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

143 self.identifier = identifier 

144 

145 

146class Locale: 

147 """Representation of a specific locale. 

148 

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

150 >>> repr(locale) 

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

152 >>> locale.display_name 

153 'English (United States)' 

154 

155 The constructor preserves the casing of the components. Use lowercase 

156 language codes, uppercase territory and variant codes, and title-case 

157 script codes. A `Locale` object can also be instantiated from a raw locale 

158 string with :meth:`parse`, which normalizes the casing of these codes: 

159 

160 >>> locale = Locale.parse('EN-us', sep='-') 

161 >>> repr(locale) 

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

163 

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

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

166 

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

168 '.' 

169 

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

171 `UnknownLocaleError` is raised: 

172 

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

174 Traceback (most recent call last): 

175 ... 

176 UnknownLocaleError: unknown locale 'en_XX' 

177 

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

179 """ 

180 

181 def __init__( 

182 self, 

183 language: str, 

184 territory: str | None = None, 

185 script: str | None = None, 

186 variant: str | None = None, 

187 modifier: str | None = None, 

188 ) -> None: 

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

190 

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

192 >>> locale.language 

193 'en' 

194 >>> locale.territory 

195 'US' 

196 

197 The components are stored as supplied, without case normalization. 

198 Use :meth:`parse` for case-insensitive parsing of locale identifier 

199 strings. 

200 

201 :param language: the lowercase language code 

202 :param territory: the uppercase territory (country or region) code, 

203 or a numeric region code 

204 :param script: the title-case script code 

205 :param variant: the uppercase variant code 

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

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

208 requested locale 

209 """ 

210 #: the language code 

211 self.language = language 

212 #: the territory (country or region) code 

213 self.territory = territory 

214 #: the script code 

215 self.script = script 

216 #: the variant code 

217 self.variant = variant 

218 #: the modifier 

219 self.modifier = modifier 

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

221 

222 identifier = str(self) 

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

224 if localedata.exists(identifier): 

225 self.__data_identifier = identifier 

226 elif localedata.exists(identifier_without_modifier): 

227 self.__data_identifier = identifier_without_modifier 

228 else: 

229 raise UnknownLocaleError(identifier) 

230 

231 @classmethod 

232 def default( 

233 cls, 

234 category: str | None = None, 

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

236 ) -> Locale: 

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

238 

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

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

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

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

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

244 

245 The following fallbacks to the variable are always considered: 

246 

247 - ``LANGUAGE`` 

248 - ``LC_ALL`` 

249 - ``LC_CTYPE`` 

250 - ``LANG`` 

251 

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

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

254 """ 

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

256 # aliases dictionary. 

257 locale_string = default_locale(category, aliases=aliases) 

258 return cls.parse(locale_string) 

259 

260 @classmethod 

261 def negotiate( 

262 cls, 

263 preferred: Iterable[str], 

264 available: Iterable[str], 

265 sep: str = '_', 

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

267 ) -> Locale | None: 

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

269 

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

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

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

273 Locale('de') 

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

275 

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

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

278 case is ignored in the comparison: 

279 

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

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

282 

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

284 :param available: the list of locale identifiers available 

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

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

287 """ 

288 identifier = negotiate_locale(preferred, available, sep=sep, aliases=aliases) 

289 if identifier: 

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

291 return None 

292 

293 @classmethod 

294 def parse( 

295 cls, 

296 identifier: Locale | str | None, 

297 sep: str = '_', 

298 resolve_likely_subtags: bool = True, 

299 ) -> Locale: 

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

301 

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

303 >>> l.display_name 

304 'Deutsch (Deutschland)' 

305 

306 For string identifiers, language, territory, script, and variant codes 

307 are case-insensitive and normalized to their usual casing. Modifiers 

308 retain their casing. 

309 

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

311 Locale('zh', territory='TW', script='Hant') 

312 

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

314 object, that object is returned: 

315 

316 >>> Locale.parse(l) 

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

318 

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

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

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

322 

323 >>> Locale.parse(None) 

324 Traceback (most recent call last): 

325 ... 

326 TypeError: ... 

327 

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

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

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

331 language tag: 

332 

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

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

335 

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

337 

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

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

340 

341 :param identifier: the locale identifier string 

342 :param sep: optional component separator 

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

344 have its likely subtag resolved if the 

345 locale otherwise does not exist. For 

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

347 locale that exists but Babel can 

348 automatically expand it to the full 

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

350 expansion is only taking place if no 

351 locale exists otherwise. For instance 

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

353 by itself. 

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

355 identifier 

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

357 requested locale 

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

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

360 """ 

361 if isinstance(identifier, Locale): 

362 return identifier 

363 

364 if not identifier: 

365 msg = ( 

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

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

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

369 f"variables for the API you tried to use." 

370 ) 

371 if isinstance(identifier, str): 

372 # `parse_locale` would raise a ValueError, so let's do that here 

373 raise ValueError(msg) 

374 raise TypeError(msg) 

375 

376 if not isinstance(identifier, str): 

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

378 

379 # C/POSIX is not a CLDR language. Same mapping default_locale uses. 

380 posix_stem = identifier.partition(".")[0] 

381 if posix_stem.upper() in {"C", "POSIX"}: 

382 identifier = "en_US_POSIX" 

383 

384 parts = parse_locale(identifier, sep=sep) 

385 input_id = get_locale_identifier(parts) 

386 

387 def _try_load(parts): 

388 try: 

389 return cls(*parts) 

390 except UnknownLocaleError: 

391 return None 

392 

393 def _try_load_reducing(parts): 

394 # Success on first hit, return it. 

395 locale = _try_load(parts) 

396 if locale is not None: 

397 return locale 

398 

399 # Now try without script and variant 

400 locale = _try_load(parts[:2]) 

401 if locale is not None: 

402 return locale 

403 

404 locale = _try_load(parts) 

405 if locale is not None: 

406 return locale 

407 if not resolve_likely_subtags: 

408 raise UnknownLocaleError(input_id) 

409 

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

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

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

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

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

415 # maximize and minimize locale tags. 

416 

417 if len(parts) == 5: 

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

419 else: 

420 language, territory, script, variant = parts 

421 modifier = None 

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

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

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

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

426 

427 if territory == 'ZZ': 

428 territory = None 

429 if script == 'Zzzz': 

430 script = None 

431 

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

433 

434 # First match: try the whole identifier 

435 new_id = get_locale_identifier(parts) 

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

437 if likely_subtag is not None: 

438 locale = _try_load_reducing(parse_locale(likely_subtag)) 

439 if locale is not None: 

440 return locale 

441 

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

443 # simplified identifier that is just the language 

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

445 if likely_subtag is not None: 

446 parts2 = parse_locale(likely_subtag) 

447 if len(parts2) == 5: 

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

449 else: 

450 language2, _, script2, variant2 = parts2 

451 modifier2 = None 

452 locale = _try_load_reducing( 

453 (language2, territory, script2, variant2, modifier2), 

454 ) 

455 if locale is not None: 

456 return locale 

457 

458 raise UnknownLocaleError(input_id) 

459 

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

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

462 if not hasattr(other, key): 

463 return False 

464 return ( 

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

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

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

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

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

470 ) 

471 

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

473 return not self.__eq__(other) 

474 

475 def __hash__(self) -> int: 

476 return hash((self.language, self.territory, self.script, self.variant, self.modifier)) 

477 

478 def __repr__(self) -> str: 

479 parameters = [''] 

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

481 value = getattr(self, key) 

482 if value is not None: 

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

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

485 

486 def __str__(self) -> str: 

487 return get_locale_identifier( 

488 (self.language, self.territory, self.script, self.variant, self.modifier), 

489 ) 

490 

491 @property 

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

493 if self.__data is None: 

494 self.__data = localedata.get_locale_data(self.__data_identifier) 

495 return self.__data 

496 

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

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

499 

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

501 variant, if those are specified. 

502 

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

504 'Chinese (Simplified, China)' 

505 

506 Modifiers are currently passed through verbatim: 

507 

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

509 'Italian (Italy, euro)' 

510 

511 :param locale: the locale to use 

512 """ 

513 if locale is None: 

514 locale = self 

515 locale = Locale.parse(locale) 

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

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

518 details = [] 

519 if self.script: 

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

521 if self.territory: 

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

523 if self.variant: 

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

525 if self.modifier: 

526 details.append(self.modifier) 

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

528 if detail_string: 

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

530 return retval 

531 

532 @property 

533 def display_name(self) -> str | None: 

534 """ 

535 The localized display name of the locale. 

536 

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

538 'English' 

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

540 'English (United States)' 

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

542 'svenska' 

543 """ 

544 return self.get_display_name() 

545 

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

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

548 

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

550 'Chinesisch' 

551 

552 .. versionadded:: 1.0 

553 

554 :param locale: the locale to use 

555 """ 

556 if locale is None: 

557 locale = self 

558 locale = Locale.parse(locale) 

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

560 

561 @property 

562 def language_name(self) -> str | None: 

563 """ 

564 The localized language name of the locale. 

565 

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

567 'English' 

568 """ 

569 return self.get_language_name() 

570 

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

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

573 if locale is None: 

574 locale = self 

575 locale = Locale.parse(locale) 

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

577 

578 @property 

579 def territory_name(self) -> str | None: 

580 """ 

581 The localized territory name of the locale if available. 

582 

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

584 'Deutschland' 

585 """ 

586 return self.get_territory_name() 

587 

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

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

590 if locale is None: 

591 locale = self 

592 locale = Locale.parse(locale) 

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

594 

595 @property 

596 def script_name(self) -> str | None: 

597 """ 

598 The localized script name of the locale if available. 

599 

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

601 'latinica' 

602 """ 

603 return self.get_script_name() 

604 

605 @property 

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

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

608 

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

610 'German' 

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

612 'German (Germany)' 

613 """ 

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

615 

616 # { General Locale Display Names 

617 

618 @property 

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

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

621 

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

623 'Japanisch' 

624 

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

626 more information. 

627 """ 

628 return self._data['languages'] 

629 

630 @property 

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

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

633 

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

635 'Hiragana' 

636 

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

638 for more information. 

639 """ 

640 return self._data['scripts'] 

641 

642 @property 

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

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

645 

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

647 'Alemania' 

648 

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

650 for more information. 

651 """ 

652 return self._data['territories'] 

653 

654 @property 

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

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

657 

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

659 'Alte deutsche Rechtschreibung' 

660 """ 

661 return self._data['variants'] 

662 

663 # { Number Formatting 

664 

665 @property 

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

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

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

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

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

671 

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

673 'Colombian Peso' 

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

675 'Kolumbianischer Peso' 

676 """ 

677 return self._data['currency_names'] 

678 

679 @property 

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

681 """Mapping of currency codes to symbols. 

682 

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

684 '$' 

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

686 'US$' 

687 """ 

688 return self._data['currency_symbols'] 

689 

690 @property 

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

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

693 

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

695 Babel versions. 

696 

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

698 ',' 

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

700 '٫' 

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

702 '.' 

703 """ 

704 return self._data['number_symbols'] 

705 

706 @property 

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

708 """ 

709 Mapping of other numbering systems available for the locale. 

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

711 

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

713 'grek' 

714 

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

716 Babel versions. 

717 """ 

718 return self._data['numbering_systems'] 

719 

720 @property 

721 def default_numbering_system(self) -> str: 

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

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

724 'latn' 

725 """ 

726 return self._data['default_numbering_system'] 

727 

728 @property 

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

730 """Locale patterns for decimal number formatting. 

731 

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

733 Babel versions. 

734 

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

736 <NumberPattern '#,##0.###'> 

737 """ 

738 return self._data['decimal_formats'] 

739 

740 @property 

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

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

743 

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

745 Babel versions. 

746 

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

748 <NumberPattern '0K'> 

749 """ 

750 return self._data['compact_decimal_formats'] 

751 

752 @property 

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

754 """Locale patterns for currency number formatting. 

755 

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

757 Babel versions. 

758 

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

760 <NumberPattern '\\xa4#,##0.00'> 

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

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

763 """ 

764 return self._data['currency_formats'] 

765 

766 @property 

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

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

769 

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

771 Babel versions. 

772 

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

774 <NumberPattern '¤0K'> 

775 """ 

776 return self._data['compact_currency_formats'] 

777 

778 @property 

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

780 """Locale patterns for percent number formatting. 

781 

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

783 Babel versions. 

784 

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

786 <NumberPattern '#,##0%'> 

787 """ 

788 return self._data['percent_formats'] 

789 

790 @property 

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

792 """Locale patterns for scientific number formatting. 

793 

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

795 Babel versions. 

796 

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

798 <NumberPattern '#E0'> 

799 """ 

800 return self._data['scientific_formats'] 

801 

802 # { Calendar Information and Date Formatting 

803 

804 @property 

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

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

807 

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

809 'AM' 

810 """ 

811 try: 

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

813 except KeyError: 

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

815 

816 @property 

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

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

819 

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

821 """ 

822 return self._data['day_periods'] 

823 

824 @property 

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

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

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

828 

829 @property 

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

831 """Locale display names for weekdays. 

832 

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

834 'Donnerstag' 

835 """ 

836 return self._data['days'] 

837 

838 @property 

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

840 """Locale display names for months. 

841 

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

843 'Oktober' 

844 """ 

845 return self._data['months'] 

846 

847 @property 

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

849 """Locale display names for quarters. 

850 

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

852 '1. Quartal' 

853 """ 

854 return self._data['quarters'] 

855 

856 @property 

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

858 """Locale display names for eras. 

859 

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

861 Babel versions. 

862 

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

864 'Anno Domini' 

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

866 'BC' 

867 """ 

868 return self._data['eras'] 

869 

870 @property 

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

872 """Locale display names for time zones. 

873 

874 The keys are canonical IANA time zone IDs like ``Asia/Kolkata``, 

875 not ``Asia/Calcutta``, like the ``zone_aliases`` global. 

876 

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

878 Babel versions. 

879 

880 >>> Locale('en', 'US').time_zones['Etc/UTC']['long']['standard'] 

881 'Coordinated Universal Time' 

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

883 'St. John’s' 

884 >>> Locale('de').time_zones['Asia/Kolkata']['city'] 

885 'Kalkutta' 

886 """ 

887 return self._data['time_zones'] 

888 

889 @property 

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

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

892 

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

894 have the same GMT offset and daylight savings time. 

895 

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

897 Babel versions. 

898 

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

900 'Central European Summer Time' 

901 

902 .. versionadded:: 0.9 

903 """ 

904 return self._data['meta_zones'] 

905 

906 @property 

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

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

909 

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

911 Babel versions. 

912 

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

914 '%(1)s (%(0)s)' 

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

916 'Horário %s' 

917 >>> Locale('sl').zone_formats['gmt'] 

918 'GMT %s' 

919 >>> Locale('sl').zone_formats['gmt_utc'] 

920 'UTC %s' 

921 

922 .. versionadded:: 0.9 

923 """ 

924 return self._data['zone_formats'] 

925 

926 @property 

927 def first_week_day(self) -> int: 

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

929 

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

931 0 

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

933 6 

934 """ 

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

936 

937 @property 

938 def weekend_start(self) -> int: 

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

940 

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

942 5 

943 """ 

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

945 

946 @property 

947 def weekend_end(self) -> int: 

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

949 

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

951 6 

952 """ 

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

954 

955 @property 

956 def min_week_days(self) -> int: 

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

958 the first week of a year or month. 

959 

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

961 4 

962 """ 

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

964 

965 @property 

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

967 """Locale patterns for date formatting. 

968 

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

970 Babel versions. 

971 

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

973 <DateTimePattern 'M/d/yy'> 

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

975 <DateTimePattern 'd MMMM y'> 

976 """ 

977 return self._data['date_formats'] 

978 

979 @property 

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

981 """Locale patterns for time formatting. 

982 

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

984 Babel versions. 

985 

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

987 <DateTimePattern 'h:mm\\u202fa'> 

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

989 <DateTimePattern 'HH:mm:ss z'> 

990 """ 

991 return self._data['time_formats'] 

992 

993 @property 

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

995 """Locale patterns for datetime formatting. 

996 

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

998 Babel versions. 

999 

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

1001 '{1}, {0}' 

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

1003 '{1} {0}' 

1004 """ 

1005 return self._data['datetime_formats'] 

1006 

1007 @property 

1008 def datetime_formats_relative(self) -> localedata.LocaleDataDict: 

1009 """Locale patterns for combining a relative date with a time. 

1010 

1011 Introduced in CLDR 48. 

1012 

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

1014 Babel versions. 

1015 

1016 >>> Locale('en').datetime_formats_relative['full'] 

1017 "{1} 'at' {0}" 

1018 

1019 .. versionadded:: 2.19 

1020 """ 

1021 return self._data['datetime_formats_relative'] 

1022 

1023 @property 

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

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

1026 

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

1028 <DateTimePattern 'E, M/d'> 

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

1030 <DateTimePattern 'E dd/MM'> 

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

1032 <DateTimePattern 'HH'> 

1033 """ 

1034 return self._data['datetime_skeletons'] 

1035 

1036 @property 

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

1038 """Locale patterns for interval formatting. 

1039 

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

1041 Babel versions. 

1042 

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

1044 smallest changing component: 

1045 

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

1047 ['E d.\\u2009–\\u2009', 'E d.M.'] 

1048 

1049 .. seealso:: 

1050 

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

1052 

1053 

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

1055 """ 

1056 return self._data['interval_formats'] 

1057 

1058 @property 

1059 def plural_form(self) -> PluralRule: 

1060 """Plural rules for the locale. 

1061 

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

1063 'one' 

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

1065 'other' 

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

1067 'one' 

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

1069 'many' 

1070 """ 

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

1072 

1073 @property 

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

1075 """Patterns for generating lists 

1076 

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

1078 Babel versions. 

1079 

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

1081 '{0}, {1}' 

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

1083 '{0}, and {1}' 

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

1085 '{0} and {1}' 

1086 """ 

1087 return self._data['list_patterns'] 

1088 

1089 @property 

1090 def ordinal_form(self) -> PluralRule: 

1091 """Plural rules for the locale. 

1092 

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

1094 'one' 

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

1096 'two' 

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

1098 'few' 

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

1100 'other' 

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

1102 'other' 

1103 """ 

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

1105 

1106 @property 

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

1108 """Localized names for various measurement systems. 

1109 

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

1111 'américain' 

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

1113 'US' 

1114 

1115 """ 

1116 return self._data['measurement_systems'] 

1117 

1118 @property 

1119 def character_order(self) -> str: 

1120 """The text direction for the language. 

1121 

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

1123 'left-to-right' 

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

1125 'right-to-left' 

1126 """ 

1127 return self._data['character_order'] 

1128 

1129 @property 

1130 def text_direction(self) -> str: 

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

1132 

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

1134 'ltr' 

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

1136 'rtl' 

1137 """ 

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

1139 

1140 @property 

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

1142 """Display names for units of measurement. 

1143 

1144 .. seealso:: 

1145 

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

1147 

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

1149 Babel versions. 

1150 

1151 """ 

1152 return self._data['unit_display_names'] 

1153 

1154 

1155def default_locale( 

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

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

1158) -> str | None: 

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

1160 environment variables. 

1161 

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

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

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

1165 >>> default_locale('LC_MESSAGES') 

1166 'fr_FR' 

1167 

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

1169 "en_US_POSIX" locale: 

1170 

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

1172 >>> default_locale('LC_MESSAGES') 

1173 'en_US_POSIX' 

1174 

1175 The following fallbacks to the variable are always considered: 

1176 

1177 - ``LANGUAGE`` 

1178 - ``LC_ALL`` 

1179 - ``LC_CTYPE`` 

1180 - ``LANG`` 

1181 

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

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

1184 """ 

1185 

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

1187 if category: 

1188 if isinstance(category, str): 

1189 varnames = (category, *varnames) 

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

1191 varnames = (*category, *varnames) 

1192 else: 

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

1194 

1195 for name in varnames: 

1196 if not name: 

1197 continue 

1198 locale = os.getenv(name) 

1199 if locale: 

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

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

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

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

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

1205 locale = 'en_US_POSIX' 

1206 elif aliases and locale in aliases: 

1207 locale = aliases[locale] 

1208 try: 

1209 return get_locale_identifier(parse_locale(locale)) 

1210 except ValueError: 

1211 pass 

1212 return None 

1213 

1214 

1215def negotiate_locale( 

1216 preferred: Iterable[str], 

1217 available: Iterable[str], 

1218 sep: str = '_', 

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

1220) -> str | None: 

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

1222 

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

1224 'de_DE' 

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

1226 'de' 

1227 

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

1229 locale identifier: 

1230 

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

1232 'de_DE' 

1233 

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

1235 'de_DE' 

1236 

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

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

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

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

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

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

1243 identifiers to identifiers including the territory: 

1244 

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

1246 'ja_JP' 

1247 

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

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

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

1251 such cases, too: 

1252 

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

1254 'nb_NO' 

1255 

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

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

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

1259 

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

1261 :param available: the list of locale strings available 

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

1263 strings 

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

1265 """ 

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

1267 for locale in preferred: 

1268 ll = locale.lower() 

1269 if ll in available: 

1270 return locale 

1271 if aliases: 

1272 alias = aliases.get(ll) 

1273 if alias: 

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

1275 if alias.lower() in available: 

1276 return alias 

1277 parts = locale.split(sep) 

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

1279 return parts[0] 

1280 return None 

1281 

1282 

1283def parse_locale( 

1284 identifier: str, 

1285 sep: str = '_', 

1286) -> ( 

1287 tuple[str, str | None, str | None, str | None] 

1288 | tuple[str, str | None, str | None, str | None, str | None] 

1289): 

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

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

1292 

1293 >>> parse_locale('zh_CN') 

1294 ('zh', 'CN', None, None) 

1295 >>> parse_locale('zh_Hans_CN') 

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

1297 >>> parse_locale('ca_es_valencia') 

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

1299 >>> parse_locale('en_150') 

1300 ('en', '150', None, None) 

1301 >>> parse_locale('en_us_posix') 

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

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

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

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

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

1307 >>> parse_locale('it_IT@') 

1308 ('it', 'IT', None, None) 

1309 

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

1311 specified using the `sep` parameter. 

1312 

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

1314 

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

1316 ('zh', 'CN', None, None) 

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

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

1319 

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

1321 is raised: 

1322 

1323 >>> parse_locale('not_a_LOCALE_String') 

1324 Traceback (most recent call last): 

1325 ... 

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

1327 

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

1329 kept: 

1330 

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

1332 ('en', 'US', None, None) 

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

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

1335 

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

1337 

1338 :param identifier: the locale identifier string 

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

1340 identifier 

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

1342 identifier 

1343 """ 

1344 if not identifier: 

1345 raise ValueError("empty locale identifier") 

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

1347 if '.' in identifier: 

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

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

1350 

1351 parts = identifier.split(sep) 

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

1353 if not lang.isalpha(): 

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

1355 

1356 script = territory = variant = None 

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

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

1359 

1360 if parts: 

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

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

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

1364 territory = parts.pop(0) 

1365 

1366 if parts and ( 

1367 len(parts[0]) == 4 

1368 and parts[0][0].isdigit() 

1369 or len(parts[0]) >= 5 

1370 and parts[0][0].isalpha() 

1371 ): 

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

1373 

1374 if parts: 

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

1376 

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

1378 if modifier: 

1379 return lang, territory, script, variant, modifier 

1380 else: 

1381 return lang, territory, script, variant 

1382 

1383 

1384def get_locale_identifier( 

1385 tup: tuple[str] 

1386 | tuple[str, str | None] 

1387 | tuple[str, str | None, str | None] 

1388 | tuple[str, str | None, str | None, str | None] 

1389 | tuple[str, str | None, str | None, str | None, str | None], 

1390 sep: str = "_", 

1391) -> str: 

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

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

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

1395 

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

1397 'de_DE_1999@custom' 

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

1399 'fi@custom' 

1400 

1401 

1402 .. versionadded:: 1.0 

1403 

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

1405 :param sep: the separator for the identifier. 

1406 """ 

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

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

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

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

1411 

1412 

1413def get_cldr_version() -> str: 

1414 """Return the Unicode CLDR version used by this Babel installation. 

1415 

1416 Generally, you should be able to assume that the return value of this 

1417 function is a string representing a version number, e.g. '47'. 

1418 

1419 >>> get_cldr_version() 

1420 '49' 

1421 

1422 .. versionadded:: 2.18 

1423 

1424 :rtype: str 

1425 """ 

1426 return str(get_global("cldr")["version"])