123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726 |
- """
- babel.support
- ~~~~~~~~~~~~~
- Several classes and functions that help with integrating and using Babel
- in applications.
- .. note: the code in this module is not used by Babel itself
- :copyright: (c) 2013-2025 by the Babel Team.
- :license: BSD, see LICENSE for more details.
- """
- from __future__ import annotations
- import gettext
- import locale
- import os
- from collections.abc import Iterator
- from typing import TYPE_CHECKING, Any, Callable, Iterable, Literal
- from babel.core import Locale
- from babel.dates import format_date, format_datetime, format_time, format_timedelta
- from babel.numbers import (
- format_compact_currency,
- format_compact_decimal,
- format_currency,
- format_decimal,
- format_percent,
- format_scientific,
- )
- if TYPE_CHECKING:
- import datetime as _datetime
- from decimal import Decimal
- from babel.dates import _PredefinedTimeFormat
- class Format:
- """Wrapper class providing the various date and number formatting functions
- bound to a specific locale and time-zone.
- >>> from babel.util import UTC
- >>> from datetime import date
- >>> fmt = Format('en_US', UTC)
- >>> fmt.date(date(2007, 4, 1))
- u'Apr 1, 2007'
- >>> fmt.decimal(1.2345)
- u'1.234'
- """
- def __init__(
- self,
- locale: Locale | str,
- tzinfo: _datetime.tzinfo | None = None,
- *,
- numbering_system: Literal["default"] | str = "latn",
- ) -> None:
- """Initialize the formatter.
- :param locale: the locale identifier or `Locale` instance
- :param tzinfo: the time-zone info (a `tzinfo` instance or `None`)
- :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
- The special value "default" will use the default numbering system of the locale.
- """
- self.locale = Locale.parse(locale)
- self.tzinfo = tzinfo
- self.numbering_system = numbering_system
- def date(
- self,
- date: _datetime.date | None = None,
- format: _PredefinedTimeFormat | str = 'medium',
- ) -> str:
- """Return a date formatted according to the given pattern.
- >>> from datetime import date
- >>> fmt = Format('en_US')
- >>> fmt.date(date(2007, 4, 1))
- u'Apr 1, 2007'
- """
- return format_date(date, format, locale=self.locale)
- def datetime(
- self,
- datetime: _datetime.date | None = None,
- format: _PredefinedTimeFormat | str = 'medium',
- ) -> str:
- """Return a date and time formatted according to the given pattern.
- >>> from datetime import datetime
- >>> from babel.dates import get_timezone
- >>> fmt = Format('en_US', tzinfo=get_timezone('US/Eastern'))
- >>> fmt.datetime(datetime(2007, 4, 1, 15, 30))
- u'Apr 1, 2007, 11:30:00\u202fAM'
- """
- return format_datetime(datetime, format, tzinfo=self.tzinfo, locale=self.locale)
- def time(
- self,
- time: _datetime.time | _datetime.datetime | None = None,
- format: _PredefinedTimeFormat | str = 'medium',
- ) -> str:
- """Return a time formatted according to the given pattern.
- >>> from datetime import datetime
- >>> from babel.dates import get_timezone
- >>> fmt = Format('en_US', tzinfo=get_timezone('US/Eastern'))
- >>> fmt.time(datetime(2007, 4, 1, 15, 30))
- u'11:30:00\u202fAM'
- """
- return format_time(time, format, tzinfo=self.tzinfo, locale=self.locale)
- def timedelta(
- self,
- delta: _datetime.timedelta | int,
- granularity: Literal["year", "month", "week", "day", "hour", "minute", "second"] = "second",
- threshold: float = 0.85,
- format: Literal["narrow", "short", "medium", "long"] = "long",
- add_direction: bool = False,
- ) -> str:
- """Return a time delta according to the rules of the given locale.
- >>> from datetime import timedelta
- >>> fmt = Format('en_US')
- >>> fmt.timedelta(timedelta(weeks=11))
- u'3 months'
- """
- return format_timedelta(delta, granularity=granularity,
- threshold=threshold,
- format=format, add_direction=add_direction,
- locale=self.locale)
- def number(self, number: float | Decimal | str) -> str:
- """Return an integer number formatted for the locale.
- >>> fmt = Format('en_US')
- >>> fmt.number(1099)
- u'1,099'
- """
- return format_decimal(number, locale=self.locale, numbering_system=self.numbering_system)
- def decimal(self, number: float | Decimal | str, format: str | None = None) -> str:
- """Return a decimal number formatted for the locale.
- >>> fmt = Format('en_US')
- >>> fmt.decimal(1.2345)
- u'1.234'
- """
- return format_decimal(number, format, locale=self.locale, numbering_system=self.numbering_system)
- def compact_decimal(
- self,
- number: float | Decimal | str,
- format_type: Literal['short', 'long'] = 'short',
- fraction_digits: int = 0,
- ) -> str:
- """Return a number formatted in compact form for the locale.
- >>> fmt = Format('en_US')
- >>> fmt.compact_decimal(123456789)
- u'123M'
- >>> fmt.compact_decimal(1234567, format_type='long', fraction_digits=2)
- '1.23 million'
- """
- return format_compact_decimal(
- number,
- format_type=format_type,
- fraction_digits=fraction_digits,
- locale=self.locale,
- numbering_system=self.numbering_system,
- )
- def currency(self, number: float | Decimal | str, currency: str) -> str:
- """Return a number in the given currency formatted for the locale.
- """
- return format_currency(number, currency, locale=self.locale, numbering_system=self.numbering_system)
- def compact_currency(
- self,
- number: float | Decimal | str,
- currency: str,
- format_type: Literal['short'] = 'short',
- fraction_digits: int = 0,
- ) -> str:
- """Return a number in the given currency formatted for the locale
- using the compact number format.
- >>> Format('en_US').compact_currency(1234567, "USD", format_type='short', fraction_digits=2)
- '$1.23M'
- """
- return format_compact_currency(number, currency, format_type=format_type, fraction_digits=fraction_digits,
- locale=self.locale, numbering_system=self.numbering_system)
- def percent(self, number: float | Decimal | str, format: str | None = None) -> str:
- """Return a number formatted as percentage for the locale.
- >>> fmt = Format('en_US')
- >>> fmt.percent(0.34)
- u'34%'
- """
- return format_percent(number, format, locale=self.locale, numbering_system=self.numbering_system)
- def scientific(self, number: float | Decimal | str) -> str:
- """Return a number formatted using scientific notation for the locale.
- """
- return format_scientific(number, locale=self.locale, numbering_system=self.numbering_system)
- class LazyProxy:
- """Class for proxy objects that delegate to a specified function to evaluate
- the actual object.
- >>> def greeting(name='world'):
- ... return 'Hello, %s!' % name
- >>> lazy_greeting = LazyProxy(greeting, name='Joe')
- >>> print(lazy_greeting)
- Hello, Joe!
- >>> u' ' + lazy_greeting
- u' Hello, Joe!'
- >>> u'(%s)' % lazy_greeting
- u'(Hello, Joe!)'
- This can be used, for example, to implement lazy translation functions that
- delay the actual translation until the string is actually used. The
- rationale for such behavior is that the locale of the user may not always
- be available. In web applications, you only know the locale when processing
- a request.
- The proxy implementation attempts to be as complete as possible, so that
- the lazy objects should mostly work as expected, for example for sorting:
- >>> greetings = [
- ... LazyProxy(greeting, 'world'),
- ... LazyProxy(greeting, 'Joe'),
- ... LazyProxy(greeting, 'universe'),
- ... ]
- >>> greetings.sort()
- >>> for greeting in greetings:
- ... print(greeting)
- Hello, Joe!
- Hello, universe!
- Hello, world!
- """
- __slots__ = ['_func', '_args', '_kwargs', '_value', '_is_cache_enabled', '_attribute_error']
- if TYPE_CHECKING:
- _func: Callable[..., Any]
- _args: tuple[Any, ...]
- _kwargs: dict[str, Any]
- _is_cache_enabled: bool
- _value: Any
- _attribute_error: AttributeError | None
- def __init__(self, func: Callable[..., Any], *args: Any, enable_cache: bool = True, **kwargs: Any) -> None:
- # Avoid triggering our own __setattr__ implementation
- object.__setattr__(self, '_func', func)
- object.__setattr__(self, '_args', args)
- object.__setattr__(self, '_kwargs', kwargs)
- object.__setattr__(self, '_is_cache_enabled', enable_cache)
- object.__setattr__(self, '_value', None)
- object.__setattr__(self, '_attribute_error', None)
- @property
- def value(self) -> Any:
- if self._value is None:
- try:
- value = self._func(*self._args, **self._kwargs)
- except AttributeError as error:
- object.__setattr__(self, '_attribute_error', error)
- raise
- if not self._is_cache_enabled:
- return value
- object.__setattr__(self, '_value', value)
- return self._value
- def __contains__(self, key: object) -> bool:
- return key in self.value
- def __bool__(self) -> bool:
- return bool(self.value)
- def __dir__(self) -> list[str]:
- return dir(self.value)
- def __iter__(self) -> Iterator[Any]:
- return iter(self.value)
- def __len__(self) -> int:
- return len(self.value)
- def __str__(self) -> str:
- return str(self.value)
- def __add__(self, other: object) -> Any:
- return self.value + other
- def __radd__(self, other: object) -> Any:
- return other + self.value
- def __mod__(self, other: object) -> Any:
- return self.value % other
- def __rmod__(self, other: object) -> Any:
- return other % self.value
- def __mul__(self, other: object) -> Any:
- return self.value * other
- def __rmul__(self, other: object) -> Any:
- return other * self.value
- def __call__(self, *args: Any, **kwargs: Any) -> Any:
- return self.value(*args, **kwargs)
- def __lt__(self, other: object) -> bool:
- return self.value < other
- def __le__(self, other: object) -> bool:
- return self.value <= other
- def __eq__(self, other: object) -> bool:
- return self.value == other
- def __ne__(self, other: object) -> bool:
- return self.value != other
- def __gt__(self, other: object) -> bool:
- return self.value > other
- def __ge__(self, other: object) -> bool:
- return self.value >= other
- def __delattr__(self, name: str) -> None:
- delattr(self.value, name)
- def __getattr__(self, name: str) -> Any:
- if self._attribute_error is not None:
- raise self._attribute_error
- return getattr(self.value, name)
- def __setattr__(self, name: str, value: Any) -> None:
- setattr(self.value, name, value)
- def __delitem__(self, key: Any) -> None:
- del self.value[key]
- def __getitem__(self, key: Any) -> Any:
- return self.value[key]
- def __setitem__(self, key: Any, value: Any) -> None:
- self.value[key] = value
- def __copy__(self) -> LazyProxy:
- return LazyProxy(
- self._func,
- enable_cache=self._is_cache_enabled,
- *self._args, # noqa: B026
- **self._kwargs,
- )
- def __deepcopy__(self, memo: Any) -> LazyProxy:
- from copy import deepcopy
- return LazyProxy(
- deepcopy(self._func, memo),
- enable_cache=deepcopy(self._is_cache_enabled, memo),
- *deepcopy(self._args, memo), # noqa: B026
- **deepcopy(self._kwargs, memo),
- )
- class NullTranslations(gettext.NullTranslations):
- if TYPE_CHECKING:
- _info: dict[str, str]
- _fallback: NullTranslations | None
- DEFAULT_DOMAIN = None
- def __init__(self, fp: gettext._TranslationsReader | None = None) -> None:
- """Initialize a simple translations class which is not backed by a
- real catalog. Behaves similar to gettext.NullTranslations but also
- offers Babel's on *gettext methods (e.g. 'dgettext()').
- :param fp: a file-like object (ignored in this class)
- """
- # These attributes are set by gettext.NullTranslations when a catalog
- # is parsed (fp != None). Ensure that they are always present because
- # some *gettext methods (including '.gettext()') rely on the attributes.
- self._catalog: dict[tuple[str, Any] | str, str] = {}
- self.plural: Callable[[float | Decimal], int] = lambda n: int(n != 1)
- super().__init__(fp=fp)
- self.files = list(filter(None, [getattr(fp, 'name', None)]))
- self.domain = self.DEFAULT_DOMAIN
- self._domains: dict[str, NullTranslations] = {}
- def dgettext(self, domain: str, message: str) -> str:
- """Like ``gettext()``, but look the message up in the specified
- domain.
- """
- return self._domains.get(domain, self).gettext(message)
- def ldgettext(self, domain: str, message: str) -> str:
- """Like ``lgettext()``, but look the message up in the specified
- domain.
- """
- import warnings
- warnings.warn(
- 'ldgettext() is deprecated, use dgettext() instead',
- DeprecationWarning,
- stacklevel=2,
- )
- return self._domains.get(domain, self).lgettext(message)
- def udgettext(self, domain: str, message: str) -> str:
- """Like ``ugettext()``, but look the message up in the specified
- domain.
- """
- return self._domains.get(domain, self).ugettext(message)
- # backward compatibility with 0.9
- dugettext = udgettext
- def dngettext(self, domain: str, singular: str, plural: str, num: int) -> str:
- """Like ``ngettext()``, but look the message up in the specified
- domain.
- """
- return self._domains.get(domain, self).ngettext(singular, plural, num)
- def ldngettext(self, domain: str, singular: str, plural: str, num: int) -> str:
- """Like ``lngettext()``, but look the message up in the specified
- domain.
- """
- import warnings
- warnings.warn(
- 'ldngettext() is deprecated, use dngettext() instead',
- DeprecationWarning,
- stacklevel=2,
- )
- return self._domains.get(domain, self).lngettext(singular, plural, num)
- def udngettext(self, domain: str, singular: str, plural: str, num: int) -> str:
- """Like ``ungettext()`` but look the message up in the specified
- domain.
- """
- return self._domains.get(domain, self).ungettext(singular, plural, num)
- # backward compatibility with 0.9
- dungettext = udngettext
- # Most of the downwards code, until it gets included in stdlib, from:
- # https://bugs.python.org/file10036/gettext-pgettext.patch
- #
- # The encoding of a msgctxt and a msgid in a .mo file is
- # msgctxt + "\x04" + msgid (gettext version >= 0.15)
- CONTEXT_ENCODING = '%s\x04%s'
- def pgettext(self, context: str, message: str) -> str | object:
- """Look up the `context` and `message` id in the catalog and return the
- corresponding message string, as an 8-bit string encoded with the
- catalog's charset encoding, if known. If there is no entry in the
- catalog for the `message` id and `context` , and a fallback has been
- set, the look up is forwarded to the fallback's ``pgettext()``
- method. Otherwise, the `message` id is returned.
- """
- ctxt_msg_id = self.CONTEXT_ENCODING % (context, message)
- missing = object()
- tmsg = self._catalog.get(ctxt_msg_id, missing)
- if tmsg is missing:
- tmsg = self._catalog.get((ctxt_msg_id, self.plural(1)), missing)
- if tmsg is not missing:
- return tmsg
- if self._fallback:
- return self._fallback.pgettext(context, message)
- return message
- def lpgettext(self, context: str, message: str) -> str | bytes | object:
- """Equivalent to ``pgettext()``, but the translation is returned in the
- preferred system encoding, if no other encoding was explicitly set with
- ``bind_textdomain_codeset()``.
- """
- import warnings
- warnings.warn(
- 'lpgettext() is deprecated, use pgettext() instead',
- DeprecationWarning,
- stacklevel=2,
- )
- tmsg = self.pgettext(context, message)
- encoding = getattr(self, "_output_charset", None) or locale.getpreferredencoding()
- return tmsg.encode(encoding) if isinstance(tmsg, str) else tmsg
- def npgettext(self, context: str, singular: str, plural: str, num: int) -> str:
- """Do a plural-forms lookup of a message id. `singular` is used as the
- message id for purposes of lookup in the catalog, while `num` is used to
- determine which plural form to use. The returned message string is an
- 8-bit string encoded with the catalog's charset encoding, if known.
- If the message id for `context` is not found in the catalog, and a
- fallback is specified, the request is forwarded to the fallback's
- ``npgettext()`` method. Otherwise, when ``num`` is 1 ``singular`` is
- returned, and ``plural`` is returned in all other cases.
- """
- ctxt_msg_id = self.CONTEXT_ENCODING % (context, singular)
- try:
- tmsg = self._catalog[(ctxt_msg_id, self.plural(num))]
- return tmsg
- except KeyError:
- if self._fallback:
- return self._fallback.npgettext(context, singular, plural, num)
- if num == 1:
- return singular
- else:
- return plural
- def lnpgettext(self, context: str, singular: str, plural: str, num: int) -> str | bytes:
- """Equivalent to ``npgettext()``, but the translation is returned in the
- preferred system encoding, if no other encoding was explicitly set with
- ``bind_textdomain_codeset()``.
- """
- import warnings
- warnings.warn(
- 'lnpgettext() is deprecated, use npgettext() instead',
- DeprecationWarning,
- stacklevel=2,
- )
- ctxt_msg_id = self.CONTEXT_ENCODING % (context, singular)
- try:
- tmsg = self._catalog[(ctxt_msg_id, self.plural(num))]
- encoding = getattr(self, "_output_charset", None) or locale.getpreferredencoding()
- return tmsg.encode(encoding)
- except KeyError:
- if self._fallback:
- return self._fallback.lnpgettext(context, singular, plural, num)
- if num == 1:
- return singular
- else:
- return plural
- def upgettext(self, context: str, message: str) -> str:
- """Look up the `context` and `message` id in the catalog and return the
- corresponding message string, as a Unicode string. If there is no entry
- in the catalog for the `message` id and `context`, and a fallback has
- been set, the look up is forwarded to the fallback's ``upgettext()``
- method. Otherwise, the `message` id is returned.
- """
- ctxt_message_id = self.CONTEXT_ENCODING % (context, message)
- missing = object()
- tmsg = self._catalog.get(ctxt_message_id, missing)
- if tmsg is missing:
- if self._fallback:
- return self._fallback.upgettext(context, message)
- return str(message)
- assert isinstance(tmsg, str)
- return tmsg
- def unpgettext(self, context: str, singular: str, plural: str, num: int) -> str:
- """Do a plural-forms lookup of a message id. `singular` is used as the
- message id for purposes of lookup in the catalog, while `num` is used to
- determine which plural form to use. The returned message string is a
- Unicode string.
- If the message id for `context` is not found in the catalog, and a
- fallback is specified, the request is forwarded to the fallback's
- ``unpgettext()`` method. Otherwise, when `num` is 1 `singular` is
- returned, and `plural` is returned in all other cases.
- """
- ctxt_message_id = self.CONTEXT_ENCODING % (context, singular)
- try:
- tmsg = self._catalog[(ctxt_message_id, self.plural(num))]
- except KeyError:
- if self._fallback:
- return self._fallback.unpgettext(context, singular, plural, num)
- tmsg = str(singular) if num == 1 else str(plural)
- return tmsg
- def dpgettext(self, domain: str, context: str, message: str) -> str | object:
- """Like `pgettext()`, but look the message up in the specified
- `domain`.
- """
- return self._domains.get(domain, self).pgettext(context, message)
- def udpgettext(self, domain: str, context: str, message: str) -> str:
- """Like `upgettext()`, but look the message up in the specified
- `domain`.
- """
- return self._domains.get(domain, self).upgettext(context, message)
- # backward compatibility with 0.9
- dupgettext = udpgettext
- def ldpgettext(self, domain: str, context: str, message: str) -> str | bytes | object:
- """Equivalent to ``dpgettext()``, but the translation is returned in the
- preferred system encoding, if no other encoding was explicitly set with
- ``bind_textdomain_codeset()``.
- """
- return self._domains.get(domain, self).lpgettext(context, message)
- def dnpgettext(self, domain: str, context: str, singular: str, plural: str, num: int) -> str:
- """Like ``npgettext``, but look the message up in the specified
- `domain`.
- """
- return self._domains.get(domain, self).npgettext(context, singular,
- plural, num)
- def udnpgettext(self, domain: str, context: str, singular: str, plural: str, num: int) -> str:
- """Like ``unpgettext``, but look the message up in the specified
- `domain`.
- """
- return self._domains.get(domain, self).unpgettext(context, singular,
- plural, num)
- # backward compatibility with 0.9
- dunpgettext = udnpgettext
- def ldnpgettext(self, domain: str, context: str, singular: str, plural: str, num: int) -> str | bytes:
- """Equivalent to ``dnpgettext()``, but the translation is returned in
- the preferred system encoding, if no other encoding was explicitly set
- with ``bind_textdomain_codeset()``.
- """
- return self._domains.get(domain, self).lnpgettext(context, singular,
- plural, num)
- ugettext = gettext.NullTranslations.gettext
- ungettext = gettext.NullTranslations.ngettext
- class Translations(NullTranslations, gettext.GNUTranslations):
- """An extended translation catalog class."""
- DEFAULT_DOMAIN = 'messages'
- def __init__(self, fp: gettext._TranslationsReader | None = None, domain: str | None = None):
- """Initialize the translations catalog.
- :param fp: the file-like object the translation should be read from
- :param domain: the message domain (default: 'messages')
- """
- super().__init__(fp=fp)
- self.domain = domain or self.DEFAULT_DOMAIN
- ugettext = gettext.GNUTranslations.gettext
- ungettext = gettext.GNUTranslations.ngettext
- @classmethod
- def load(
- cls,
- dirname: str | os.PathLike[str] | None = None,
- locales: Iterable[str | Locale] | Locale | str | None = None,
- domain: str | None = None,
- ) -> NullTranslations:
- """Load translations from the given directory.
- :param dirname: the directory containing the ``MO`` files
- :param locales: the list of locales in order of preference (items in
- this list can be either `Locale` objects or locale
- strings)
- :param domain: the message domain (default: 'messages')
- """
- if not domain:
- domain = cls.DEFAULT_DOMAIN
- filename = gettext.find(domain, dirname, _locales_to_names(locales))
- if not filename:
- return NullTranslations()
- with open(filename, 'rb') as fp:
- return cls(fp=fp, domain=domain)
- def __repr__(self) -> str:
- version = self._info.get('project-id-version')
- return f'<{type(self).__name__}: "{version}">'
- def add(self, translations: Translations, merge: bool = True):
- """Add the given translations to the catalog.
- If the domain of the translations is different than that of the
- current catalog, they are added as a catalog that is only accessible
- by the various ``d*gettext`` functions.
- :param translations: the `Translations` instance with the messages to
- add
- :param merge: whether translations for message domains that have
- already been added should be merged with the existing
- translations
- """
- domain = getattr(translations, 'domain', self.DEFAULT_DOMAIN)
- if merge and domain == self.domain:
- return self.merge(translations)
- existing = self._domains.get(domain)
- if merge and isinstance(existing, Translations):
- existing.merge(translations)
- else:
- translations.add_fallback(self)
- self._domains[domain] = translations
- return self
- def merge(self, translations: Translations):
- """Merge the given translations into the catalog.
- Message translations in the specified catalog override any messages
- with the same identifier in the existing catalog.
- :param translations: the `Translations` instance with the messages to
- merge
- """
- if isinstance(translations, gettext.GNUTranslations):
- self._catalog.update(translations._catalog)
- if isinstance(translations, Translations):
- self.files.extend(translations.files)
- return self
- def _locales_to_names(
- locales: Iterable[str | Locale] | Locale | str | None,
- ) -> list[str] | None:
- """Normalize a `locales` argument to a list of locale names.
- :param locales: the list of locales in order of preference (items in
- this list can be either `Locale` objects or locale
- strings)
- """
- if locales is None:
- return None
- if isinstance(locales, Locale):
- return [str(locales)]
- if isinstance(locales, str):
- return [locales]
- return [str(locale) for locale in locales]
|