| 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: |
| |
| 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]) |
| 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: |
| |
| try: |
| return legacy_translate(lang, key, **kwargs) |
| except Exception: |
| return key |
|
|
| try: |
| return str(template).format(**kwargs) |
| except Exception: |
| return str(template) |
|
|