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

@@ -9,6 +9,7 @@ from database.models import WordSource
from services.user_service import UserService
from services.vocabulary_service import VocabularyService
from services.ai_service import ai_service
from utils.i18n import t
router = Router()
@@ -26,20 +27,16 @@ async def cmd_import(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
await state.set_state(ImportStates.waiting_for_text)
lang = user.language_interface or 'ru'
await message.answer(
"📖 <b>Импорт слов из текста</b>\n\n"
"Отправь мне текст на английском языке, и я извлеку из него "
"полезные слова для изучения.\n\n"
"Можно отправить:\n"
"• Отрывок из книги или статьи\n"
"• Текст песни\n"
"• Описание чего-либо\n"
"• Любой интересный текст\n\n"
"Отправь /cancel для отмены."
t(lang, 'import.title') + "\n\n" +
t(lang, 'import.desc') + "\n\n" +
t(lang, 'import.can_send') + "\n\n" +
t(lang, 'import.cancel_hint')
)
@@ -56,38 +53,32 @@ async def process_text(message: Message, state: FSMContext):
text = message.text.strip()
if len(text) < 50:
await message.answer(
"⚠️ Текст слишком короткий. Отправь текст минимум из 50 символов.\n"
"Или используй /cancel для отмены."
)
await message.answer(t('ru', 'import.too_short'))
return
if len(text) > 3000:
await message.answer(
"⚠️ Текст слишком длинный (максимум 3000 символов).\n"
"Отправь текст покороче или используй /cancel для отмены."
)
await message.answer(t('ru', 'import.too_long'))
return
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
# Показываем индикатор обработки
processing_msg = await message.answer("🔄 Анализирую текст и извлекаю слова...")
processing_msg = await message.answer(t(user.language_interface or 'ru', 'import.processing'))
# Извлекаем слова через AI
words = await ai_service.extract_words_from_text(
text=text,
level=user.level.value,
max_words=15
max_words=15,
learning_lang=user.learning_language,
translation_lang=user.language_interface,
)
await processing_msg.delete()
if not words:
await message.answer(
"Не удалось извлечь слова из текста. Попробуй другой текст или повтори позже."
)
await message.answer(t(user.language_interface or 'ru', 'import.failed'))
await state.clear()
return
@@ -107,7 +98,10 @@ async def process_text(message: Message, state: FSMContext):
async def show_extracted_words(message: Message, words: list):
"""Показать извлечённые слова с кнопками для добавления"""
text = f"📚 <b>Найдено слов: {len(words)}</b>\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'
text = t(lang, 'import.found_header', n=len(words)) + "\n\n"
for idx, word_data in enumerate(words, 1):
text += (
@@ -125,7 +119,7 @@ async def show_extracted_words(message: Message, words: list):
text += "\n"
text += "Выбери слова, которые хочешь добавить в словарь:"
text += t(lang, 'words.choose')
# Создаем кнопки для каждого слова (по 2 в ряд)
keyboard = []
@@ -143,12 +137,12 @@ async def show_extracted_words(message: Message, words: list):
# Кнопка "Добавить все"
keyboard.append([
InlineKeyboardButton(text="✅ Добавить все", callback_data="import_all_words")
InlineKeyboardButton(text=t(lang, 'words.add_all_btn'), callback_data="import_all_words")
])
# Кнопка "Закрыть"
keyboard.append([
InlineKeyboardButton(text="❌ Закрыть", callback_data="close_import")
InlineKeyboardButton(text=t(lang, 'words.close_btn'), callback_data="close_import")
])
reply_markup = InlineKeyboardMarkup(inline_keyboard=keyboard)
@@ -158,6 +152,8 @@ async def show_extracted_words(message: Message, words: list):
@router.callback_query(F.data.startswith("import_word_"), ImportStates.viewing_words)
async def import_single_word(callback: CallbackQuery, state: FSMContext):
"""Добавить одно слово из импорта"""
# Отвечаем сразу, операция может занять время
await callback.answer()
word_index = int(callback.data.split("_")[2])
data = await state.get_data()
@@ -171,6 +167,7 @@ async def import_single_word(callback: CallbackQuery, state: FSMContext):
word_data = words[word_index]
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
# Проверяем, нет ли уже такого слова
existing = await VocabularyService.get_word_by_original(
session, user_id, word_data['word']
@@ -181,24 +178,34 @@ async def import_single_word(callback: CallbackQuery, state: FSMContext):
return
# Добавляем слово
learn = user.learning_language if user else 'en'
ui = user.language_interface if user else 'ru'
ctx = word_data.get('context')
examples = ([{learn: ctx, ui: ''}] if ctx else [])
await VocabularyService.add_word(
session=session,
user_id=user_id,
word_original=word_data['word'],
word_translation=word_data['translation'],
source_lang=user.learning_language if user else None,
translation_lang=user.language_interface if user else None,
transcription=word_data.get('transcription'),
examples=[{"en": word_data.get('context', ''), "ru": ""}] if word_data.get('context') else [],
examples=examples,
source=WordSource.CONTEXT,
category='imported',
difficulty_level=data.get('level')
)
await callback.answer(f"✅ Слово '{word_data['word']}' добавлено в словарь")
lang = (user.language_interface if user else 'ru') or 'ru'
await callback.message.answer(t(lang, 'import.added_single', word=word_data['word']))
@router.callback_query(F.data == "import_all_words", ImportStates.viewing_words)
async def import_all_words(callback: CallbackQuery, state: FSMContext):
"""Добавить все слова из импорта"""
# Сразу отвечаем, так как операция может занять заметное время
await callback.answer()
data = await state.get_data()
words = data.get('words', [])
user_id = data.get('user_id')
@@ -207,6 +214,7 @@ async def import_all_words(callback: CallbackQuery, state: FSMContext):
skipped_count = 0
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
for word_data in words:
# Проверяем, нет ли уже такого слова
existing = await VocabularyService.get_word_by_original(
@@ -218,27 +226,34 @@ async def import_all_words(callback: CallbackQuery, state: FSMContext):
continue
# Добавляем слово
learn = user.learning_language if user else 'en'
ui = user.language_interface if user else 'ru'
ctx = word_data.get('context')
examples = ([{learn: ctx, ui: ''}] if ctx else [])
await VocabularyService.add_word(
session=session,
user_id=user_id,
word_original=word_data['word'],
word_translation=word_data['translation'],
source_lang=user.learning_language if user else None,
translation_lang=user.language_interface if user else None,
transcription=word_data.get('transcription'),
examples=[{"en": word_data.get('context', ''), "ru": ""}] if word_data.get('context') else [],
examples=examples,
source=WordSource.CONTEXT,
category='imported',
difficulty_level=data.get('level')
)
added_count += 1
result_text = f"✅ Добавлено слов: <b>{added_count}</b>"
lang = (user.language_interface if user else 'ru') or 'ru'
result_text = t(lang, 'import.added_count', n=added_count)
if skipped_count > 0:
result_text += f"\n⚠️ Пропущено (уже в словаре): {skipped_count}"
result_text += "\n" + t(lang, 'import.skipped_count', n=skipped_count)
await callback.message.edit_reply_markup(reply_markup=None)
await callback.message.answer(result_text)
await state.clear()
await callback.answer()
@router.callback_query(F.data == "close_import", ImportStates.viewing_words)

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):
"""Обработать ответ на вопрос"""

View File

@@ -7,6 +7,7 @@ from aiogram.fsm.state import State, StatesGroup
from database.db import async_session_maker
from services.user_service import UserService
from services.ai_service import ai_service
from utils.i18n import t
router = Router()
@@ -19,12 +20,12 @@ class PracticeStates(StatesGroup):
# Доступные сценарии
SCENARIOS = {
"restaurant": "🍽️ Ресторан",
"shopping": "🛍️ Магазин",
"travel": "✈️ Путешествие",
"work": "💼 Работа",
"doctor": "🏥 Врач",
"casual": "💬 Общение"
"restaurant": {"ru": "🍽️ Ресторан", "en": "🍽️ Restaurant", "ja": "🍽️ レストラン"},
"shopping": {"ru": "🛍️ Магазин", "en": "🛍️ Shopping", "ja": "🛍️ ショッピング"},
"travel": {"ru": "✈️ Путешествие","en": "✈️ Travel", "ja": "✈️ 旅行"},
"work": {"ru": "💼 Работа", "en": "💼 Work", "ja": "💼 仕事"},
"doctor": {"ru": "🏥 Врач", "en": "🏥 Doctor", "ja": "🏥 医者"},
"casual": {"ru": "💬 Общение", "en": "💬 Casual", "ja": "💬 会話"}
}
@@ -35,15 +36,16 @@ async def cmd_practice(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
# Показываем выбор сценария
keyboard = []
for scenario_id, scenario_name in SCENARIOS.items():
lang = user.language_interface or 'ru'
for scenario_id, names in SCENARIOS.items():
keyboard.append([
InlineKeyboardButton(
text=scenario_name,
text=names.get(lang, names.get('ru')),
callback_data=f"scenario_{scenario_id}"
)
])
@@ -53,33 +55,36 @@ async def cmd_practice(message: Message, state: FSMContext):
await state.update_data(user_id=user.id, level=user.level.value)
await state.set_state(PracticeStates.choosing_scenario)
await message.answer(
"💬 <b>Диалоговая практика с AI</b>\n\n"
"Выбери сценарий для разговора:\n\n"
"• AI будет играть роль собеседника\n"
"• Ты можешь общаться на английском\n"
"• AI будет исправлять твои ошибки\n"
"• Используй /stop для завершения диалога\n\n"
"Выбери сценарий:",
reply_markup=reply_markup
)
await message.answer(t(user.language_interface or 'ru', 'practice.start_text'), reply_markup=reply_markup)
@router.callback_query(F.data.startswith("scenario_"), PracticeStates.choosing_scenario)
async def start_scenario(callback: CallbackQuery, state: FSMContext):
"""Начать диалог с выбранным сценарием"""
# Отвечаем сразу на callback, дальнейшие операции могут занять время
await callback.answer()
scenario = callback.data.split("_")[1]
data = await state.get_data()
level = data.get('level', 'B1')
# Определяем языки пользователя
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
ui_lang = (user.language_interface if user else 'ru') or 'ru'
learn_lang = (user.learning_language if user else 'en') or 'en'
# Удаляем клавиатуру
await callback.message.edit_reply_markup(reply_markup=None)
# Показываем индикатор
thinking_msg = await callback.message.answer("🤔 AI готовится к диалогу...")
thinking_msg = await callback.message.answer(t(ui_lang, 'practice.thinking_prepare'))
# Начинаем диалог
conversation_start = await ai_service.start_conversation(scenario, level)
conversation_start = await ai_service.start_conversation(
scenario,
level,
learning_lang=learn_lang,
translation_lang=ui_lang
)
await thinking_msg.delete()
@@ -92,28 +97,30 @@ async def start_scenario(callback: CallbackQuery, state: FSMContext):
)
await state.set_state(PracticeStates.in_conversation)
# Формируем сообщение
# Формируем сообщение (перевод скрыт, доступен по кнопке)
text = (
f"<b>{SCENARIOS[scenario]}</b>\n\n"
f"<b>{SCENARIOS[scenario].get(ui_lang, SCENARIOS[scenario]['ru'])}</b>\n\n"
f"📝 <i>{conversation_start.get('context', '')}</i>\n\n"
f"<b>AI:</b> {conversation_start.get('message', '')}\n"
f"<i>({conversation_start.get('translation', '')})</i>\n\n"
"💡 <b>Подсказки:</b>\n"
f"<b>AI:</b> {conversation_start.get('message', '')}\n\n"
f"{t(ui_lang, 'practice.hints')}\n"
)
for suggestion in conversation_start.get('suggestions', []):
text += f"{suggestion}\n"
text += "\n📝 Напиши свой ответ на английском или используй /stop для завершения"
text += t(ui_lang, 'practice.write_or_stop')
# Сохраняем перевод под индексом 0
translations = {0: conversation_start.get('translation', '')}
await state.update_data(translations=translations)
# Кнопки управления
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💡 Показать подсказки", callback_data="show_hints")],
[InlineKeyboardButton(text="🔚 Завершить диалог", callback_data="stop_practice")]
[InlineKeyboardButton(text=t(ui_lang, 'practice.show_translation_btn'), callback_data="show_tr_0")],
[InlineKeyboardButton(text=t(ui_lang, 'practice.stop_btn'), callback_data="stop_practice")]
])
await callback.message.answer(text, reply_markup=keyboard)
await callback.answer()
@router.message(Command("stop"), PracticeStates.in_conversation)
@@ -123,12 +130,16 @@ async def stop_practice(message: Message, state: FSMContext):
message_count = data.get('message_count', 0)
await state.clear()
await message.answer(
f"✅ <b>Диалог завершён!</b>\n\n"
f"Сообщений обменено: <b>{message_count}</b>\n\n"
"Отличная работа! Продолжай практиковаться.\n"
"Используй /practice для нового диалога."
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'
end_text = (
t(lang, 'practice.end_title') + "\n\n" +
t(lang, 'practice.end_exchanged', n=message_count) + "\n\n" +
t(lang, 'practice.end_keep') + "\n" +
t(lang, 'practice.end_hint')
)
await message.answer(end_text)
@router.callback_query(F.data == "stop_practice", PracticeStates.in_conversation)
@@ -140,12 +151,16 @@ async def stop_practice_callback(callback: CallbackQuery, state: FSMContext):
await callback.message.delete()
await state.clear()
await callback.message.answer(
f"✅ <b>Диалог завершён!</b>\n\n"
f"Сообщений обменено: <b>{message_count}</b>\n\n"
"Отличная работа! Продолжай практиковаться.\n"
"Используй /practice для нового диалога."
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'
end_text = (
t(lang, 'practice.end_title') + "\n\n" +
t(lang, 'practice.end_exchanged', n=message_count) + "\n\n" +
t(lang, 'practice.end_keep') + "\n" +
t(lang, 'practice.end_hint')
)
await callback.message.answer(end_text)
await callback.answer()
@@ -155,7 +170,9 @@ async def handle_conversation(message: Message, state: FSMContext):
user_message = message.text.strip()
if not user_message:
await message.answer("Напиши что-нибудь на английском или используй /stop для завершения")
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', 'practice.empty_prompt'))
return
data = await state.get_data()
@@ -165,7 +182,7 @@ async def handle_conversation(message: Message, state: FSMContext):
message_count = data.get('message_count', 0)
# Показываем индикатор
thinking_msg = await message.answer("🤔 AI думает...")
thinking_msg = await message.answer(t((user2.language_interface if user2 else 'ru') or 'ru', 'practice.thinking'))
# Добавляем сообщение пользователя в историю
conversation_history.append({
@@ -173,12 +190,20 @@ async def handle_conversation(message: Message, state: FSMContext):
"content": user_message
})
# Определяем языки пользователя для ответа
async with async_session_maker() as session:
user2 = await UserService.get_user_by_telegram_id(session, message.from_user.id)
ui_lang2 = (user2.language_interface if user2 else 'ru') or 'ru'
learn_lang2 = (user2.learning_language if user2 else 'en') or 'en'
# Получаем ответ от AI
ai_response = await ai_service.continue_conversation(
conversation_history=conversation_history,
user_message=user_message,
scenario=scenario,
level=level
level=level,
learning_lang=learn_lang2,
translation_lang=ui_lang2
)
await thinking_msg.delete()
@@ -196,33 +221,76 @@ async def handle_conversation(message: Message, state: FSMContext):
message_count=message_count
)
# Формируем ответ
# Формируем ответ (перевод скрыт, доступен по кнопке)
# Язык пользователя для текста
text = ""
# Показываем feedback, если есть ошибки
feedback = ai_response.get('feedback', {})
if feedback.get('has_errors') and feedback.get('corrections'):
text += f"⚠️ <b>Исправления:</b>\n{feedback['corrections']}\n\n"
text += f"⚠️ {t(ui_lang2, 'practice.corrections')}\n{feedback['corrections']}\n\n"
if feedback.get('comment'):
text += f"💬 {feedback['comment']}\n\n"
# Ответ AI
text += (
f"<b>AI:</b> {ai_response.get('response', '')}\n"
f"<i>({ai_response.get('translation', '')})</i>\n\n"
f"<b>AI:</b> {ai_response.get('response', '')}\n\n"
)
# Подсказки
suggestions = ai_response.get('suggestions', [])
if suggestions:
text += "💡 <b>Подсказки:</b>\n"
text += t(ui_lang2, 'practice.hints') + "\n"
for suggestion in suggestions[:3]:
text += f"{suggestion}\n"
# Сохраняем перевод под новым индексом
translations = data.get('translations', {}) or {}
this_idx = message_count # после инкремента это текущий номер сообщения AI
translations[this_idx] = ai_response.get('translation', '')
await state.update_data(translations=translations)
# Кнопки
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔚 Завершить диалог", callback_data="stop_practice")]
[InlineKeyboardButton(text=t(lang2, 'practice.show_translation_btn'), callback_data=f"show_tr_{this_idx}")],
[InlineKeyboardButton(text=t(lang2, 'practice.stop_btn'), callback_data="stop_practice")]
])
await message.answer(text, reply_markup=keyboard)
@router.callback_query(F.data.startswith("show_tr_"), PracticeStates.in_conversation)
async def show_translation(callback: CallbackQuery, state: FSMContext):
"""Показать перевод для конкретного сообщения AI"""
try:
idx = int(callback.data.split("_")[-1])
except Exception:
await callback.answer(t((user.language_interface if user else 'ru') or 'ru', 'practice.translation_unavailable'), show_alert=True)
return
data = await state.get_data()
translations = data.get('translations', {}) or {}
tr_text = translations.get(idx)
if not tr_text:
await callback.answer(t((user.language_interface if user else 'ru') or 'ru', 'practice.translation_unavailable'), show_alert=True)
return
# Вставляем перевод в существующее сообщение
orig = callback.message.text or ""
# Определяем язык
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'
marker = t(lang, 'common.translation') + ":"
if marker in orig:
await callback.answer(t(lang, 'practice.translation_already'))
return
new_text = f"{orig}\n{marker} <i>{tr_text}</i>"
try:
await callback.message.edit_text(new_text, reply_markup=callback.message.reply_markup)
except Exception:
# Если не удалось отредактировать (например, старое сообщение), отправим отдельным сообщением как запасной вариант
await callback.message.answer(f"{marker} <i>{tr_text}</i>")
await callback.answer()

View File

@@ -6,6 +6,7 @@ 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
router = Router()
@@ -22,19 +23,19 @@ async def cmd_reminder(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
# Формируем текст
status = "✅ Включены" if user.reminders_enabled else "❌ Выключены"
time_text = user.daily_task_time if user.daily_task_time else "Не установлено"
lang = (user.language_interface if user else 'ru') or 'ru'
status = t(lang, 'reminder.status_on') if user.reminders_enabled else t(lang, 'reminder.status_off')
time_text = user.daily_task_time if user.daily_task_time else t(lang, 'reminder.time_not_set')
text = (
f"⏰ <b>Напоминания</b>\n\n"
f"Статус: {status}\n"
f"Время: {time_text} UTC\n\n"
f"Напоминания помогут не забывать о ежедневной практике.\n"
f"Бот будет присылать сообщение в выбранное время каждый день."
t(lang, 'reminder.title') + "\n\n" +
t(lang, 'reminder.status_line', status=status) + "\n" +
t(lang, 'reminder.time_line', time=time_text) + "\n\n" +
t(lang, 'reminder.desc1') + "\n" +
t(lang, 'reminder.desc2')
)
# Создаем кнопки
@@ -42,15 +43,15 @@ async def cmd_reminder(message: Message):
if user.reminders_enabled:
keyboard.append([
InlineKeyboardButton(text="❌ Выключить", callback_data="reminder_disable")
InlineKeyboardButton(text=t(lang, 'reminder.btn_disable'), callback_data="reminder_disable")
])
else:
keyboard.append([
InlineKeyboardButton(text="✅ Включить", callback_data="reminder_enable")
InlineKeyboardButton(text=t(lang, 'reminder.btn_enable'), callback_data="reminder_enable")
])
keyboard.append([
InlineKeyboardButton(text="⏰ Изменить время", callback_data="reminder_set_time")
InlineKeyboardButton(text=t(lang, 'reminder.btn_change_time'), callback_data="reminder_set_time")
])
reply_markup = InlineKeyboardMarkup(inline_keyboard=keyboard)
@@ -65,7 +66,7 @@ async def enable_reminders(callback: CallbackQuery):
if not user.daily_task_time:
await callback.answer(
"Сначала установи время напоминаний!",
t(user.language_interface or 'ru', 'reminder.set_time_first'),
show_alert=True
)
return
@@ -73,11 +74,12 @@ async def enable_reminders(callback: CallbackQuery):
user.reminders_enabled = True
await session.commit()
await callback.answer("✅ Напоминания включены!")
lang = (user.language_interface if user else 'ru') or 'ru'
await callback.answer(t(lang, 'reminder.enabled_toast'))
await callback.message.edit_text(
f"✅ <b>Напоминания включены!</b>\n\n"
f"Время: {user.daily_task_time} UTC\n\n"
f"Ты будешь получать ежедневные напоминания о практике."
t(lang, 'reminder.enabled_title') + "\n\n" +
t(lang, 'reminder.time_line', time=user.daily_task_time) + "\n\n" +
t(lang, 'reminder.enabled_desc')
)
@@ -90,10 +92,11 @@ async def disable_reminders(callback: CallbackQuery):
user.reminders_enabled = False
await session.commit()
await callback.answer("❌ Напоминания выключены")
lang = (user.language_interface if user else 'ru') or 'ru'
await callback.answer(t(lang, 'reminder.disabled_toast'))
await callback.message.edit_text(
"❌ <b>Напоминания выключены</b>\n\n"
"Используй /reminder чтобы включить их снова."
t(lang, 'reminder.disabled_title') + "\n\n" +
t(lang, 'reminder.disabled_desc')
)
@@ -102,16 +105,15 @@ async def set_reminder_time_prompt(callback: CallbackQuery, state: FSMContext):
"""Запросить время для напоминаний"""
await state.set_state(ReminderStates.waiting_for_time)
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(
"⏰ <b>Установка времени напоминаний</b>\n\n"
"Отправь время в формате <b>HH:MM</b> (UTC)\n\n"
"Примеры:\n"
"• <code>09:00</code> - 9 утра по UTC\n"
"• <code>18:30</code> - 18:30 по UTC\n"
"• <code>20:00</code> - 8 вечера по UTC\n\n"
"💡 UTC = МСК - 3 часа\n"
"(если хочешь 12:00 по МСК, введи 09:00)\n\n"
"Отправь /cancel для отмены"
t(lang, 'reminder.set_title') + "\n\n" +
t(lang, 'reminder.set_desc') + "\n\n" +
t(lang, 'reminder.set_examples') + "\n\n" +
t(lang, 'reminder.set_utc_hint') + "\n\n" +
t(lang, 'reminder.cancel_hint')
)
await callback.answer()
@@ -120,7 +122,7 @@ async def set_reminder_time_prompt(callback: CallbackQuery, state: FSMContext):
async def cancel_set_time(message: Message, state: FSMContext):
"""Отменить установку времени"""
await state.clear()
await message.answer("❌ Установка времени отменена")
await message.answer(t('ru', 'reminder.cancelled'))
@router.message(ReminderStates.waiting_for_time)
@@ -143,11 +145,7 @@ async def process_reminder_time(message: Message, state: FSMContext):
formatted_time = f"{hour:02d}:{minute:02d}"
except:
await message.answer(
"❌ Неверный формат времени!\n\n"
"Используй формат <b>HH:MM</b> (например, 09:00 или 18:30)\n"
"Или отправь /cancel для отмены"
)
await message.answer(t('ru', 'reminder.invalid_format'))
return
# Сохраняем время
@@ -163,10 +161,13 @@ async def process_reminder_time(message: Message, state: FSMContext):
await state.clear()
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(
f"✅ <b>Время установлено!</b>\n\n"
f"Напоминания: <b>{formatted_time} UTC</b>\n"
f"Статус: <b>Включены</b>\n\n"
f"Ты будешь получать ежедневные напоминания о практике.\n"
f"Используй /reminder для изменения настроек."
t(lang, 'reminder.time_set_title') + "\n\n" +
t(lang, 'reminder.time_line', time=formatted_time) + "\n" +
t(lang, 'reminder.status_on_line') + "\n\n" +
t(lang, 'reminder.enabled_desc') + "\n" +
t(lang, 'reminder.use_settings')
)

View File

@@ -5,60 +5,130 @@ 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
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)
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text=f"📊 Уровень: {user.level.value}",
text=(
"📊 Level: " if is_en else ("📊 レベル: " if is_ja else "📊 Уровень: ")
) + f"{user.level.value}",
callback_data="settings_level"
)],
[InlineKeyboardButton(
text=f"🌐 Язык интерфейса: {'🇷🇺 Русский' if user.language_interface == 'ru' else '🇬🇧 English'}",
text=(
"🎯 Learning language: " if is_en else ("🎯 学習言語: " if is_ja else "🎯 Язык изучения: ")
) + (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 "🇷🇺 Русский")),
callback_data="settings_language"
)],
[InlineKeyboardButton(
text="❌ Закрыть",
text=("❌ Close" if is_en else ("❌ 閉じる" if is_ja else "❌ Закрыть")),
callback_data="settings_close"
)]
])
return keyboard
def get_level_keyboard() -> InlineKeyboardMarkup:
def get_level_keyboard(user=None) -> InlineKeyboardMarkup:
"""Клавиатура выбора уровня"""
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"),
]
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"),
]
keyboard = []
for level_name, callback_data in levels:
keyboard.append([InlineKeyboardButton(text=level_name, callback_data=callback_data)])
keyboard.append([InlineKeyboardButton(text="⬅️ Назад", callback_data="settings_back")])
back_label = "⬅️ Back" if lang == 'en' else ("⬅️ 戻る" if lang == 'ja' else "⬅️ Назад")
keyboard.append([InlineKeyboardButton(text=back_label, callback_data="settings_back")])
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_language_keyboard() -> InlineKeyboardMarkup:
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 "⬅️ Назад")
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🇷🇺 Русский", callback_data="set_lang_ru")],
[InlineKeyboardButton(text="🇬🇧 English (скоро)", callback_data="set_lang_en")],
[InlineKeyboardButton(text="⬅️ Назад", callback_data="settings_back")]
[InlineKeyboardButton(text="🇬🇧 English", callback_data="set_lang_en")],
[InlineKeyboardButton(text="🇯🇵 日本語", callback_data="set_lang_ja")],
[InlineKeyboardButton(text=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 "⬅️ Назад")
# Пары (код -> подпись)
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 "🇯🇵 Японский")),
]
keyboard = [[InlineKeyboardButton(text=label, callback_data=f"set_learning_{code}")] for code, label in options]
keyboard.append([InlineKeyboardButton(text=back, callback_data="settings_back")])
return InlineKeyboardMarkup(inline_keyboard=keyboard)
@router.message(Command("settings"))
async def cmd_settings(message: Message):
"""Обработчик команды /settings"""
@@ -69,12 +139,13 @@ async def cmd_settings(message: Message):
await message.answer("Сначала запусти бота командой /start")
return
settings_text = (
"⚙️ <b>Настройки</b>\n\n"
f"📊 Уровень английского: <b>{user.level.value}</b>\n"
f"🌐 Язык интерфейса: <b>{'Русский' if user.language_interface == 'ru' else 'English'}</b>\n\n"
"Выбери, что хочешь изменить:"
)
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}"
await message.answer(settings_text, reply_markup=get_settings_keyboard(user))
@@ -82,14 +153,53 @@ async def cmd_settings(message: Message):
@router.callback_query(F.data == "settings_level")
async def settings_level(callback: CallbackQuery):
"""Показать выбор уровня"""
await callback.message.edit_text(
"📊 <b>Выбери свой уровень английского:</b>\n\n"
"<b>A1-A2</b> - Начинающий\n"
"<b>B1-B2</b> - Средний\n"
"<b>C1-C2</b> - Продвинутый\n\n"
"Это влияет на сложность предлагаемых слов и заданий.",
reply_markup=get_level_keyboard()
)
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))
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 = 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))
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 = 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 = "⬅️ К настройкам"
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text=back, callback_data="settings_back")]])
)
await callback.answer()
@@ -105,12 +215,13 @@ async def set_level(callback: CallbackQuery):
# Обновляем уровень
await UserService.update_user_level(session, user.id, LanguageLevel[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 "⬅️ К настройкам")
await callback.message.edit_text(
f"✅ Уровень изменен на <b>{level_str}</b>\n\n"
"Теперь ты будешь получать слова и задания, соответствующие твоему уровню!",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="⬅️ К настройкам", callback_data="settings_back")]
])
msg,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text=back, callback_data="settings_back")]])
)
await callback.answer()
@@ -119,10 +230,14 @@ async def set_level(callback: CallbackQuery):
@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 = 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 "Это изменит язык всех сообщений бота.")
await callback.message.edit_text(
"🌐 <b>Выбери язык интерфейса:</b>\n\n"
"Это изменит язык всех сообщений бота.",
reply_markup=get_language_keyboard()
title + desc,
reply_markup=get_language_keyboard(user)
)
await callback.answer()
@@ -130,24 +245,29 @@ 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
if lang == "en":
await callback.answer("Английский интерфейс скоро будет доступен! 🚧", show_alert=True)
return
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 callback.message.edit_text(
f"✅ Язык интерфейса: <b>{'Русский' if lang == 'ru' else 'English'}</b>",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="⬅️ К настройкам", callback_data="settings_back")]
])
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text=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.answer()
@@ -159,12 +279,13 @@ async def settings_back(callback: CallbackQuery):
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
if user:
settings_text = (
"⚙️ <b>Настройки</b>\n\n"
f"📊 Уровень английского: <b>{user.level.value}</b>\n"
f"🌐 Язык интерфейса: <b>{'Русский' if user.language_interface == 'ru' else 'English'}</b>\n\n"
"Выбери, что хочешь изменить:"
)
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}"
await callback.message.edit_text(settings_text, reply_markup=get_settings_keyboard(user))

View File

@@ -12,40 +12,30 @@ from aiogram.fsm.context import FSMContext
from database.db import async_session_maker
from services.user_service import UserService
from utils.i18n import t
router = Router()
# Тексты кнопок главного меню
BTN_ADD = " Добавить слово"
BTN_VOCAB = "📚 Словарь"
BTN_TASK = "🧠 Задание"
BTN_PRACTICE = "💬 Практика"
BTN_WORDS = "🎯 Тематические слова"
BTN_IMPORT = "📖 Импорт из текста"
BTN_STATS = "📊 Статистика"
BTN_SETTINGS = "⚙️ Настройки"
def main_menu_keyboard() -> ReplyKeyboardMarkup:
def main_menu_keyboard(lang: str = 'ru') -> ReplyKeyboardMarkup:
"""Клавиатура с основными командами (кнопки отправляют команды)."""
return ReplyKeyboardMarkup(
resize_keyboard=True,
keyboard=[
[
KeyboardButton(text=BTN_ADD),
KeyboardButton(text=BTN_VOCAB),
KeyboardButton(text=t(lang, "menu.add")),
KeyboardButton(text=t(lang, "menu.vocab")),
],
[
KeyboardButton(text=BTN_TASK),
KeyboardButton(text=BTN_PRACTICE),
KeyboardButton(text=t(lang, "menu.task")),
KeyboardButton(text=t(lang, "menu.practice")),
],
[
KeyboardButton(text=BTN_WORDS),
KeyboardButton(text=BTN_IMPORT),
KeyboardButton(text=t(lang, "menu.words")),
KeyboardButton(text=t(lang, "menu.import")),
],
[
KeyboardButton(text=BTN_STATS),
KeyboardButton(text=BTN_SETTINGS),
KeyboardButton(text=t(lang, "menu.stats")),
KeyboardButton(text=t(lang, "menu.settings")),
],
],
)
@@ -66,129 +56,112 @@ async def cmd_start(message: Message, state: FSMContext):
username=message.from_user.username
)
lang = (user.language_interface or 'ru')
if is_new_user:
# Новый пользователь
await message.answer(
f"👋 Привет, {message.from_user.first_name}!\n\n"
f"Я бот для изучения английского языка. Помогу тебе:\n"
f"📚 Пополнять словарный запас (ручное/тематическое/из текста)\n"
f"✍️ Выполнять интерактивные задания\n"
f"💬 Практиковать язык в диалоге с AI\n"
f"📊 Отслеживать свой прогресс\n\n"
f"<b>Команды:</b>\n"
f"• /add [слово] - добавить слово\n"
f"• /words [тема] - тематическая подборка\n"
f"• /import - импорт из текста\n"
f"• /vocabulary - мой словарь\n"
f"• /task - задания\n"
f"• /practice - диалог с AI\n"
f"• /stats - статистика\n"
f"• /settings - настройки\n"
f"• /reminder - напоминания\n"
f"• /help - полная справка",
reply_markup=main_menu_keyboard(),
t(lang, "start.new_intro", first_name=message.from_user.first_name),
reply_markup=main_menu_keyboard(lang),
)
# Предлагаем пройти тест уровня
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📊 Пройти тест уровня", callback_data="offer_level_test")],
[InlineKeyboardButton(text="➡️ Пропустить", callback_data="skip_level_test")]
[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 message.answer(
"🎯 <b>Определим твой уровень?</b>\n\n"
"Короткий тест (7 вопросов) поможет подобрать задания под твой уровень.\n"
"Это займёт 2-3 минуты.\n\n"
"Или можешь пропустить и установить уровень вручную позже в /settings",
reply_markup=keyboard
)
await message.answer(t(lang, "start.offer_test"), reply_markup=keyboard)
else:
# Существующий пользователь
await message.answer(
f"С возвращением, {message.from_user.first_name}! 👋\n\n"
f"Готов продолжить обучение?\n\n"
f"<b>Быстрый доступ:</b>\n"
f"• /vocabulary - посмотреть словарь\n"
f"• /task - получить задание\n"
f"• /practice - практика диалога\n"
f"• /words [тема] - тематическая подборка\n"
f"• /stats - статистика\n"
f"• /help - все команды",
reply_markup=main_menu_keyboard(),
t(lang, "start.return", first_name=message.from_user.first_name),
reply_markup=main_menu_keyboard(lang),
)
@router.message(Command("menu"))
async def cmd_menu(message: Message):
"""Показать клавиатуру с основными командами."""
await message.answer("Главное меню доступно ниже ⤵️", reply_markup=main_menu_keyboard())
# Определяем язык пользователя
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))
# Обработчики кнопок главного меню (по тексту)
@router.message(F.text == BTN_ADD)
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):
from bot.handlers.vocabulary import AddWordStates
await message.answer(
"Отправь слово, которое хочешь добавить:\n"
"Например: <code>/add elephant</code>\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, 'add.prompt'))
await state.set_state(AddWordStates.waiting_for_word)
@router.message(F.text == BTN_VOCAB)
@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(F.text == BTN_TASK)
@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(F.text == BTN_PRACTICE)
@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(F.text == BTN_IMPORT)
@router.message(_menu_match('menu.import'))
async def btn_import_pressed(message: Message, state: FSMContext):
from bot.handlers.import_text import cmd_import
await cmd_import(message, state)
@router.message(F.text == BTN_STATS)
@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(F.text == BTN_SETTINGS)
@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(F.text == BTN_WORDS)
@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 = (
"📚 <b>Тематические подборки слов</b>\n\n"
"Используй: <code>/words [тема]</code>\n\n"
"Популярные темы:"
t(lang, 'words.help_title') + "\n\n" +
t(lang, 'words.help_usage') + "\n\n" +
t(lang, 'words.popular')
)
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="✈️ Путешествия", callback_data="menu_theme_travel")],
[InlineKeyboardButton(text="🍔 Еда", callback_data="menu_theme_food")],
[InlineKeyboardButton(text="💼 Работа", callback_data="menu_theme_work")],
[InlineKeyboardButton(text="🌿 Природа", callback_data="menu_theme_nature")],
[InlineKeyboardButton(text="💻 Технологии", callback_data="menu_theme_technology")],
[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)
@@ -201,21 +174,29 @@ async def pick_theme_from_menu(callback: CallbackQuery, state: FSMContext):
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("Сначала /start", show_alert=True)
await callback.answer(t('ru', 'common.start_first'), show_alert=True)
return
generating = await callback.message.answer(f"🔄 Генерирую подборку слов по теме '{theme}'...")
words = await ai_service.generate_thematic_words(theme=theme, level=user.level.value, count=10)
lang = (user.language_interface or 'ru')
generating = await callback.message.answer(t(lang, 'words.generating', theme=theme))
words = await ai_service.generate_thematic_words(
theme=theme,
level=user.level.value,
count=10,
learning_lang=user.learning_language,
translation_lang=user.language_interface,
)
await generating.delete()
if not words:
await callback.message.answer("Не удалось сгенерировать подборку. Попробуй позже.")
await callback.answer()
await callback.message.answer(t(lang, 'words.generate_failed'))
return
# Сохраняем в состояние как в /words
@@ -223,30 +204,16 @@ async def pick_theme_from_menu(callback: CallbackQuery, state: FSMContext):
await state.set_state(WordsStates.viewing_words)
await show_words_list(callback.message, words, theme)
await callback.answer()
@router.message(Command("help"))
async def cmd_help(message: Message):
"""Обработчик команды /help"""
await message.answer(
"<b>📖 Справка по командам:</b>\n\n"
"<b>Управление словарём:</b>\n"
"• /add [слово] - добавить слово в словарь\n"
"• /vocabulary - просмотр словаря\n"
"• /words [тема] - тематическая подборка слов\n"
"• /import - импортировать слова из текста\n\n"
"<b>Обучение:</b>\n"
"• /task - задание (перевод, заполнение пропусков)\n"
"• /practice - диалог с ИИ (6 сценариев)\n"
"• /level_test - тест определения уровня\n\n"
"<b>Статистика:</b>\n"
"• /stats - твой прогресс\n\n"
"<b>Настройки:</b>\n"
"• /settings - уровень и язык\n"
"• /reminder - ежедневные напоминания\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, "start.help"))
@router.callback_query(F.data == "offer_level_test")
@@ -261,13 +228,8 @@ async def offer_level_test_callback(callback: CallbackQuery, state: FSMContext):
@router.callback_query(F.data == "skip_level_test")
async def skip_level_test_callback(callback: CallbackQuery):
"""Пропустить тест уровня"""
await callback.message.edit_text(
"✅ Хорошо!\n\n"
"Ты можешь пройти тест позже командой /level_test\n"
"или установить уровень вручную в /settings\n\n"
"Давай начнём! Попробуй:\n"
"• /words travel - тематическая подборка\n"
"• /practice - диалог с AI\n"
"• /add hello - добавить слово"
)
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()

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)

View File

@@ -9,6 +9,7 @@ from database.models import WordSource
from services.user_service import UserService
from services.vocabulary_service import VocabularyService
from services.ai_service import ai_service
from utils.i18n import t
router = Router()
@@ -26,11 +27,10 @@ async def cmd_add(message: Message, state: FSMContext):
parts = message.text.split(maxsplit=1)
if len(parts) < 2:
await message.answer(
"Отправь слово, которое хочешь добавить:\n"
"Например: <code>/add elephant</code>\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, 'add.prompt'))
await state.set_state(AddWordStates.waiting_for_word)
return
@@ -52,7 +52,7 @@ async def process_word_addition(message: Message, state: FSMContext, word: str):
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
# Проверяем, есть ли уже такое слово
@@ -66,10 +66,17 @@ async def process_word_addition(message: Message, state: FSMContext, word: str):
return
# Показываем индикатор загрузки
processing_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'
processing_msg = await message.answer(t(lang, 'add.searching'))
# Получаем перевод через AI
word_data = await ai_service.translate_word(word)
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
source_lang = user.learning_language if user else 'en'
ui_lang = user.language_interface if user else 'ru'
word_data = await ai_service.translate_word(word, source_lang=source_lang, translation_lang=ui_lang)
# Удаляем сообщение о загрузке
await processing_msg.delete()
@@ -77,26 +84,28 @@ async def process_word_addition(message: Message, state: FSMContext, word: str):
# Формируем примеры
examples_text = ""
if word_data.get("examples"):
examples_text = "\n\n<b>Примеры:</b>\n"
examples_text = "\n\n" + t(lang, 'add.examples_header') + "\n"
for idx, example in enumerate(word_data["examples"][:2], 1):
examples_text += f"{idx}. {example['en']}\n <i>{example['ru']}</i>\n"
src = example.get(source_lang) or example.get('en') or example.get('ru') or ''
tr = example.get(ui_lang) or example.get('ru') or example.get('en') or ''
examples_text += f"{idx}. {src}\n <i>{tr}</i>\n"
# Отправляем карточку слова
card_text = (
f"📝 <b>{word_data['word']}</b>\n"
f"🔊 [{word_data.get('transcription', '')}]\n\n"
f"🇷🇺 {word_data['translation']}\n"
f"📂 Категория: {word_data.get('category', 'общая')}\n"
f"📊 Уровень: {word_data.get('difficulty', 'A1')}"
f"{t(lang, 'add.translation_label')}: {word_data['translation']}\n"
f"{t(lang, 'add.category_label')}: {word_data.get('category', '')}\n"
f"{t(lang, 'add.level_label')}: {word_data.get('difficulty', 'A1')}"
f"{examples_text}\n\n"
f"Добавить это слово в словарь?"
f"{t(lang, 'add.confirm_question')}"
)
# Создаём inline-кнопки
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✅ Добавить", callback_data=f"add_word_confirm"),
InlineKeyboardButton(text="❌ Отмена", callback_data="add_word_cancel")
InlineKeyboardButton(text=t(lang, 'add.btn_add'), callback_data=f"add_word_confirm"),
InlineKeyboardButton(text=t(lang, 'add.btn_cancel'), callback_data="add_word_cancel")
]
])
@@ -110,6 +119,8 @@ async def process_word_addition(message: Message, state: FSMContext, word: str):
@router.callback_query(F.data == "add_word_confirm", AddWordStates.waiting_for_confirmation)
async def confirm_add_word(callback: CallbackQuery, state: FSMContext):
"""Подтверждение добавления слова"""
# Отвечаем сразу, запись в БД и подсчёт могут занять время
await callback.answer()
data = await state.get_data()
word_data = data.get("word_data")
user_id = data.get("user_id")
@@ -121,6 +132,8 @@ async def confirm_add_word(callback: CallbackQuery, state: FSMContext):
user_id=user_id,
word_original=word_data["word"],
word_translation=word_data["translation"],
source_lang=source_lang,
translation_lang=ui_lang,
transcription=word_data.get("transcription"),
examples={"examples": word_data.get("examples", [])},
category=word_data.get("category"),
@@ -129,22 +142,26 @@ async def confirm_add_word(callback: CallbackQuery, state: FSMContext):
)
# Получаем общее количество слов
words_count = await VocabularyService.get_words_count(session, user_id)
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
words_count = await VocabularyService.get_words_count(session, user_id, learning_lang=user.learning_language)
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(
f"✅ Слово '<b>{word_data['word']}</b>' добавлено в твой словарь!\n\n"
f"Всего слов в словаре: {words_count}\n\n"
f"Продолжай добавлять новые слова или используй /task для практики!"
t(lang, 'add.added_success', word=word_data['word'], count=words_count)
)
await state.clear()
await callback.answer()
@router.callback_query(F.data == "add_word_cancel")
async def cancel_add_word(callback: CallbackQuery, state: FSMContext):
"""Отмена добавления слова"""
await callback.message.edit_text("Отменено. Можешь добавить другое слово командой /add")
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, 'add.cancelled'))
await state.clear()
await callback.answer()
@@ -156,27 +173,26 @@ async def cmd_vocabulary(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
# Получаем слова пользователя
words = await VocabularyService.get_user_words(session, user.id, limit=10)
total_count = await VocabularyService.get_words_count(session, user.id)
words = await VocabularyService.get_user_words(session, user.id, limit=10, learning_lang=user.learning_language)
total_count = await VocabularyService.get_words_count(session, user.id, learning_lang=user.learning_language)
if not words:
await message.answer(
"📚 Твой словарь пока пуст!\n\n"
"Добавь первое слово командой /add или просто отправь мне слово."
)
lang = (user.language_interface if user else 'ru') or 'ru'
await message.answer(t(lang, 'vocab.empty'))
return
# Формируем список слов
words_list = "<b>📚 Твой словарь:</b>\n\n"
lang = (user.language_interface if user else 'ru') or 'ru'
words_list = t(lang, 'vocab.header') + "\n\n"
for idx, word in enumerate(words, 1):
progress = ""
if word.times_reviewed > 0:
accuracy = int((word.correct_answers / word.times_reviewed) * 100)
progress = f" ({accuracy}% точность)"
progress = " " + t(lang, 'vocab.accuracy_inline', n=accuracy)
words_list += (
f"{idx}. <b>{word.word_original}</b> — {word.word_translation}\n"
@@ -184,8 +200,8 @@ async def cmd_vocabulary(message: Message):
)
if total_count > 10:
words_list += f"\n<i>Показаны последние 10 из {total_count} слов</i>"
words_list += "\n" + t(lang, 'vocab.shown_last', n=total_count)
else:
words_list += f"\n<i>Всего слов: {total_count}</i>"
words_list += "\n" + t(lang, 'vocab.total', n=total_count)
await message.answer(words_list)

View File

@@ -9,6 +9,7 @@ from database.models import WordSource
from services.user_service import UserService
from services.vocabulary_service import VocabularyService
from services.ai_service import ai_service
from utils.i18n import t
router = Router()
@@ -25,44 +26,42 @@ async def cmd_words(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
# Извлекаем тему из команды
command_parts = message.text.split(maxsplit=1)
if len(command_parts) < 2:
await message.answer(
"📚 <b>Тематические подборки слов</b>\n\n"
"Используй: <code>/words [тема]</code>\n\n"
"Примеры:\n"
"• <code>/words travel</code> - путешествия\n"
"• <code>/words food</code> - еда\n"
"• <code>/words work</code> - работа\n"
"• <code>/words nature</code> - природа\n"
"• <code>/words technology</code> - технологии\n\n"
"Я сгенерирую 10 слов по теме, подходящих для твоего уровня!"
lang = user.language_interface or 'ru'
help_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')
)
await message.answer(help_text)
return
theme = command_parts[1].strip()
# Показываем индикатор генерации
generating_msg = await message.answer(f"🔄 Генерирую подборку слов по теме '{theme}'...")
lang = user.language_interface or 'ru'
generating_msg = await message.answer(t(lang, 'words.generating', theme=theme))
# Генерируем слова через AI
words = await ai_service.generate_thematic_words(
theme=theme,
level=user.level.value,
count=10
count=10,
learning_lang=user.learning_language,
translation_lang=user.language_interface,
)
await generating_msg.delete()
if not words:
await message.answer(
"Не удалось сгенерировать подборку. Попробуй другую тему или повтори позже."
)
await message.answer(t(lang, 'words.generate_failed'))
return
# Сохраняем данные в состоянии
@@ -81,7 +80,12 @@ async def cmd_words(message: Message, state: FSMContext):
async def show_words_list(message: Message, words: list, theme: str):
"""Показать список слов с кнопками для добавления"""
text = f"📚 <b>Подборка слов: {theme}</b>\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'
text = t(lang, 'words.header', theme=theme) + "\n\n"
for idx, word_data in enumerate(words, 1):
text += (
@@ -91,7 +95,7 @@ async def show_words_list(message: Message, words: list, theme: str):
f" <i>{word_data.get('example', '')}</i>\n\n"
)
text += "Выбери слова, которые хочешь добавить в словарь:"
text += t(lang, 'words.choose')
# Создаем кнопки для каждого слова (по 2 в ряд)
keyboard = []
@@ -109,12 +113,12 @@ async def show_words_list(message: Message, words: list, theme: str):
# Кнопка "Добавить все"
keyboard.append([
InlineKeyboardButton(text="✅ Добавить все", callback_data="add_all_words")
InlineKeyboardButton(text=t(lang, 'words.add_all_btn'), callback_data="add_all_words")
])
# Кнопка "Закрыть"
keyboard.append([
InlineKeyboardButton(text="❌ Закрыть", callback_data="close_words")
InlineKeyboardButton(text=t(lang, 'words.close_btn'), callback_data="close_words")
])
reply_markup = InlineKeyboardMarkup(inline_keyboard=keyboard)
@@ -124,6 +128,8 @@ async def show_words_list(message: Message, words: list, theme: str):
@router.callback_query(F.data.startswith("add_word_"), WordsStates.viewing_words)
async def add_single_word(callback: CallbackQuery, state: FSMContext):
"""Добавить одно слово из подборки"""
# Отвечаем сразу, операция может занять время
await callback.answer()
word_index = int(callback.data.split("_")[2])
data = await state.get_data()
@@ -131,40 +137,60 @@ async def add_single_word(callback: CallbackQuery, state: FSMContext):
user_id = data.get('user_id')
if word_index >= len(words):
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.answer(t(lang, 'words.err_not_found'))
return
word_data = words[word_index]
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
# Проверяем, нет ли уже такого слова
existing = await VocabularyService.get_word_by_original(
session, user_id, word_data['word']
)
if existing:
await callback.answer(f"Слово '{word_data['word']}' уже в словаре", show_alert=True)
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.answer(t(lang, 'words.already_exists', word=word_data['word']), show_alert=True)
return
# Добавляем слово
# Формируем examples с учётом языков
learn = user.learning_language if user else 'en'
ui = user.language_interface if user else 'ru'
ex = word_data.get('example')
examples = ([{learn: ex, ui: ''}] if ex else [])
await VocabularyService.add_word(
session=session,
user_id=user_id,
word_original=word_data['word'],
word_translation=word_data['translation'],
source_lang=user.learning_language if user else None,
translation_lang=user.language_interface if user else None,
transcription=word_data.get('transcription'),
examples=[{"en": word_data.get('example', ''), "ru": ""}] if word_data.get('example') else [],
examples=examples,
source=WordSource.SUGGESTED,
category=data.get('theme', 'general'),
difficulty_level=data.get('level')
)
await callback.answer(f"✅ Слово '{word_data['word']}' добавлено в словарь")
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, 'words.added_single', word=word_data['word']))
@router.callback_query(F.data == "add_all_words", WordsStates.viewing_words)
async def add_all_words(callback: CallbackQuery, state: FSMContext):
"""Добавить все слова из подборки"""
# Сразу отвечаем на callback, так как добавление может занять время
await callback.answer()
data = await state.get_data()
words = data.get('words', [])
user_id = data.get('user_id')
@@ -174,6 +200,7 @@ async def add_all_words(callback: CallbackQuery, state: FSMContext):
skipped_count = 0
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
for word_data in words:
# Проверяем, нет ли уже такого слова
existing = await VocabularyService.get_word_by_original(
@@ -185,13 +212,20 @@ async def add_all_words(callback: CallbackQuery, state: FSMContext):
continue
# Добавляем слово
learn = user.learning_language if user else 'en'
ui = user.language_interface if user else 'ru'
ex = word_data.get('example')
examples = ([{learn: ex, ui: ''}] if ex else [])
await VocabularyService.add_word(
session=session,
user_id=user_id,
word_original=word_data['word'],
word_translation=word_data['translation'],
source_lang=user.learning_language if user else None,
translation_lang=user.language_interface if user else None,
transcription=word_data.get('transcription'),
examples=[{"en": word_data.get('example', ''), "ru": ""}] if word_data.get('example') else [],
examples=examples,
source=WordSource.SUGGESTED,
category=theme,
difficulty_level=data.get('level')
@@ -205,7 +239,6 @@ async def add_all_words(callback: CallbackQuery, state: FSMContext):
await callback.message.edit_reply_markup(reply_markup=None)
await callback.message.answer(result_text)
await state.clear()
await callback.answer()
@router.callback_query(F.data == "close_words", WordsStates.viewing_words)