Merge pull request #33 from yazhog/codex/refactor-localization-for-configurable-default-language

Use configurable default language
This commit is contained in:
yazhog
2025-09-19 10:58:07 +03:00
committed by GitHub
5 changed files with 181 additions and 40 deletions

View File

@@ -267,6 +267,7 @@ MAINTENANCE_AUTO_ENABLE=true
MAINTENANCE_MESSAGE=Ведутся технические работы. Сервис временно недоступен. Попробуйте позже.
# ===== ЛОКАЛИЗАЦИЯ =====
# Укажите язык из AVAILABLE_LANGUAGES. При некорректном значении используется ru.
DEFAULT_LANGUAGE=ru
AVAILABLE_LANGUAGES=ru,en

View File

@@ -478,6 +478,7 @@ MAINTENANCE_AUTO_ENABLE=true
MAINTENANCE_MESSAGE=Ведутся технические работы. Сервис временно недоступен. Попробуйте позже.
# ===== ЛОКАЛИЗАЦИЯ =====
# Укажите язык из AVAILABLE_LANGUAGES. При некорректном значении используется ru.
DEFAULT_LANGUAGE=ru
AVAILABLE_LANGUAGES=ru,en

View File

@@ -13,7 +13,7 @@ import logging
logger = logging.getLogger(__name__)
def get_rules_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
def get_rules_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
texts = get_texts(language)
return InlineKeyboardMarkup(inline_keyboard=[
[
@@ -59,7 +59,7 @@ def get_post_registration_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKe
def get_main_menu_keyboard(
language: str = "ru",
language: str = DEFAULT_LANGUAGE,
is_admin: bool = False,
has_had_paid_subscription: bool = False,
has_active_subscription: bool = False,
@@ -171,14 +171,14 @@ def get_main_menu_keyboard(
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_back_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
def get_back_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
texts = get_texts(language)
return InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=texts.BACK, callback_data="back_to_menu")]
])
def get_insufficient_balance_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
def get_insufficient_balance_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
texts = get_texts(language)
return InlineKeyboardMarkup(inline_keyboard=[
[
@@ -192,7 +192,7 @@ def get_insufficient_balance_keyboard(language: str = "ru") -> InlineKeyboardMar
def get_subscription_keyboard(
language: str = "ru",
language: str = DEFAULT_LANGUAGE,
has_subscription: bool = False,
is_trial: bool = False,
subscription=None
@@ -264,7 +264,7 @@ def get_subscription_keyboard(
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_trial_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
def get_trial_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
texts = get_texts(language)
return InlineKeyboardMarkup(inline_keyboard=[
[
@@ -274,7 +274,7 @@ def get_trial_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
])
def get_subscription_period_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
def get_subscription_period_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
texts = get_texts(language)
keyboard = []
@@ -305,7 +305,7 @@ def get_subscription_period_keyboard(language: str = "ru") -> InlineKeyboardMark
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_traffic_packages_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
def get_traffic_packages_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
import logging
logger = logging.getLogger(__name__)
@@ -364,7 +364,7 @@ def get_traffic_packages_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_countries_keyboard(countries: List[dict], selected: List[str], language: str = "ru") -> InlineKeyboardMarkup:
def get_countries_keyboard(countries: List[dict], selected: List[str], language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
texts = get_texts(language)
keyboard = []
@@ -402,7 +402,7 @@ def get_countries_keyboard(countries: List[dict], selected: List[str], language:
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_devices_keyboard(current: int, language: str = "ru") -> InlineKeyboardMarkup:
def get_devices_keyboard(current: int, language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
texts = get_texts(language)
keyboard = []
@@ -447,7 +447,7 @@ def _get_device_declension(count: int) -> str:
else:
return "устройств"
def get_subscription_confirm_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
def get_subscription_confirm_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
texts = get_texts(language)
return InlineKeyboardMarkup(inline_keyboard=[
[
@@ -457,7 +457,7 @@ def get_subscription_confirm_keyboard(language: str = "ru") -> InlineKeyboardMar
])
def get_balance_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
def get_balance_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
texts = get_texts(language)
keyboard = [
@@ -473,7 +473,7 @@ def get_balance_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_payment_methods_keyboard(amount_kopeks: int, language: str = "ru") -> InlineKeyboardMarkup:
def get_payment_methods_keyboard(amount_kopeks: int, language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
texts = get_texts(language)
keyboard = []
@@ -543,7 +543,7 @@ def get_yookassa_payment_keyboard(
payment_id: str,
amount_kopeks: int,
confirmation_url: str,
language: str = "ru"
language: str = DEFAULT_LANGUAGE
) -> InlineKeyboardMarkup:
texts = get_texts(language)
return InlineKeyboardMarkup(inline_keyboard=[
@@ -567,7 +567,7 @@ def get_yookassa_payment_keyboard(
]
])
def get_autopay_notification_keyboard(subscription_id: int, language: str = "ru") -> InlineKeyboardMarkup:
def get_autopay_notification_keyboard(subscription_id: int, language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
texts = get_texts(language)
return InlineKeyboardMarkup(inline_keyboard=[
@@ -585,7 +585,7 @@ def get_autopay_notification_keyboard(subscription_id: int, language: str = "ru"
]
])
def get_subscription_expiring_keyboard(subscription_id: int, language: str = "ru") -> InlineKeyboardMarkup:
def get_subscription_expiring_keyboard(subscription_id: int, language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
texts = get_texts(language)
return InlineKeyboardMarkup(inline_keyboard=[
@@ -609,7 +609,7 @@ def get_subscription_expiring_keyboard(subscription_id: int, language: str = "ru
]
])
def get_referral_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
def get_referral_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
texts = get_texts(language)
keyboard = [
@@ -648,7 +648,7 @@ def get_referral_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_support_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
def get_support_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
texts = get_texts(language)
return InlineKeyboardMarkup(inline_keyboard=[
[
@@ -664,7 +664,7 @@ def get_pagination_keyboard(
current_page: int,
total_pages: int,
callback_prefix: str,
language: str = "ru"
language: str = DEFAULT_LANGUAGE
) -> List[List[InlineKeyboardButton]]:
texts = get_texts(language)
keyboard = []
@@ -696,7 +696,7 @@ def get_pagination_keyboard(
def get_confirmation_keyboard(
confirm_data: str,
cancel_data: str = "cancel",
language: str = "ru"
language: str = DEFAULT_LANGUAGE
) -> InlineKeyboardMarkup:
texts = get_texts(language)
return InlineKeyboardMarkup(inline_keyboard=[
@@ -707,7 +707,7 @@ def get_confirmation_keyboard(
])
def get_autopay_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
def get_autopay_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
texts = get_texts(language)
return InlineKeyboardMarkup(inline_keyboard=[
[
@@ -723,7 +723,7 @@ def get_autopay_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
])
def get_autopay_days_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
def get_autopay_days_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
texts = get_texts(language)
keyboard = []
@@ -752,7 +752,7 @@ def _get_days_suffix(days: int) -> str:
def get_extend_subscription_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
def get_extend_subscription_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
texts = get_texts(language)
keyboard = []
@@ -775,7 +775,7 @@ def get_extend_subscription_keyboard(language: str = "ru") -> InlineKeyboardMark
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_add_traffic_keyboard(language: str = "ru", subscription_end_date: datetime = None) -> InlineKeyboardMarkup:
def get_add_traffic_keyboard(language: str = DEFAULT_LANGUAGE, subscription_end_date: datetime = None) -> InlineKeyboardMarkup:
from app.utils.pricing_utils import get_remaining_months
from app.config import settings
texts = get_texts(language)
@@ -833,7 +833,7 @@ def get_add_traffic_keyboard(language: str = "ru", subscription_end_date: dateti
return InlineKeyboardMarkup(inline_keyboard=buttons)
def get_change_devices_keyboard(current_devices: int, language: str = "ru", subscription_end_date: datetime = None) -> InlineKeyboardMarkup:
def get_change_devices_keyboard(current_devices: int, language: str = DEFAULT_LANGUAGE, subscription_end_date: datetime = None) -> InlineKeyboardMarkup:
from app.utils.pricing_utils import get_remaining_months
from app.config import settings
texts = get_texts(language)
@@ -902,7 +902,7 @@ def get_change_devices_keyboard(current_devices: int, language: str = "ru", subs
return InlineKeyboardMarkup(inline_keyboard=buttons)
def get_confirm_change_devices_keyboard(new_devices_count: int, price: int, language: str = "ru") -> InlineKeyboardMarkup:
def get_confirm_change_devices_keyboard(new_devices_count: int, price: int, language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
texts = get_texts(language)
return InlineKeyboardMarkup(inline_keyboard=[
@@ -921,7 +921,7 @@ def get_confirm_change_devices_keyboard(new_devices_count: int, price: int, lang
])
def get_reset_traffic_confirm_keyboard(price_kopeks: int, language: str = "ru") -> InlineKeyboardMarkup:
def get_reset_traffic_confirm_keyboard(price_kopeks: int, language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
from app.config import settings
if settings.is_traffic_fixed():
@@ -947,7 +947,7 @@ def get_manage_countries_keyboard(
countries: List[dict],
selected: List[str],
current_subscription_countries: List[str],
language: str = "ru",
language: str = DEFAULT_LANGUAGE,
subscription_end_date: datetime = None
) -> InlineKeyboardMarkup:
from app.utils.pricing_utils import get_remaining_months
@@ -1014,7 +1014,7 @@ def get_manage_countries_keyboard(
return InlineKeyboardMarkup(inline_keyboard=buttons)
def get_device_selection_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
def get_device_selection_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
from app.config import settings
texts = get_texts(language)
@@ -1047,7 +1047,7 @@ def get_device_selection_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
def get_connection_guide_keyboard(
subscription_url: str,
app: dict,
language: str = "ru"
language: str = DEFAULT_LANGUAGE
) -> InlineKeyboardMarkup:
from app.handlers.subscription import create_deep_link
texts = get_texts(language)
@@ -1087,7 +1087,7 @@ def get_connection_guide_keyboard(
def get_app_selection_keyboard(
device_type: str,
apps: list,
language: str = "ru"
language: str = DEFAULT_LANGUAGE
) -> InlineKeyboardMarkup:
texts = get_texts(language)
keyboard = []
@@ -1120,7 +1120,7 @@ def get_specific_app_keyboard(
subscription_url: str,
app: dict,
device_type: str,
language: str = "ru"
language: str = DEFAULT_LANGUAGE
) -> InlineKeyboardMarkup:
from app.handlers.subscription import create_deep_link
texts = get_texts(language)
@@ -1194,7 +1194,7 @@ def get_cryptobot_payment_keyboard(
amount_usd: float,
asset: str,
bot_invoice_url: str,
language: str = "ru"
language: str = DEFAULT_LANGUAGE
) -> InlineKeyboardMarkup:
texts = get_texts(language)
return InlineKeyboardMarkup(inline_keyboard=[
@@ -1221,7 +1221,7 @@ def get_cryptobot_payment_keyboard(
def get_devices_management_keyboard(
devices: List[dict],
pagination,
language: str = "ru"
language: str = DEFAULT_LANGUAGE
) -> InlineKeyboardMarkup:
texts = get_texts(language)
@@ -1287,7 +1287,7 @@ def get_devices_management_keyboard(
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_updated_subscription_settings_keyboard(language: str = "ru", show_countries_management: bool = True) -> InlineKeyboardMarkup:
def get_updated_subscription_settings_keyboard(language: str = DEFAULT_LANGUAGE, show_countries_management: bool = True) -> InlineKeyboardMarkup:
from app.config import settings
texts = get_texts(language)
@@ -1322,7 +1322,7 @@ def get_updated_subscription_settings_keyboard(language: str = "ru", show_countr
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_device_reset_confirm_keyboard(device_info: str, device_index: int, page: int, language: str = "ru") -> InlineKeyboardMarkup:
def get_device_reset_confirm_keyboard(device_info: str, device_index: int, page: int, language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
texts = get_texts(language)
return InlineKeyboardMarkup(inline_keyboard=[
@@ -1341,7 +1341,7 @@ def get_device_reset_confirm_keyboard(device_info: str, device_index: int, page:
])
def get_device_management_help_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
def get_device_management_help_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
texts = get_texts(language)
return InlineKeyboardMarkup(inline_keyboard=[

View File

@@ -9,14 +9,22 @@ from typing import Any, Dict
from app.config import settings
DEFAULT_LANGUAGE = "ru"
_logger = logging.getLogger(__name__)
_FALLBACK_LANGUAGE = "ru"
_BASE_DIR = Path(__file__).resolve().parent
_DEFAULT_LOCALES_DIR = _BASE_DIR / "locales"
def _normalize_language_code(value: Any) -> str:
if isinstance(value, str):
return value.strip().lower()
if value is None:
return ""
return str(value).strip().lower()
def _resolve_user_locales_dir() -> Path:
path = Path(settings.LOCALES_PATH).expanduser()
if not path.is_absolute():
@@ -24,6 +32,106 @@ def _resolve_user_locales_dir() -> Path:
return path
def _locale_file_exists(language: str) -> bool:
code = _normalize_language_code(language)
if not code:
return False
default_candidate = _DEFAULT_LOCALES_DIR / f"{code}.json"
if default_candidate.exists():
return True
user_dir = _resolve_user_locales_dir()
for extension in (".json", ".yml", ".yaml"):
if (user_dir / f"{code}{extension}").exists():
return True
return False
def _select_fallback_language(available_map: Dict[str, str]) -> str:
candidates = []
if _FALLBACK_LANGUAGE:
candidates.append(_FALLBACK_LANGUAGE)
candidates.extend(available_map.values())
seen = set()
for candidate in candidates:
normalized = _normalize_language_code(candidate)
if not normalized or normalized in seen:
continue
seen.add(normalized)
if normalized in available_map:
return available_map[normalized]
if _locale_file_exists(normalized):
return normalized
if _FALLBACK_LANGUAGE and _locale_file_exists(_FALLBACK_LANGUAGE):
return _FALLBACK_LANGUAGE
return _FALLBACK_LANGUAGE or "ru"
def _determine_default_language() -> str:
try:
raw_default = settings.DEFAULT_LANGUAGE
except AttributeError:
raw_default = None
configured = raw_default.strip() if isinstance(raw_default, str) else ""
try:
available_languages = settings.get_available_languages()
except Exception as error: # pragma: no cover - defensive logging
_logger.warning("Failed to load available languages from settings: %s", error)
available_languages = []
available_map = {
_normalize_language_code(lang): lang.strip()
for lang in available_languages
if isinstance(lang, str) and lang.strip()
}
if configured:
normalized_configured = _normalize_language_code(configured)
if normalized_configured in available_map:
return available_map[normalized_configured]
if not available_map and _locale_file_exists(normalized_configured):
return normalized_configured
if _locale_file_exists(normalized_configured):
_logger.warning(
"Configured default language '%s' is not listed in AVAILABLE_LANGUAGES. Falling back to '%s'.",
configured,
_FALLBACK_LANGUAGE,
)
else:
_logger.warning(
"Configured default language '%s' is not available. Falling back to '%s'.",
configured,
_FALLBACK_LANGUAGE,
)
else:
_logger.debug("DEFAULT_LANGUAGE is not set. Falling back to '%s'.", _FALLBACK_LANGUAGE)
fallback_language = _select_fallback_language(available_map)
if _normalize_language_code(fallback_language) != _normalize_language_code(_FALLBACK_LANGUAGE):
_logger.warning(
"Fallback language '%s' is not available. Using '%s' instead.",
_FALLBACK_LANGUAGE,
fallback_language,
)
return fallback_language or _FALLBACK_LANGUAGE
DEFAULT_LANGUAGE = _determine_default_language()
def _normalize_key(raw_key: Any) -> str:
key = str(raw_key).strip().replace(" ", "_")
return key.upper()

View File

@@ -17,7 +17,9 @@ _cached_rules: Dict[str, str] = {}
def _build_dynamic_values(language: str) -> Dict[str, Any]:
if language == "ru":
language_code = (language or DEFAULT_LANGUAGE).split("-")[0].lower()
if language_code == "ru":
return {
"PERIOD_14_DAYS": f"📅 14 дней - {settings.format_price(settings.PRICE_14_DAYS)}",
"PERIOD_30_DAYS": f"📅 30 дней - {settings.format_price(settings.PRICE_30_DAYS)}",
@@ -44,6 +46,35 @@ def _build_dynamic_values(language: str) -> Dict[str, Any]:
"⏰ Время ответа: обычно в течение 1-2 часов\n"
),
}
if language_code == "en":
return {
"PERIOD_14_DAYS": f"📅 14 days - {settings.format_price(settings.PRICE_14_DAYS)}",
"PERIOD_30_DAYS": f"📅 30 days - {settings.format_price(settings.PRICE_30_DAYS)}",
"PERIOD_60_DAYS": f"📅 60 days - {settings.format_price(settings.PRICE_60_DAYS)}",
"PERIOD_90_DAYS": f"📅 90 days - {settings.format_price(settings.PRICE_90_DAYS)}",
"PERIOD_180_DAYS": f"📅 180 days - {settings.format_price(settings.PRICE_180_DAYS)}",
"PERIOD_360_DAYS": f"📅 360 days - {settings.format_price(settings.PRICE_360_DAYS)}",
"TRAFFIC_5GB": f"📊 5 GB - {settings.format_price(settings.PRICE_TRAFFIC_5GB)}",
"TRAFFIC_10GB": f"📊 10 GB - {settings.format_price(settings.PRICE_TRAFFIC_10GB)}",
"TRAFFIC_25GB": f"📊 25 GB - {settings.format_price(settings.PRICE_TRAFFIC_25GB)}",
"TRAFFIC_50GB": f"📊 50 GB - {settings.format_price(settings.PRICE_TRAFFIC_50GB)}",
"TRAFFIC_100GB": f"📊 100 GB - {settings.format_price(settings.PRICE_TRAFFIC_100GB)}",
"TRAFFIC_250GB": f"📊 250 GB - {settings.format_price(settings.PRICE_TRAFFIC_250GB)}",
"TRAFFIC_UNLIMITED": f"📊 Unlimited - {settings.format_price(settings.PRICE_TRAFFIC_UNLIMITED)}",
"SUPPORT_INFO": (
"\n🛠️ <b>Technical support</b>\n\n"
"For any questions contact our support:\n\n"
f"👤 {settings.SUPPORT_USERNAME}\n\n"
"We can help with:\n"
"• Connection setup\n"
"• Troubleshooting issues\n"
"• Payment questions\n"
"• Other requests\n\n"
"⏰ Response time: usually within 1-2 hours\n"
),
}
return {}