From 8d25e9b760197247d0158f53e344c725db28d041 Mon Sep 17 00:00:00 2001 From: yazhog Date: Fri, 19 Sep 2025 11:43:54 +0300 Subject: [PATCH] Localize stars payment and common handler texts --- app/handlers/common.py | 14 +- app/handlers/menu.py | 29 ++- app/handlers/promocode.py | 5 +- app/handlers/stars_payments.py | 100 +++++-- app/handlers/start.py | 433 +++++++++++++++++++------------ app/localization/locales/en.json | 33 ++- app/localization/locales/ru.json | 24 +- 7 files changed, 421 insertions(+), 217 deletions(-) diff --git a/app/handlers/common.py b/app/handlers/common.py index daa32376..62579d8e 100644 --- a/app/handlers/common.py +++ b/app/handlers/common.py @@ -18,8 +18,11 @@ async def handle_unknown_callback( texts = get_texts(db_user.language if db_user else "ru") await callback.answer( - "❓ Неизвестная команда. Попробуйте ещё раз.", - show_alert=True + texts.t( + "UNKNOWN_CALLBACK_ALERT", + "❓ Неизвестная команда. Попробуйте ещё раз.", + ), + show_alert=True, ) logger.warning(f"Неизвестный callback: {callback.data} от пользователя {callback.from_user.id}") @@ -49,8 +52,11 @@ async def handle_unknown_message( texts = get_texts(db_user.language if db_user else "ru") await message.answer( - "❓ Не понимаю эту команду. Используйте кнопки меню.", - reply_markup=get_back_keyboard(db_user.language if db_user else "ru") + texts.t( + "UNKNOWN_COMMAND_MESSAGE", + "❓ Не понимаю эту команду. Используйте кнопки меню.", + ), + reply_markup=get_back_keyboard(db_user.language if db_user else "ru"), ) diff --git a/app/handlers/menu.py b/app/handlers/menu.py index 77c2b42f..81d495f9 100644 --- a/app/handlers/menu.py +++ b/app/handlers/menu.py @@ -176,25 +176,34 @@ def _get_subscription_status(user: User, texts) -> str: "💎 Активна\n⚠️ истекает сегодня!", ) + +def _insert_random_message(base_text: str, random_message: str, action_prompt: str) -> str: + if not random_message: + return base_text + + prompt = action_prompt or "" + if prompt and prompt in base_text: + parts = base_text.split(prompt, 1) + if len(parts) == 2: + return f"{parts[0]}\n{random_message}\n\n{prompt}{parts[1]}" + return base_text.replace(prompt, f"\n{random_message}\n\n{prompt}", 1) + + return f"{base_text}\n\n{random_message}" + + async def get_main_menu_text(user, texts, db: AsyncSession): - + base_text = texts.MAIN_MENU.format( user_name=user.full_name, subscription_status=_get_subscription_status(user, texts) ) + action_prompt = texts.t("MAIN_MENU_ACTION_PROMPT", "Выберите действие:") + try: random_message = await get_random_active_message(db) if random_message: - if "Выберите действие:" in base_text: - parts = base_text.split("Выберите действие:") - if len(parts) == 2: - return f"{parts[0]}\n{random_message}\n\nВыберите действие:{parts[1]}" - - if "Выберите действие:" in base_text: - return base_text.replace("Выберите действие:", f"\n{random_message}\n\nВыберите действие:") - else: - return f"{base_text}\n\n{random_message}" + return _insert_random_message(base_text, random_message, action_prompt) except Exception as e: logger.error(f"Ошибка получения случайного сообщения: {e}") diff --git a/app/handlers/promocode.py b/app/handlers/promocode.py index 93447780..62b3bc8d 100644 --- a/app/handlers/promocode.py +++ b/app/handlers/promocode.py @@ -44,7 +44,10 @@ async def process_promocode( if not code: await message.answer( - "❌ Введите корректный промокод", + texts.t( + "PROMOCODE_EMPTY_INPUT", + "❌ Введите корректный промокод", + ), reply_markup=get_back_keyboard(db_user.language) ) return diff --git a/app/handlers/stars_payments.py b/app/handlers/stars_payments.py index db169b32..6d1e84e7 100644 --- a/app/handlers/stars_payments.py +++ b/app/handlers/stars_payments.py @@ -7,23 +7,29 @@ from app.database.models import User from app.services.payment_service import PaymentService from app.external.telegram_stars import TelegramStarsService from app.database.crud.user import get_user_by_telegram_id +from app.localization.loader import DEFAULT_LANGUAGE from app.localization.texts import get_texts logger = logging.getLogger(__name__) async def handle_pre_checkout_query(query: types.PreCheckoutQuery): + texts = get_texts(DEFAULT_LANGUAGE) + try: logger.info(f"📋 Pre-checkout query от {query.from_user.id}: {query.total_amount} XTR, payload: {query.invoice_payload}") - + if not query.invoice_payload or not query.invoice_payload.startswith("balance_"): logger.warning(f"Невалидный payload: {query.invoice_payload}") await query.answer( ok=False, - error_message="Ошибка валидации платежа. Попробуйте еще раз." + error_message=texts.t( + "STARS_PRECHECK_INVALID_PAYLOAD", + "Ошибка валидации платежа. Попробуйте еще раз.", + ), ) return - + try: from app.database.database import get_db async for db in get_db(): @@ -32,26 +38,36 @@ async def handle_pre_checkout_query(query: types.PreCheckoutQuery): logger.warning(f"Пользователь {query.from_user.id} не найден в БД") await query.answer( ok=False, - error_message="Пользователь не найден. Обратитесь в поддержку." + error_message=texts.t( + "STARS_PRECHECK_USER_NOT_FOUND", + "Пользователь не найден. Обратитесь в поддержку.", + ), ) return - break + texts = get_texts(user.language or DEFAULT_LANGUAGE) + break except Exception as db_error: logger.error(f"Ошибка подключения к БД в pre_checkout_query: {db_error}") await query.answer( ok=False, - error_message="Техническая ошибка. Попробуйте позже." + error_message=texts.t( + "STARS_PRECHECK_TECHNICAL_ERROR", + "Техническая ошибка. Попробуйте позже.", + ), ) return - + await query.answer(ok=True) logger.info(f"✅ Pre-checkout одобрен для пользователя {query.from_user.id}") - + except Exception as e: logger.error(f"Ошибка в pre_checkout_query: {e}", exc_info=True) await query.answer( ok=False, - error_message="Техническая ошибка. Попробуйте позже." + error_message=texts.t( + "STARS_PRECHECK_TECHNICAL_ERROR", + "Техническая ошибка. Попробуйте позже.", + ), ) @@ -60,25 +76,32 @@ async def handle_successful_payment( db: AsyncSession, **kwargs ): + texts = get_texts(DEFAULT_LANGUAGE) + try: payment = message.successful_payment user_id = message.from_user.id - + logger.info( f"💳 Успешный Stars платеж от {user_id}: " f"{payment.total_amount} XTR, " f"payload: {payment.invoice_payload}, " f"charge_id: {payment.telegram_payment_charge_id}" ) - + user = await get_user_by_telegram_id(db, user_id) + texts = get_texts(user.language if user and user.language else DEFAULT_LANGUAGE) + if not user: logger.error(f"Пользователь {user_id} не найден при обработке Stars платежа") await message.answer( - "❌ Ошибка: пользователь не найден. Обратитесь в поддержку." + texts.t( + "STARS_PAYMENT_USER_NOT_FOUND", + "❌ Ошибка: пользователь не найден. Обратитесь в поддержку.", + ) ) return - + payment_service = PaymentService(message.bot) success = await payment_service.process_stars_payment( db=db, @@ -91,8 +114,6 @@ async def handle_successful_payment( if success: rubles_amount = TelegramStarsService.calculate_rubles_from_stars(payment.total_amount) - user_language = user.language if user else "ru" - texts = get_texts(user_language) has_active_subscription = ( user and user.subscription @@ -114,17 +135,36 @@ async def handle_successful_payment( keyboard = InlineKeyboardMarkup( inline_keyboard=[ [first_button], - [InlineKeyboardButton(text="💰 Мой баланс", callback_data="menu_balance")], - [InlineKeyboardButton(text="🏠 Главное меню", callback_data="back_to_menu")], + [ + InlineKeyboardButton( + text=texts.MY_BALANCE_BUTTON, + callback_data="menu_balance", + ) + ], + [ + InlineKeyboardButton( + text=texts.t("MAIN_MENU_BUTTON", "🏠 Главное меню"), + callback_data="back_to_menu", + ) + ], ] ) + transaction_id_short = payment.telegram_payment_charge_id[:8] + await message.answer( - f"🎉 Платеж успешно обработан!\n\n" - f"⭐ Потрачено звезд: {payment.total_amount}\n" - f"💰 Зачислено на баланс: {int(rubles_amount)} ₽\n" - f"🆔 ID транзакции: {payment.telegram_payment_charge_id[:8]}...\n\n" - f"Спасибо за пополнение! 🚀", + texts.t( + "STARS_PAYMENT_SUCCESS", + "🎉 Платеж успешно обработан!\n\n" + "⭐ Потрачено звезд: {stars_spent}\n" + "💰 Зачислено на баланс: {amount} ₽\n" + "🆔 ID транзакции: {transaction_id}...\n\n" + "Спасибо за пополнение! 🚀", + ).format( + stars_spent=payment.total_amount, + amount=int(rubles_amount), + transaction_id=transaction_id_short, + ), parse_mode="HTML", reply_markup=keyboard, ) @@ -136,15 +176,21 @@ async def handle_successful_payment( else: logger.error(f"Ошибка обработки Stars платежа для пользователя {user.id}") await message.answer( - "❌ Произошла ошибка при зачислении средств. " - "Обратитесь в поддержку, платеж будет проверен вручную." + texts.t( + "STARS_PAYMENT_ENROLLMENT_ERROR", + "❌ Произошла ошибка при зачислении средств. " + "Обратитесь в поддержку, платеж будет проверен вручную.", + ) ) - + except Exception as e: logger.error(f"Ошибка в successful_payment: {e}", exc_info=True) await message.answer( - "❌ Техническая ошибка при обработке платежа. " - "Обратитесь в поддержку для решения проблемы." + texts.t( + "STARS_PAYMENT_PROCESSING_ERROR", + "❌ Техническая ошибка при обработке платежа. " + "Обратитесь в поддержку для решения проблемы.", + ) ) diff --git a/app/handlers/start.py b/app/handlers/start.py index 0004925c..2a0dbb57 100644 --- a/app/handlers/start.py +++ b/app/handlers/start.py @@ -44,26 +44,34 @@ async def handle_potential_referral_code( user = await get_user_by_telegram_id(db, message.from_user.id) if user and user.status == UserStatus.ACTIVE.value: return False - + + data = await state.get_data() or {} + language = ( + data.get("language") + or (getattr(user, "language", None) if user else None) + or "ru" + ) + texts = get_texts(language) + potential_code = message.text.strip() if len(potential_code) < 4 or len(potential_code) > 20: return False - + referrer = await get_user_by_referral_code(db, potential_code) if not referrer: - await message.answer( + await message.answer(texts.t( + "REFERRAL_CODE_INVALID_HELP", "❌ Неверный реферальный код.\n\n" "💡 Если у вас есть реферальный код, убедитесь что он введен правильно.\n" - "⏭️ Для продолжения регистрации без реферального кода используйте команду /start" - ) - return True - - data = await state.get_data() or {} + "⏭️ Для продолжения регистрации без реферального кода используйте команду /start", + )) + return True + data['referral_code'] = potential_code data['referrer_id'] = referrer.id await state.set_data(data) - - await message.answer("✅ Реферальный код принят!") + + await message.answer(texts.t("REFERRAL_CODE_ACCEPTED", "✅ Реферальный код принят!")) logger.info(f"✅ Реферальный код {potential_code} применен для пользователя {message.from_user.id}") if current_state != RegistrationStates.waiting_for_referral_code.state: @@ -132,7 +140,12 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession, texts = get_texts(user.language) if referral_code and not user.referred_by_id: - await message.answer("ℹ️ Вы уже зарегистрированы в системе. Реферальная ссылка не может быть применена.") + await message.answer( + texts.t( + "ALREADY_REGISTERED_REFERRAL", + "ℹ️ Вы уже зарегистрированы в системе. Реферальная ссылка не может быть применена.", + ) + ) has_active_subscription = user.subscription is not None subscription_is_active = False @@ -242,7 +255,10 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession, else: try: await message.answer( - "У вас есть реферальный код? Введите его или нажмите 'Пропустить'", + texts.t( + "REFERRAL_CODE_QUESTION", + "У вас есть реферальный код? Введите его или нажмите 'Пропустить'", + ), reply_markup=get_referral_code_keyboard(language) ) await state.set_state(RegistrationStates.waiting_for_referral_code) @@ -276,11 +292,14 @@ async def process_rules_accept( current_state = await state.get_state() logger.info(f"📊 Текущее состояние: {current_state}") + language = "ru" + texts = get_texts(language) + try: await callback.answer() - - data = await state.get_data() - language = data.get('language', 'ru') + + data = await state.get_data() or {} + language = data.get('language', language) texts = get_texts(language) if callback.data == 'rules_accept': @@ -293,10 +312,13 @@ async def process_rules_accept( logger.warning(f"⚠️ Не удалось удалить сообщение с правилами: {e}") try: await callback.message.edit_text( - "✅ Правила приняты! Завершаем регистрацию...", + texts.t( + "RULES_ACCEPTED_PROCESSING", + "✅ Правила приняты! Завершаем регистрацию...", + ), reply_markup=None ) - except: + except Exception: pass if data.get('referral_code'): @@ -316,7 +338,10 @@ async def process_rules_accept( else: try: await callback.message.answer( - "У вас есть реферальный код? Введите его или нажмите 'Пропустить'", + texts.t( + "REFERRAL_CODE_QUESTION", + "У вас есть реферальный код? Введите его или нажмите 'Пропустить'", + ), reply_markup=get_referral_code_keyboard(language) ) await state.set_state(RegistrationStates.waiting_for_referral_code) @@ -328,9 +353,12 @@ async def process_rules_accept( else: logger.info(f"❌ Правила отклонены пользователем {callback.from_user.id}") + rules_required_text = texts.t( + "RULES_REQUIRED", + "Для использования бота необходимо принять правила сервиса.", + ) + try: - rules_required_text = getattr(texts, 'RULES_REQUIRED', - "Для использования бота необходимо принять правила сервиса.") await callback.message.edit_text( rules_required_text, reply_markup=get_rules_keyboard(language) @@ -338,7 +366,7 @@ async def process_rules_accept( except Exception as e: logger.error(f"Ошибка при показе сообщения об отклонении правил: {e}") await callback.message.edit_text( - "Для использования бота необходимо принять правила сервиса.", + rules_required_text, reply_markup=get_rules_keyboard(language) ) @@ -346,13 +374,20 @@ async def process_rules_accept( except Exception as e: logger.error(f"❌ Ошибка обработки правил: {e}", exc_info=True) - await callback.answer("❌ Произошла ошибка. Попробуйте еще раз.", show_alert=True) - + await callback.answer( + texts.t("ERROR_TRY_AGAIN", "❌ Произошла ошибка. Попробуйте еще раз."), + show_alert=True, + ) + try: - data = await state.get_data() - language = data.get('language', 'ru') + data = await state.get_data() or {} + language = data.get('language', language) + texts = get_texts(language) await callback.message.answer( - "Произошла ошибка. Попробуйте принять правила еще раз:", + texts.t( + "ERROR_RULES_RETRY", + "Произошла ошибка. Попробуйте принять правила еще раз:", + ), reply_markup=get_rules_keyboard(language) ) await state.set_state(RegistrationStates.waiting_for_rules_accept) @@ -368,20 +403,20 @@ async def process_referral_code_input( logger.info(f"🎫 REFERRAL: Обработка реферального кода: {message.text}") - data = await state.get_data() + data = await state.get_data() or {} language = data.get('language', 'ru') texts = get_texts(language) - + referral_code = message.text.strip() - + referrer = await get_user_by_referral_code(db, referral_code) if referrer: data['referrer_id'] = referrer.id await state.set_data(data) - await message.answer("✅ Реферальный код применен!") + await message.answer(texts.t("REFERRAL_CODE_ACCEPTED", "✅ Реферальный код принят!")) logger.info(f"✅ Реферальный код применен") else: - await message.answer("❌ Неверный реферальный код") + await message.answer(texts.t("REFERRAL_CODE_INVALID", "❌ Неверный реферальный код")) logger.info(f"❌ Неверный реферальный код") return @@ -389,14 +424,18 @@ async def process_referral_code_input( async def process_referral_code_skip( - callback: types.CallbackQuery, - state: FSMContext, + callback: types.CallbackQuery, + state: FSMContext, db: AsyncSession ): - + logger.info(f"⭐️ SKIP: Пропуск реферального кода от пользователя {callback.from_user.id}") await callback.answer() - + + data = await state.get_data() or {} + language = data.get('language', 'ru') + texts = get_texts(language) + try: await callback.message.delete() logger.info(f"🗑️ Сообщение с вопросом о реферальном коде удалено") @@ -404,7 +443,7 @@ async def process_referral_code_skip( logger.warning(f"⚠️ Не удалось удалить сообщение с вопросом о реферальном коде: {e}") try: await callback.message.edit_text( - "✅ Завершаем регистрацию...", + texts.t("REGISTRATION_COMPLETING", "✅ Завершаем регистрацию..."), reply_markup=None ) except: @@ -429,9 +468,14 @@ async def complete_registration_from_callback( logger.warning(f"⚠️ Пользователь {callback.from_user.id} уже активен! Показываем главное меню.") texts = get_texts(existing_user.language) - data = await state.get_data() + data = await state.get_data() or {} if data.get('referral_code') and not existing_user.referred_by_id: - await callback.message.answer("ℹ️ Вы уже зарегистрированы в системе. Реферальная ссылка не может быть применена.") + await callback.message.answer( + texts.t( + "ALREADY_REGISTERED_REFERRAL", + "ℹ️ Вы уже зарегистрированы в системе. Реферальная ссылка не может быть применена.", + ) + ) await db.refresh(existing_user, ['subscription']) @@ -459,12 +503,17 @@ async def complete_registration_from_callback( ) except Exception as e: logger.error(f"Ошибка при показе главного меню существующему пользователю: {e}") - await callback.message.answer(f"Добро пожаловать, {existing_user.full_name}!") + await callback.message.answer( + texts.t( + "WELCOME_FALLBACK", + "Добро пожаловать, {user_name}!", + ).format(user_name=existing_user.full_name) + ) await state.clear() return - data = await state.get_data() + data = await state.get_data() or {} language = data.get('language', 'ru') texts = get_texts(language) @@ -576,7 +625,12 @@ async def complete_registration_from_callback( logger.info(f"✅ Главное меню показано пользователю {user.telegram_id}") except Exception as e: logger.error(f"Ошибка при показе главного меню: {e}") - await callback.message.answer(f"Добро пожаловать, {user.full_name}!") + await callback.message.answer( + texts.t( + "WELCOME_FALLBACK", + "Добро пожаловать, {user_name}!", + ).format(user_name=user.full_name) + ) logger.info(f"✅ Регистрация завершена для пользователя: {user.telegram_id}") @@ -594,9 +648,14 @@ async def complete_registration( logger.warning(f"⚠️ Пользователь {message.from_user.id} уже активен! Показываем главное меню.") texts = get_texts(existing_user.language) - data = await state.get_data() + data = await state.get_data() or {} if data.get('referral_code') and not existing_user.referred_by_id: - await message.answer("ℹ️ Вы уже зарегистрированы в системе. Реферальная ссылка не может быть применена.") + await message.answer( + texts.t( + "ALREADY_REGISTERED_REFERRAL", + "ℹ️ Вы уже зарегистрированы в системе. Реферальная ссылка не может быть применена.", + ) + ) await db.refresh(existing_user, ['subscription']) @@ -624,12 +683,17 @@ async def complete_registration( ) except Exception as e: logger.error(f"Ошибка при показе главного меню существующему пользователю: {e}") - await message.answer(f"Добро пожаловать, {existing_user.full_name}!") + await message.answer( + texts.t( + "WELCOME_FALLBACK", + "Добро пожаловать, {user_name}!", + ).format(user_name=existing_user.full_name) + ) await state.clear() return - data = await state.get_data() + data = await state.get_data() or {} language = data.get('language', 'ru') texts = get_texts(language) @@ -741,54 +805,92 @@ async def complete_registration( logger.info(f"✅ Главное меню показано пользователю {user.telegram_id}") except Exception as e: logger.error(f"Ошибка при показе главного меню: {e}") - await message.answer(f"Добро пожаловать, {user.full_name}!") + await message.answer( + texts.t( + "WELCOME_FALLBACK", + "Добро пожаловать, {user_name}!", + ).format(user_name=user.full_name) + ) logger.info(f"✅ Регистрация завершена для пользователя: {user.telegram_id}") def _get_subscription_status(user, texts): - if not user or not hasattr(user, 'subscription'): - return getattr(texts, 'SUBSCRIPTION_NONE', 'Нет активной подписки') - - if not user.subscription: - return getattr(texts, 'SUBSCRIPTION_NONE', 'Нет активной подписки') - + if not user or not hasattr(user, "subscription") or not user.subscription: + return texts.t("SUBSCRIPTION_NONE", "Нет активной подписки") + subscription = user.subscription - + from datetime import datetime + + end_date = getattr(subscription, "end_date", None) current_time = datetime.utcnow() - - if hasattr(subscription, 'end_date') and subscription.end_date <= current_time: - return f"🔴 Истекла\n📅 {subscription.end_date.strftime('%d.%m.%Y')}" - - if hasattr(subscription, 'end_date'): - days_left = (subscription.end_date - current_time).days - else: - days_left = 0 - - is_trial = getattr(subscription, 'is_trial', False) - + + if end_date and end_date <= current_time: + return texts.t( + "SUB_STATUS_EXPIRED", + "🔴 Истекла\n📅 {end_date}", + ).format(end_date=end_date.strftime('%d.%m.%Y')) + + if not end_date: + return texts.t("SUBSCRIPTION_ACTIVE", "✅ Активна") + + days_left = (end_date - current_time).days + is_trial = getattr(subscription, "is_trial", False) + if is_trial: if days_left > 1: - return f"🎁 Тестовая подписка\n📅 до {subscription.end_date.strftime('%d.%m.%Y')} ({days_left} дн.)" - elif days_left == 1: - return "🎁 Тестовая подписка\n⚠️ истекает завтра!" - else: - return "🎁 Тестовая подписка\n⚠️ истекает сегодня!" - else: - if days_left > 7: - return f"💎 Активна\n📅 до {subscription.end_date.strftime('%d.%m.%Y')} ({days_left} дн.)" - elif days_left > 1: - return f"💎 Активна\n⚠️ истекает через {days_left} дн." - elif days_left == 1: - return "💎 Активна\n⚠️ истекает завтра!" - else: - return "💎 Активна\n⚠️ истекает сегодня!" + return texts.t( + "SUB_STATUS_TRIAL_ACTIVE", + "🎁 Тестовая подписка\n📅 до {end_date} ({days} дн.)", + ).format(end_date=end_date.strftime('%d.%m.%Y'), days=days_left) + if days_left == 1: + return texts.t( + "SUB_STATUS_TRIAL_TOMORROW", + "🎁 Тестовая подписка\n⚠️ истекает завтра!", + ) + return texts.t( + "SUB_STATUS_TRIAL_TODAY", + "🎁 Тестовая подписка\n⚠️ истекает сегодня!", + ) + if days_left > 7: + return texts.t( + "SUB_STATUS_ACTIVE_LONG", + "💎 Активна\n📅 до {end_date} ({days} дн.)", + ).format(end_date=end_date.strftime('%d.%m.%Y'), days=days_left) + if days_left > 1: + return texts.t( + "SUB_STATUS_ACTIVE_FEW_DAYS", + "💎 Активна\n⚠️ истекает через {days} дн.", + ).format(days=days_left) + if days_left == 1: + return texts.t( + "SUB_STATUS_ACTIVE_TOMORROW", + "💎 Активна\n⚠️ истекает завтра!", + ) + return texts.t( + "SUB_STATUS_ACTIVE_TODAY", + "💎 Активна\n⚠️ истекает сегодня!", + ) def _get_subscription_status_simple(texts): - return getattr(texts, 'SUBSCRIPTION_NONE', 'Нет активной подписки') + return texts.t("SUBSCRIPTION_NONE", "Нет активной подписки") + + +def _insert_random_message(base_text: str, random_message: str, action_prompt: str) -> str: + if not random_message: + return base_text + + prompt = action_prompt or "" + if prompt and prompt in base_text: + parts = base_text.split(prompt, 1) + if len(parts) == 2: + return f"{parts[0]}\n{random_message}\n\n{prompt}{parts[1]}" + return base_text.replace(prompt, f"\n{random_message}\n\n{prompt}", 1) + + return f"{base_text}\n\n{random_message}" def get_referral_code_keyboard(language: str): @@ -797,59 +899,47 @@ def get_referral_code_keyboard(language: str): texts = get_texts(language) return InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton( - text="⭐️ Пропустить", + text=texts.t("REFERRAL_CODE_SKIP", "⭐️ Пропустить"), callback_data="referral_skip" )] ]) async def get_main_menu_text(user, texts, db: AsyncSession): - + base_text = texts.MAIN_MENU.format( user_name=user.full_name, subscription_status=_get_subscription_status(user, texts) ) - + + action_prompt = texts.t("MAIN_MENU_ACTION_PROMPT", "Выберите действие:") + try: random_message = await get_random_active_message(db) if random_message: - if "Выберите действие:" in base_text: - parts = base_text.split("Выберите действие:") - if len(parts) == 2: - return f"{parts[0]}\n{random_message}\n\nВыберите действие:{parts[1]}" - - if "Выберите действие:" in base_text: - return base_text.replace("Выберите действие:", f"\n{random_message}\n\nВыберите действие:") - else: - return f"{base_text}\n\n{random_message}" - + return _insert_random_message(base_text, random_message, action_prompt) + except Exception as e: logger.error(f"Ошибка получения случайного сообщения: {e}") - + return base_text async def get_main_menu_text_simple(user_name, texts, db: AsyncSession): - + base_text = texts.MAIN_MENU.format( user_name=user_name, subscription_status=_get_subscription_status_simple(texts) ) - + + action_prompt = texts.t("MAIN_MENU_ACTION_PROMPT", "Выберите действие:") + try: random_message = await get_random_active_message(db) if random_message: - if "Выберите действие:" in base_text: - parts = base_text.split("Выберите действие:") - if len(parts) == 2: - return f"{parts[0]}\n{random_message}\n\nВыберите действие:{parts[1]}" - - if "Выберите действие:" in base_text: - return base_text.replace("Выберите действие:", f"\n{random_message}\n\nВыберите действие:") - else: - return f"{base_text}\n\n{random_message}" - + return _insert_random_message(base_text, random_message, action_prompt) + except Exception as e: logger.error(f"Ошибка получения случайного сообщения: {e}") - + return base_text @@ -860,90 +950,90 @@ async def required_sub_channel_check( db: AsyncSession, db_user=None ): + language = "ru" + texts = get_texts(language) + try: + state_data = await state.get_data() or {} + + user = db_user + if not user: + user = await get_user_by_telegram_id(db, query.from_user.id) + + if user and getattr(user, "language", None): + language = user.language + elif state_data.get("language"): + language = state_data["language"] + + texts = get_texts(language) + chat_member = await bot.get_chat_member( chat_id=settings.CHANNEL_SUB_ID, user_id=query.from_user.id ) - + if chat_member.status not in [ChatMemberStatus.MEMBER, ChatMemberStatus.ADMINISTRATOR, ChatMemberStatus.CREATOR]: - return await query.answer("❌ Вы не подписались на канал!", show_alert=True) - - await query.answer("✅ Спасибо за подписку", show_alert=True) - + return await query.answer( + texts.t("CHANNEL_SUBSCRIBE_REQUIRED_ALERT", "❌ Вы не подписались на канал!"), + show_alert=True, + ) + + await query.answer( + texts.t("CHANNEL_SUBSCRIBE_THANKS", "✅ Спасибо за подписку"), + show_alert=True, + ) + try: await query.message.delete() except Exception as e: logger.warning(f"Не удалось удалить сообщение: {e}") - - user = await get_user_by_telegram_id(db, query.from_user.id) - + if user and user.status != UserStatus.DELETED.value: - from app.localization.texts import get_texts - from app.handlers.start import get_main_menu_text - from app.keyboards.inline import get_main_menu_keyboard - - texts = get_texts(user.language) - - has_active_subscription = user.subscription is not None - subscription_is_active = False - - if user.subscription: - subscription_is_active = user.subscription.is_active - + has_active_subscription = bool(user.subscription) + subscription_is_active = bool(user.subscription and user.subscription.is_active) + menu_text = await get_main_menu_text(user, texts, db) - + from app.utils.message_patch import LOGO_PATH from aiogram.types import FSInputFile - + + keyboard = get_main_menu_keyboard( + language=user.language, + is_admin=settings.is_admin(user.telegram_id), + has_had_paid_subscription=user.has_had_paid_subscription, + has_active_subscription=has_active_subscription, + subscription_is_active=subscription_is_active, + balance_kopeks=user.balance_kopeks, + subscription=user.subscription, + ) + if settings.ENABLE_LOGO_MODE: await bot.send_photo( chat_id=query.from_user.id, photo=FSInputFile(LOGO_PATH), caption=menu_text, - reply_markup=get_main_menu_keyboard( - language=user.language, - is_admin=settings.is_admin(user.telegram_id), - has_had_paid_subscription=user.has_had_paid_subscription, - has_active_subscription=has_active_subscription, - subscription_is_active=subscription_is_active, - balance_kopeks=user.balance_kopeks, - subscription=user.subscription - ), - parse_mode="HTML" + reply_markup=keyboard, + parse_mode="HTML", ) else: await bot.send_message( chat_id=query.from_user.id, text=menu_text, - reply_markup=get_main_menu_keyboard( - language=user.language, - is_admin=settings.is_admin(user.telegram_id), - has_had_paid_subscription=user.has_had_paid_subscription, - has_active_subscription=has_active_subscription, - subscription_is_active=subscription_is_active, - balance_kopeks=user.balance_kopeks, - subscription=user.subscription - ), - parse_mode="HTML" + reply_markup=keyboard, + parse_mode="HTML", ) else: - from app.localization.texts import get_texts from app.keyboards.inline import get_rules_keyboard - - language = 'ru' - texts = get_texts(language) - - data = await state.get_data() or {} - data['language'] = language - await state.set_data(data) - + + state_data['language'] = language + await state.set_data(state_data) + if settings.SKIP_RULES_ACCEPT: if settings.SKIP_REFERRAL_CODE: from app.utils.user_utils import generate_unique_referral_code - + referral_code = await generate_unique_referral_code(db, query.from_user.id) - + user = await create_user( db=db, telegram_id=query.from_user.id, @@ -951,44 +1041,45 @@ async def required_sub_channel_check( first_name=query.from_user.first_name, last_name=query.from_user.last_name, language=language, - referral_code=referral_code + referral_code=referral_code, ) - + await bot.send_message( chat_id=query.from_user.id, - text=f"Добро пожаловать, {user.full_name}!", + text=texts.t("WELCOME_FALLBACK", "Добро пожаловать, {user_name}!").format(user_name=user.full_name), ) else: await bot.send_message( chat_id=query.from_user.id, - text="У вас есть реферальный код? Введите его или нажмите 'Пропустить'", - reply_markup=get_referral_code_keyboard(language) + text=texts.t( + "REFERRAL_CODE_QUESTION", + "У вас есть реферальный код? Введите его или нажмите 'Пропустить'", + ), + reply_markup=get_referral_code_keyboard(language), ) await state.set_state(RegistrationStates.waiting_for_referral_code) else: from app.utils.message_patch import LOGO_PATH from aiogram.types import FSInputFile - + if settings.ENABLE_LOGO_MODE: await bot.send_photo( chat_id=query.from_user.id, photo=FSInputFile(LOGO_PATH), caption=texts.RULES_TEXT, - reply_markup=get_rules_keyboard(language) + reply_markup=get_rules_keyboard(language), ) else: await bot.send_message( chat_id=query.from_user.id, text=texts.RULES_TEXT, - reply_markup=get_rules_keyboard(language) + reply_markup=get_rules_keyboard(language), ) await state.set_state(RegistrationStates.waiting_for_rules_accept) - + except Exception as e: logger.error(f"Ошибка в required_sub_channel_check: {e}") - await query.answer("❌ Произошла ошибка!", show_alert=True) - - + await query.answer(f"{texts.ERROR}!", show_alert=True) def register_handlers(dp: Dispatcher): diff --git a/app/localization/locales/en.json b/app/localization/locales/en.json index 27fb1f56..af21b2ee 100644 --- a/app/localization/locales/en.json +++ b/app/localization/locales/en.json @@ -11,6 +11,8 @@ "CHANNEL_CHECK_BUTTON": "✅ I have joined", "CHANNEL_REQUIRED_TEXT": "🔒 Please join the announcement channel to access the bot, then press the button below.", "CHANNEL_SUBSCRIBE_BUTTON": "🔗 Subscribe", + "CHANNEL_SUBSCRIBE_REQUIRED_ALERT": "❌ You haven't joined the channel!", + "CHANNEL_SUBSCRIBE_THANKS": "✅ Thanks for subscribing", "CHECK_STATUS_BUTTON": "📊 Check status", "CHOOSE_ANOTHER_DEVICE": "📱 Choose another device", "CONFIRM": "? Confirm", @@ -28,11 +30,16 @@ "DEVICE_GUIDE_WINDOWS": "💻 Windows", "DISABLE_BUTTON": "❌ Disable", "ENABLE_BUTTON": "✅ Enable", - "ERROR": "? An error occurred", + "ERROR": "❌ An error occurred", + "ERROR_TRY_AGAIN": "❌ An error occurred. Please try again.", + "ERROR_RULES_RETRY": "An error occurred. Please try accepting the rules again:", "GO_TO_BALANCE_TOP_UP": "💳 Go to balance top up", "INSUFFICIENT_BALANCE": "❌ Insufficient balance. \" \n Top up {amount} and try again.", "LANGUAGE_SELECTED": "🌐 Interface language set: English", "LOADING": "? Loading...", + "MAIN_MENU": "👤 {user_name}\n\n📱 Subscription: {subscription_status}\n\nChoose an option:\n", + "MAIN_MENU_ACTION_PROMPT": "Choose an option:", + "MAIN_MENU_BUTTON": "🏠 Main menu", "MANAGE_DEVICES_BUTTON": "🔧 Manage devices", "MENU_BALANCE": "💰 Balance", "MENU_SUBSCRIPTION": "📱 Subscription", @@ -57,11 +64,18 @@ "PENDING_CANCEL_BUTTON": "⌛ Cancel", "POST_REGISTRATION_TRIAL_BUTTON": "🚀 Activate free trial 🚀", "REFERRAL_ANALYTICS_BUTTON": "📊 Analytics", + "REFERRAL_CODE_ACCEPTED": "✅ Referral code accepted!", + "REFERRAL_CODE_INVALID": "❌ Invalid referral code", + "REFERRAL_CODE_INVALID_HELP": "❌ Invalid referral code.\n\n💡 If you have a referral code, please double-check the spelling.\n⏭️ To continue without a referral code, use the /start command.", + "REFERRAL_CODE_QUESTION": "\n🤝 Do you have a friend's referral code?\n\nIf you have a promo code or referral link, enter it now to receive a bonus!\n\nSend the code or tap \"Skip\":\n", + "REFERRAL_CODE_SKIP": "⏭️ Skip", + "ALREADY_REGISTERED_REFERRAL": "ℹ️ You are already registered. A referral link cannot be applied.", "REFERRAL_LIST_BUTTON": "👥 Referral list", "RESET_ALL_DEVICES_BUTTON": "🔄 Reset all devices", "RESET_DEVICE_CONFIRM_BUTTON": "✅ Reset this device", "RESET_TRAFFIC_BUTTON": "🔄 Reset traffic", "RULES_HEADER": "📋 Service Rules", + "RULES_ACCEPTED_PROCESSING": "✅ Rules accepted! Completing registration...", "RULES_TEXT_DEFAULT": "📋 Service Usage Rules\n\n1. Do not use the service for illegal activity\n2. Avoid sharing pirated or malicious content\n3. Spam and phishing are prohibited\n4. Using the service for DDoS attacks is forbidden\n5. One account is intended for one person\n6. Refunds are provided only in exceptional cases\n7. The administration may block accounts that violate the rules\n\nBy using the service you agree to follow these rules.", "SEND_CONTACT_BUTTON": "📱 Share contact", "SEND_LOCATION_BUTTON": "📍 Share location", @@ -78,11 +92,24 @@ "SUB_STATUS_TRIAL_ACTIVE": "🎁 Trial subscription\n📅 until {end_date} ({days} days)", "SUB_STATUS_TRIAL_TODAY": "🎁 Trial subscription\n⚠️ expires today!", "SUB_STATUS_TRIAL_TOMORROW": "🎁 Trial subscription\n⚠️ expires tomorrow!", - "SUCCESS": "? Success", + "SUBSCRIPTION_ACTIVE": "✅ Active", + "SUCCESS": "✅ Success", + "REGISTRATION_COMPLETING": "✅ Completing registration...", "SWITCH_TRAFFIC_BUTTON": "🔄 Switch traffic", "TOPUP_BALANCE_BUTTON": "💳 Top up balance", "TRAFFIC_PACKAGES_NOT_CONFIGURED": "⚠️ Traffic packages are not configured", "TRIAL_ACTIVATE_BUTTON": "🎁 Activate", + "PROMOCODE_EMPTY_INPUT": "❌ Please enter a valid promo code", + "STARS_PAYMENT_ENROLLMENT_ERROR": "❌ Failed to credit funds. Please contact support; the payment will be verified manually.", + "STARS_PAYMENT_PROCESSING_ERROR": "❌ Technical error processing the payment. Please contact support for assistance.", + "STARS_PAYMENT_SUCCESS": "🎉 Payment processed successfully!\n\n⭐ Stars spent: {stars_spent}\n💰 Added to balance: {amount} ₽\n🆔 Transaction ID: {transaction_id}...\n\nThank you for topping up! 🚀", + "STARS_PAYMENT_USER_NOT_FOUND": "❌ Error: user not found. Please contact support.", + "STARS_PRECHECK_INVALID_PAYLOAD": "Payment validation error. Please try again.", + "STARS_PRECHECK_TECHNICAL_ERROR": "Technical error. Please try again later.", + "STARS_PRECHECK_USER_NOT_FOUND": "User not found. Please contact support.", + "UNKNOWN_CALLBACK_ALERT": "❓ Unknown action. Please try again.", + "UNKNOWN_COMMAND_MESSAGE": "❓ I didn't understand that command. Use the menu buttons.", "WELCOME": "\n🎉 Welcome to VPN Service!\n\nOur service provides fast and secure internet access without restrictions.\n\n🔐 Advantages:\n• High connection speed\n• Servers in different countries \n• Reliable data protection\n• 24/7 support\n\nTo get started, select interface language:\n", + "WELCOME_FALLBACK": "Welcome, {user_name}!", "YES": "? Yes" -} \ No newline at end of file +} diff --git a/app/localization/locales/ru.json b/app/localization/locales/ru.json index 339439d3..93f122a9 100644 --- a/app/localization/locales/ru.json +++ b/app/localization/locales/ru.json @@ -38,6 +38,8 @@ "CHANNEL_CHECK_BUTTON": "✅ Я подписался", "CHANNEL_REQUIRED_TEXT": "🔒 Для использования бота подпишитесь на новостной канал, а затем нажмите кнопку ниже.", "CHANNEL_SUBSCRIBE_BUTTON": "🔗 Подписаться", + "CHANNEL_SUBSCRIBE_REQUIRED_ALERT": "❌ Вы не подписались на канал!", + "CHANNEL_SUBSCRIBE_THANKS": "✅ Спасибо за подписку", "CHECK_STATUS_BUTTON": "📊 Проверить статус", "CHOOSE_ANOTHER_DEVICE": "📱 Выбрать другое устройство", "CONFIRM": "✅ Подтвердить", @@ -62,6 +64,8 @@ "DISABLE_BUTTON": "❌ Выключить", "ENABLE_BUTTON": "✅ Включить", "ERROR": "❌ Произошла ошибка", + "ERROR_TRY_AGAIN": "❌ Произошла ошибка. Попробуйте еще раз.", + "ERROR_RULES_RETRY": "Произошла ошибка. Попробуйте принять правила еще раз:", "GO_TO_BALANCE_TOP_UP": "💳 Перейти к пополнению баланса", "INSUFFICIENT_BALANCE": "❌ Недостаточно средств на балансе. \n \n Пополните баланс на {amount} и попробуйте снова.\n ", "INVALID_AMOUNT": "❌ Неверная сумма", @@ -70,6 +74,8 @@ "MAINTENANCE_MODE_ACTIVE": "\n🔧 Технические работы!\n\nСервис временно недоступен. Ведутся технические работы по улучшению качества обслуживания.\n\n⏰ Ориентировочное время завершения: неизвестно\n🔄 Попробуйте позже\n\nПриносим извинения за временные неудобства.\n", "MAINTENANCE_MODE_API_ERROR": "\n🔧 Технические работы!\n\nСервис временно недоступен из-за проблем с подключением к серверам.\n\n⏰ Мы работаем над восстановлением. Попробуйте через несколько минут.\n\n🔄 Последняя проверка: {last_check}\n", "MAIN_MENU": "👤 {user_name}\n \n📱 Подписка: {subscription_status}\n\nВыберите действие:\n", + "MAIN_MENU_ACTION_PROMPT": "Выберите действие:", + "MAIN_MENU_BUTTON": "🏠 Главное меню", "MANAGE_DEVICES_BUTTON": "🔧 Управление устройствами", "MENU_ADMIN": "⚙️ Админ-панель", "MENU_BALANCE": "💰 Баланс", @@ -109,15 +115,19 @@ "PERIOD_90_DAYS": "📅 90 дней - {settings.format_price(settings.PRICE_90_DAYS)}", "POST_REGISTRATION_TRIAL_BUTTON": "🚀 Подключиться бесплатно 🚀", "PROMOCODE_ENTER": "🎫 Введите промокод:", + "PROMOCODE_EMPTY_INPUT": "❌ Введите корректный промокод", "PROMOCODE_EXPIRED": "❌ Промокод истек", "PROMOCODE_INVALID": "❌ Неверный промокод", "PROMOCODE_SUCCESS": "🎉 Промокод активирован! {description}", "PROMOCODE_USED": "❌ Промокод уже использован", "REFERRAL_ANALYTICS_BUTTON": "📊 Аналитика", "REFERRAL_CODE_APPLIED": "🎁 Реферальный код применен! Вы получите бонус после первой покупки.", + "REFERRAL_CODE_ACCEPTED": "✅ Реферальный код принят!", "REFERRAL_CODE_INVALID": "❌ Неверный реферальный код", + "REFERRAL_CODE_INVALID_HELP": "❌ Неверный реферальный код.\n\n💡 Если у вас есть реферальный код, убедитесь что он введен правильно.\n⏭️ Для продолжения регистрации без реферального кода используйте команду /start", "REFERRAL_CODE_QUESTION": "\n🤝 У вас есть реферальный код от друга?\n\nЕсли у вас есть промокод или реферальная ссылка от друга, введите её сейчас, чтобы получить бонус!\n\nВведите код или нажмите \"Пропустить\":\n", "REFERRAL_CODE_SKIP": "⏭️ Пропустить", + "ALREADY_REGISTERED_REFERRAL": "ℹ️ Вы уже зарегистрированы в системе. Реферальная ссылка не может быть применена.", "REFERRAL_INFO": "\n🤝 Реферальная программа\n\n👥 Приглашено: {referrals_count} друзей\n💰 Заработано: {earned_amount}\n\n🔗 Ваша реферальная ссылка:\n{referral_link}\n\n🎫 Ваш промокод:\n{referral_code}\n\n💰 Условия:\n• За каждого друга: {registration_bonus}\n• Процент с пополнений: {commission_percent}%\n", "REFERRAL_INVITE_MESSAGE": "\n🎯 Приглашение в VPN сервис\n\nПривет! Приглашаю тебя в отличный VPN сервис!\n\n🎁 По моей ссылке ты получишь бонус: {bonus}\n\n🔗 Переходи: {link}\n🎫 Или используй промокод: {code}\n\n💪 Быстро, надежно, недорого!\n", "REFERRAL_LIST_BUTTON": "👥 Список рефералов", @@ -125,6 +135,7 @@ "RESET_DEVICE_CONFIRM_BUTTON": "✅ Да, сбросить это устройство", "RESET_TRAFFIC_BUTTON": "🔄 Сбросить трафик", "RULES_ACCEPT": "✅ Принимаю правила", + "RULES_ACCEPTED_PROCESSING": "✅ Правила приняты! Завершаем регистрацию...", "RULES_DECLINE": "❌ Не принимаю", "RULES_HEADER": "📋 Правила сервиса", "RULES_REQUIRED": "❗️ Для использования сервиса необходимо принять правила!", @@ -159,6 +170,7 @@ "SUB_STATUS_TRIAL_TODAY": "🎁 Тестовая подписка\n⚠️ истекает сегодня!", "SUB_STATUS_TRIAL_TOMORROW": "🎁 Тестовая подписка\n⚠️ истекает завтра!", "SUCCESS": "✅ Успешно", + "REGISTRATION_COMPLETING": "✅ Завершаем регистрацию...", "SUPPORT_INFO": "\n🛠️ Техническая поддержка\n\nПо всем вопросам обращайтесь к нашей поддержке:\n\n👤 {settings.SUPPORT_USERNAME}\n\nМы поможем с:\n• Настройкой подключения\n• Решением технических проблем \n• Вопросами по оплате\n• Другими вопросами\n\n⏰ Время ответа: обычно в течение 1-2 часов\n", "SWITCH_TRAFFIC_BUTTON": "🔄 Переключить трафик", "SWITCH_TRAFFIC_CONFIRM": "\n🔄 Подтверждение переключения трафика\n\nТекущий лимит: {current_traffic}\nНовый лимит: {new_traffic}\n\nДействие: {action}\n💰 {cost}\n\nПодтвердить переключение?\n", @@ -170,6 +182,13 @@ "TOP_UP_AMOUNT": "💳 Введите сумму для пополнения (в рублях):", "TOP_UP_METHODS": "\n💳 Выберите способ оплаты\n\nСумма: {amount}\n", "TOP_UP_STARS": "⭐ Telegram Stars", + "STARS_PAYMENT_ENROLLMENT_ERROR": "❌ Произошла ошибка при зачислении средств. Обратитесь в поддержку, платеж будет проверен вручную.", + "STARS_PAYMENT_PROCESSING_ERROR": "❌ Техническая ошибка при обработке платежа. Обратитесь в поддержку для решения проблемы.", + "STARS_PAYMENT_SUCCESS": "🎉 Платеж успешно обработан!\n\n⭐ Потрачено звезд: {stars_spent}\n💰 Зачислено на баланс: {amount} ₽\n🆔 ID транзакции: {transaction_id}...\n\nСпасибо за пополнение! 🚀", + "STARS_PAYMENT_USER_NOT_FOUND": "❌ Ошибка: пользователь не найден. Обратитесь в поддержку.", + "STARS_PRECHECK_INVALID_PAYLOAD": "Ошибка валидации платежа. Попробуйте еще раз.", + "STARS_PRECHECK_TECHNICAL_ERROR": "Техническая ошибка. Попробуйте позже.", + "STARS_PRECHECK_USER_NOT_FOUND": "Пользователь не найден. Обратитесь в поддержку.", "TOP_UP_TRIBUTE": "💎 Банковская карта", "TRAFFIC_100GB": "📊 100 ГБ - {settings.format_price(settings.PRICE_TRAFFIC_100GB)}", "TRAFFIC_10GB": "📊 10 ГБ - {settings.format_price(settings.PRICE_TRAFFIC_10GB)}", @@ -186,7 +205,10 @@ "TRIAL_ALREADY_USED": "❌ Тестовая подписка уже была использована", "TRIAL_AVAILABLE": "\n🎁 Тестовая подписка\n\nВы можете получить бесплатную тестовую подписку:\n\n⏰ Период: {days} дней\n📈 Трафик: {traffic} ГБ\n📱 Устройства: {devices} шт.\n🌍 Сервер: {server_name}\n\nАктивировать тестовую подписку?\n", "TRIAL_ENDING_SOON": "\n🎁 Тестовая подписка скоро закончится!\n\nВаша тестовая подписка истекает через несколько часов.\n\n💎 Не хотите остаться без VPN?\nПереходите на полную подписку!\n\n🔥 Специальное предложение:\n• 30 дней всего за {price}\n• Безлимитный трафик \n• Все серверы доступны\n• Скорость до 1ГБит/сек\n\n⚡️ Успейте оформить до окончания тестового периода!\n", + "UNKNOWN_CALLBACK_ALERT": "❓ Неизвестная команда. Попробуйте ещё раз.", + "UNKNOWN_COMMAND_MESSAGE": "❓ Не понимаю эту команду. Используйте кнопки меню.", "USER_NOT_FOUND": "❌ Пользователь не найден", "WELCOME": "\n🎉 Добро пожаловать в VPN сервис!\n\nНаш сервис предоставляет быстрый и безопасный доступ к интернету без ограничений.\n\n🔐 Преимущества:\n• Высокая скорость подключения\n• Серверы в разных странах\n• Надежная защита данных\n• Круглосуточная поддержка\n\nДля начала работы выберите язык интерфейса:\n", + "WELCOME_FALLBACK": "Добро пожаловать, {user_name}!", "YES": "✅ Да" -} \ No newline at end of file +}