Files
tg_bot_language/bot/handlers/start.py
mamonov.ep 3e5c1be464 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>
2025-12-07 16:35:08 +03:00

485 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from aiogram import Router, F
from aiogram.filters import CommandStart, Command
from aiogram.types import (
Message,
InlineKeyboardMarkup,
InlineKeyboardButton,
CallbackQuery,
ReplyKeyboardMarkup,
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, 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(
resize_keyboard=True,
keyboard=[
[
KeyboardButton(text=t(lang, "menu.add")),
KeyboardButton(text=t(lang, "menu.vocab")),
],
[
KeyboardButton(text=t(lang, "menu.task")),
KeyboardButton(text=t(lang, "menu.practice")),
],
[
KeyboardButton(text=t(lang, "menu.stats")),
KeyboardButton(text=t(lang, "menu.settings")),
],
],
)
@router.message(CommandStart())
async def cmd_start(message: Message, state: FSMContext):
"""Обработчик команды /start"""
async with async_session_maker() as session:
# Проверяем, существует ли пользователь
existing_user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
is_new_user = existing_user is None
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')
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"))
async def cmd_menu(message: Message):
"""Показать клавиатуру с основными командами."""
# Определяем язык пользователя
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
lang = (user.language_interface if user else 'ru') or 'ru'
await message.answer(t(lang, "menu.below"), reply_markup=main_menu_keyboard(lang))
# Обработчики кнопок главного меню (по тексту)
def _menu_match(key: str):
labels = {t('ru', key), t('en', key), t('ja', key)}
return lambda m: m.text in labels
@router.message(_menu_match('menu.add'))
async def btn_add_pressed(message: Message, state: FSMContext):
"""Показать меню добавления слов"""
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
lang = (user.language_interface if user else 'ru') or 'ru'
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=t(lang, 'add_menu.manual'), callback_data="add_manual")],
[InlineKeyboardButton(text=t(lang, 'add_menu.thematic'), callback_data="add_thematic")],
[InlineKeyboardButton(text=t(lang, 'add_menu.import'), callback_data="add_import")]
])
await message.answer(t(lang, 'add_menu.title'), reply_markup=keyboard)
@router.callback_query(F.data == "add_manual")
async def add_manual_callback(callback: CallbackQuery, state: FSMContext):
"""Добавить слово вручную"""
await callback.answer()
from bot.handlers.vocabulary import AddWordStates
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = (user.language_interface if user else 'ru') or 'ru'
await state.set_state(AddWordStates.waiting_for_word)
await callback.message.edit_text(t(lang, 'add.prompt'))
@router.callback_query(F.data == "add_thematic")
async def add_thematic_callback(callback: CallbackQuery):
"""Тематические слова"""
await callback.answer()
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = (user.language_interface if user else 'ru') or 'ru'
# Показываем подсказку по использованию /words
text = (
t(lang, 'words.help_title') + "\n\n" +
t(lang, 'words.help_usage') + "\n\n" +
t(lang, 'words.help_examples') + "\n\n" +
t(lang, 'words.help_note')
)
# Популярные темы как кнопки
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text=t(lang, 'words.topic_travel'), callback_data="words_travel"),
InlineKeyboardButton(text=t(lang, 'words.topic_food'), callback_data="words_food")
],
[
InlineKeyboardButton(text=t(lang, 'words.topic_work'), callback_data="words_work"),
InlineKeyboardButton(text=t(lang, 'words.topic_technology'), callback_data="words_technology")
],
[InlineKeyboardButton(text="⬅️ " + t(lang, 'settings.back'), callback_data="back_to_add_menu")]
])
await callback.message.edit_text(text, reply_markup=keyboard)
@router.callback_query(F.data.startswith("words_"))
async def words_topic_callback(callback: CallbackQuery, state: FSMContext):
"""Генерация слов по теме"""
topic = callback.data.replace("words_", "")
await callback.answer()
await callback.message.delete()
from bot.handlers.words import generate_words_for_theme
await generate_words_for_theme(callback.message, state, topic, callback.from_user.id)
@router.callback_query(F.data == "add_import")
async def add_import_callback(callback: CallbackQuery):
"""Показать меню импорта"""
await callback.answer()
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = (user.language_interface if user else 'ru') or 'ru'
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=t(lang, 'import_menu.from_text'), callback_data="import_from_text")],
[InlineKeyboardButton(text=t(lang, 'import_menu.from_file'), callback_data="import_from_file")],
[InlineKeyboardButton(text="⬅️ " + t(lang, 'settings.back'), callback_data="back_to_add_menu")]
])
await callback.message.edit_text(t(lang, 'import_menu.title'), reply_markup=keyboard)
@router.callback_query(F.data == "back_to_add_menu")
async def back_to_add_menu_callback(callback: CallbackQuery):
"""Вернуться в меню добавления"""
await callback.answer()
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = (user.language_interface if user else 'ru') or 'ru'
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=t(lang, 'add_menu.manual'), callback_data="add_manual")],
[InlineKeyboardButton(text=t(lang, 'add_menu.thematic'), callback_data="add_thematic")],
[InlineKeyboardButton(text=t(lang, 'add_menu.import'), callback_data="add_import")]
])
await callback.message.edit_text(t(lang, 'add_menu.title'), reply_markup=keyboard)
@router.message(_menu_match('menu.vocab'))
async def btn_vocab_pressed(message: Message):
from bot.handlers.vocabulary import cmd_vocabulary
await cmd_vocabulary(message)
@router.message(_menu_match('menu.task'))
async def btn_task_pressed(message: Message, state: FSMContext):
from bot.handlers.tasks import cmd_task
await cmd_task(message, state)
@router.message(_menu_match('menu.practice'))
async def btn_practice_pressed(message: Message, state: FSMContext):
from bot.handlers.practice import cmd_practice
await cmd_practice(message, state)
@router.message(_menu_match('menu.import'))
async def btn_import_pressed(message: Message, state: FSMContext):
"""Показать меню импорта"""
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
lang = (user.language_interface if user else 'ru') or 'ru'
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=t(lang, 'import_menu.from_text'), callback_data="import_from_text")],
[InlineKeyboardButton(text=t(lang, 'import_menu.from_file'), callback_data="import_from_file")]
])
await message.answer(t(lang, 'import_menu.title'), reply_markup=keyboard)
@router.callback_query(F.data == "import_from_text")
async def import_from_text_callback(callback: CallbackQuery, state: FSMContext):
"""Импорт из текста"""
await callback.answer()
from bot.handlers.import_text import ImportStates
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
if not user:
await callback.message.edit_text(t('ru', 'common.start_first'))
return
lang = user.language_interface or 'ru'
await state.set_state(ImportStates.waiting_for_text)
await callback.message.edit_text(
t(lang, 'import.title') + "\n\n" +
t(lang, 'import.desc') + "\n\n" +
t(lang, 'import.can_send') + "\n\n" +
t(lang, 'import.cancel_hint')
)
@router.callback_query(F.data == "import_from_file")
async def import_from_file_callback(callback: CallbackQuery):
"""Импорт из файла"""
await callback.answer()
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = (user.language_interface if user else 'ru') or 'ru'
await callback.message.edit_text(t(lang, 'import_menu.file_hint'))
@router.message(_menu_match('menu.stats'))
async def btn_stats_pressed(message: Message):
from bot.handlers.tasks import cmd_stats
await cmd_stats(message)
@router.message(_menu_match('menu.settings'))
async def btn_settings_pressed(message: Message):
from bot.handlers.settings import cmd_settings
await cmd_settings(message)
@router.message(_menu_match('menu.words'))
async def btn_words_pressed(message: Message, state: FSMContext):
"""Подсказать про тематические слова и показать быстрые темы."""
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
lang = (user.language_interface if user else 'ru') or 'ru'
text = (
t(lang, 'words.help_title') + "\n\n" +
t(lang, 'words.help_usage') + "\n\n" +
t(lang, 'words.popular')
)
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=t(lang, 'words.topic_travel'), callback_data="menu_theme_travel")],
[InlineKeyboardButton(text=t(lang, 'words.topic_food'), callback_data="menu_theme_food")],
[InlineKeyboardButton(text=t(lang, 'words.topic_work'), callback_data="menu_theme_work")],
[InlineKeyboardButton(text=t(lang, 'words.topic_nature'), callback_data="menu_theme_nature")],
[InlineKeyboardButton(text=t(lang, 'words.topic_technology'), callback_data="menu_theme_technology")],
])
await message.answer(text, reply_markup=keyboard)
@router.callback_query(F.data.startswith("menu_theme_"))
async def pick_theme_from_menu(callback: CallbackQuery, state: FSMContext):
"""Сгенерировать слова по выбранной теме из меню и показать список."""
from database.db import async_session_maker
from services.user_service import UserService
from services.ai_service import ai_service
from bot.handlers.words import show_words_list, WordsStates
# Сразу отвечаем на callback, чтобы избежать таймаута
await callback.answer()
theme = callback.data.split("menu_theme_")[-1]
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
if not user:
await callback.answer(t('ru', 'common.start_first'), show_alert=True)
return
lang = (user.language_interface or 'ru')
current_level = get_user_level_for_language(user)
generating = await callback.message.answer(t(lang, 'words.generating', theme=theme))
words = await ai_service.generate_thematic_words(
theme=theme,
level=current_level,
count=10,
learning_lang=user.learning_language,
translation_lang=get_user_translation_lang(user),
)
await generating.delete()
if not words:
await callback.message.answer(t(lang, 'words.generate_failed'))
return
# Сохраняем в состояние как в /words
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, user.id)
@router.message(Command("help"))
async def cmd_help(message: Message):
"""Обработчик команды /help"""
# Определяем язык пользователя
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
lang = (user.language_interface if user else 'ru') or 'ru'
await message.answer(t(lang, "start.help"))
@router.callback_query(F.data == "offer_level_test")
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, telegram_id=callback.from_user.id)
await callback.answer()
@router.callback_query(F.data == "skip_level_test")
async def skip_level_test_callback(callback: CallbackQuery):
"""Пропустить тест уровня"""
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = (user.language_interface if user else 'ru') or 'ru'
await callback.message.edit_text(t(lang, 'start.skip_msg'))
await callback.answer()