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

@@ -8,6 +8,7 @@ from database.db import async_session_maker
from services.user_service import UserService
from services.task_service import TaskService
from services.ai_service import ai_service
from utils.i18n import t
router = Router()
@@ -25,17 +26,18 @@ async def cmd_task(message: Message, state: FSMContext):
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
# Генерируем задания разных типов
tasks = await TaskService.generate_mixed_tasks(session, user.id, count=5)
tasks = await TaskService.generate_mixed_tasks(
session, user.id, count=5,
learning_lang=user.learning_language,
translation_lang=user.language_interface,
)
if not tasks:
await message.answer(
"📚 У тебя пока нет слов для практики!\n\n"
"Добавь несколько слов командой /add, а затем возвращайся."
)
await message.answer(t(user.level.value and (user.language_interface or 'ru') or 'ru', 'tasks.no_words'))
return
# Сохраняем задания в состоянии
@@ -64,15 +66,20 @@ async def show_current_task(message: Message, state: FSMContext):
task = tasks[current_index]
# Определяем язык пользователя
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'
task_text = (
f"📝 <b>Задание {current_index + 1} из {len(tasks)}</b>\n\n"
t(lang, 'tasks.header', i=current_index + 1, n=len(tasks)) + "\n\n" +
f"{task['question']}\n"
)
if task.get('transcription'):
task_text += f"🔊 [{task['transcription']}]\n"
task_text += f"\n💡 Напиши свой ответ:"
task_text += t(lang, 'tasks.write_answer')
await state.set_state(TaskStates.waiting_for_answer)
await message.answer(task_text)
@@ -92,7 +99,12 @@ async def process_answer(message: Message, state: FSMContext):
task = tasks[current_index]
# Показываем индикатор проверки
checking_msg = await message.answer("⏳ Проверяю ответ...")
# Язык пользователя
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'
checking_msg = await message.answer(t(lang, 'tasks.checking'))
# Проверяем ответ через AI
check_result = await ai_service.check_answer(
@@ -108,13 +120,13 @@ async def process_answer(message: Message, state: FSMContext):
# Формируем ответ
if is_correct:
result_text = f"✅ <b>Правильно!</b>\n\n"
result_text = t(lang, 'tasks.correct') + "\n\n"
correct_count += 1
else:
result_text = f"❌ <b>Неправильно</b>\n\n"
result_text = t(lang, 'tasks.incorrect') + "\n\n"
result_text += f"Твой ответ: <i>{user_answer}</i>\n"
result_text += f"Правильный ответ: <b>{task['correct_answer']}</b>\n\n"
result_text += f"{t(lang, 'tasks.your_answer')}: <i>{user_answer}</i>\n"
result_text += f"{t(lang, 'tasks.right_answer')}: <b>{task['correct_answer']}</b>\n\n"
if feedback:
result_text += f"💬 {feedback}\n\n"
@@ -151,8 +163,8 @@ async def process_answer(message: Message, state: FSMContext):
# Показываем результат и кнопку "Далее"
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="➡️ Следующее задание", callback_data="next_task")],
[InlineKeyboardButton(text="🔚 Завершить", callback_data="stop_tasks")]
[InlineKeyboardButton(text=t(lang, 'tasks.next_btn'), callback_data="next_task")],
[InlineKeyboardButton(text=t(lang, 'tasks.stop_btn'), callback_data="stop_tasks")]
])
await message.answer(result_text, reply_markup=keyboard)
@@ -173,7 +185,10 @@ async def stop_tasks_callback(callback: CallbackQuery, state: FSMContext):
"""Остановить выполнение заданий через кнопку"""
await state.clear()
await callback.message.edit_reply_markup(reply_markup=None)
await callback.message.answer("Задания завершены. Используй /task, чтобы начать заново.")
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, 'tasks.finished'))
await callback.answer()
@@ -182,7 +197,10 @@ async def stop_tasks_callback(callback: CallbackQuery, state: FSMContext):
async def stop_tasks(message: Message, state: FSMContext):
"""Остановить выполнение заданий командой /stop"""
await state.clear()
await message.answer("Задания остановлены. Используй /task, чтобы начать заново.")
# Определяем язык пользователя
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
await message.answer(t((user.language_interface if user else 'ru') or 'ru', 'tasks.stopped'))
@router.message(Command("cancel"), TaskStates.doing_tasks)
@@ -190,7 +208,10 @@ async def stop_tasks(message: Message, state: FSMContext):
async def cancel_tasks(message: Message, state: FSMContext):
"""Отмена выполнения заданий командой /cancel"""
await state.clear()
await message.answer("Отменено. Можешь вернуться к заданиям командой /task.")
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, 'tasks.cancelled'))
async def finish_tasks(message: Message, state: FSMContext):
@@ -205,24 +226,29 @@ async def finish_tasks(message: Message, state: FSMContext):
# Определяем эмодзи на основе результата
if accuracy >= 90:
emoji = "🏆"
comment = "Отличный результат!"
comment_key = 'excellent'
elif accuracy >= 70:
emoji = "👍"
comment = "Хорошая работа!"
comment_key = 'good'
elif accuracy >= 50:
emoji = "📚"
comment = "Неплохо, продолжай практиковаться!"
comment_key = 'average'
else:
emoji = "💪"
comment = "Повтори эти слова еще раз!"
comment_key = 'poor'
# Язык пользователя
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'
result_text = (
f"{emoji} <b>Задание завершено!</b>\n\n"
f"Правильных ответов: <b>{correct_count}</b> из {total_count}\n"
f"Точность: <b>{accuracy}%</b>\n\n"
f"{comment}\n\n"
f"Используй /task для нового задания\n"
f"Используй /stats для просмотра статистики"
t(lang, 'tasks.finish_title', emoji=emoji) + "\n\n" +
t(lang, 'tasks.correct_of', correct=correct_count, total=total_count) + "\n" +
t(lang, 'tasks.accuracy', accuracy=accuracy) + "\n\n" +
t(lang, f"tasks.comment.{comment_key}") + "\n\n" +
t(lang, 'tasks.use_task') + "\n" +
t(lang, 'tasks.use_stats')
)
await state.clear()
@@ -236,26 +262,27 @@ async def cmd_stats(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
# Получаем статистику
stats = await TaskService.get_user_stats(session, user.id)
lang = (user.language_interface if user else 'ru') or 'ru'
stats_text = (
f"📊 <b>Твоя статистика</b>\n\n"
f"📚 Слов в словаре: <b>{stats['total_words']}</b>\n"
f"📖 Слов изучено: <b>{stats['reviewed_words']}</b>\n"
f"✍️ Заданий выполнено: <b>{stats['total_tasks']}</b>\n"
f"✅ Правильных ответов: <b>{stats['correct_tasks']}</b>\n"
f"🎯 Точность: <b>{stats['accuracy']}%</b>\n\n"
t(lang, 'stats.header') + "\n\n" +
t(lang, 'stats.total_words', n=stats['total_words']) + "\n" +
t(lang, 'stats.studied_words', n=stats['reviewed_words']) + "\n" +
t(lang, 'stats.total_tasks', n=stats['total_tasks']) + "\n" +
t(lang, 'stats.correct_tasks', n=stats['correct_tasks']) + "\n" +
t(lang, 'stats.accuracy', n=stats['accuracy']) + "\n\n"
)
if stats['total_words'] == 0:
stats_text += "Добавь слова командой /add чтобы начать обучение!"
stats_text += t(lang, 'stats.hint_add_words')
elif stats['total_tasks'] == 0:
stats_text += "Выполни первое задание командой /task!"
stats_text += t(lang, 'stats.hint_first_task')
else:
stats_text += "Продолжай практиковаться! 💪"
stats_text += t(lang, 'stats.hint_keep_practice')
await message.answer(stats_text)