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:
2025-12-07 16:35:08 +03:00
parent d937b37a3b
commit 3e5c1be464
14 changed files with 360 additions and 81 deletions

View File

@@ -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()