feat: JLPT levels for Japanese, custom practice scenarios, UI improvements
- Add separate level systems: CEFR (A1-C2) for European languages, JLPT (N5-N1) for Japanese - Store levels per language in new `levels_by_language` JSON field - Add custom scenario option in AI practice mode - Show action buttons after practice ends (new dialogue, tasks, words) - Fix level display across all handlers to use correct level system - Add Alembic migration for levels_by_language field - Update all locale files (ru, en, ja) with new keys 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -4,52 +4,39 @@ from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKe
|
||||
from aiogram.fsm.context import FSMContext
|
||||
|
||||
from database.db import async_session_maker
|
||||
from database.models import LanguageLevel
|
||||
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 _is_en(user) -> bool:
|
||||
try:
|
||||
return (getattr(user, 'language_interface', 'ru') or 'ru') == 'en'
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _is_ja(user) -> bool:
|
||||
try:
|
||||
return (getattr(user, 'language_interface', 'ru') or 'ru') == 'ja'
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def get_settings_keyboard(user) -> InlineKeyboardMarkup:
|
||||
"""Создать клавиатуру настроек"""
|
||||
is_en = _is_en(user)
|
||||
is_ja = _is_ja(user)
|
||||
lang = get_user_lang(user)
|
||||
ui_lang_code = getattr(user, 'language_interface', 'ru') or 'ru'
|
||||
current_level = get_user_level_for_language(user)
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(
|
||||
text=(
|
||||
"📊 Level: " if is_en else ("📊 レベル: " if is_ja else "📊 Уровень: ")
|
||||
) + f"{user.level.value}",
|
||||
text=t(lang, 'settings.level_prefix') + f"{current_level}",
|
||||
callback_data="settings_level"
|
||||
)],
|
||||
[InlineKeyboardButton(
|
||||
text=(
|
||||
"🎯 Learning language: " if is_en else ("🎯 学習言語: " if is_ja else "🎯 Язык изучения: ")
|
||||
) + (user.learning_language.upper()),
|
||||
text=t(lang, 'settings.learning_prefix') + user.learning_language.upper(),
|
||||
callback_data="settings_learning"
|
||||
)],
|
||||
[InlineKeyboardButton(
|
||||
text=(
|
||||
"🌐 Interface language: " if is_en else ("🌐 インターフェース言語: " if is_ja else "🌐 Язык интерфейса: ")
|
||||
) + ("🇬🇧 English" if getattr(user, 'language_interface', 'ru') == 'en' else ("🇯🇵 日本語" if getattr(user, 'language_interface', 'ru') == 'ja' else "🇷🇺 Русский")),
|
||||
text=t(lang, 'settings.interface_prefix') + t(lang, f'settings.lang_name.{ui_lang_code}'),
|
||||
callback_data="settings_language"
|
||||
)],
|
||||
[InlineKeyboardButton(
|
||||
text=("❌ Close" if is_en else ("❌ 閉じる" if is_ja else "❌ Закрыть")),
|
||||
text=t(lang, 'settings.close'),
|
||||
callback_data="settings_close"
|
||||
)]
|
||||
])
|
||||
@@ -57,75 +44,49 @@ def get_settings_keyboard(user) -> InlineKeyboardMarkup:
|
||||
|
||||
|
||||
def get_level_keyboard(user=None) -> InlineKeyboardMarkup:
|
||||
"""Клавиатура выбора уровня"""
|
||||
lang = getattr(user, 'language_interface', 'ru') if user is not None else 'ru'
|
||||
if lang == 'en':
|
||||
levels = [
|
||||
("A1 - Beginner", "set_level_A1"),
|
||||
("A2 - Elementary", "set_level_A2"),
|
||||
("B1 - Intermediate", "set_level_B1"),
|
||||
("B2 - Upper-intermediate", "set_level_B2"),
|
||||
("C1 - Advanced", "set_level_C1"),
|
||||
("C2 - Proficient", "set_level_C2"),
|
||||
]
|
||||
elif lang == 'ja':
|
||||
levels = [
|
||||
("A1 - 初級", "set_level_A1"),
|
||||
("A2 - 初級(上)", "set_level_A2"),
|
||||
("B1 - 中級", "set_level_B1"),
|
||||
("B2 - 中級(上)", "set_level_B2"),
|
||||
("C1 - 上級", "set_level_C1"),
|
||||
("C2 - ネイティブ", "set_level_C2"),
|
||||
]
|
||||
else:
|
||||
levels = [
|
||||
("A1 - Начальный", "set_level_A1"),
|
||||
("A2 - Элементарный", "set_level_A2"),
|
||||
("B1 - Средний", "set_level_B1"),
|
||||
("B2 - Выше среднего", "set_level_B2"),
|
||||
("C1 - Продвинутый", "set_level_C1"),
|
||||
("C2 - Профессиональный", "set_level_C2"),
|
||||
]
|
||||
"""Клавиатура выбора уровня (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_name, callback_data in levels:
|
||||
keyboard.append([InlineKeyboardButton(text=level_name, callback_data=callback_data)])
|
||||
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}")])
|
||||
|
||||
back_label = "⬅️ Back" if lang == 'en' else ("⬅️ 戻る" if lang == 'ja' else "⬅️ Назад")
|
||||
keyboard.append([InlineKeyboardButton(text=back_label, callback_data="settings_back")])
|
||||
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 = getattr(user, 'language_interface', 'ru') if user is not None else 'ru'
|
||||
back = "⬅️ Back" if lang == 'en' else ("⬅️ 戻る" if lang == 'ja' else "⬅️ Назад")
|
||||
lang = get_user_lang(user)
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🇷🇺 Русский", callback_data="set_lang_ru")],
|
||||
[InlineKeyboardButton(text="🇬🇧 English", callback_data="set_lang_en")],
|
||||
[InlineKeyboardButton(text="🇯🇵 日本語", callback_data="set_lang_ja")],
|
||||
[InlineKeyboardButton(text=back, callback_data="settings_back")]
|
||||
[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_learning_language_keyboard(user=None) -> InlineKeyboardMarkup:
|
||||
"""Клавиатура выбора языка изучения"""
|
||||
lang = getattr(user, 'language_interface', 'ru') if user is not None else 'ru'
|
||||
back = "⬅️ Back" if lang == 'en' else ("⬅️ 戻る" if lang == 'ja' else "⬅️ Назад")
|
||||
lang = get_user_lang(user)
|
||||
|
||||
# Пары (код -> подпись)
|
||||
options = [
|
||||
("en", "🇬🇧 English" if lang != 'ja' else "🇬🇧 英語"),
|
||||
("es", "🇪🇸 Spanish" if lang == 'en' else ("🇪🇸 スペイン語" if lang == 'ja' else "🇪🇸 Испанский")),
|
||||
("de", "🇩🇪 German" if lang == 'en' else ("🇩🇪 ドイツ語" if lang == 'ja' else "🇩🇪 Немецкий")),
|
||||
("fr", "🇫🇷 French" if lang == 'en' else ("🇫🇷 フランス語" if lang == 'ja' else "🇫🇷 Французский")),
|
||||
("ja", "🇯🇵 Japanese" if lang == 'en' else ("🇯🇵 日本語" if lang == 'ja' else "🇯🇵 Японский")),
|
||||
("en", t(lang, 'settings.learning_lang.en')),
|
||||
("es", t(lang, 'settings.learning_lang.es')),
|
||||
("de", t(lang, 'settings.learning_lang.de')),
|
||||
("fr", t(lang, 'settings.learning_lang.fr')),
|
||||
("ja", t(lang, 'settings.learning_lang.ja')),
|
||||
]
|
||||
|
||||
keyboard = [[InlineKeyboardButton(text=label, callback_data=f"set_learning_{code}")] for code, label in options]
|
||||
keyboard.append([InlineKeyboardButton(text=back, callback_data="settings_back")])
|
||||
keyboard.append([InlineKeyboardButton(text=t(lang, 'settings.back'), callback_data="settings_back")])
|
||||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
|
||||
|
||||
@@ -136,16 +97,19 @@ async def cmd_settings(message: Message):
|
||||
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
|
||||
|
||||
if not user:
|
||||
await message.answer("Сначала запусти бота командой /start")
|
||||
await message.answer(t('ru', 'common.start_first'))
|
||||
return
|
||||
|
||||
lang = getattr(user, 'language_interface', 'ru')
|
||||
title = "⚙️ <b>Settings</b>\n\n" if lang == 'en' else ("⚙️ <b>設定</b>\n\n" if lang == 'ja' else "⚙️ <b>Настройки</b>\n\n")
|
||||
level_label = "📊 English level: " if lang == 'en' else ("📊 英語レベル: " if lang == 'ja' else "📊 Уровень английского: ")
|
||||
lang_label = "🌐 Interface language: " if lang == 'en' else ("🌐 インターフェース言語: " if lang == 'ja' else "🌐 Язык интерфейса: ")
|
||||
lang_value = 'English' if lang == 'en' else ('日本語' if lang == 'ja' else ('Русский' if user.language_interface == 'ru' else 'English'))
|
||||
footer = "Choose what to change:" if lang == 'en' else ("変更したい項目を選択:" if lang == 'ja' else "Выбери, что хочешь изменить:")
|
||||
settings_text = f"{title}{level_label}<b>{user.level.value}</b>\n{lang_label}<b>{lang_value}</b>\n\n{footer}"
|
||||
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))
|
||||
|
||||
@@ -155,15 +119,13 @@ 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 = getattr(user, 'language_interface', 'ru') if user else 'ru'
|
||||
title = "📊 <b>Choose your English level:</b>\n\n" if lang == 'en' else ("📊 <b>英語レベルを選択:</b>\n\n" if lang == 'ja' else "📊 <b>Выбери свой уровень английского:</b>\n\n")
|
||||
body = (
|
||||
"<b>A1-A2</b> - Beginner\n<b>B1-B2</b> - Intermediate\n<b>C1-C2</b> - Advanced\n\n" if lang == 'en' else (
|
||||
"<b>A1-A2</b> - 初級\n<b>B1-B2</b> - 中級\n<b>C1-C2</b> - 上級\n\n" if lang == 'ja' else
|
||||
"<b>A1-A2</b> - Начинающий\n<b>B1-B2</b> - Средний\n<b>C1-C2</b> - Продвинутый\n\n"
|
||||
))
|
||||
tail = "This affects difficulty of suggested words and tasks." if lang == 'en' else ("これは提案される単語や課題の難易度に影響します。" if lang == 'ja' else "Это влияет на сложность предлагаемых слов и заданий.")
|
||||
await callback.message.edit_text(title + body + tail, reply_markup=get_level_keyboard(user))
|
||||
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()
|
||||
|
||||
|
||||
@@ -172,9 +134,8 @@ 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 = getattr(user, 'language_interface', 'ru') if user else 'ru'
|
||||
title = "🎯 <b>Select learning language:</b>\n\n" if lang == 'en' else ("🎯 <b>学習言語を選択:</b>\n\n" if lang == 'ja' else "🎯 <b>Выбери язык изучения:</b>\n\n")
|
||||
await callback.message.edit_text(title, reply_markup=get_learning_language_keyboard(user))
|
||||
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()
|
||||
|
||||
|
||||
@@ -186,42 +147,32 @@ async def set_learning_language(callback: CallbackQuery):
|
||||
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 = getattr(user, 'language_interface', 'ru')
|
||||
if lang == 'en':
|
||||
text = f"✅ Learning language: <b>{code.upper()}</b>"
|
||||
back = "⬅️ Back to settings"
|
||||
elif lang == 'ja':
|
||||
text = f"✅ 学習言語: <b>{code.upper()}</b>"
|
||||
back = "⬅️ 設定に戻る"
|
||||
else:
|
||||
text = f"✅ Язык изучения: <b>{code.upper()}</b>"
|
||||
back = "⬅️ К настройкам"
|
||||
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=back, callback_data="settings_back")]])
|
||||
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):
|
||||
"""Установить уровень"""
|
||||
level_str = callback.data.split("_")[-1] # A1, A2, B1, B2, C1, C2
|
||||
"""Установить уровень (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:
|
||||
# Обновляем уровень
|
||||
await UserService.update_user_level(session, user.id, LanguageLevel[level_str])
|
||||
# Передаём строковый уровень, UserService сам разберётся с системой
|
||||
await UserService.update_user_level(session, user.id, level_str)
|
||||
|
||||
lang = getattr(user, 'language_interface', 'ru')
|
||||
changed = "✅ Level changed to " if lang == 'en' else ("✅ レベルが変更されました: " if lang == 'ja' else "✅ Уровень изменен на ")
|
||||
msg = changed + f"<b>{level_str}</b>\n\n" + ("You will now receive words and tasks matching your level!" if lang == 'en' else ("これからレベルに合った単語と課題が出題されます!" if lang == 'ja' else "Теперь ты будешь получать слова и задания, соответствующие твоему уровню!"))
|
||||
back = "⬅️ Back to settings" if lang == 'en' else ("⬅️ 設定に戻る" if lang == 'ja' else "⬅️ К настройкам")
|
||||
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=back, callback_data="settings_back")]])
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text=t(lang, 'settings.back_to_settings'), callback_data="settings_back")]])
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
@@ -232,11 +183,9 @@ 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 = getattr(user, 'language_interface', 'ru') if user else 'ru'
|
||||
title = "🌐 <b>Select interface language:</b>\n\n" if lang == 'en' else ("🌐 <b>インターフェース言語を選択:</b>\n\n" if lang == 'ja' else "🌐 <b>Выбери язык интерфейса:</b>\n\n")
|
||||
desc = "This will change the language of bot messages." if lang == 'en' else ("ボットの表示言語が変更されます。" if lang == 'ja' else "Это изменит язык всех сообщений бота.")
|
||||
lang = get_user_lang(user)
|
||||
await callback.message.edit_text(
|
||||
title + desc,
|
||||
t(lang, 'settings.lang_title') + t(lang, 'settings.lang_desc'),
|
||||
reply_markup=get_language_keyboard(user)
|
||||
)
|
||||
await callback.answer()
|
||||
@@ -245,29 +194,21 @@ async def settings_language(callback: CallbackQuery):
|
||||
@router.callback_query(F.data.startswith("set_lang_"))
|
||||
async def set_language(callback: CallbackQuery):
|
||||
"""Установить язык"""
|
||||
lang = callback.data.split("_")[-1] # ru | en | ja
|
||||
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, lang)
|
||||
if lang == 'en':
|
||||
text = "✅ Interface language: <b>English</b>"
|
||||
back = "⬅️ Back"
|
||||
elif lang == 'ja':
|
||||
text = "✅ インターフェース言語: <b>日本語</b>"
|
||||
back = "⬅️ 戻る"
|
||||
else:
|
||||
text = "✅ Язык интерфейса: <b>Русский</b>"
|
||||
back = "⬅️ К настройкам"
|
||||
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=back, callback_data="settings_back")]])
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text=t(new_lang, 'settings.back'), callback_data="settings_back")]])
|
||||
)
|
||||
# Обновляем клавиатуру чата на выбранный язык
|
||||
menu_updated = "Main menu updated ⤵️" if lang == 'en' else ("メインメニューを更新しました ⤵️" if lang == 'ja' else "Клавиатура обновлена ⤵️")
|
||||
await callback.message.answer(menu_updated, reply_markup=main_menu_keyboard(lang))
|
||||
await callback.message.answer(t(new_lang, 'settings.menu_updated'), reply_markup=main_menu_keyboard(new_lang))
|
||||
|
||||
await callback.answer()
|
||||
|
||||
@@ -279,13 +220,16 @@ async def settings_back(callback: CallbackQuery):
|
||||
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
|
||||
|
||||
if user:
|
||||
lang = getattr(user, 'language_interface', 'ru')
|
||||
title = "⚙️ <b>Settings</b>\n\n" if lang == 'en' else ("⚙️ <b>設定</b>\n\n" if lang == 'ja' else "⚙️ <b>Настройки</b>\n\n")
|
||||
level_label = "📊 English level: " if lang == 'en' else ("📊 英語レベル: " if lang == 'ja' else "📊 Уровень английского: ")
|
||||
lang_label = "🌐 Interface language: " if lang == 'en' else ("🌐 インターフェース言語: " if lang == 'ja' else "🌐 Язык интерфейса: ")
|
||||
lang_value = 'English' if user.language_interface == 'en' else ('日本語' if user.language_interface == 'ja' else 'Русский')
|
||||
footer = "Choose what to change:" if lang == 'en' else ("変更したい項目を選択:" if lang == 'ja' else "Выбери, что хочешь изменить:")
|
||||
settings_text = f"{title}{level_label}<b>{user.level.value}</b>\n{lang_label}<b>{lang_value}</b>\n\n{footer}"
|
||||
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))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user