""" GoTelegram v2.5.0 Bot — i18n module Provides per-user language preferences and a simple t()/tf() API. Usage: from i18n import t, tf, set_user_lang, get_user_lang, get_language_name msg = t(user_id, "menu_status") msg = tf(user_id, "backup_created_fmt", filename) Language files live next to this module in lang/.json. Per-user choices are persisted to USER_LANG_FILE (one JSON dict: user_id -> code). """ import json import logging import os from pathlib import Path from typing import Dict, Optional logger = logging.getLogger(__name__) # ── Paths ───────────────────────────────────────────────────────────────── _MODULE_DIR = Path(__file__).resolve().parent LANG_DIR = _MODULE_DIR / "lang" USER_LANG_FILE = Path("/opt/gotelegram-bot/user_langs.json") # Supported codes; keep in sync with lang/*.json SUPPORTED_LANGS = ("en", "ru") DEFAULT_LANG = os.getenv("BOT_LANG", "en").strip().lower() or "en" if DEFAULT_LANG not in SUPPORTED_LANGS: DEFAULT_LANG = "en" LANG_NAMES = { "en": "English", "ru": "Русский", } # ── Caches ──────────────────────────────────────────────────────────────── _LANG_CACHE: Dict[str, Dict[str, str]] = {} _USER_LANGS: Dict[int, str] = {} _USER_LANGS_LOADED = False def _load_lang_file(code: str) -> Dict[str, str]: """Load lang/.json into the cache and return it.""" if code in _LANG_CACHE: return _LANG_CACHE[code] path = LANG_DIR / f"{code}.json" try: with open(path, "r", encoding="utf-8") as f: data = json.load(f) if not isinstance(data, dict): raise ValueError("lang file must contain a top-level object") _LANG_CACHE[code] = data return data except FileNotFoundError: logger.warning("lang file not found: %s", path) except Exception as e: logger.warning("failed to load %s: %s", path, e) _LANG_CACHE[code] = {} return _LANG_CACHE[code] def _load_user_langs() -> None: """Load per-user language preferences from USER_LANG_FILE.""" global _USER_LANGS, _USER_LANGS_LOADED _USER_LANGS_LOADED = True try: if USER_LANG_FILE.exists(): with open(USER_LANG_FILE, "r", encoding="utf-8") as f: raw = json.load(f) if isinstance(raw, dict): _USER_LANGS = { int(k): v for k, v in raw.items() if isinstance(v, str) and v in SUPPORTED_LANGS } except Exception as e: logger.warning("failed to load user_langs: %s", e) _USER_LANGS = {} def _save_user_langs() -> None: """Persist per-user language preferences.""" try: USER_LANG_FILE.parent.mkdir(parents=True, exist_ok=True) with open(USER_LANG_FILE, "w", encoding="utf-8") as f: json.dump( {str(k): v for k, v in _USER_LANGS.items()}, f, ensure_ascii=False, indent=2, ) except Exception as e: logger.warning("failed to save user_langs: %s", e) # ── Public API ──────────────────────────────────────────────────────────── def get_user_lang(user_id: Optional[int]) -> str: """Return the language code for the given user (or DEFAULT_LANG).""" if not _USER_LANGS_LOADED: _load_user_langs() if user_id is None: return DEFAULT_LANG return _USER_LANGS.get(int(user_id), DEFAULT_LANG) def set_user_lang(user_id: int, code: str) -> bool: """Set the per-user language preference and persist it.""" if not _USER_LANGS_LOADED: _load_user_langs() code = (code or "").strip().lower() if code not in SUPPORTED_LANGS: return False _USER_LANGS[int(user_id)] = code _save_user_langs() return True def get_language_name(code: str) -> str: return LANG_NAMES.get(code, code) def t(user_id: Optional[int], key: str, default: Optional[str] = None) -> str: """Translate key for the given user. Falls back to English, then default/key.""" code = get_user_lang(user_id) table = _load_lang_file(code) if key in table: return table[key] if code != "en": en_table = _load_lang_file("en") if key in en_table: return en_table[key] return default if default is not None else key def tf(user_id: Optional[int], key: str, *args, default: Optional[str] = None) -> str: """Format a translated string with positional args using %-formatting.""" template = t(user_id, key, default=default) try: return template % args if args else template except (TypeError, ValueError): return template