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

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

277 statements  

1""" 

2 babel.support 

3 ~~~~~~~~~~~~~ 

4 

5 Several classes and functions that help with integrating and using Babel 

6 in applications. 

7 

8 .. note: the code in this module is not used by Babel itself 

9 

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

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

12""" 

13from __future__ import annotations 

14 

15import gettext 

16import locale 

17import os 

18from collections.abc import Iterator 

19from typing import TYPE_CHECKING, Any, Callable, Iterable, Literal 

20 

21from babel.core import Locale 

22from babel.dates import format_date, format_datetime, format_time, format_timedelta 

23from babel.numbers import ( 

24 format_compact_currency, 

25 format_compact_decimal, 

26 format_currency, 

27 format_decimal, 

28 format_percent, 

29 format_scientific, 

30) 

31 

32if TYPE_CHECKING: 

33 import datetime as _datetime 

34 from decimal import Decimal 

35 

36 from babel.dates import _PredefinedTimeFormat 

37 

38 

39class Format: 

40 """Wrapper class providing the various date and number formatting functions 

41 bound to a specific locale and time-zone. 

42 

43 >>> from babel.util import UTC 

44 >>> from datetime import date 

45 >>> fmt = Format('en_US', UTC) 

46 >>> fmt.date(date(2007, 4, 1)) 

47 u'Apr 1, 2007' 

48 >>> fmt.decimal(1.2345) 

49 u'1.234' 

50 """ 

51 

52 def __init__( 

53 self, 

54 locale: Locale | str, 

55 tzinfo: _datetime.tzinfo | None = None, 

56 *, 

57 numbering_system: Literal["default"] | str = "latn", 

58 ) -> None: 

59 """Initialize the formatter. 

60 

61 :param locale: the locale identifier or `Locale` instance 

62 :param tzinfo: the time-zone info (a `tzinfo` instance or `None`) 

63 :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn". 

64 The special value "default" will use the default numbering system of the locale. 

65 """ 

66 self.locale = Locale.parse(locale) 

67 self.tzinfo = tzinfo 

68 self.numbering_system = numbering_system 

69 

70 def date( 

71 self, 

72 date: _datetime.date | None = None, 

73 format: _PredefinedTimeFormat | str = 'medium', 

74 ) -> str: 

75 """Return a date formatted according to the given pattern. 

76 

77 >>> from datetime import date 

78 >>> fmt = Format('en_US') 

79 >>> fmt.date(date(2007, 4, 1)) 

80 u'Apr 1, 2007' 

81 """ 

82 return format_date(date, format, locale=self.locale) 

83 

84 def datetime( 

85 self, 

86 datetime: _datetime.date | None = None, 

87 format: _PredefinedTimeFormat | str = 'medium', 

88 ) -> str: 

89 """Return a date and time formatted according to the given pattern. 

90 

91 >>> from datetime import datetime 

92 >>> from babel.dates import get_timezone 

93 >>> fmt = Format('en_US', tzinfo=get_timezone('US/Eastern')) 

94 >>> fmt.datetime(datetime(2007, 4, 1, 15, 30)) 

95 u'Apr 1, 2007, 11:30:00\u202fAM' 

96 """ 

97 return format_datetime(datetime, format, tzinfo=self.tzinfo, locale=self.locale) 

98 

99 def time( 

100 self, 

101 time: _datetime.time | _datetime.datetime | None = None, 

102 format: _PredefinedTimeFormat | str = 'medium', 

103 ) -> str: 

104 """Return a time formatted according to the given pattern. 

105 

106 >>> from datetime import datetime 

107 >>> from babel.dates import get_timezone 

108 >>> fmt = Format('en_US', tzinfo=get_timezone('US/Eastern')) 

109 >>> fmt.time(datetime(2007, 4, 1, 15, 30)) 

110 u'11:30:00\u202fAM' 

111 """ 

112 return format_time(time, format, tzinfo=self.tzinfo, locale=self.locale) 

113 

114 def timedelta( 

115 self, 

116 delta: _datetime.timedelta | int, 

117 granularity: Literal["year", "month", "week", "day", "hour", "minute", "second"] = "second", 

118 threshold: float = 0.85, 

119 format: Literal["narrow", "short", "medium", "long"] = "long", 

120 add_direction: bool = False, 

121 ) -> str: 

122 """Return a time delta according to the rules of the given locale. 

123 

124 >>> from datetime import timedelta 

125 >>> fmt = Format('en_US') 

126 >>> fmt.timedelta(timedelta(weeks=11)) 

127 u'3 months' 

128 """ 

129 return format_timedelta(delta, granularity=granularity, 

130 threshold=threshold, 

131 format=format, add_direction=add_direction, 

132 locale=self.locale) 

133 

134 def number(self, number: float | Decimal | str) -> str: 

135 """Return an integer number formatted for the locale. 

136 

137 >>> fmt = Format('en_US') 

138 >>> fmt.number(1099) 

139 u'1,099' 

140 """ 

141 return format_decimal(number, locale=self.locale, numbering_system=self.numbering_system) 

142 

143 def decimal(self, number: float | Decimal | str, format: str | None = None) -> str: 

144 """Return a decimal number formatted for the locale. 

145 

146 >>> fmt = Format('en_US') 

147 >>> fmt.decimal(1.2345) 

148 u'1.234' 

149 """ 

150 return format_decimal(number, format, locale=self.locale, numbering_system=self.numbering_system) 

151 

152 def compact_decimal( 

153 self, 

154 number: float | Decimal | str, 

155 format_type: Literal['short', 'long'] = 'short', 

156 fraction_digits: int = 0, 

157 ) -> str: 

158 """Return a number formatted in compact form for the locale. 

159 

160 >>> fmt = Format('en_US') 

161 >>> fmt.compact_decimal(123456789) 

162 u'123M' 

163 >>> fmt.compact_decimal(1234567, format_type='long', fraction_digits=2) 

164 '1.23 million' 

165 """ 

166 return format_compact_decimal( 

167 number, 

168 format_type=format_type, 

169 fraction_digits=fraction_digits, 

170 locale=self.locale, 

171 numbering_system=self.numbering_system, 

172 ) 

173 

174 def currency(self, number: float | Decimal | str, currency: str) -> str: 

175 """Return a number in the given currency formatted for the locale. 

176 """ 

177 return format_currency(number, currency, locale=self.locale, numbering_system=self.numbering_system) 

178 

179 def compact_currency( 

180 self, 

181 number: float | Decimal | str, 

182 currency: str, 

183 format_type: Literal['short'] = 'short', 

184 fraction_digits: int = 0, 

185 ) -> str: 

186 """Return a number in the given currency formatted for the locale 

187 using the compact number format. 

188 

189 >>> Format('en_US').compact_currency(1234567, "USD", format_type='short', fraction_digits=2) 

190 '$1.23M' 

191 """ 

192 return format_compact_currency(number, currency, format_type=format_type, fraction_digits=fraction_digits, 

193 locale=self.locale, numbering_system=self.numbering_system) 

194 

195 def percent(self, number: float | Decimal | str, format: str | None = None) -> str: 

196 """Return a number formatted as percentage for the locale. 

197 

198 >>> fmt = Format('en_US') 

199 >>> fmt.percent(0.34) 

200 u'34%' 

201 """ 

202 return format_percent(number, format, locale=self.locale, numbering_system=self.numbering_system) 

203 

204 def scientific(self, number: float | Decimal | str) -> str: 

205 """Return a number formatted using scientific notation for the locale. 

206 """ 

207 return format_scientific(number, locale=self.locale, numbering_system=self.numbering_system) 

208 

209 

210class LazyProxy: 

211 """Class for proxy objects that delegate to a specified function to evaluate 

212 the actual object. 

213 

214 >>> def greeting(name='world'): 

215 ... return 'Hello, %s!' % name 

216 >>> lazy_greeting = LazyProxy(greeting, name='Joe') 

217 >>> print(lazy_greeting) 

218 Hello, Joe! 

219 >>> u' ' + lazy_greeting 

220 u' Hello, Joe!' 

221 >>> u'(%s)' % lazy_greeting 

222 u'(Hello, Joe!)' 

223 

224 This can be used, for example, to implement lazy translation functions that 

225 delay the actual translation until the string is actually used. The 

226 rationale for such behavior is that the locale of the user may not always 

227 be available. In web applications, you only know the locale when processing 

228 a request. 

229 

230 The proxy implementation attempts to be as complete as possible, so that 

231 the lazy objects should mostly work as expected, for example for sorting: 

232 

233 >>> greetings = [ 

234 ... LazyProxy(greeting, 'world'), 

235 ... LazyProxy(greeting, 'Joe'), 

236 ... LazyProxy(greeting, 'universe'), 

237 ... ] 

238 >>> greetings.sort() 

239 >>> for greeting in greetings: 

240 ... print(greeting) 

241 Hello, Joe! 

242 Hello, universe! 

243 Hello, world! 

244 """ 

245 __slots__ = ['_func', '_args', '_kwargs', '_value', '_is_cache_enabled', '_attribute_error'] 

246 

247 if TYPE_CHECKING: 

248 _func: Callable[..., Any] 

249 _args: tuple[Any, ...] 

250 _kwargs: dict[str, Any] 

251 _is_cache_enabled: bool 

252 _value: Any 

253 _attribute_error: AttributeError | None 

254 

255 def __init__(self, func: Callable[..., Any], *args: Any, enable_cache: bool = True, **kwargs: Any) -> None: 

256 # Avoid triggering our own __setattr__ implementation 

257 object.__setattr__(self, '_func', func) 

258 object.__setattr__(self, '_args', args) 

259 object.__setattr__(self, '_kwargs', kwargs) 

260 object.__setattr__(self, '_is_cache_enabled', enable_cache) 

261 object.__setattr__(self, '_value', None) 

262 object.__setattr__(self, '_attribute_error', None) 

263 

264 @property 

265 def value(self) -> Any: 

266 if self._value is None: 

267 try: 

268 value = self._func(*self._args, **self._kwargs) 

269 except AttributeError as error: 

270 object.__setattr__(self, '_attribute_error', error) 

271 raise 

272 

273 if not self._is_cache_enabled: 

274 return value 

275 object.__setattr__(self, '_value', value) 

276 return self._value 

277 

278 def __contains__(self, key: object) -> bool: 

279 return key in self.value 

280 

281 def __bool__(self) -> bool: 

282 return bool(self.value) 

283 

284 def __dir__(self) -> list[str]: 

285 return dir(self.value) 

286 

287 def __iter__(self) -> Iterator[Any]: 

288 return iter(self.value) 

289 

290 def __len__(self) -> int: 

291 return len(self.value) 

292 

293 def __str__(self) -> str: 

294 return str(self.value) 

295 

296 def __add__(self, other: object) -> Any: 

297 return self.value + other 

298 

299 def __radd__(self, other: object) -> Any: 

300 return other + self.value 

301 

302 def __mod__(self, other: object) -> Any: 

303 return self.value % other 

304 

305 def __rmod__(self, other: object) -> Any: 

306 return other % self.value 

307 

308 def __mul__(self, other: object) -> Any: 

309 return self.value * other 

310 

311 def __rmul__(self, other: object) -> Any: 

312 return other * self.value 

313 

314 def __call__(self, *args: Any, **kwargs: Any) -> Any: 

315 return self.value(*args, **kwargs) 

316 

317 def __lt__(self, other: object) -> bool: 

318 return self.value < other 

319 

320 def __le__(self, other: object) -> bool: 

321 return self.value <= other 

322 

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

324 return self.value == other 

325 

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

327 return self.value != other 

328 

329 def __gt__(self, other: object) -> bool: 

330 return self.value > other 

331 

332 def __ge__(self, other: object) -> bool: 

333 return self.value >= other 

334 

335 def __delattr__(self, name: str) -> None: 

336 delattr(self.value, name) 

337 

338 def __getattr__(self, name: str) -> Any: 

339 if self._attribute_error is not None: 

340 raise self._attribute_error 

341 return getattr(self.value, name) 

342 

343 def __setattr__(self, name: str, value: Any) -> None: 

344 setattr(self.value, name, value) 

345 

346 def __delitem__(self, key: Any) -> None: 

347 del self.value[key] 

348 

349 def __getitem__(self, key: Any) -> Any: 

350 return self.value[key] 

351 

352 def __setitem__(self, key: Any, value: Any) -> None: 

353 self.value[key] = value 

354 

355 def __copy__(self) -> LazyProxy: 

356 return LazyProxy( 

357 self._func, 

358 enable_cache=self._is_cache_enabled, 

359 *self._args, # noqa: B026 

360 **self._kwargs, 

361 ) 

362 

363 def __deepcopy__(self, memo: Any) -> LazyProxy: 

364 from copy import deepcopy 

365 return LazyProxy( 

366 deepcopy(self._func, memo), 

367 enable_cache=deepcopy(self._is_cache_enabled, memo), 

368 *deepcopy(self._args, memo), # noqa: B026 

369 **deepcopy(self._kwargs, memo), 

370 ) 

371 

372 

373class NullTranslations(gettext.NullTranslations): 

374 

375 if TYPE_CHECKING: 

376 _info: dict[str, str] 

377 _fallback: NullTranslations | None 

378 

379 DEFAULT_DOMAIN = None 

380 

381 def __init__(self, fp: gettext._TranslationsReader | None = None) -> None: 

382 """Initialize a simple translations class which is not backed by a 

383 real catalog. Behaves similar to gettext.NullTranslations but also 

384 offers Babel's on *gettext methods (e.g. 'dgettext()'). 

385 

386 :param fp: a file-like object (ignored in this class) 

387 """ 

388 # These attributes are set by gettext.NullTranslations when a catalog 

389 # is parsed (fp != None). Ensure that they are always present because 

390 # some *gettext methods (including '.gettext()') rely on the attributes. 

391 self._catalog: dict[tuple[str, Any] | str, str] = {} 

392 self.plural: Callable[[float | Decimal], int] = lambda n: int(n != 1) 

393 super().__init__(fp=fp) 

394 self.files = list(filter(None, [getattr(fp, 'name', None)])) 

395 self.domain = self.DEFAULT_DOMAIN 

396 self._domains: dict[str, NullTranslations] = {} 

397 

398 def dgettext(self, domain: str, message: str) -> str: 

399 """Like ``gettext()``, but look the message up in the specified 

400 domain. 

401 """ 

402 return self._domains.get(domain, self).gettext(message) 

403 

404 def ldgettext(self, domain: str, message: str) -> str: 

405 """Like ``lgettext()``, but look the message up in the specified 

406 domain. 

407 """ 

408 import warnings 

409 warnings.warn( 

410 'ldgettext() is deprecated, use dgettext() instead', 

411 DeprecationWarning, 

412 stacklevel=2, 

413 ) 

414 return self._domains.get(domain, self).lgettext(message) 

415 

416 def udgettext(self, domain: str, message: str) -> str: 

417 """Like ``ugettext()``, but look the message up in the specified 

418 domain. 

419 """ 

420 return self._domains.get(domain, self).ugettext(message) 

421 # backward compatibility with 0.9 

422 dugettext = udgettext 

423 

424 def dngettext(self, domain: str, singular: str, plural: str, num: int) -> str: 

425 """Like ``ngettext()``, but look the message up in the specified 

426 domain. 

427 """ 

428 return self._domains.get(domain, self).ngettext(singular, plural, num) 

429 

430 def ldngettext(self, domain: str, singular: str, plural: str, num: int) -> str: 

431 """Like ``lngettext()``, but look the message up in the specified 

432 domain. 

433 """ 

434 import warnings 

435 warnings.warn( 

436 'ldngettext() is deprecated, use dngettext() instead', 

437 DeprecationWarning, 

438 stacklevel=2, 

439 ) 

440 return self._domains.get(domain, self).lngettext(singular, plural, num) 

441 

442 def udngettext(self, domain: str, singular: str, plural: str, num: int) -> str: 

443 """Like ``ungettext()`` but look the message up in the specified 

444 domain. 

445 """ 

446 return self._domains.get(domain, self).ungettext(singular, plural, num) 

447 # backward compatibility with 0.9 

448 dungettext = udngettext 

449 

450 # Most of the downwards code, until it gets included in stdlib, from: 

451 # https://bugs.python.org/file10036/gettext-pgettext.patch 

452 # 

453 # The encoding of a msgctxt and a msgid in a .mo file is 

454 # msgctxt + "\x04" + msgid (gettext version >= 0.15) 

455 CONTEXT_ENCODING = '%s\x04%s' 

456 

457 def pgettext(self, context: str, message: str) -> str | object: 

458 """Look up the `context` and `message` id in the catalog and return the 

459 corresponding message string, as an 8-bit string encoded with the 

460 catalog's charset encoding, if known. If there is no entry in the 

461 catalog for the `message` id and `context` , and a fallback has been 

462 set, the look up is forwarded to the fallback's ``pgettext()`` 

463 method. Otherwise, the `message` id is returned. 

464 """ 

465 ctxt_msg_id = self.CONTEXT_ENCODING % (context, message) 

466 missing = object() 

467 tmsg = self._catalog.get(ctxt_msg_id, missing) 

468 if tmsg is missing: 

469 tmsg = self._catalog.get((ctxt_msg_id, self.plural(1)), missing) 

470 if tmsg is not missing: 

471 return tmsg 

472 if self._fallback: 

473 return self._fallback.pgettext(context, message) 

474 return message 

475 

476 def lpgettext(self, context: str, message: str) -> str | bytes | object: 

477 """Equivalent to ``pgettext()``, but the translation is returned in the 

478 preferred system encoding, if no other encoding was explicitly set with 

479 ``bind_textdomain_codeset()``. 

480 """ 

481 import warnings 

482 warnings.warn( 

483 'lpgettext() is deprecated, use pgettext() instead', 

484 DeprecationWarning, 

485 stacklevel=2, 

486 ) 

487 tmsg = self.pgettext(context, message) 

488 encoding = getattr(self, "_output_charset", None) or locale.getpreferredencoding() 

489 return tmsg.encode(encoding) if isinstance(tmsg, str) else tmsg 

490 

491 def npgettext(self, context: str, singular: str, plural: str, num: int) -> str: 

492 """Do a plural-forms lookup of a message id. `singular` is used as the 

493 message id for purposes of lookup in the catalog, while `num` is used to 

494 determine which plural form to use. The returned message string is an 

495 8-bit string encoded with the catalog's charset encoding, if known. 

496 

497 If the message id for `context` is not found in the catalog, and a 

498 fallback is specified, the request is forwarded to the fallback's 

499 ``npgettext()`` method. Otherwise, when ``num`` is 1 ``singular`` is 

500 returned, and ``plural`` is returned in all other cases. 

501 """ 

502 ctxt_msg_id = self.CONTEXT_ENCODING % (context, singular) 

503 try: 

504 tmsg = self._catalog[(ctxt_msg_id, self.plural(num))] 

505 return tmsg 

506 except KeyError: 

507 if self._fallback: 

508 return self._fallback.npgettext(context, singular, plural, num) 

509 if num == 1: 

510 return singular 

511 else: 

512 return plural 

513 

514 def lnpgettext(self, context: str, singular: str, plural: str, num: int) -> str | bytes: 

515 """Equivalent to ``npgettext()``, but the translation is returned in the 

516 preferred system encoding, if no other encoding was explicitly set with 

517 ``bind_textdomain_codeset()``. 

518 """ 

519 import warnings 

520 warnings.warn( 

521 'lnpgettext() is deprecated, use npgettext() instead', 

522 DeprecationWarning, 

523 stacklevel=2, 

524 ) 

525 ctxt_msg_id = self.CONTEXT_ENCODING % (context, singular) 

526 try: 

527 tmsg = self._catalog[(ctxt_msg_id, self.plural(num))] 

528 encoding = getattr(self, "_output_charset", None) or locale.getpreferredencoding() 

529 return tmsg.encode(encoding) 

530 except KeyError: 

531 if self._fallback: 

532 return self._fallback.lnpgettext(context, singular, plural, num) 

533 if num == 1: 

534 return singular 

535 else: 

536 return plural 

537 

538 def upgettext(self, context: str, message: str) -> str: 

539 """Look up the `context` and `message` id in the catalog and return the 

540 corresponding message string, as a Unicode string. If there is no entry 

541 in the catalog for the `message` id and `context`, and a fallback has 

542 been set, the look up is forwarded to the fallback's ``upgettext()`` 

543 method. Otherwise, the `message` id is returned. 

544 """ 

545 ctxt_message_id = self.CONTEXT_ENCODING % (context, message) 

546 missing = object() 

547 tmsg = self._catalog.get(ctxt_message_id, missing) 

548 if tmsg is missing: 

549 if self._fallback: 

550 return self._fallback.upgettext(context, message) 

551 return str(message) 

552 assert isinstance(tmsg, str) 

553 return tmsg 

554 

555 def unpgettext(self, context: str, singular: str, plural: str, num: int) -> str: 

556 """Do a plural-forms lookup of a message id. `singular` is used as the 

557 message id for purposes of lookup in the catalog, while `num` is used to 

558 determine which plural form to use. The returned message string is a 

559 Unicode string. 

560 

561 If the message id for `context` is not found in the catalog, and a 

562 fallback is specified, the request is forwarded to the fallback's 

563 ``unpgettext()`` method. Otherwise, when `num` is 1 `singular` is 

564 returned, and `plural` is returned in all other cases. 

565 """ 

566 ctxt_message_id = self.CONTEXT_ENCODING % (context, singular) 

567 try: 

568 tmsg = self._catalog[(ctxt_message_id, self.plural(num))] 

569 except KeyError: 

570 if self._fallback: 

571 return self._fallback.unpgettext(context, singular, plural, num) 

572 tmsg = str(singular) if num == 1 else str(plural) 

573 return tmsg 

574 

575 def dpgettext(self, domain: str, context: str, message: str) -> str | object: 

576 """Like `pgettext()`, but look the message up in the specified 

577 `domain`. 

578 """ 

579 return self._domains.get(domain, self).pgettext(context, message) 

580 

581 def udpgettext(self, domain: str, context: str, message: str) -> str: 

582 """Like `upgettext()`, but look the message up in the specified 

583 `domain`. 

584 """ 

585 return self._domains.get(domain, self).upgettext(context, message) 

586 # backward compatibility with 0.9 

587 dupgettext = udpgettext 

588 

589 def ldpgettext(self, domain: str, context: str, message: str) -> str | bytes | object: 

590 """Equivalent to ``dpgettext()``, but the translation is returned in the 

591 preferred system encoding, if no other encoding was explicitly set with 

592 ``bind_textdomain_codeset()``. 

593 """ 

594 return self._domains.get(domain, self).lpgettext(context, message) 

595 

596 def dnpgettext(self, domain: str, context: str, singular: str, plural: str, num: int) -> str: 

597 """Like ``npgettext``, but look the message up in the specified 

598 `domain`. 

599 """ 

600 return self._domains.get(domain, self).npgettext(context, singular, 

601 plural, num) 

602 

603 def udnpgettext(self, domain: str, context: str, singular: str, plural: str, num: int) -> str: 

604 """Like ``unpgettext``, but look the message up in the specified 

605 `domain`. 

606 """ 

607 return self._domains.get(domain, self).unpgettext(context, singular, 

608 plural, num) 

609 # backward compatibility with 0.9 

610 dunpgettext = udnpgettext 

611 

612 def ldnpgettext(self, domain: str, context: str, singular: str, plural: str, num: int) -> str | bytes: 

613 """Equivalent to ``dnpgettext()``, but the translation is returned in 

614 the preferred system encoding, if no other encoding was explicitly set 

615 with ``bind_textdomain_codeset()``. 

616 """ 

617 return self._domains.get(domain, self).lnpgettext(context, singular, 

618 plural, num) 

619 

620 ugettext = gettext.NullTranslations.gettext 

621 ungettext = gettext.NullTranslations.ngettext 

622 

623 

624class Translations(NullTranslations, gettext.GNUTranslations): 

625 """An extended translation catalog class.""" 

626 

627 DEFAULT_DOMAIN = 'messages' 

628 

629 def __init__(self, fp: gettext._TranslationsReader | None = None, domain: str | None = None): 

630 """Initialize the translations catalog. 

631 

632 :param fp: the file-like object the translation should be read from 

633 :param domain: the message domain (default: 'messages') 

634 """ 

635 super().__init__(fp=fp) 

636 self.domain = domain or self.DEFAULT_DOMAIN 

637 

638 ugettext = gettext.GNUTranslations.gettext 

639 ungettext = gettext.GNUTranslations.ngettext 

640 

641 @classmethod 

642 def load( 

643 cls, 

644 dirname: str | os.PathLike[str] | None = None, 

645 locales: Iterable[str | Locale] | Locale | str | None = None, 

646 domain: str | None = None, 

647 ) -> NullTranslations: 

648 """Load translations from the given directory. 

649 

650 :param dirname: the directory containing the ``MO`` files 

651 :param locales: the list of locales in order of preference (items in 

652 this list can be either `Locale` objects or locale 

653 strings) 

654 :param domain: the message domain (default: 'messages') 

655 """ 

656 if not domain: 

657 domain = cls.DEFAULT_DOMAIN 

658 filename = gettext.find(domain, dirname, _locales_to_names(locales)) 

659 if not filename: 

660 return NullTranslations() 

661 with open(filename, 'rb') as fp: 

662 return cls(fp=fp, domain=domain) 

663 

664 def __repr__(self) -> str: 

665 version = self._info.get('project-id-version') 

666 return f'<{type(self).__name__}: "{version}">' 

667 

668 def add(self, translations: Translations, merge: bool = True): 

669 """Add the given translations to the catalog. 

670 

671 If the domain of the translations is different than that of the 

672 current catalog, they are added as a catalog that is only accessible 

673 by the various ``d*gettext`` functions. 

674 

675 :param translations: the `Translations` instance with the messages to 

676 add 

677 :param merge: whether translations for message domains that have 

678 already been added should be merged with the existing 

679 translations 

680 """ 

681 domain = getattr(translations, 'domain', self.DEFAULT_DOMAIN) 

682 if merge and domain == self.domain: 

683 return self.merge(translations) 

684 

685 existing = self._domains.get(domain) 

686 if merge and isinstance(existing, Translations): 

687 existing.merge(translations) 

688 else: 

689 translations.add_fallback(self) 

690 self._domains[domain] = translations 

691 

692 return self 

693 

694 def merge(self, translations: Translations): 

695 """Merge the given translations into the catalog. 

696 

697 Message translations in the specified catalog override any messages 

698 with the same identifier in the existing catalog. 

699 

700 :param translations: the `Translations` instance with the messages to 

701 merge 

702 """ 

703 if isinstance(translations, gettext.GNUTranslations): 

704 self._catalog.update(translations._catalog) 

705 if isinstance(translations, Translations): 

706 self.files.extend(translations.files) 

707 

708 return self 

709 

710 

711def _locales_to_names( 

712 locales: Iterable[str | Locale] | Locale | str | None, 

713) -> list[str] | None: 

714 """Normalize a `locales` argument to a list of locale names. 

715 

716 :param locales: the list of locales in order of preference (items in 

717 this list can be either `Locale` objects or locale 

718 strings) 

719 """ 

720 if locales is None: 

721 return None 

722 if isinstance(locales, Locale): 

723 return [str(locales)] 

724 if isinstance(locales, str): 

725 return [locales] 

726 return [str(locale) for locale in locales]