Source code for betty.locale

"""
Provide the Locale API.
"""

from __future__ import annotations

from typing import Sequence, TypeAlias

from babel import Locale, negotiate_locale as babel_negotiate_locale
from babel.core import UnknownLocaleError
from langcodes import Language

from betty import fs
from betty.json.schema import String

_LOCALE_DIRECTORY_PATH = fs.ASSETS_DIRECTORY_PATH / "locale"

NO_LINGUISTIC_CONTENT = "zxx"
UNDETERMINED_LOCALE = "und"
UNCODED_LOCALE = "mis"
MULTIPLE_LOCALES = "mul"
SPECIAL_LOCALES = (
    NO_LINGUISTIC_CONTENT,
    UNDETERMINED_LOCALE,
    UNCODED_LOCALE,
    MULTIPLE_LOCALES,
)

DEFAULT_LOCALE = "en-US"


[docs] def merge_locales(*locales: str) -> str: """ Merge locales into a single locale. """ unique_locales = set(locales) if len(unique_locales) == 0: return NO_LINGUISTIC_CONTENT elif len(unique_locales) == 1: return next(iter(unique_locales)) # Strip locales without linguistic content. if NO_LINGUISTIC_CONTENT in unique_locales: return merge_locales(*(unique_locales - {NO_LINGUISTIC_CONTENT})) return MULTIPLE_LOCALES
[docs] def to_babel_identifier(locale: Localey) -> str: """ Convert a locale or locale metadata to a Babel locale identifier. :raises ValueError: """ if isinstance(locale, Locale): return str(locale) language_data = Language.get(locale) return "_".join( part for part in [ language_data.language, language_data.script, language_data.territory, ] if part )
[docs] def to_locale(locale: Localey) -> str: """ Ensure that a locale or locale metadata is a locale. """ if isinstance(locale, str): return locale return "-".join( part for part in [ locale.language, locale.script, locale.territory, ] if part )
Localey: TypeAlias = str | Locale
[docs] def get_data(locale: Localey) -> Locale: """ Get locale metadata. :raises betty.locale.InvalidLocale: Raised if the given identifier is not a valid locale. :raises betty.locale.LocaleNotFoundError: Raised if the given locale cannot be found. """ if isinstance(locale, Locale): return locale try: return Locale.parse(to_babel_identifier(locale)) except ValueError: from betty.locale.error import InvalidLocale raise InvalidLocale(locale) from None except UnknownLocaleError: from betty.locale.error import LocaleNotFound raise LocaleNotFound(locale) from None
[docs] def get_display_name( locale: Localey, display_locale: Localey | None = None ) -> str | None: """ Return a locale's human-readable display name. """ locale_data = get_data(locale) return locale_data.get_display_name( get_data(display_locale) if display_locale else locale_data )
[docs] def negotiate_locale( preferred_locales: Localey | Sequence[Localey], available_locales: Sequence[Localey] ) -> Locale | None: """ Negotiate the preferred locale from a sequence. """ if isinstance(preferred_locales, (str, Locale)): preferred_locales = [preferred_locales] return _negotiate_locale( [to_babel_identifier(locale) for locale in preferred_locales], {to_babel_identifier(locale) for locale in available_locales}, False, )
def _negotiate_locale( preferred_locale_babel_identifiers: Sequence[str], available_locale_babel_identifiers: set[str], root: bool, ) -> Locale | None: negotiated_locale = babel_negotiate_locale( preferred_locale_babel_identifiers, available_locale_babel_identifiers ) if negotiated_locale is not None: return Locale.parse(negotiated_locale) if not root: return _negotiate_locale( [ ( babel_identifier.split("_")[0] if "_" in babel_identifier else babel_identifier ) for babel_identifier in preferred_locale_babel_identifiers ], available_locale_babel_identifiers, True, ) return None
[docs] class LocaleSchema(String): """ The JSON Schema for locales. """
[docs] def __init__(self): super().__init__( def_name="locale", title="Locale", description="A BCP 47 locale identifier (https://www.ietf.org/rfc/bcp/bcp47.txt).", )