"""
The :class:`Lang` class provides facilities for easy internationalization.
The idea here is to have simply tools that provide locale-specific keywords,
texts, and number formats.
"""
from typing import Callable, Final, Iterable
from matplotlib import rc # type: ignore
from matplotlib.font_manager import ( # type: ignore
FontProperties,
findSystemFonts,
)
from pycommons.types import check_int_range, type_error
from moptipy.utils.strings import sanitize_name
[docs]
class Lang:
"""A language-based dictionary for locale-specific keywords."""
def __init__(self, name: str, font: str, decimal_stepwidth: int = 3,
data: dict[str, str] | None = None,
is_default: bool = False):
"""
Instantiate the language formatter.
:param name: the short name
:param font: the font name
:param decimal_stepwidth: the decimal step width
:param data: the data
:param is_default: is this the default language?
"""
#: the name of the locale
self.__name: Final[str] = sanitize_name(name)
if not isinstance(font, str):
raise type_error(font, "font", str)
if not isinstance(is_default, bool):
raise type_error(is_default, "is_default", bool)
font = font.strip()
if not font:
raise ValueError(f"The font cannot be {font!r}.")
#: the font name
self.__font: Final[str] = font
#: the decimal step width
self.__decimal_stepwidth: Final[int] = check_int_range(
decimal_stepwidth, "decimal_stepwidth", 1, 10)
#: the dictionary with the translation data
self.__dict: Final[dict[str, str]] = {}
if data:
self.extend(data)
#: is this the default language?
self.__is_default: Final[bool] = is_default
# register the language
dc: Final[dict[str, Lang]] = Lang.__get_langs()
if self.__name in dc:
raise ValueError(f"Language {self.__name!r} already registered.")
if is_default:
for lang in dc.values():
if lang.__is_default:
raise ValueError(
f"Language {self.__name!r} cannot be default "
f"language, {lang.__name!r} already is!")
dc[self.__name] = self
if is_default:
self.set_current()
[docs]
def extend(self, data: dict[str, str]) -> None:
"""
Add a set of entries to this dictionary.
:param data: the language-specific data
"""
if not isinstance(data, dict):
raise type_error(data, "data", dict)
for ko, v in data.items():
k = sanitize_name(ko)
if k != ko:
raise ValueError(f"key {ko!r} is different from "
f"its sanitized version {k!r}.")
if (k in self.__dict) and (self.__dict[k] != v):
raise ValueError(
f"Key {k!r} appears twice, already assigned to "
f"{self.__dict[k]!r}, cannot assign to {v!r}.")
if not isinstance(v, str):
raise type_error(v, f"value for key {k!r}", str)
if not v:
raise ValueError(f"Value for key {k!r} cannot be {v!r}.")
self.__dict[k] = v
[docs]
def filename(self, base: str) -> str:
"""
Make a suitable filename by appending the language id.
:param str base: the basename
:return: the filename
:rtype: str
>>> print(Lang.get("en").filename("test"))
test
>>> print(Lang.get("zh").filename("test"))
test_zh
"""
base = sanitize_name(base)
return base if self.__is_default else f"{base}_{self.__name}"
def __repr__(self):
"""
Get the language's name.
:return: the language's name
:rtype: str
"""
return self.__name
def __getitem__(self, item: str) -> str:
"""
Get the language formatting code.
:param item: the item to get
:return: the language-specific code
"""
if not isinstance(item, str):
raise type_error(item, "item", str)
return self.__dict[item]
[docs]
def font(self) -> str:
"""
Get the default font for this language.
:return: the default font for this language
>>> print(Lang.get("en").font())
DejaVu Sans
>>> print(Lang.get("zh").font())
Noto Sans SC
"""
return self.__font
@staticmethod
def __get_langs() -> dict[str, "Lang"]:
"""
Get the languages decode.
:return: the languages decode
"""
att: Final[str] = "__map"
if not hasattr(Lang.__get_langs, att):
setattr(Lang.__get_langs, att, {})
return getattr(Lang.__get_langs, att)
[docs]
@staticmethod
def get(name: str) -> "Lang":
"""
Get the language of the given key.
:param name: the language name
:return: the language
"""
name = sanitize_name(name)
lang: Lang | None = Lang.__get_langs().get(name, None)
if lang:
return lang
raise ValueError(f"Unknown language {name!r}.")
[docs]
@staticmethod
def current() -> "Lang":
"""
Get the current language.
:return: the current language
>>> Lang.get("en").set_current()
>>> print(Lang.current().filename("b"))
b
>>> Lang.get("zh").set_current()
>>> print(Lang.current().filename("b"))
b_zh
"""
lang: Final[Lang] = getattr(Lang.__get_langs, "__current")
if not lang:
raise ValueError("Huh?")
return lang
[docs]
def set_current(self) -> None:
"""Mark this language as the current one."""
setattr(Lang.__get_langs, "__current", self)
rc("font", family=self.font())
[docs]
@staticmethod
def all_langs() -> Iterable["Lang"]:
"""
Get all presently loaded languages.
:return: an Iterable of the languages
"""
val = list(Lang.__get_langs().values())
val.sort(key=lambda x: x.__name)
return val
[docs]
@staticmethod
def translate(key: str) -> str:
"""
Translate the given key to a string in the current language.
:param key: the key
:returns: the value of the key in the current language
>>> EN.extend({'a': 'b'})
>>> EN.set_current()
>>> print(Lang.translate("a"))
b
>>> DE.extend({'a': 'c'})
>>> Lang.get("de").set_current()
>>> print(Lang.translate("a"))
c
"""
return Lang.current()[key]
[docs]
@staticmethod
def translate_call(key: str) -> Callable:
"""
Get a callable that always returns the current translation of a key.
The callable will ignore all of its parameters and just return the
translation. This means that you can pass parameters to it and they
will be ignored.
:param key: the key to translate
:returns: the callable doing the translation
>>> cal = Lang.translate_call("a")
>>> EN.extend({'a': 'b'})
>>> EN.set_current()
>>> print(cal())
b
>>> print(cal(1, 2, 3))
b
>>> DE.extend({'a': 'c'})
>>> Lang.get("de").set_current()
>>> print(cal("x"))
c
"""
def __trc(*_, ___key=key) -> str:
return Lang.current()[___key]
return __trc
[docs]
@staticmethod
def translate_func(func: str) -> Callable[[str], str]:
"""
Create a lambda taking a dimensions and presenting a function thereof.
:param func: the function name
:returns: the function
>>> Lang.get("en").set_current()
>>> Lang.get("en").extend({"ERT": "ERT"})
>>> Lang.get("en").extend({"FEs": "time in FEs"})
>>> f = Lang.translate_func("ERT")
>>> print(f("FEs"))
ERT\u2009[time in FEs]
>>> Lang.get("de").set_current()
>>> Lang.get("de").extend({"ERT": "ERT"})
>>> Lang.get("de").extend({"FEs": "Zeit in FEs"})
>>> print(f("FEs"))
ERT\u2009[Zeit in FEs]
"""
def __tf(dim: str, f: str = func) -> str:
return f"{Lang.translate(f)}\u2009[{Lang.translate(dim)}]"
return __tf
# noinspection PyBroadException
def __get_font(choices: list[str]) -> str:
"""
Try to find an installed version of the specified font.
:param choices: the choices of the fonts
:returns: the installed name of the font, or the value of `choices[0]`
if no font was found
"""
if not isinstance(choices, list):
raise type_error(choices, "choices", list)
if len(choices) <= 0:
raise ValueError("no font choices are provided!")
attr: Final[str] = "fonts_list"
func: Final = globals()["__get_font"]
# get the list of installed fonts
font_list: list[str]
if not hasattr(func, attr):
font_list = []
for fname in findSystemFonts():
try:
font_name = FontProperties(
fname=fname).get_name().strip()
if font_name.encode("ascii", "ignore").decode() == font_name:
font_list.append(font_name)
except Exception: # noqa
# We can ignore the exceptions here.
continue # nosec
if len(font_list) <= 0:
raise ValueError("Did not find any font.")
setattr(func, attr, font_list)
else:
font_list = getattr(func, attr)
# find the installed font
for choice in choices:
clc: str = choice.lower()
found_inside: str | None = None
found_start: str | None = None
for got in font_list:
gotlc = got.lower()
if clc == gotlc:
return got
if gotlc.startswith(clc):
found_start = got
if clc in gotlc:
found_inside = got
if found_start:
return found_start
if found_inside:
return found_inside
return choices[0] # nothing found ... return default
#: the English language
EN: Final[Lang] = Lang("en",
__get_font(["DejaVu Sans", "Calibri",
"Arial", "Helvetica"]),
3, is_default=True)
#: the German language
DE: Final[Lang] = Lang("de", EN.font(), 3)
#: the Chinese language
ZH: Final[Lang] = Lang("zh",
__get_font(["Noto Sans SC", "FangSong", "SimSun",
"Arial Unicode", "SimHei"]),
4)
del __get_font # get rid of no-longer needed data such as fonts list