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)