1from __future__ import annotations
2
3import re
4
5from pathlib import Path
6from typing import Any
7from typing import ClassVar
8from typing import Dict
9from typing import cast
10
11
12class Locale:
13 """
14 Represent a specific locale.
15 """
16
17 _cache: ClassVar[dict[str, Locale]] = {}
18
19 def __init__(self, locale: str, data: Any) -> None:
20 self._locale: str = locale
21 self._data: Any = data
22 self._key_cache: dict[str, str] = {}
23
24 @classmethod
25 def load(cls, locale: str | Locale) -> Locale:
26 from importlib import import_module, resources
27
28 if isinstance(locale, Locale):
29 return locale
30
31 locale = cls.normalize_locale(locale)
32 if locale in cls._cache:
33 return cls._cache[locale]
34
35 # Checking locale existence
36 actual_locale = locale
37 locale_path = cast(Path, resources.files(__package__).joinpath(actual_locale))
38 while not locale_path.exists():
39 if actual_locale == locale:
40 raise ValueError(f"Locale [{locale}] does not exist.")
41
42 actual_locale = actual_locale.split("_")[0]
43
44 m = import_module(f"pendulum.locales.{actual_locale}.locale")
45
46 cls._cache[locale] = cls(locale, m.locale)
47
48 return cls._cache[locale]
49
50 @classmethod
51 def normalize_locale(cls, locale: str) -> str:
52 m = re.fullmatch("([a-z]{2})[-_]([a-z]{2})", locale, re.I)
53 if m:
54 return f"{m.group(1).lower()}_{m.group(2).lower()}"
55 else:
56 return locale.lower()
57
58 def get(self, key: str, default: Any | None = None) -> Any:
59 if key in self._key_cache:
60 return self._key_cache[key]
61
62 parts = key.split(".")
63 try:
64 result = self._data[parts[0]]
65 for part in parts[1:]:
66 result = result[part]
67 except KeyError:
68 result = default
69
70 self._key_cache[key] = result
71
72 return self._key_cache[key]
73
74 def translation(self, key: str) -> Any:
75 return self.get(f"translations.{key}")
76
77 def plural(self, number: int) -> str:
78 return cast(str, self._data["plural"](number))
79
80 def ordinal(self, number: int) -> str:
81 return cast(str, self._data["ordinal"](number))
82
83 def ordinalize(self, number: int) -> str:
84 ordinal = self.get(f"custom.ordinal.{self.ordinal(number)}")
85
86 if not ordinal:
87 return f"{number}"
88
89 return f"{number}{ordinal}"
90
91 def match_translation(self, key: str, value: Any) -> dict[str, str] | None:
92 translations = self.translation(key)
93 if value not in translations.values():
94 return None
95
96 return cast(Dict[str, str], {v: k for k, v in translations.items()}[value])
97
98 def __repr__(self) -> str:
99 return f"{self.__class__.__name__}('{self._locale}')"