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""" 

2babel.support 

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

4 

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

6in applications. 

7 

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

9 

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

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

12""" 

13 

14from __future__ import annotations 

15 

16import gettext 

17import locale 

18import os 

19from collections.abc import Iterator 

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

21 

22from babel.core import Locale 

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

24from babel.numbers import ( 

25 format_compact_currency, 

26 format_compact_decimal, 

27 format_currency, 

28 format_decimal, 

29 format_percent, 

30 format_scientific, 

31) 

32 

33if TYPE_CHECKING: 

34 import datetime as _datetime 

35 from decimal import Decimal 

36 

37 from babel.dates import _PredefinedTimeFormat 

38 

39 

40class Format: 

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

42 bound to a specific locale and time-zone. 

43 

44 >>> from babel.util import UTC 

45 >>> from datetime import date 

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

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

48 'Apr 1, 2007' 

49 >>> fmt.decimal(1.2345) 

50 '1.234' 

51 """ 

52 

53 def __init__( 

54 self, 

55 locale: Locale | str, 

56 tzinfo: _datetime.tzinfo | None = None, 

57 *, 

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

59 ) -> None: 

60 """Initialize the formatter. 

61 

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

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

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

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

66 """ 

67 self.locale = Locale.parse(locale) 

68 self.tzinfo = tzinfo 

69 self.numbering_system = numbering_system 

70 

71 def date( 

72 self, 

73 date: _datetime.date | None = None, 

74 format: _PredefinedTimeFormat | str = 'medium', 

75 ) -> str: 

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

77 

78 >>> from datetime import date 

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

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

81 'Apr 1, 2007' 

82 """ 

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

84 

85 def datetime( 

86 self, 

87 datetime: _datetime.date | None = None, 

88 format: _PredefinedTimeFormat | str = 'medium', 

89 ) -> str: 

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

91 

92 >>> from datetime import datetime 

93 >>> from babel.dates import get_timezone 

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

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

96 'Apr 1, 2007, 11:30:00\\u202fAM' 

97 """ 

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

99 

100 def time( 

101 self, 

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

103 format: _PredefinedTimeFormat | str = 'medium', 

104 ) -> str: 

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

106 

107 >>> from datetime import datetime 

108 >>> from babel.dates import get_timezone 

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

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

111 '11:30:00\\u202fAM' 

112 """ 

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

114 

115 def timedelta( 

116 self, 

117 delta: _datetime.timedelta | int, 

118 granularity: Literal[ 

119 "year", 

120 "month", 

121 "week", 

122 "day", 

123 "hour", 

124 "minute", 

125 "second", 

126 ] = "second", 

127 threshold: float = 0.85, 

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

129 add_direction: bool = False, 

130 ) -> str: 

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

132 

133 >>> from datetime import timedelta 

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

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

136 '3 months' 

137 """ 

138 return format_timedelta( 

139 delta, 

140 granularity=granularity, 

141 threshold=threshold, 

142 format=format, 

143 add_direction=add_direction, 

144 locale=self.locale, 

145 ) 

146 

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

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

149 

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

151 >>> fmt.number(1099) 

152 '1,099' 

153 """ 

154 return format_decimal( 

155 number, 

156 locale=self.locale, 

157 numbering_system=self.numbering_system, 

158 ) 

159 

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

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

162 

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

164 >>> fmt.decimal(1.2345) 

165 '1.234' 

166 """ 

167 return format_decimal( 

168 number, 

169 format, 

170 locale=self.locale, 

171 numbering_system=self.numbering_system, 

172 ) 

173 

174 def compact_decimal( 

175 self, 

176 number: float | Decimal | str, 

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

178 fraction_digits: int = 0, 

179 ) -> str: 

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

181 

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

183 >>> fmt.compact_decimal(123456789) 

184 '123M' 

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

186 '1.23 million' 

187 """ 

188 return format_compact_decimal( 

189 number, 

190 format_type=format_type, 

191 fraction_digits=fraction_digits, 

192 locale=self.locale, 

193 numbering_system=self.numbering_system, 

194 ) 

195 

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

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

198 return format_currency( 

199 number, 

200 currency, 

201 locale=self.locale, 

202 numbering_system=self.numbering_system, 

203 ) 

204 

205 def compact_currency( 

206 self, 

207 number: float | Decimal | str, 

208 currency: str, 

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

210 fraction_digits: int = 0, 

211 ) -> str: 

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

213 using the compact number format. 

214 

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

216 '$1.23M' 

217 """ 

218 return format_compact_currency( 

219 number, 

220 currency, 

221 format_type=format_type, 

222 fraction_digits=fraction_digits, 

223 locale=self.locale, 

224 numbering_system=self.numbering_system, 

225 ) 

226 

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

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

229 

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

231 >>> fmt.percent(0.34) 

232 '34%' 

233 """ 

234 return format_percent( 

235 number, 

236 format, 

237 locale=self.locale, 

238 numbering_system=self.numbering_system, 

239 ) 

240 

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

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

243 return format_scientific( 

244 number, 

245 locale=self.locale, 

246 numbering_system=self.numbering_system, 

247 ) 

248 

249 

250class LazyProxy: 

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

252 the actual object. 

253 

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

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

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

257 >>> print(lazy_greeting) 

258 Hello, Joe! 

259 >>> ' ' + lazy_greeting 

260 ' Hello, Joe!' 

261 >>> '(%s)' % lazy_greeting 

262 '(Hello, Joe!)' 

263 

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

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

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

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

268 a request. 

269 

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

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

272 

273 >>> greetings = [ 

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

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

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

277 ... ] 

278 >>> greetings.sort() 

279 >>> for greeting in greetings: 

280 ... print(greeting) 

281 Hello, Joe! 

282 Hello, universe! 

283 Hello, world! 

284 """ 

285 

286 __slots__ = [ 

287 '_func', 

288 '_args', 

289 '_kwargs', 

290 '_value', 

291 '_is_cache_enabled', 

292 '_attribute_error', 

293 ] 

294 

295 if TYPE_CHECKING: 

296 _func: Callable[..., Any] 

297 _args: tuple[Any, ...] 

298 _kwargs: dict[str, Any] 

299 _is_cache_enabled: bool 

300 _value: Any 

301 _attribute_error: AttributeError | None 

302 

303 def __init__( 

304 self, 

305 func: Callable[..., Any], 

306 *args: Any, 

307 enable_cache: bool = True, 

308 **kwargs: Any, 

309 ) -> None: 

310 # Avoid triggering our own __setattr__ implementation 

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

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

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

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

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

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

317 

318 @property 

319 def value(self) -> Any: 

320 if self._value is None: 

321 try: 

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

323 except AttributeError as error: 

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

325 raise 

326 

327 if not self._is_cache_enabled: 

328 return value 

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

330 return self._value 

331 

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

333 return key in self.value 

334 

335 def __bool__(self) -> bool: 

336 return bool(self.value) 

337 

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

339 return dir(self.value) 

340 

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

342 return iter(self.value) 

343 

344 def __len__(self) -> int: 

345 return len(self.value) 

346 

347 def __str__(self) -> str: 

348 return str(self.value) 

349 

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

351 return self.value + other 

352 

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

354 return other + self.value 

355 

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

357 return self.value % other 

358 

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

360 return other % self.value 

361 

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

363 return self.value * other 

364 

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

366 return other * self.value 

367 

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

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

370 

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

372 return self.value < other 

373 

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

375 return self.value <= other 

376 

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

378 return self.value == other 

379 

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

381 return self.value != other 

382 

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

384 return self.value > other 

385 

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

387 return self.value >= other 

388 

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

390 delattr(self.value, name) 

391 

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

393 if self._attribute_error is not None: 

394 raise self._attribute_error 

395 return getattr(self.value, name) 

396 

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

398 setattr(self.value, name, value) 

399 

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

401 del self.value[key] 

402 

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

404 return self.value[key] 

405 

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

407 self.value[key] = value 

408 

409 def __copy__(self) -> LazyProxy: 

410 return LazyProxy( 

411 self._func, 

412 enable_cache=self._is_cache_enabled, 

413 *self._args, # noqa: B026 

414 **self._kwargs, 

415 ) 

416 

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

418 from copy import deepcopy 

419 

420 return LazyProxy( 

421 deepcopy(self._func, memo), 

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

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

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

425 ) 

426 

427 

428class NullTranslations(gettext.NullTranslations): 

429 if TYPE_CHECKING: 

430 _info: dict[str, str] 

431 _fallback: NullTranslations | None 

432 

433 DEFAULT_DOMAIN = None 

434 

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

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

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

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

439 

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

441 """ 

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

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

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

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

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

447 super().__init__(fp=fp) 

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

449 self.domain = self.DEFAULT_DOMAIN 

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

451 

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

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

454 domain. 

455 """ 

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

457 

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

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

460 domain. 

461 """ 

462 import warnings 

463 

464 warnings.warn( 

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

466 DeprecationWarning, 

467 stacklevel=2, 

468 ) 

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

470 

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

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

473 domain. 

474 """ 

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

476 

477 # backward compatibility with 0.9 

478 dugettext = udgettext 

479 

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

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

482 domain. 

483 """ 

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

485 

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

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

488 domain. 

489 """ 

490 import warnings 

491 

492 warnings.warn( 

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

494 DeprecationWarning, 

495 stacklevel=2, 

496 ) 

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

498 

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

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

501 domain. 

502 """ 

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

504 

505 # backward compatibility with 0.9 

506 dungettext = udngettext 

507 

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

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

510 # 

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

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

513 CONTEXT_ENCODING = '%s\x04%s' 

514 

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

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

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

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

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

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

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

522 """ 

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

524 missing = object() 

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

526 if tmsg is missing: 

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

528 if tmsg is not missing: 

529 return tmsg 

530 if self._fallback: 

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

532 return message 

533 

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

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

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

537 ``bind_textdomain_codeset()``. 

538 """ 

539 import warnings 

540 

541 warnings.warn( 

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

543 DeprecationWarning, 

544 stacklevel=2, 

545 ) 

546 tmsg = self.pgettext(context, message) 

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

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

549 

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

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

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

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

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

555 

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

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

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

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

560 """ 

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

562 try: 

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

564 return tmsg 

565 except KeyError: 

566 if self._fallback: 

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

568 if num == 1: 

569 return singular 

570 else: 

571 return plural 

572 

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

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

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

576 ``bind_textdomain_codeset()``. 

577 """ 

578 import warnings 

579 

580 warnings.warn( 

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

582 DeprecationWarning, 

583 stacklevel=2, 

584 ) 

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

586 try: 

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

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

589 return tmsg.encode(encoding) 

590 except KeyError: 

591 if self._fallback: 

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

593 if num == 1: 

594 return singular 

595 else: 

596 return plural 

597 

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

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

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

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

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

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

604 """ 

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

606 missing = object() 

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

608 if tmsg is missing: 

609 if self._fallback: 

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

611 return str(message) 

612 assert isinstance(tmsg, str) 

613 return tmsg 

614 

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

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

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

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

619 Unicode string. 

620 

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

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

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

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

625 """ 

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

627 try: 

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

629 except KeyError: 

630 if self._fallback: 

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

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

633 return tmsg 

634 

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

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

637 `domain`. 

638 """ 

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

640 

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

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

643 `domain`. 

644 """ 

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

646 

647 # backward compatibility with 0.9 

648 dupgettext = udpgettext 

649 

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

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

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

653 ``bind_textdomain_codeset()``. 

654 """ 

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

656 

657 def dnpgettext(self, domain: str, context: str, singular: str, plural: str, num: int) -> str: # fmt: skip 

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

659 `domain`. 

660 """ 

661 return self._domains.get(domain, self).npgettext(context, singular, plural, num) 

662 

663 def udnpgettext(self, domain: str, context: str, singular: str, plural: str, num: int) -> str: # fmt: skip 

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

665 `domain`. 

666 """ 

667 return self._domains.get(domain, self).unpgettext(context, singular, plural, num) 

668 

669 # backward compatibility with 0.9 

670 dunpgettext = udnpgettext 

671 

672 def ldnpgettext( 

673 self, 

674 domain: str, 

675 context: str, 

676 singular: str, 

677 plural: str, 

678 num: int, 

679 ) -> str | bytes: 

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

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

682 with ``bind_textdomain_codeset()``. 

683 """ 

684 return self._domains.get(domain, self).lnpgettext(context, singular, plural, num) 

685 

686 ugettext = gettext.NullTranslations.gettext 

687 ungettext = gettext.NullTranslations.ngettext 

688 

689 

690class Translations(NullTranslations, gettext.GNUTranslations): 

691 """An extended translation catalog class.""" 

692 

693 DEFAULT_DOMAIN = 'messages' 

694 

695 def __init__( 

696 self, 

697 fp: gettext._TranslationsReader | None = None, 

698 domain: str | None = None, 

699 ): 

700 """Initialize the translations catalog. 

701 

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

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

704 """ 

705 super().__init__(fp=fp) 

706 self.domain = domain or self.DEFAULT_DOMAIN 

707 

708 ugettext = gettext.GNUTranslations.gettext 

709 ungettext = gettext.GNUTranslations.ngettext 

710 

711 @classmethod 

712 def load( 

713 cls, 

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

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

716 domain: str | None = None, 

717 ) -> NullTranslations: 

718 """Load translations from the given directory. 

719 

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

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

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

723 strings) 

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

725 """ 

726 if not domain: 

727 domain = cls.DEFAULT_DOMAIN 

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

729 if not filename: 

730 return NullTranslations() 

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

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

733 

734 def __repr__(self) -> str: 

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

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

737 

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

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

740 

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

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

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

744 

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

746 add 

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

748 already been added should be merged with the existing 

749 translations 

750 """ 

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

752 if merge and domain == self.domain: 

753 return self.merge(translations) 

754 

755 existing = self._domains.get(domain) 

756 if merge and isinstance(existing, Translations): 

757 existing.merge(translations) 

758 else: 

759 translations.add_fallback(self) 

760 self._domains[domain] = translations 

761 

762 return self 

763 

764 def merge(self, translations: Translations): 

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

766 

767 Message translations in the specified catalog override any messages 

768 with the same identifier in the existing catalog. 

769 

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

771 merge 

772 """ 

773 if isinstance(translations, gettext.GNUTranslations): 

774 self._catalog.update(translations._catalog) 

775 if isinstance(translations, Translations): 

776 self.files.extend(translations.files) 

777 

778 return self 

779 

780 

781def _locales_to_names( 

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

783) -> list[str] | None: 

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

785 

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

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

788 strings) 

789 """ 

790 if locales is None: 

791 return None 

792 if isinstance(locales, Locale): 

793 return [str(locales)] 

794 if isinstance(locales, str): 

795 return [locales] 

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