ICRA/app/i18n.py

107 lines
3.3 KiB
Python

"""Translation helpers and language-aware mixins."""
from __future__ import annotations
import contextlib
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict
try: # Python 3.11+
import tomllib # type: ignore[attr-defined]
except ModuleNotFoundError: # pragma: no cover - fallback
with contextlib.suppress(ModuleNotFoundError):
import tomli as tomllib # type: ignore[assignment]
if "tomllib" not in globals():
tomllib = None # type: ignore[assignment]
LANG_DIR = Path(__file__).resolve().parent / "lang"
FALLBACK_LANGUAGE = "en"
def _available_language_files() -> Dict[str, Path]:
files: Dict[str, Path] = {}
if LANG_DIR.exists():
for path in LANG_DIR.glob("*.toml"):
files[path.stem.lower()] = path
return files
def _load_translations(lang: str) -> Dict[str, str]:
if tomllib is None:
return {}
lang_files = _available_language_files()
path = lang_files.get(lang.lower())
if path is None:
return {}
try:
with path.open("rb") as handle:
data = tomllib.load(handle)
except (OSError, AttributeError, ValueError, TypeError): # type: ignore[arg-type]
return {}
translations = data.get("translations")
if not isinstance(translations, dict):
return {}
out: Dict[str, str] = {}
for key, value in translations.items():
if isinstance(key, str) and isinstance(value, str):
out[key] = value
return out
@dataclass
class Translator:
"""Simple lookup-based translator with file-backed dictionaries."""
language: str = FALLBACK_LANGUAGE
_translations: Dict[str, str] = field(default_factory=dict, init=False)
_fallback: Dict[str, str] = field(default_factory=dict, init=False)
def __post_init__(self) -> None:
self._fallback = _load_translations(FALLBACK_LANGUAGE)
self.set_language(self.language)
def set_language(self, language: str) -> None:
chosen = language.lower()
data = _load_translations(chosen)
if not data:
chosen = FALLBACK_LANGUAGE
data = _load_translations(FALLBACK_LANGUAGE)
self.language = chosen
self._translations = data or {}
def translate(self, key: str, **values: Any) -> str:
template = self._translations.get(key) or self._fallback.get(key) or key
if values:
try:
return template.format(**values)
except (KeyError, ValueError):
return template
return template
class I18nMixin:
"""Mixin providing translated text helpers."""
language: str
translator: Translator
def init_i18n(self, language: str | None = None) -> None:
self.translator = Translator()
self.set_language(language or FALLBACK_LANGUAGE)
def set_language(self, language: str) -> None:
self.translator.set_language(language)
self.language = self.translator.language
def _t(self, key: str, **values: Any) -> str:
return self.translator.translate(key, **values)
@property
def available_languages(self) -> tuple[str, ...]:
return tuple(sorted(_available_language_files().keys()))
__all__ = ["I18nMixin", "Translator", "LANG_DIR", "FALLBACK_LANGUAGE"]