feat: add translation language setting & onboarding flow
- Add separate translation_language setting (independent from interface language) - Implement 3-step onboarding for new users: 1. Choose interface language 2. Choose learning language 3. Choose translation language - Fix localization issues when using callback.message (user_id from state) - Add UserService.get_user_by_id() method - Add get_user_translation_lang() helper in i18n - Update all handlers to use correct translation language - Add localization keys for onboarding (ru/en/ja) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -9,14 +9,49 @@ from aiogram.types import (
|
||||
KeyboardButton,
|
||||
)
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
|
||||
from database.db import async_session_maker
|
||||
from services.user_service import UserService
|
||||
from utils.i18n import t
|
||||
from utils.i18n import t, get_user_translation_lang
|
||||
from utils.levels import get_user_level_for_language
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
class OnboardingStates(StatesGroup):
|
||||
"""Состояния онбординга для новых пользователей"""
|
||||
choosing_interface_lang = State()
|
||||
choosing_learning_lang = State()
|
||||
choosing_translation_lang = State()
|
||||
|
||||
|
||||
def onboarding_interface_keyboard() -> InlineKeyboardMarkup:
|
||||
"""Клавиатура выбора языка интерфейса при онбординге"""
|
||||
return InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🇷🇺 Русский", callback_data="onboard_interface_ru")],
|
||||
[InlineKeyboardButton(text="🇬🇧 English", callback_data="onboard_interface_en")],
|
||||
[InlineKeyboardButton(text="🇯🇵 日本語", callback_data="onboard_interface_ja")],
|
||||
])
|
||||
|
||||
|
||||
def onboarding_learning_keyboard(lang: str) -> InlineKeyboardMarkup:
|
||||
"""Клавиатура выбора языка изучения при онбординге"""
|
||||
return InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text=t(lang, 'onboarding.lang_en'), callback_data="onboard_learning_en")],
|
||||
[InlineKeyboardButton(text=t(lang, 'onboarding.lang_ja'), callback_data="onboard_learning_ja")],
|
||||
])
|
||||
|
||||
|
||||
def onboarding_translation_keyboard(lang: str) -> InlineKeyboardMarkup:
|
||||
"""Клавиатура выбора языка перевода при онбординге"""
|
||||
return InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text=t(lang, 'settings.lang_name.ru'), callback_data="onboard_translation_ru")],
|
||||
[InlineKeyboardButton(text=t(lang, 'settings.lang_name.en'), callback_data="onboard_translation_en")],
|
||||
[InlineKeyboardButton(text=t(lang, 'settings.lang_name.ja'), callback_data="onboard_translation_ja")],
|
||||
])
|
||||
|
||||
|
||||
def main_menu_keyboard(lang: str = 'ru') -> ReplyKeyboardMarkup:
|
||||
"""Клавиатура с основными командами (кнопки отправляют команды)."""
|
||||
return ReplyKeyboardMarkup(
|
||||
@@ -46,35 +81,111 @@ async def cmd_start(message: Message, state: FSMContext):
|
||||
existing_user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
|
||||
is_new_user = existing_user is None
|
||||
|
||||
# Создаём или получаем пользователя
|
||||
user = await UserService.get_or_create_user(
|
||||
session,
|
||||
telegram_id=message.from_user.id,
|
||||
username=message.from_user.username
|
||||
)
|
||||
if is_new_user:
|
||||
# Новый пользователь - начинаем онбординг
|
||||
# Сначала создаём пользователя с дефолтными значениями
|
||||
user = await UserService.get_or_create_user(
|
||||
session,
|
||||
telegram_id=message.from_user.id,
|
||||
username=message.from_user.username
|
||||
)
|
||||
|
||||
# Приветствие и первый вопрос - язык интерфейса
|
||||
await message.answer(
|
||||
f"👋 Welcome! / Привет! / ようこそ!\n\n"
|
||||
"🌐 Choose your interface language:\n"
|
||||
"🌐 Выбери язык интерфейса:\n"
|
||||
"🌐 インターフェース言語を選択:",
|
||||
reply_markup=onboarding_interface_keyboard()
|
||||
)
|
||||
await state.set_state(OnboardingStates.choosing_interface_lang)
|
||||
return
|
||||
|
||||
# Существующий пользователь
|
||||
user = existing_user
|
||||
lang = (user.language_interface or 'ru')
|
||||
|
||||
if is_new_user:
|
||||
# Новый пользователь
|
||||
await message.answer(
|
||||
t(lang, "start.new_intro", first_name=message.from_user.first_name),
|
||||
reply_markup=main_menu_keyboard(lang),
|
||||
)
|
||||
await message.answer(
|
||||
t(lang, "start.return", first_name=message.from_user.first_name),
|
||||
reply_markup=main_menu_keyboard(lang),
|
||||
)
|
||||
|
||||
# Предлагаем пройти тест уровня
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text=t(lang, 'start.offer_btn'), callback_data="offer_level_test")],
|
||||
[InlineKeyboardButton(text=t(lang, 'start.skip_btn'), callback_data="skip_level_test")]
|
||||
])
|
||||
|
||||
await message.answer(t(lang, "start.offer_test"), reply_markup=keyboard)
|
||||
else:
|
||||
# Существующий пользователь
|
||||
await message.answer(
|
||||
t(lang, "start.return", first_name=message.from_user.first_name),
|
||||
reply_markup=main_menu_keyboard(lang),
|
||||
)
|
||||
# === Обработчики онбординга ===
|
||||
|
||||
@router.callback_query(F.data.startswith("onboard_interface_"), OnboardingStates.choosing_interface_lang)
|
||||
async def onboard_set_interface(callback: CallbackQuery, state: FSMContext):
|
||||
"""Установить язык интерфейса при онбординге"""
|
||||
lang = callback.data.split("_")[-1] # ru | en | ja
|
||||
|
||||
async with async_session_maker() as session:
|
||||
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
|
||||
if user:
|
||||
await UserService.update_user_language(session, user.id, lang)
|
||||
|
||||
await state.update_data(interface_lang=lang)
|
||||
|
||||
# Второй вопрос - язык изучения
|
||||
await callback.message.edit_text(
|
||||
t(lang, 'onboarding.step2_title'),
|
||||
reply_markup=onboarding_learning_keyboard(lang)
|
||||
)
|
||||
await state.set_state(OnboardingStates.choosing_learning_lang)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("onboard_learning_"), OnboardingStates.choosing_learning_lang)
|
||||
async def onboard_set_learning(callback: CallbackQuery, state: FSMContext):
|
||||
"""Установить язык изучения при онбординге"""
|
||||
learning_lang = callback.data.split("_")[-1] # en | ja
|
||||
data = await state.get_data()
|
||||
lang = data.get('interface_lang', 'ru')
|
||||
|
||||
async with async_session_maker() as session:
|
||||
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
|
||||
if user:
|
||||
await UserService.update_user_learning_language(session, user.id, learning_lang)
|
||||
|
||||
await state.update_data(learning_lang=learning_lang)
|
||||
|
||||
# Третий вопрос - язык перевода
|
||||
await callback.message.edit_text(
|
||||
t(lang, 'onboarding.step3_title'),
|
||||
reply_markup=onboarding_translation_keyboard(lang)
|
||||
)
|
||||
await state.set_state(OnboardingStates.choosing_translation_lang)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("onboard_translation_"), OnboardingStates.choosing_translation_lang)
|
||||
async def onboard_set_translation(callback: CallbackQuery, state: FSMContext):
|
||||
"""Установить язык перевода при онбординге и завершить"""
|
||||
translation_lang = callback.data.split("_")[-1] # ru | en | ja
|
||||
data = await state.get_data()
|
||||
lang = data.get('interface_lang', 'ru')
|
||||
|
||||
async with async_session_maker() as session:
|
||||
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
|
||||
if user:
|
||||
await UserService.update_user_translation_language(session, user.id, translation_lang)
|
||||
|
||||
await state.clear()
|
||||
|
||||
# Приветствие с выбранными настройками
|
||||
await callback.message.edit_text(t(lang, 'onboarding.complete'))
|
||||
|
||||
# Показываем главное меню и предлагаем тест уровня
|
||||
await callback.message.answer(
|
||||
t(lang, "start.new_intro", first_name=callback.from_user.first_name),
|
||||
reply_markup=main_menu_keyboard(lang),
|
||||
)
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text=t(lang, 'start.offer_btn'), callback_data="offer_level_test")],
|
||||
[InlineKeyboardButton(text=t(lang, 'start.skip_btn'), callback_data="skip_level_test")]
|
||||
])
|
||||
await callback.message.answer(t(lang, "start.offer_test"), reply_markup=keyboard)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.message(Command("menu"))
|
||||
@@ -329,7 +440,7 @@ async def pick_theme_from_menu(callback: CallbackQuery, state: FSMContext):
|
||||
level=current_level,
|
||||
count=10,
|
||||
learning_lang=user.learning_language,
|
||||
translation_lang=user.language_interface,
|
||||
translation_lang=get_user_translation_lang(user),
|
||||
)
|
||||
await generating.delete()
|
||||
|
||||
@@ -341,7 +452,7 @@ async def pick_theme_from_menu(callback: CallbackQuery, state: FSMContext):
|
||||
await state.update_data(theme=theme, words=words, user_id=user.id, level=current_level)
|
||||
await state.set_state(WordsStates.viewing_words)
|
||||
|
||||
await show_words_list(callback.message, words, theme)
|
||||
await show_words_list(callback.message, words, theme, user.id)
|
||||
|
||||
|
||||
@router.message(Command("help"))
|
||||
@@ -359,7 +470,7 @@ async def offer_level_test_callback(callback: CallbackQuery, state: FSMContext):
|
||||
"""Начать тест уровня из приветствия"""
|
||||
from bot.handlers.level_test import start_level_test
|
||||
await callback.message.delete()
|
||||
await start_level_test(callback.message, state)
|
||||
await start_level_test(callback.message, state, telegram_id=callback.from_user.id)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user