|
""" |
|
babel.localedata |
|
~~~~~~~~~~~~~~~~ |
|
|
|
Low-level locale data access. |
|
|
|
:note: The `Locale` class, which uses this module under the hood, provides a |
|
more convenient interface for accessing the locale data. |
|
|
|
:copyright: (c) 2013-2024 by the Babel Team. |
|
:license: BSD, see LICENSE for more details. |
|
""" |
|
|
|
from __future__ import annotations |
|
|
|
import os |
|
import pickle |
|
import re |
|
import sys |
|
import threading |
|
from collections import abc |
|
from collections.abc import Iterator, Mapping, MutableMapping |
|
from functools import lru_cache |
|
from itertools import chain |
|
from typing import Any |
|
|
|
_cache: dict[str, Any] = {} |
|
_cache_lock = threading.RLock() |
|
_dirname = os.path.join(os.path.dirname(__file__), 'locale-data') |
|
_windows_reserved_name_re = re.compile("^(con|prn|aux|nul|com[0-9]|lpt[0-9])$", re.I) |
|
|
|
|
|
def normalize_locale(name: str) -> str | None: |
|
"""Normalize a locale ID by stripping spaces and apply proper casing. |
|
|
|
Returns the normalized locale ID string or `None` if the ID is not |
|
recognized. |
|
""" |
|
if not name or not isinstance(name, str): |
|
return None |
|
name = name.strip().lower() |
|
for locale_id in chain.from_iterable([_cache, locale_identifiers()]): |
|
if name == locale_id.lower(): |
|
return locale_id |
|
|
|
|
|
def resolve_locale_filename(name: os.PathLike[str] | str) -> str: |
|
""" |
|
Resolve a locale identifier to a `.dat` path on disk. |
|
""" |
|
|
|
|
|
name = os.path.basename(name) |
|
|
|
|
|
if sys.platform == "win32" and _windows_reserved_name_re.match(os.path.splitext(name)[0]): |
|
raise ValueError(f"Name {name} is invalid on Windows") |
|
|
|
|
|
return os.path.join(_dirname, f"{name}.dat") |
|
|
|
|
|
def exists(name: str) -> bool: |
|
"""Check whether locale data is available for the given locale. |
|
|
|
Returns `True` if it exists, `False` otherwise. |
|
|
|
:param name: the locale identifier string |
|
""" |
|
if not name or not isinstance(name, str): |
|
return False |
|
if name in _cache: |
|
return True |
|
file_found = os.path.exists(resolve_locale_filename(name)) |
|
return True if file_found else bool(normalize_locale(name)) |
|
|
|
|
|
@lru_cache(maxsize=None) |
|
def locale_identifiers() -> list[str]: |
|
"""Return a list of all locale identifiers for which locale data is |
|
available. |
|
|
|
This data is cached after the first invocation. |
|
You can clear the cache by calling `locale_identifiers.cache_clear()`. |
|
|
|
.. versionadded:: 0.8.1 |
|
|
|
:return: a list of locale identifiers (strings) |
|
""" |
|
return [ |
|
stem |
|
for stem, extension in |
|
(os.path.splitext(filename) for filename in os.listdir(_dirname)) |
|
if extension == '.dat' and stem != 'root' |
|
] |
|
|
|
|
|
def _is_non_likely_script(name: str) -> bool: |
|
"""Return whether the locale is of the form ``lang_Script``, |
|
and the script is not the likely script for the language. |
|
|
|
This implements the behavior of the ``nonlikelyScript`` value of the |
|
``localRules`` attribute for parent locales added in CLDR 45. |
|
""" |
|
from babel.core import get_global, parse_locale |
|
|
|
try: |
|
lang, territory, script, variant, *rest = parse_locale(name) |
|
except ValueError: |
|
return False |
|
|
|
if lang and script and not territory and not variant and not rest: |
|
likely_subtag = get_global('likely_subtags').get(lang) |
|
_, _, likely_script, *_ = parse_locale(likely_subtag) |
|
return script != likely_script |
|
return False |
|
|
|
|
|
def load(name: os.PathLike[str] | str, merge_inherited: bool = True) -> dict[str, Any]: |
|
"""Load the locale data for the given locale. |
|
|
|
The locale data is a dictionary that contains much of the data defined by |
|
the Common Locale Data Repository (CLDR). This data is stored as a |
|
collection of pickle files inside the ``babel`` package. |
|
|
|
>>> d = load('en_US') |
|
>>> d['languages']['sv'] |
|
u'Swedish' |
|
|
|
Note that the results are cached, and subsequent requests for the same |
|
locale return the same dictionary: |
|
|
|
>>> d1 = load('en_US') |
|
>>> d2 = load('en_US') |
|
>>> d1 is d2 |
|
True |
|
|
|
:param name: the locale identifier string (or "root") |
|
:param merge_inherited: whether the inherited data should be merged into |
|
the data of the requested locale |
|
:raise `IOError`: if no locale data file is found for the given locale |
|
identifier, or one of the locales it inherits from |
|
""" |
|
name = os.path.basename(name) |
|
_cache_lock.acquire() |
|
try: |
|
data = _cache.get(name) |
|
if not data: |
|
|
|
if name == 'root' or not merge_inherited: |
|
data = {} |
|
else: |
|
from babel.core import get_global |
|
parent = get_global('parent_exceptions').get(name) |
|
if not parent: |
|
if _is_non_likely_script(name): |
|
parent = 'root' |
|
else: |
|
parts = name.split('_') |
|
parent = "root" if len(parts) == 1 else "_".join(parts[:-1]) |
|
data = load(parent).copy() |
|
filename = resolve_locale_filename(name) |
|
with open(filename, 'rb') as fileobj: |
|
if name != 'root' and merge_inherited: |
|
merge(data, pickle.load(fileobj)) |
|
else: |
|
data = pickle.load(fileobj) |
|
_cache[name] = data |
|
return data |
|
finally: |
|
_cache_lock.release() |
|
|
|
|
|
def merge(dict1: MutableMapping[Any, Any], dict2: Mapping[Any, Any]) -> None: |
|
"""Merge the data from `dict2` into the `dict1` dictionary, making copies |
|
of nested dictionaries. |
|
|
|
>>> d = {1: 'foo', 3: 'baz'} |
|
>>> merge(d, {1: 'Foo', 2: 'Bar'}) |
|
>>> sorted(d.items()) |
|
[(1, 'Foo'), (2, 'Bar'), (3, 'baz')] |
|
|
|
:param dict1: the dictionary to merge into |
|
:param dict2: the dictionary containing the data that should be merged |
|
""" |
|
for key, val2 in dict2.items(): |
|
if val2 is not None: |
|
val1 = dict1.get(key) |
|
if isinstance(val2, dict): |
|
if val1 is None: |
|
val1 = {} |
|
if isinstance(val1, Alias): |
|
val1 = (val1, val2) |
|
elif isinstance(val1, tuple): |
|
alias, others = val1 |
|
others = others.copy() |
|
merge(others, val2) |
|
val1 = (alias, others) |
|
else: |
|
val1 = val1.copy() |
|
merge(val1, val2) |
|
else: |
|
val1 = val2 |
|
dict1[key] = val1 |
|
|
|
|
|
class Alias: |
|
"""Representation of an alias in the locale data. |
|
|
|
An alias is a value that refers to some other part of the locale data, |
|
as specified by the `keys`. |
|
""" |
|
|
|
def __init__(self, keys: tuple[str, ...]) -> None: |
|
self.keys = tuple(keys) |
|
|
|
def __repr__(self) -> str: |
|
return f"<{type(self).__name__} {self.keys!r}>" |
|
|
|
def resolve(self, data: Mapping[str | int | None, Any]) -> Mapping[str | int | None, Any]: |
|
"""Resolve the alias based on the given data. |
|
|
|
This is done recursively, so if one alias resolves to a second alias, |
|
that second alias will also be resolved. |
|
|
|
:param data: the locale data |
|
:type data: `dict` |
|
""" |
|
base = data |
|
for key in self.keys: |
|
data = data[key] |
|
if isinstance(data, Alias): |
|
data = data.resolve(base) |
|
elif isinstance(data, tuple): |
|
alias, others = data |
|
data = alias.resolve(base) |
|
return data |
|
|
|
|
|
class LocaleDataDict(abc.MutableMapping): |
|
"""Dictionary wrapper that automatically resolves aliases to the actual |
|
values. |
|
""" |
|
|
|
def __init__(self, data: MutableMapping[str | int | None, Any], base: Mapping[str | int | None, Any] | None = None): |
|
self._data = data |
|
if base is None: |
|
base = data |
|
self.base = base |
|
|
|
def __len__(self) -> int: |
|
return len(self._data) |
|
|
|
def __iter__(self) -> Iterator[str | int | None]: |
|
return iter(self._data) |
|
|
|
def __getitem__(self, key: str | int | None) -> Any: |
|
orig = val = self._data[key] |
|
if isinstance(val, Alias): |
|
val = val.resolve(self.base) |
|
if isinstance(val, tuple): |
|
alias, others = val |
|
val = alias.resolve(self.base).copy() |
|
merge(val, others) |
|
if isinstance(val, dict): |
|
val = LocaleDataDict(val, base=self.base) |
|
if val is not orig: |
|
self._data[key] = val |
|
return val |
|
|
|
def __setitem__(self, key: str | int | None, value: Any) -> None: |
|
self._data[key] = value |
|
|
|
def __delitem__(self, key: str | int | None) -> None: |
|
del self._data[key] |
|
|
|
def copy(self) -> LocaleDataDict: |
|
return LocaleDataDict(self._data.copy(), base=self.base) |
|
|