feat(i18n): localize start/help/menu, practice, words, import, reminder, vocabulary, tasks/stats for RU/EN/JA; add JSON-based i18n helper\n\nfeat(lang): support learning/translation languages across AI flows; hide translations with buttons; store examples per lang\n\nfeat(vocab): add source_lang and translation_lang to Vocabulary, unique constraint (user_id, source_lang, word_original); filter /vocabulary by user.learning_language\n\nchore(migrations): add Alembic setup + migration to add vocab lang columns; env.py reads app settings and supports asyncpg URLs\n\nfix(words/import): pass learning_lang + translation_lang everywhere; fix menu themes generation\n\nfeat(settings): add learning language selector; update main menu on language change

This commit is contained in:
2025-12-04 19:40:01 +03:00
parent 6223351ccf
commit 472771229f
22 changed files with 1587 additions and 471 deletions

View File

@@ -26,23 +26,17 @@ async def cmd_level_test(message: Message, state: FSMContext):
async def start_level_test(message: Message, state: FSMContext):
"""Начать тест определения уровня"""
# Показываем описание теста
await message.answer(
"📊 <b>Тест определения уровня</b>\n\n"
"Этот короткий тест поможет определить твой уровень английского.\n\n"
"📋 Тест включает 7 вопросов:\n"
"• Грамматика\n"
"• Лексика\n"
"• Понимание\n\n"
"⏱ Займёт около 2-3 минут\n\n"
"Готов начать?"
)
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, 'level_test.intro'))
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="✅ Начать тест", callback_data="start_test")],
[InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_test")]
[InlineKeyboardButton(text=t(lang, 'level_test.start_btn'), callback_data="start_test")],
[InlineKeyboardButton(text=t(lang, 'level_test.cancel_btn'), callback_data="cancel_test")]
])
await message.answer("Нажми кнопку когда будешь готов:", reply_markup=keyboard)
await message.answer(t(lang, 'level_test.press_button'), reply_markup=keyboard)
@router.callback_query(F.data == "cancel_test")
@@ -50,13 +44,18 @@ async def cancel_test(callback: CallbackQuery, state: FSMContext):
"""Отменить тест"""
await state.clear()
await callback.message.delete()
await callback.message.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.answer(t(lang, 'level_test.cancelled'))
await callback.answer()
@router.callback_query(F.data == "start_test")
async def begin_test(callback: CallbackQuery, state: FSMContext):
"""Начать прохождение теста"""
# Сразу отвечаем на callback, чтобы избежать истечения таймаута
await callback.answer()
await callback.message.delete()
# Показываем индикатор загрузки
@@ -86,7 +85,6 @@ async def begin_test(callback: CallbackQuery, state: FSMContext):
# Показываем первый вопрос
await show_question(callback.message, state)
await callback.answer()
async def show_question(message: Message, state: FSMContext):
@@ -103,10 +101,14 @@ async def show_question(message: Message, state: FSMContext):
question = questions[current_idx]
# Формируем текст вопроса
# Язык интерфейса
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 = (
f"❓ <b>Вопрос {current_idx + 1} из {len(questions)}</b>\n\n"
t(lang, 'level_test.q_header', i=current_idx + 1, n=len(questions)) + "\n\n" +
f"<b>{question['question']}</b>\n"
f"<i>{question.get('question_ru', '')}</i>\n\n"
)
# Создаем кнопки с вариантами ответа
@@ -120,10 +122,52 @@ async def show_question(message: Message, state: FSMContext):
)
])
# Кнопка для показа перевода вопроса (локализованная)
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.chat.id)
from utils.i18n import t
lang = (user.language_interface if user else 'ru') or 'ru'
keyboard.append([
InlineKeyboardButton(text=t(lang, 'level_test.show_translation_btn'), callback_data=f"show_qtr_{current_idx}")
])
reply_markup = InlineKeyboardMarkup(inline_keyboard=keyboard)
await message.answer(text, reply_markup=reply_markup)
@router.callback_query(F.data.startswith("show_qtr_"), LevelTestStates.taking_test)
async def show_question_translation(callback: CallbackQuery, state: FSMContext):
"""Показать перевод текущего вопроса"""
try:
idx = int(callback.data.split("_")[-1])
except Exception:
await callback.answer("Перевод недоступен", show_alert=True)
return
data = await state.get_data()
questions = data.get('questions', [])
if not (0 <= idx < len(questions)):
await callback.answer("Перевод недоступен", show_alert=True)
return
ru = questions[idx].get('question_ru') or "Перевод недоступен"
# Вставляем перевод в текущий текст сообщения
orig = callback.message.text or ""
marker = "Перевод вопроса:"
if marker in orig:
await callback.answer("Перевод уже показан")
return
new_text = f"{orig}\n{marker} <i>{ru}</i>"
try:
await callback.message.edit_text(new_text, reply_markup=callback.message.reply_markup)
except Exception:
# Запасной путь, если редактирование невозможно
await callback.message.answer(f"{marker} <i>{ru}</i>")
await callback.answer()
@router.callback_query(F.data.startswith("answer_"), LevelTestStates.taking_test)
async def process_answer(callback: CallbackQuery, state: FSMContext):
"""Обработать ответ на вопрос"""