Files
anten-ka 0d087831d8 v2.4.0 — internationalization (EN/RU) + custom git templates
- i18n engine (lib/i18n.sh, lib/lang/en.sh, lib/lang/ru.sh)
- first-run language picker, persisted to .language + config.json
- install.sh, common.sh, backup.sh, templates_catalog.sh wired through t()/tf()
- backup.sh preserves .language marker and records language in metadata.json
- custom git template feature (first item in pro template picker)
  * validates HTTPS URLs, rejects shell metachars
  * 100MB size guard, 90s clone timeout
  * auto-detects index.html in dist/public/build/_site/site/docs/out/www
- bot v2.4.0: i18n.py + lang/{en,ru}.json, /lang command, language toggle button
- bot: custom git template via text input with waiter gating
2026-04-10 11:26:02 +03:00

143 lines
4.9 KiB
Python

"""
GoTelegram v2.4 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/<code>.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/<code>.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