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,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)