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
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
1"""
2babel.support
3~~~~~~~~~~~~~
5Several classes and functions that help with integrating and using Babel
6in applications.
8.. note: the code in this module is not used by Babel itself
10:copyright: (c) 2013-2026 by the Babel Team.
11:license: BSD, see LICENSE for more details.
12"""
14from __future__ import annotations
16import gettext
17import locale
18import os
19from collections.abc import Iterator
20from typing import TYPE_CHECKING, Any, Callable, Iterable, Literal
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)
33if TYPE_CHECKING:
34 import datetime as _datetime
35 from decimal import Decimal
37 from babel.dates import _PredefinedTimeFormat
40class Format:
41 """Wrapper class providing the various date and number formatting functions
42 bound to a specific locale and time-zone.
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 """
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.
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
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.
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)
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.
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)
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.
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)
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.
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 )
147 def number(self, number: float | Decimal | str) -> str:
148 """Return an integer number formatted for the locale.
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 )
160 def decimal(self, number: float | Decimal | str, format: str | None = None) -> str:
161 """Return a decimal number formatted for the locale.
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 )
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.
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 )
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 )
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.
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 )
227 def percent(self, number: float | Decimal | str, format: str | None = None) -> str:
228 """Return a number formatted as percentage for the locale.
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 )
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 )
250class LazyProxy:
251 """Class for proxy objects that delegate to a specified function to evaluate
252 the actual object.
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!)'
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.
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:
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 """
286 __slots__ = [
287 '_func',
288 '_args',
289 '_kwargs',
290 '_value',
291 '_is_cache_enabled',
292 '_attribute_error',
293 ]
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
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)
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
327 if not self._is_cache_enabled:
328 return value
329 object.__setattr__(self, '_value', value)
330 return self._value
332 def __contains__(self, key: object) -> bool:
333 return key in self.value
335 def __bool__(self) -> bool:
336 return bool(self.value)
338 def __dir__(self) -> list[str]:
339 return dir(self.value)
341 def __iter__(self) -> Iterator[Any]:
342 return iter(self.value)
344 def __len__(self) -> int:
345 return len(self.value)
347 def __str__(self) -> str:
348 return str(self.value)
350 def __add__(self, other: object) -> Any:
351 return self.value + other
353 def __radd__(self, other: object) -> Any:
354 return other + self.value
356 def __mod__(self, other: object) -> Any:
357 return self.value % other
359 def __rmod__(self, other: object) -> Any:
360 return other % self.value
362 def __mul__(self, other: object) -> Any:
363 return self.value * other
365 def __rmul__(self, other: object) -> Any:
366 return other * self.value
368 def __call__(self, *args: Any, **kwargs: Any) -> Any:
369 return self.value(*args, **kwargs)
371 def __lt__(self, other: object) -> bool:
372 return self.value < other
374 def __le__(self, other: object) -> bool:
375 return self.value <= other
377 def __eq__(self, other: object) -> bool:
378 return self.value == other
380 def __ne__(self, other: object) -> bool:
381 return self.value != other
383 def __gt__(self, other: object) -> bool:
384 return self.value > other
386 def __ge__(self, other: object) -> bool:
387 return self.value >= other
389 def __delattr__(self, name: str) -> None:
390 delattr(self.value, name)
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)
397 def __setattr__(self, name: str, value: Any) -> None:
398 setattr(self.value, name, value)
400 def __delitem__(self, key: Any) -> None:
401 del self.value[key]
403 def __getitem__(self, key: Any) -> Any:
404 return self.value[key]
406 def __setitem__(self, key: Any, value: Any) -> None:
407 self.value[key] = value
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 )
417 def __deepcopy__(self, memo: Any) -> LazyProxy:
418 from copy import deepcopy
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 )
428class NullTranslations(gettext.NullTranslations):
429 if TYPE_CHECKING:
430 _info: dict[str, str]
431 _fallback: NullTranslations | None
433 DEFAULT_DOMAIN = None
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()').
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] = {}
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)
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
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)
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)
477 # backward compatibility with 0.9
478 dugettext = udgettext
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)
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
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)
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)
505 # backward compatibility with 0.9
506 dungettext = udngettext
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'
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
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
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
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.
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
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
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
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
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.
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
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)
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)
647 # backward compatibility with 0.9
648 dupgettext = udpgettext
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)
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)
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)
669 # backward compatibility with 0.9
670 dunpgettext = udnpgettext
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)
686 ugettext = gettext.NullTranslations.gettext
687 ungettext = gettext.NullTranslations.ngettext
690class Translations(NullTranslations, gettext.GNUTranslations):
691 """An extended translation catalog class."""
693 DEFAULT_DOMAIN = 'messages'
695 def __init__(
696 self,
697 fp: gettext._TranslationsReader | None = None,
698 domain: str | None = None,
699 ):
700 """Initialize the translations catalog.
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
708 ugettext = gettext.GNUTranslations.gettext
709 ungettext = gettext.GNUTranslations.ngettext
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.
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)
734 def __repr__(self) -> str:
735 version = self._info.get('project-id-version')
736 return f'<{type(self).__name__}: "{version}">'
738 def add(self, translations: Translations, merge: bool = True):
739 """Add the given translations to the catalog.
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.
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)
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
762 return self
764 def merge(self, translations: Translations):
765 """Merge the given translations into the catalog.
767 Message translations in the specified catalog override any messages
768 with the same identifier in the existing catalog.
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)
778 return self
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.
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]