mirror of
https://github.com/anten-ka/gotelegram_pro.git
synced 2026-05-19 12:16:01 +00:00
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
This commit is contained in:
142
gotelegram-bot/i18n.py
Normal file
142
gotelegram-bot/i18n.py
Normal file
@@ -0,0 +1,142 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user