test / bot /utils /translator.py
mtaaz's picture
Upload 91 files
2b5eba7 verified
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from bot.i18n import SUPPORTED_LANGUAGES, translate as legacy_translate
class Translator:
"""Centralized JSON translator with English default and legacy fallback."""
def __init__(self, bot: Any, locales_dir: str = "bot/locales") -> None:
self.bot = bot
self.locales_dir = Path(locales_dir)
self._cache: dict[str, dict[str, str]] = {}
self.default_language = "en"
self.supported_languages = set(SUPPORTED_LANGUAGES) if SUPPORTED_LANGUAGES else {"en", "ar"}
self._load_locales()
def _load_locales(self) -> None:
# Include locale files that exist on disk, even if legacy i18n list is behind.
file_langs = {
p.stem for p in self.locales_dir.glob("*.json")
if p.is_file()
}
self.supported_languages = set(self.supported_languages) | file_langs
for lang in self.supported_languages:
path = self.locales_dir / f"{lang}.json"
try:
data = json.loads(path.read_text(encoding="utf-8")) if path.exists() else {}
except Exception:
data = {}
self._cache[lang] = data if isinstance(data, dict) else {}
self._merge_english_defaults()
@staticmethod
def _deep_merge_defaults(defaults: dict[str, Any], target: dict[str, Any]) -> dict[str, Any]:
merged: dict[str, Any] = dict(target)
for key, value in defaults.items():
if key not in merged:
merged[key] = value
continue
if isinstance(value, dict) and isinstance(merged.get(key), dict):
merged[key] = Translator._deep_merge_defaults(value, merged[key]) # type: ignore[arg-type]
return merged
def _merge_english_defaults(self) -> None:
"""Guarantee all locale packs have the full key tree using English as baseline."""
en_pack = self._cache.get(self.default_language, {})
if not isinstance(en_pack, dict):
return
for lang, pack in list(self._cache.items()):
if not isinstance(pack, dict):
self._cache[lang] = dict(en_pack)
continue
if lang == self.default_language:
continue
self._cache[lang] = self._deep_merge_defaults(en_pack, pack)
def _lookup_key(self, lang: str, key: str) -> str | None:
"""Lookup a translation key supporting nested dotted paths."""
data = self._cache.get(lang, {})
if key in data:
value = data.get(key)
return str(value) if isinstance(value, str) else None
cursor: Any = data
for part in key.split("."):
if not isinstance(cursor, dict) or part not in cursor:
return None
cursor = cursor[part]
return str(cursor) if isinstance(cursor, str) else None
async def get(self, key: str, guild_id: int | None = None, **kwargs: object) -> str:
lang = await self.bot.get_guild_language(guild_id) if guild_id else self.default_language
if lang not in self.supported_languages:
lang = self.default_language
template = self._lookup_key(lang, key)
if template is None:
template = self._lookup_key(self.default_language, key)
if template is None:
# Backward compatibility for existing i18n keys during migration.
try:
return legacy_translate(lang, key, **kwargs)
except Exception:
return key
try:
return str(template).format(**kwargs)
except Exception:
return str(template)