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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user