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"""
2 babel.support
3 ~~~~~~~~~~~~~
5 Several classes and functions that help with integrating and using Babel
6 in applications.
8 .. note: the code in this module is not used by Babel itself
10 :copyright: (c) 2013-2025 by the Babel Team.
11 :license: BSD, see LICENSE for more details.
12"""
13from __future__ import annotations
15import gettext
16import locale
17import os
18from collections.abc import Iterator
19from typing import TYPE_CHECKING, Any, Callable, Iterable, Literal
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)
32if TYPE_CHECKING:
33 import datetime as _datetime
34 from decimal import Decimal
36 from babel.dates import _PredefinedTimeFormat
39class Format:
40 """Wrapper class providing the various date and number formatting functions
41 bound to a specific locale and time-zone.
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 """
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.
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
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.
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)
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.
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)
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.
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)
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.
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)
134 def number(self, number: float | Decimal | str) -> str:
135 """Return an integer number formatted for the locale.
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)
143 def decimal(self, number: float | Decimal | str, format: str | None = None) -> str:
144 """Return a decimal number formatted for the locale.
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)
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.
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 )
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)
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.
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)
195 def percent(self, number: float | Decimal | str, format: str | None = None) -> str:
196 """Return a number formatted as percentage for the locale.
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)
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)
210class LazyProxy:
211 """Class for proxy objects that delegate to a specified function to evaluate
212 the actual object.
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!)'
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.
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:
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']
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
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)
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
273 if not self._is_cache_enabled:
274 return value
275 object.__setattr__(self, '_value', value)
276 return self._value
278 def __contains__(self, key: object) -> bool:
279 return key in self.value
281 def __bool__(self) -> bool:
282 return bool(self.value)
284 def __dir__(self) -> list[str]:
285 return dir(self.value)
287 def __iter__(self) -> Iterator[Any]:
288 return iter(self.value)
290 def __len__(self) -> int:
291 return len(self.value)
293 def __str__(self) -> str:
294 return str(self.value)
296 def __add__(self, other: object) -> Any:
297 return self.value + other
299 def __radd__(self, other: object) -> Any:
300 return other + self.value
302 def __mod__(self, other: object) -> Any:
303 return self.value % other
305 def __rmod__(self, other: object) -> Any:
306 return other % self.value
308 def __mul__(self, other: object) -> Any:
309 return self.value * other
311 def __rmul__(self, other: object) -> Any:
312 return other * self.value
314 def __call__(self, *args: Any, **kwargs: Any) -> Any:
315 return self.value(*args, **kwargs)
317 def __lt__(self, other: object) -> bool:
318 return self.value < other
320 def __le__(self, other: object) -> bool:
321 return self.value <= other
323 def __eq__(self, other: object) -> bool:
324 return self.value == other
326 def __ne__(self, other: object) -> bool:
327 return self.value != other
329 def __gt__(self, other: object) -> bool:
330 return self.value > other
332 def __ge__(self, other: object) -> bool:
333 return self.value >= other
335 def __delattr__(self, name: str) -> None:
336 delattr(self.value, name)
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)
343 def __setattr__(self, name: str, value: Any) -> None:
344 setattr(self.value, name, value)
346 def __delitem__(self, key: Any) -> None:
347 del self.value[key]
349 def __getitem__(self, key: Any) -> Any:
350 return self.value[key]
352 def __setitem__(self, key: Any, value: Any) -> None:
353 self.value[key] = value
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 )
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 )
373class NullTranslations(gettext.NullTranslations):
375 if TYPE_CHECKING:
376 _info: dict[str, str]
377 _fallback: NullTranslations | None
379 DEFAULT_DOMAIN = None
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()').
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] = {}
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)
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)
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
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)
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)
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
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'
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
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
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.
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
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
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
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.
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
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)
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
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)
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)
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
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)
620 ugettext = gettext.NullTranslations.gettext
621 ungettext = gettext.NullTranslations.ngettext
624class Translations(NullTranslations, gettext.GNUTranslations):
625 """An extended translation catalog class."""
627 DEFAULT_DOMAIN = 'messages'
629 def __init__(self, fp: gettext._TranslationsReader | None = None, domain: str | None = None):
630 """Initialize the translations catalog.
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
638 ugettext = gettext.GNUTranslations.gettext
639 ungettext = gettext.GNUTranslations.ngettext
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.
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)
664 def __repr__(self) -> str:
665 version = self._info.get('project-id-version')
666 return f'<{type(self).__name__}: "{version}">'
668 def add(self, translations: Translations, merge: bool = True):
669 """Add the given translations to the catalog.
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.
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)
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
692 return self
694 def merge(self, translations: Translations):
695 """Merge the given translations into the catalog.
697 Message translations in the specified catalog override any messages
698 with the same identifier in the existing catalog.
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)
708 return self
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.
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]