from __future__ import annotations
import asyncio
import logging
from typing import Any, Dict
from app.config import settings
from app.localization.loader import (
DEFAULT_LANGUAGE,
clear_locale_cache,
load_locale,
)
_logger = logging.getLogger(__name__)
_cached_rules: Dict[str, str] = {}
_LANGUAGE_ALIASES = {
"uk": "ua",
}
_DYNAMIC_LANGUAGE_CONFIGS = {
"ru": {
"traffic_pattern": "📊 {size} ГБ - {price}",
"unlimited_pattern": "📊 Безлимит - {price}",
"support_info": (
"\n🛟 Поддержка\n\n"
"Это центр тикетов: создавайте обращения, просматривайте ответы и историю.\n\n"
"• 🎫 Создать тикет — опишите проблему или вопрос\n"
"• 📋 Мои тикеты — статус и переписка\n"
"• 💬 Связаться — написать напрямую (если нужно)\n\n"
"Старайтесь использовать тикеты — так мы быстрее поможем и ничего не потеряется.\n"
),
},
"en": {
"traffic_pattern": "📊 {size} GB - {price}",
"unlimited_pattern": "📊 Unlimited - {price}",
"support_info": (
"\n🛟 RemnaWave Support\n\n"
"This is the ticket center: create requests, view replies and history.\n\n"
"• 🎫 Create ticket — describe your issue or question\n"
"• 📋 My tickets — status and conversation\n"
"• 💬 Contact — message directly if needed\n\n"
"Prefer tickets — it helps us respond faster and keep context.\n"
),
},
"ua": {
"traffic_pattern": "📊 {size} ГБ - {price}",
"unlimited_pattern": "📊 Безліміт - {price}",
"support_info": (
"\n🛠️ Технічна підтримка\n\n"
"З усіх питань звертайтеся до нашої підтримки:\n\n"
"👤 {support_username}\n\n"
"Ми допоможемо з:\n"
"• Налаштуванням підключення\n"
"• Вирішенням технічних проблем\n"
"• Питаннями щодо оплати\n"
"• Іншими питаннями\n\n"
"⏰ Час відповіді: зазвичай протягом 1-2 годин\n"
),
},
"zh": {
"traffic_pattern": "📊{size}GB-{price}",
"unlimited_pattern": "📊无限-{price}",
"support_info": (
"\n🛠️ 技术支持\n\n"
"如有任何问题,请联系我们的支持团队:\n\n"
"👤 {support_username}\n\n"
"我们将帮助您:\n"
"• 设置连接\n"
"• 解决技术问题\n"
"• 付款问题\n"
"• 其他问题\n\n"
"⏰ 响应时间:通常在 1-2 小时内\n"
),
},
}
_TRAFFIC_TIERS = (
("TRAFFIC_5GB", "5", "PRICE_TRAFFIC_5GB"),
("TRAFFIC_10GB", "10", "PRICE_TRAFFIC_10GB"),
("TRAFFIC_25GB", "25", "PRICE_TRAFFIC_25GB"),
("TRAFFIC_50GB", "50", "PRICE_TRAFFIC_50GB"),
("TRAFFIC_100GB", "100", "PRICE_TRAFFIC_100GB"),
("TRAFFIC_250GB", "250", "PRICE_TRAFFIC_250GB"),
)
def _get_cached_rules_value(language: str) -> str:
if language in _cached_rules:
return _cached_rules[language]
default = _get_default_rules(language)
_cached_rules[language] = default
return default
def _build_dynamic_values(language: str) -> Dict[str, Any]:
language_code = (language or DEFAULT_LANGUAGE).split("-")[0].lower()
language_code = _LANGUAGE_ALIASES.get(language_code, language_code)
config = _DYNAMIC_LANGUAGE_CONFIGS.get(language_code)
if not config:
return {}
values: Dict[str, Any] = {}
traffic_pattern = config["traffic_pattern"]
for key, size, price_attr in _TRAFFIC_TIERS:
price_value = getattr(settings, price_attr)
values[key] = traffic_pattern.format(
size=size,
price=settings.format_price(price_value),
)
values["TRAFFIC_UNLIMITED"] = config["unlimited_pattern"].format(
price=settings.format_price(settings.PRICE_TRAFFIC_UNLIMITED)
)
support_template = config.get("support_info")
if support_template:
values["SUPPORT_INFO"] = support_template.format(
support_username=settings.SUPPORT_USERNAME
)
return values
class Texts:
def __init__(self, language: str = DEFAULT_LANGUAGE):
self.language = language or DEFAULT_LANGUAGE
raw_data = load_locale(self.language)
self._values = {key: value for key, value in raw_data.items()}
if self.language != DEFAULT_LANGUAGE:
fallback_data = load_locale(DEFAULT_LANGUAGE)
else:
fallback_data = self._values
self._fallback_values = {
key: value for key, value in fallback_data.items() if key not in self._values
}
self._values.update(_build_dynamic_values(self.language))
def __getattr__(self, item: str) -> Any:
if item == "language":
return super().__getattribute__(item)
try:
return self._get_value(item)
except KeyError as error:
raise AttributeError(item) from error
def __getitem__(self, item: str) -> Any:
return self._get_value(item)
def get(self, item: str, default: Any = None) -> Any:
try:
return self._get_value(item)
except KeyError:
return default
def t(self, key: str, default: Any = None) -> Any:
try:
return self._get_value(key)
except KeyError:
if default is not None:
return default
raise
def _get_value(self, item: str) -> Any:
if item == "RULES_TEXT":
return _get_cached_rules_value(self.language)
if item in self._values:
return self._values[item]
if item in self._fallback_values:
return self._fallback_values[item]
_logger.warning(
"Missing localization key '%s' for language '%s'",
item,
self.language,
)
raise KeyError(item)
@staticmethod
def format_price(kopeks: int) -> str:
return settings.format_price(kopeks)
@staticmethod
def format_traffic(gb: float) -> str:
if gb == 0:
return "∞ (безлимит)"
if gb >= 1024:
return f"{gb / 1024:.1f} ТБ"
return f"{gb:.0f} ГБ"
def get_texts(language: str = DEFAULT_LANGUAGE) -> Texts:
return Texts(language)
async def get_rules_from_db(language: str = DEFAULT_LANGUAGE) -> str:
try:
from app.database.database import AsyncSessionLocal
from app.database.crud.rules import get_current_rules_content
async with AsyncSessionLocal() as db:
rules = await get_current_rules_content(db, language)
if rules:
_cached_rules[language] = rules
return rules
except Exception as error: # pragma: no cover - defensive logging
_logger.warning("Failed to load rules from DB for %s: %s", language, error)
default = _get_default_rules(language)
_cached_rules[language] = default
return default
def _get_default_rules(language: str = DEFAULT_LANGUAGE) -> str:
default_key = "RULES_TEXT_DEFAULT"
locale = load_locale(language)
if default_key in locale:
return locale[default_key]
fallback = load_locale(DEFAULT_LANGUAGE)
return fallback.get(default_key, "")
def _get_default_privacy_policy(language: str = DEFAULT_LANGUAGE) -> str:
default_key = "PRIVACY_POLICY_TEXT_DEFAULT"
locale = load_locale(language)
if default_key in locale:
return locale[default_key]
fallback = load_locale(DEFAULT_LANGUAGE)
return fallback.get(default_key, "")
def get_privacy_policy(language: str = DEFAULT_LANGUAGE) -> str:
return _get_default_privacy_policy(language)
def get_rules_sync(language: str = DEFAULT_LANGUAGE) -> str:
if language in _cached_rules:
return _cached_rules[language]
try:
loop = asyncio.get_running_loop()
except RuntimeError:
return asyncio.run(get_rules(language))
loop.create_task(get_rules(language))
return _get_cached_rules_value(language)
async def get_rules(language: str = DEFAULT_LANGUAGE) -> str:
if language in _cached_rules:
return _cached_rules[language]
return await get_rules_from_db(language)
async def refresh_rules_cache(language: str = DEFAULT_LANGUAGE) -> None:
if language in _cached_rules:
del _cached_rules[language]
await get_rules_from_db(language)
def clear_rules_cache() -> None:
_cached_rules.clear()
def reload_locales() -> None:
clear_locale_cache()