Source code for iccas.i18n.lib

"""
A simple and crappy i18n library written "for fun" and because neither
gettext and python-i18n were optimal for my use case which requires
reloading of translations when a change is made without restarting the
interpreter/kernel and possibly without reloading the module.
"""
import locale
from collections import ChainMap
from dataclasses import dataclass
from functools import wraps
from io import StringIO
from itertools import chain
from pathlib import Path
from typing import List, Mapping, Optional, Tuple, Union

import yaml

from iccas.types import PathType

LanguageStrings = Mapping[str, Union[str, Mapping[str, str]]]
TranslationsMap = Mapping[str, LanguageStrings]


[docs]class Translation: def __init__(self, lang, strings): self.lang = lang self.strings = strings def __getitem__(self, str_id: str): return self.strings[str_id]
[docs] def get(self, str_id: str, count: int, few_max: int = 5, varname="count") -> str: if count < 0: raise ValueError("negative count") plural_strings = self.strings[str_id] if isinstance(plural_strings, str): return plural_strings.format(**{varname: count}) keys: Union[Tuple[str], Tuple[str, str]] if count == 0: keys = ("zero", "many") elif count == 1: keys = ("one", "many") elif count <= few_max: keys = ("few", "many") else: keys = ("many",) for key in keys: if key not in plural_strings: continue return plural_strings[key].format(**{varname: count}) else: raise KeyError( f"pluralization error for key '{str_id}' and count {count}: " f"none of keys in {keys} were specified" )
[docs]class NullTranslation(Translation): def __init__(self): super().__init__('null', {}) def __getitem__(self, str_id: str) -> str: return str_id
[docs] def get(self, str_id: str, count: int, few_max: int = 5, varname="count") -> str: return str_id
[docs]@dataclass class TranslationFile: path: Path content: dict last_modified: int
[docs] def reload_if_modified(self) -> bool: last_modified = self.path.stat().st_mtime_ns if self.last_modified != last_modified: self.content = self.read(self.path) self.last_modified = self.last_modified return True return False
[docs] @staticmethod def load(path: PathType) -> "TranslationFile": path = Path(path) content = TranslationFile.read(path) last_modified = path.stat().st_mtime_ns return TranslationFile(path=path, content=content, last_modified=last_modified)
[docs] @staticmethod def read(path: PathType): with open(path, encoding="utf-8") as f: return yaml.full_load(f)
[docs]class TranslationsManager: def __init__(self, *paths): self.files = [TranslationFile.load(path) for path in set(paths)] self.languages = set(chain.from_iterable(f.content.keys() for f in self.files)) default_lang = locale.getdefaultlocale()[0].split("_")[0] self.current_language = default_lang if default_lang in self.languages else "en" def _reload_modified_files(self): for file in self.files: file.reload_if_modified()
[docs] def injector(self, scoped_translations: Union[None, str, TranslationsMap] = None): """ Decorator that adds a ``lang`` argument to the signature and injects a ``strings`` argument of type :class:`Translation`. If the translation files are modified, they are automatically reloaded. You can also pass extra translations as dict or YAML string to the wrapped function. Nonetheless, any modification will require to reload the module to take effect. Args: scoped_translations: yaml string or dictionary """ extra_translations: TranslationsMap if isinstance(scoped_translations, dict): extra_translations = scoped_translations if scoped_translations and isinstance(scoped_translations, str): with StringIO(scoped_translations) as s: extra_translations = yaml.full_load(s) or {} elif scoped_translations is None: extra_translations = {} else: raise TypeError('scoped_translations') def decorator(func): @wraps(func) def wrapper(*args, lang: Optional[str] = None, **kwargs): lang = lang or self.current_language self._reload_modified_files() mappings: List[LanguageStrings] = ( [extra_translations[lang]] if lang in extra_translations else [] ) mappings += [f.content[lang] for f in self.files if lang in f.content] strings = Translation(lang, ChainMap(*mappings)) return func(*args, strings=strings, **kwargs) wrapper.translation_manager = self wrapper.wrapped_function = func return wrapper return decorator