Files
tg_bot_language/bot/handlers/settings.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

301 lines
13 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 Command
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.fsm.context import FSMContext
from database.db import async_session_maker
from bot.handlers.start import main_menu_keyboard
from services.user_service import UserService
from utils.i18n import t, get_user_lang
from utils.levels import (
get_user_level_for_language,
get_available_levels,
get_level_system,
get_level_key_for_i18n,
)
router = Router()
def get_translation_language(user) -> str:
"""Получить язык перевода (translation_language или language_interface как fallback)"""
return getattr(user, 'translation_language', None) or getattr(user, 'language_interface', 'ru') or 'ru'
def get_settings_keyboard(user) -> InlineKeyboardMarkup:
"""Создать клавиатуру настроек"""
lang = get_user_lang(user)
ui_lang_code = getattr(user, 'language_interface', 'ru') or 'ru'
translation_lang_code = get_translation_language(user)
current_level = get_user_level_for_language(user)
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text=t(lang, 'settings.level_prefix') + f"{current_level}",
callback_data="settings_level"
)],
[InlineKeyboardButton(
text=t(lang, 'settings.learning_prefix') + user.learning_language.upper(),
callback_data="settings_learning"
)],
[InlineKeyboardButton(
text=t(lang, 'settings.interface_prefix') + t(lang, f'settings.lang_name.{ui_lang_code}'),
callback_data="settings_language"
)],
[InlineKeyboardButton(
text=t(lang, 'settings.translation_prefix') + t(lang, f'settings.lang_name.{translation_lang_code}'),
callback_data="settings_translation"
)],
[InlineKeyboardButton(
text=t(lang, 'settings.close'),
callback_data="settings_close"
)]
])
return keyboard
def get_level_keyboard(user=None) -> InlineKeyboardMarkup:
"""Клавиатура выбора уровня (CEFR или JLPT в зависимости от языка изучения)"""
lang = get_user_lang(user)
learning_lang = getattr(user, 'learning_language', 'en') or 'en'
available_levels = get_available_levels(learning_lang)
keyboard = []
for level in available_levels:
# Ключ локализации: settings.level.a1 или settings.jlpt.n5
i18n_key = get_level_key_for_i18n(learning_lang, level)
level_name = t(lang, i18n_key)
keyboard.append([InlineKeyboardButton(text=level_name, callback_data=f"set_level_{level}")])
keyboard.append([InlineKeyboardButton(text=t(lang, 'settings.back'), callback_data="settings_back")])
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_language_keyboard(user=None) -> InlineKeyboardMarkup:
"""Клавиатура выбора языка интерфейса"""
lang = get_user_lang(user)
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=t(lang, 'settings.lang_name.ru'), callback_data="set_lang_ru")],
[InlineKeyboardButton(text=t(lang, 'settings.lang_name.en'), callback_data="set_lang_en")],
[InlineKeyboardButton(text=t(lang, 'settings.lang_name.ja'), callback_data="set_lang_ja")],
[InlineKeyboardButton(text=t(lang, 'settings.back'), callback_data="settings_back")]
])
return keyboard
def get_translation_language_keyboard(user=None) -> InlineKeyboardMarkup:
"""Клавиатура выбора языка перевода"""
lang = get_user_lang(user)
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=t(lang, 'settings.lang_name.ru'), callback_data="set_translation_ru")],
[InlineKeyboardButton(text=t(lang, 'settings.lang_name.en'), callback_data="set_translation_en")],
[InlineKeyboardButton(text=t(lang, 'settings.lang_name.ja'), callback_data="set_translation_ja")],
[InlineKeyboardButton(text=t(lang, 'settings.back'), callback_data="settings_back")]
])
return keyboard
def get_learning_language_keyboard(user=None) -> InlineKeyboardMarkup:
"""Клавиатура выбора языка изучения"""
lang = get_user_lang(user)
options = [
("en", t(lang, 'settings.learning_lang.en')),
("ja", t(lang, 'settings.learning_lang.ja')),
# TODO: добавить позже
# ("es", t(lang, 'settings.learning_lang.es')),
# ("de", t(lang, 'settings.learning_lang.de')),
# ("fr", t(lang, 'settings.learning_lang.fr')),
]
keyboard = [[InlineKeyboardButton(text=label, callback_data=f"set_learning_{code}")] for code, label in options]
keyboard.append([InlineKeyboardButton(text=t(lang, 'settings.back'), callback_data="settings_back")])
return InlineKeyboardMarkup(inline_keyboard=keyboard)
@router.message(Command("settings"))
async def cmd_settings(message: Message):
"""Обработчик команды /settings"""
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
if not user:
await message.answer(t('ru', 'common.start_first'))
return
lang = get_user_lang(user)
ui_lang_code = getattr(user, 'language_interface', 'ru') or 'ru'
lang_value = t(lang, f'settings.lang_name.{ui_lang_code}')
current_level = get_user_level_for_language(user)
settings_text = (
t(lang, 'settings.title') +
t(lang, 'settings.level_prefix') + f"<b>{current_level}</b>\n" +
t(lang, 'settings.interface_prefix') + f"<b>{lang_value}</b>\n\n" +
t(lang, 'settings.choose')
)
await message.answer(settings_text, reply_markup=get_settings_keyboard(user))
@router.callback_query(F.data == "settings_level")
async def settings_level(callback: CallbackQuery):
"""Показать выбор уровня"""
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = get_user_lang(user)
learning_lang = getattr(user, 'learning_language', 'en') or 'en'
level_system = get_level_system(learning_lang)
# Выбираем правильное описание групп уровней
groups_key = 'settings.jlpt_groups' if level_system == 'jlpt' else 'settings.level_groups'
text = t(lang, 'settings.level_title') + t(lang, groups_key) + t(lang, 'settings.level_hint')
await callback.message.edit_text(text, reply_markup=get_level_keyboard(user))
await callback.answer()
@router.callback_query(F.data == "settings_learning")
async def settings_learning(callback: CallbackQuery):
"""Показать выбор языка изучения"""
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = get_user_lang(user)
await callback.message.edit_text(t(lang, 'settings.learning_title'), reply_markup=get_learning_language_keyboard(user))
await callback.answer()
@router.callback_query(F.data.startswith("set_learning_"))
async def set_learning_language(callback: CallbackQuery):
"""Установить язык изучения"""
code = callback.data.split("_")[-1]
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, code)
lang = get_user_lang(user)
text = t(lang, 'settings.learning_changed', code=code.upper())
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text=t(lang, 'settings.back_to_settings'), callback_data="settings_back")]])
)
await callback.answer()
@router.callback_query(F.data.startswith("set_level_"))
async def set_level(callback: CallbackQuery):
"""Установить уровень (CEFR или JLPT)"""
level_str = callback.data.split("_")[-1] # A1, A2, B1, B2, C1, C2 или N5, N4, N3, N2, N1
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
if user:
# Передаём строковый уровень, UserService сам разберётся с системой
await UserService.update_user_level(session, user.id, level_str)
lang = get_user_lang(user)
msg = t(lang, 'settings.level_changed', level=level_str) + t(lang, 'settings.level_changed_hint')
await callback.message.edit_text(
msg,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text=t(lang, 'settings.back_to_settings'), callback_data="settings_back")]])
)
await callback.answer()
@router.callback_query(F.data == "settings_language")
async def settings_language(callback: CallbackQuery):
"""Показать выбор языка"""
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = get_user_lang(user)
await callback.message.edit_text(
t(lang, 'settings.lang_title') + t(lang, 'settings.lang_desc'),
reply_markup=get_language_keyboard(user)
)
await callback.answer()
@router.callback_query(F.data.startswith("set_lang_"))
async def set_language(callback: CallbackQuery):
"""Установить язык"""
new_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, new_lang)
# Используем новый язык для сообщений
text = t(new_lang, 'settings.lang_changed')
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text=t(new_lang, 'settings.back'), callback_data="settings_back")]])
)
# Обновляем клавиатуру чата на выбранный язык
await callback.message.answer(t(new_lang, 'settings.menu_updated'), reply_markup=main_menu_keyboard(new_lang))
await callback.answer()
@router.callback_query(F.data == "settings_translation")
async def settings_translation(callback: CallbackQuery):
"""Показать выбор языка перевода"""
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = get_user_lang(user)
await callback.message.edit_text(
t(lang, 'settings.translation_title') + t(lang, 'settings.translation_desc'),
reply_markup=get_translation_language_keyboard(user)
)
await callback.answer()
@router.callback_query(F.data.startswith("set_translation_"))
async def set_translation_language(callback: CallbackQuery):
"""Установить язык перевода"""
new_translation_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_translation_language(session, user.id, new_translation_lang)
lang = get_user_lang(user)
lang_name = t(lang, f'settings.lang_name.{new_translation_lang}')
text = t(lang, 'settings.translation_changed', lang_name=lang_name)
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text=t(lang, 'settings.back'), callback_data="settings_back")]])
)
await callback.answer()
@router.callback_query(F.data == "settings_back")
async def settings_back(callback: CallbackQuery):
"""Вернуться к настройкам"""
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
if user:
lang = get_user_lang(user)
ui_lang_code = getattr(user, 'language_interface', 'ru') or 'ru'
lang_value = t(lang, f'settings.lang_name.{ui_lang_code}')
current_level = get_user_level_for_language(user)
settings_text = (
t(lang, 'settings.title') +
t(lang, 'settings.level_prefix') + f"<b>{current_level}</b>\n" +
t(lang, 'settings.interface_prefix') + f"<b>{lang_value}</b>\n\n" +
t(lang, 'settings.choose')
)
await callback.message.edit_text(settings_text, reply_markup=get_settings_keyboard(user))
await callback.answer()
@router.callback_query(F.data == "settings_close")
async def settings_close(callback: CallbackQuery):
"""Закрыть настройки"""
await callback.message.delete()
await callback.answer()