diff --git a/bot/handlers/import_text.py b/bot/handlers/import_text.py index 28a6241..5b71dec 100644 --- a/bot/handlers/import_text.py +++ b/bot/handlers/import_text.py @@ -10,7 +10,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, get_user_lang +from utils.i18n import t, get_user_lang, get_user_translation_lang from utils.levels import get_user_level_for_language router = Router() @@ -87,7 +87,7 @@ async def process_text(message: Message, state: FSMContext): level=current_level, max_words=15, learning_lang=user.learning_language, - translation_lang=user.language_interface, + translation_lang=get_user_translation_lang(user), ) await processing_msg.delete() @@ -176,27 +176,29 @@ async def import_single_word(callback: CallbackQuery, state: FSMContext): user_id = data.get('user_id') if word_index >= len(words): - await callback.answer("❌ Ошибка: слово не найдено") + await callback.answer(t('ru', '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) + lang = get_user_lang(user) + # Проверяем, нет ли уже такого слова 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) + await callback.answer(t(lang, 'words.already_exists', word=word_data['word']), show_alert=True) return # Добавляем слово learn = user.learning_language if user else 'en' - ui = user.language_interface if user else 'ru' + translation_lang = get_user_translation_lang(user) ctx = word_data.get('context') - examples = ([{learn: ctx, ui: ''}] if ctx else []) + examples = ([{learn: ctx, translation_lang: ''}] if ctx else []) await VocabularyService.add_word( session=session, @@ -204,7 +206,7 @@ async def import_single_word(callback: CallbackQuery, state: FSMContext): 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, + translation_lang=translation_lang, transcription=word_data.get('transcription'), examples=examples, source=WordSource.CONTEXT, @@ -242,9 +244,9 @@ async def import_all_words(callback: CallbackQuery, state: FSMContext): # Добавляем слово learn = user.learning_language if user else 'en' - ui = user.language_interface if user else 'ru' + translation_lang = get_user_translation_lang(user) ctx = word_data.get('context') - examples = ([{learn: ctx, ui: ''}] if ctx else []) + examples = ([{learn: ctx, translation_lang: ''}] if ctx else []) await VocabularyService.add_word( session=session, @@ -252,7 +254,7 @@ async def import_all_words(callback: CallbackQuery, state: FSMContext): 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, + translation_lang=translation_lang, transcription=word_data.get('transcription'), examples=examples, source=WordSource.CONTEXT, @@ -389,7 +391,7 @@ async def handle_file_import(message: Message, state: FSMContext, bot: Bot): translations = await ai_service.translate_words_batch( words=words_to_translate, source_lang=user.learning_language, - translation_lang=user.language_interface + translation_lang=get_user_translation_lang(user) ) await processing_msg.delete() @@ -490,7 +492,7 @@ async def import_file_all_words(callback: CallbackQuery, state: FSMContext): word_original=word_data['word'], word_translation=word_data.get('translation', ''), source_lang=user.learning_language if user else None, - translation_lang=user.language_interface if user else None, + translation_lang=get_user_translation_lang(user), transcription=word_data.get('transcription'), source=WordSource.IMPORT ) diff --git a/bot/handlers/level_test.py b/bot/handlers/level_test.py index fd9e77e..15ac947 100644 --- a/bot/handlers/level_test.py +++ b/bot/handlers/level_test.py @@ -24,11 +24,14 @@ async def cmd_level_test(message: Message, state: FSMContext): await start_level_test(message, state) -async def start_level_test(message: Message, state: FSMContext): +async def start_level_test(message: Message, state: FSMContext, telegram_id: int = None): """Начать тест определения уровня""" + # Определяем ID пользователя (telegram_id передаётся при вызове из callback) + user_telegram_id = telegram_id or message.from_user.id + # Показываем описание теста async with async_session_maker() as session: - user = await UserService.get_user_by_telegram_id(session, message.from_user.id) + user = await UserService.get_user_by_telegram_id(session, user_telegram_id) lang = (user.language_interface if user else 'ru') or 'ru' await message.answer(t(lang, 'level_test.intro')) @@ -83,7 +86,8 @@ async def begin_test(callback: CallbackQuery, state: FSMContext): current_question=0, correct_answers=0, answers=[], # Для отслеживания ответов по уровням - learning_language=learning_lang + learning_language=learning_lang, + user_id=user.id ) await state.set_state(LevelTestStates.taking_test) @@ -96,6 +100,7 @@ async def show_question(message: Message, state: FSMContext): data = await state.get_data() questions = data.get('questions', []) current_idx = data.get('current_question', 0) + user_id = data.get('user_id') if current_idx >= len(questions): # Тест завершён @@ -105,9 +110,9 @@ async def show_question(message: Message, state: FSMContext): question = questions[current_idx] # Формируем текст вопроса - # Язык интерфейса + # Язык интерфейса (берём user_id из state, т.к. message может быть от бота) async with async_session_maker() as session: - user = await UserService.get_user_by_telegram_id(session, message.from_user.id) + user = await UserService.get_user_by_id(session, user_id) lang = (user.language_interface if user else 'ru') or 'ru' text = ( @@ -127,10 +132,6 @@ 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}") ]) @@ -237,6 +238,7 @@ async def finish_test(message: Message, state: FSMContext): correct_answers = data.get('correct_answers', 0) answers = data.get('answers', []) learning_lang = data.get('learning_language', 'en') + user_id = data.get('user_id') total = len(questions) accuracy = int((correct_answers / total) * 100) if total > 0 else 0 @@ -244,9 +246,9 @@ async def finish_test(message: Message, state: FSMContext): # Определяем уровень на основе правильных ответов по уровням level = determine_level(answers, learning_lang) - # Сохраняем уровень в базе данных + # Сохраняем уровень в базе данных (берём user_id из state, т.к. message может быть от бота) async with async_session_maker() as session: - user = await UserService.get_user_by_telegram_id(session, message.chat.id) + user = await UserService.get_user_by_id(session, user_id) if user: await UserService.update_user_level(session, user.id, level, learning_lang) diff --git a/bot/handlers/settings.py b/bot/handlers/settings.py index 769c54e..aa8e1aa 100644 --- a/bot/handlers/settings.py +++ b/bot/handlers/settings.py @@ -17,10 +17,16 @@ from utils.levels import ( router = Router() +def get_translation_language(user) -> str: + """Получить язык перевода (translation_language или language_interface как fallback)""" + return getattr(user, 'translation_language', None) or getattr(user, 'language_interface', 'ru') or 'ru' + + def get_settings_keyboard(user) -> InlineKeyboardMarkup: """Создать клавиатуру настроек""" lang = get_user_lang(user) ui_lang_code = getattr(user, 'language_interface', 'ru') or 'ru' + translation_lang_code = get_translation_language(user) current_level = get_user_level_for_language(user) keyboard = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton( @@ -35,6 +41,10 @@ def get_settings_keyboard(user) -> InlineKeyboardMarkup: text=t(lang, 'settings.interface_prefix') + t(lang, f'settings.lang_name.{ui_lang_code}'), callback_data="settings_language" )], + [InlineKeyboardButton( + text=t(lang, 'settings.translation_prefix') + t(lang, f'settings.lang_name.{translation_lang_code}'), + callback_data="settings_translation" + )], [InlineKeyboardButton( text=t(lang, 'settings.close'), callback_data="settings_close" @@ -73,6 +83,18 @@ def get_language_keyboard(user=None) -> InlineKeyboardMarkup: return keyboard +def get_translation_language_keyboard(user=None) -> InlineKeyboardMarkup: + """Клавиатура выбора языка перевода""" + lang = get_user_lang(user) + keyboard = InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text=t(lang, 'settings.lang_name.ru'), callback_data="set_translation_ru")], + [InlineKeyboardButton(text=t(lang, 'settings.lang_name.en'), callback_data="set_translation_en")], + [InlineKeyboardButton(text=t(lang, 'settings.lang_name.ja'), callback_data="set_translation_ja")], + [InlineKeyboardButton(text=t(lang, 'settings.back'), callback_data="settings_back")] + ]) + return keyboard + + def get_learning_language_keyboard(user=None) -> InlineKeyboardMarkup: """Клавиатура выбора языка изучения""" lang = get_user_lang(user) @@ -214,6 +236,40 @@ async def set_language(callback: CallbackQuery): await callback.answer() +@router.callback_query(F.data == "settings_translation") +async def settings_translation(callback: CallbackQuery): + """Показать выбор языка перевода""" + async with async_session_maker() as session: + user = await UserService.get_user_by_telegram_id(session, callback.from_user.id) + lang = get_user_lang(user) + await callback.message.edit_text( + t(lang, 'settings.translation_title') + t(lang, 'settings.translation_desc'), + reply_markup=get_translation_language_keyboard(user) + ) + await callback.answer() + + +@router.callback_query(F.data.startswith("set_translation_")) +async def set_translation_language(callback: CallbackQuery): + """Установить язык перевода""" + new_translation_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_translation_language(session, user.id, new_translation_lang) + lang = get_user_lang(user) + lang_name = t(lang, f'settings.lang_name.{new_translation_lang}') + text = t(lang, 'settings.translation_changed', lang_name=lang_name) + await callback.message.edit_text( + text, + reply_markup=InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text=t(lang, 'settings.back'), callback_data="settings_back")]]) + ) + + await callback.answer() + + @router.callback_query(F.data == "settings_back") async def settings_back(callback: CallbackQuery): """Вернуться к настройкам""" diff --git a/bot/handlers/start.py b/bot/handlers/start.py index b3cb8a0..82e437d 100644 --- a/bot/handlers/start.py +++ b/bot/handlers/start.py @@ -9,14 +9,49 @@ from aiogram.types import ( KeyboardButton, ) from aiogram.fsm.context import FSMContext +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 +from utils.i18n import t, get_user_translation_lang from utils.levels import get_user_level_for_language router = Router() + +class OnboardingStates(StatesGroup): + """Состояния онбординга для новых пользователей""" + choosing_interface_lang = State() + choosing_learning_lang = State() + choosing_translation_lang = State() + + +def onboarding_interface_keyboard() -> InlineKeyboardMarkup: + """Клавиатура выбора языка интерфейса при онбординге""" + return InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text="🇷🇺 Русский", callback_data="onboard_interface_ru")], + [InlineKeyboardButton(text="🇬🇧 English", callback_data="onboard_interface_en")], + [InlineKeyboardButton(text="🇯🇵 日本語", callback_data="onboard_interface_ja")], + ]) + + +def onboarding_learning_keyboard(lang: str) -> InlineKeyboardMarkup: + """Клавиатура выбора языка изучения при онбординге""" + return InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text=t(lang, 'onboarding.lang_en'), callback_data="onboard_learning_en")], + [InlineKeyboardButton(text=t(lang, 'onboarding.lang_ja'), callback_data="onboard_learning_ja")], + ]) + + +def onboarding_translation_keyboard(lang: str) -> InlineKeyboardMarkup: + """Клавиатура выбора языка перевода при онбординге""" + return InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text=t(lang, 'settings.lang_name.ru'), callback_data="onboard_translation_ru")], + [InlineKeyboardButton(text=t(lang, 'settings.lang_name.en'), callback_data="onboard_translation_en")], + [InlineKeyboardButton(text=t(lang, 'settings.lang_name.ja'), callback_data="onboard_translation_ja")], + ]) + + def main_menu_keyboard(lang: str = 'ru') -> ReplyKeyboardMarkup: """Клавиатура с основными командами (кнопки отправляют команды).""" return ReplyKeyboardMarkup( @@ -46,35 +81,111 @@ async def cmd_start(message: Message, state: FSMContext): existing_user = await UserService.get_user_by_telegram_id(session, message.from_user.id) is_new_user = existing_user is None - # Создаём или получаем пользователя - user = await UserService.get_or_create_user( - session, - telegram_id=message.from_user.id, - username=message.from_user.username - ) + if is_new_user: + # Новый пользователь - начинаем онбординг + # Сначала создаём пользователя с дефолтными значениями + user = await UserService.get_or_create_user( + session, + telegram_id=message.from_user.id, + username=message.from_user.username + ) + # Приветствие и первый вопрос - язык интерфейса + await message.answer( + f"👋 Welcome! / Привет! / ようこそ!\n\n" + "🌐 Choose your interface language:\n" + "🌐 Выбери язык интерфейса:\n" + "🌐 インターフェース言語を選択:", + reply_markup=onboarding_interface_keyboard() + ) + await state.set_state(OnboardingStates.choosing_interface_lang) + return + + # Существующий пользователь + user = existing_user lang = (user.language_interface or 'ru') - if is_new_user: - # Новый пользователь - await message.answer( - t(lang, "start.new_intro", first_name=message.from_user.first_name), - reply_markup=main_menu_keyboard(lang), - ) + await message.answer( + t(lang, "start.return", first_name=message.from_user.first_name), + reply_markup=main_menu_keyboard(lang), + ) - # Предлагаем пройти тест уровня - keyboard = InlineKeyboardMarkup(inline_keyboard=[ - [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(t(lang, "start.offer_test"), reply_markup=keyboard) - else: - # Существующий пользователь - await message.answer( - t(lang, "start.return", first_name=message.from_user.first_name), - reply_markup=main_menu_keyboard(lang), - ) +# === Обработчики онбординга === + +@router.callback_query(F.data.startswith("onboard_interface_"), OnboardingStates.choosing_interface_lang) +async def onboard_set_interface(callback: CallbackQuery, state: FSMContext): + """Установить язык интерфейса при онбординге""" + 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) + + await state.update_data(interface_lang=lang) + + # Второй вопрос - язык изучения + await callback.message.edit_text( + t(lang, 'onboarding.step2_title'), + reply_markup=onboarding_learning_keyboard(lang) + ) + await state.set_state(OnboardingStates.choosing_learning_lang) + await callback.answer() + + +@router.callback_query(F.data.startswith("onboard_learning_"), OnboardingStates.choosing_learning_lang) +async def onboard_set_learning(callback: CallbackQuery, state: FSMContext): + """Установить язык изучения при онбординге""" + learning_lang = callback.data.split("_")[-1] # en | ja + data = await state.get_data() + lang = data.get('interface_lang', 'ru') + + 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, learning_lang) + + await state.update_data(learning_lang=learning_lang) + + # Третий вопрос - язык перевода + await callback.message.edit_text( + t(lang, 'onboarding.step3_title'), + reply_markup=onboarding_translation_keyboard(lang) + ) + await state.set_state(OnboardingStates.choosing_translation_lang) + await callback.answer() + + +@router.callback_query(F.data.startswith("onboard_translation_"), OnboardingStates.choosing_translation_lang) +async def onboard_set_translation(callback: CallbackQuery, state: FSMContext): + """Установить язык перевода при онбординге и завершить""" + translation_lang = callback.data.split("_")[-1] # ru | en | ja + data = await state.get_data() + lang = data.get('interface_lang', 'ru') + + 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_translation_language(session, user.id, translation_lang) + + await state.clear() + + # Приветствие с выбранными настройками + await callback.message.edit_text(t(lang, 'onboarding.complete')) + + # Показываем главное меню и предлагаем тест уровня + await callback.message.answer( + t(lang, "start.new_intro", first_name=callback.from_user.first_name), + reply_markup=main_menu_keyboard(lang), + ) + + keyboard = InlineKeyboardMarkup(inline_keyboard=[ + [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 callback.message.answer(t(lang, "start.offer_test"), reply_markup=keyboard) + await callback.answer() @router.message(Command("menu")) @@ -329,7 +440,7 @@ async def pick_theme_from_menu(callback: CallbackQuery, state: FSMContext): level=current_level, count=10, learning_lang=user.learning_language, - translation_lang=user.language_interface, + translation_lang=get_user_translation_lang(user), ) await generating.delete() @@ -341,7 +452,7 @@ async def pick_theme_from_menu(callback: CallbackQuery, state: FSMContext): await state.update_data(theme=theme, words=words, user_id=user.id, level=current_level) await state.set_state(WordsStates.viewing_words) - await show_words_list(callback.message, words, theme) + await show_words_list(callback.message, words, theme, user.id) @router.message(Command("help")) @@ -359,7 +470,7 @@ async def offer_level_test_callback(callback: CallbackQuery, state: FSMContext): """Начать тест уровня из приветствия""" from bot.handlers.level_test import start_level_test await callback.message.delete() - await start_level_test(callback.message, state) + await start_level_test(callback.message, state, telegram_id=callback.from_user.id) await callback.answer() diff --git a/bot/handlers/tasks.py b/bot/handlers/tasks.py index c124112..f50a2d6 100644 --- a/bot/handlers/tasks.py +++ b/bot/handlers/tasks.py @@ -10,7 +10,7 @@ from services.user_service import UserService from services.task_service import TaskService from services.vocabulary_service import VocabularyService from services.ai_service import ai_service -from utils.i18n import t, get_user_lang +from utils.i18n import t, get_user_lang, get_user_translation_lang from utils.levels import get_user_level_for_language router = Router() @@ -69,7 +69,7 @@ async def start_vocabulary_tasks(callback: CallbackQuery, state: FSMContext): tasks = await TaskService.generate_mixed_tasks( session, user.id, count=5, learning_lang=user.learning_language, - translation_lang=user.language_interface, + translation_lang=get_user_translation_lang(user), ) if not tasks: @@ -122,12 +122,13 @@ async def start_new_words_tasks(callback: CallbackQuery, state: FSMContext): exclude_words = list(set(vocab_words + correct_task_words)) # Генерируем новые слова через AI + translation_lang = get_user_translation_lang(user) words = await ai_service.generate_thematic_words( theme="random everyday vocabulary", level=level, count=5, learning_lang=user.learning_language, - translation_lang=user.language_interface, + translation_lang=translation_lang, exclude_words=exclude_words if exclude_words else None, ) @@ -138,7 +139,7 @@ async def start_new_words_tasks(callback: CallbackQuery, state: FSMContext): # Преобразуем слова в задания tasks = [] - translate_prompt = t(lang, 'tasks.translate_to', lang_name=t(lang, f'lang.{user.language_interface}')) + translate_prompt = t(lang, 'tasks.translate_to', lang_name=t(lang, f'lang.{translation_lang}')) for word in words: tasks.append({ 'type': 'translate', @@ -168,6 +169,7 @@ async def show_current_task(message: Message, state: FSMContext): data = await state.get_data() tasks = data.get('tasks', []) current_index = data.get('current_task_index', 0) + user_id = data.get('user_id') if current_index >= len(tasks): # Все задания выполнены @@ -176,9 +178,9 @@ async def show_current_task(message: Message, state: FSMContext): task = tasks[current_index] - # Определяем язык пользователя + # Определяем язык пользователя (берём user_id из state, т.к. message может быть от бота) async with async_session_maker() as session: - user = await UserService.get_user_by_telegram_id(session, message.from_user.id) + user = await UserService.get_user_by_id(session, user_id) lang = (user.language_interface if user else 'ru') or 'ru' task_text = ( @@ -349,7 +351,7 @@ async def add_task_word(callback: CallbackQuery, state: FSMContext): word_original=word, word_translation=translation, source_lang=user.learning_language, - translation_lang=user.language_interface, + translation_lang=get_user_translation_lang(user), transcription=transcription, source=WordSource.AI_TASK ) @@ -409,6 +411,7 @@ async def finish_tasks(message: Message, state: FSMContext): tasks = data.get('tasks', []) correct_count = data.get('correct_count', 0) total_count = len(tasks) + user_id = data.get('user_id') accuracy = int((correct_count / total_count) * 100) if total_count > 0 else 0 @@ -426,9 +429,9 @@ async def finish_tasks(message: Message, state: FSMContext): emoji = "💪" comment_key = 'poor' - # Язык пользователя + # Язык пользователя (берём user_id из state, т.к. message может быть от бота) async with async_session_maker() as session: - user = await UserService.get_user_by_telegram_id(session, message.from_user.id) + user = await UserService.get_user_by_id(session, user_id) lang = (user.language_interface if user else 'ru') or 'ru' result_text = ( diff --git a/bot/handlers/vocabulary.py b/bot/handlers/vocabulary.py index e4ff2c7..27123f0 100644 --- a/bot/handlers/vocabulary.py +++ b/bot/handlers/vocabulary.py @@ -9,7 +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, get_user_lang +from utils.i18n import t, get_user_lang, get_user_translation_lang router = Router() @@ -73,9 +73,9 @@ async def process_word_addition(message: Message, state: FSMContext, word: str): 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' + translation_lang = get_user_translation_lang(user) word_data = await ai_service.translate_word_with_contexts( - word, source_lang=source_lang, translation_lang=ui_lang, max_translations=3 + word, source_lang=source_lang, translation_lang=translation_lang, max_translations=3 ) # Удаляем сообщение о загрузке @@ -141,7 +141,8 @@ async def confirm_add_word(callback: CallbackQuery, state: FSMContext): # Получаем пользователя для языков user = await UserService.get_user_by_telegram_id(session, callback.from_user.id) source_lang = user.learning_language if user else 'en' - ui_lang = user.language_interface if user else 'ru' + translation_lang = get_user_translation_lang(user) + ui_lang = get_user_lang(user) # Добавляем слово в базу new_word = await VocabularyService.add_word( @@ -150,7 +151,7 @@ async def confirm_add_word(callback: CallbackQuery, state: FSMContext): word_original=word_data["word"], word_translation=word_data["translation"], source_lang=source_lang, - translation_lang=ui_lang, + translation_lang=translation_lang, transcription=word_data.get("transcription"), category=word_data.get("category"), difficulty_level=word_data.get("difficulty"), @@ -168,7 +169,7 @@ async def confirm_add_word(callback: CallbackQuery, state: FSMContext): # Получаем общее количество слов words_count = await VocabularyService.get_words_count(session, user_id, learning_lang=user.learning_language) - lang = ui_lang or 'ru' + lang = ui_lang await callback.message.edit_text( t(lang, 'add.added_success', word=word_data['word'], count=words_count) diff --git a/bot/handlers/words.py b/bot/handlers/words.py index 2df057e..222abc6 100644 --- a/bot/handlers/words.py +++ b/bot/handlers/words.py @@ -9,7 +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 +from utils.i18n import t, get_user_lang, get_user_translation_lang from utils.levels import get_user_level_for_language router = Router() @@ -64,7 +64,7 @@ async def generate_words_for_theme(message: Message, state: FSMContext, theme: s level=current_level, count=10, learning_lang=user.learning_language, - translation_lang=user.language_interface, + translation_lang=get_user_translation_lang(user), ) await generating_msg.delete() @@ -83,15 +83,15 @@ async def generate_words_for_theme(message: Message, state: FSMContext, theme: s await state.set_state(WordsStates.viewing_words) # Показываем подборку - await show_words_list(message, words, theme) + await show_words_list(message, words, theme, user_id) -async def show_words_list(message: Message, words: list, theme: str): +async def show_words_list(message: Message, words: list, theme: str, user_id: int): """Показать список слов с кнопками для добавления""" # Определяем язык интерфейса для заголовка/подсказок async with async_session_maker() as session: - user = await UserService.get_user_by_telegram_id(session, message.from_user.id) + user = await UserService.get_user_by_telegram_id(session, user_id) lang = (user.language_interface if user else 'ru') or 'ru' text = t(lang, 'words.header', theme=theme) + "\n\n" @@ -171,9 +171,9 @@ async def add_single_word(callback: CallbackQuery, state: FSMContext): # Добавляем слово # Формируем examples с учётом языков learn = user.learning_language if user else 'en' - ui = user.language_interface if user else 'ru' + translation_lang = get_user_translation_lang(user) ex = word_data.get('example') - examples = ([{learn: ex, ui: ''}] if ex else []) + examples = ([{learn: ex, translation_lang: ''}] if ex else []) await VocabularyService.add_word( session=session, @@ -181,7 +181,7 @@ async def add_single_word(callback: CallbackQuery, state: FSMContext): 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, + translation_lang=translation_lang, transcription=word_data.get('transcription'), examples=examples, source=WordSource.SUGGESTED, @@ -222,9 +222,9 @@ async def add_all_words(callback: CallbackQuery, state: FSMContext): # Добавляем слово learn = user.learning_language if user else 'en' - ui = user.language_interface if user else 'ru' + translation_lang = get_user_translation_lang(user) ex = word_data.get('example') - examples = ([{learn: ex, ui: ''}] if ex else []) + examples = ([{learn: ex, translation_lang: ''}] if ex else []) await VocabularyService.add_word( session=session, @@ -232,7 +232,7 @@ async def add_all_words(callback: CallbackQuery, state: FSMContext): 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, + translation_lang=translation_lang, transcription=word_data.get('transcription'), examples=examples, source=WordSource.SUGGESTED, @@ -241,9 +241,10 @@ async def add_all_words(callback: CallbackQuery, state: FSMContext): ) added_count += 1 - result_text = f"✅ Добавлено слов: {added_count}" + 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) diff --git a/database/models.py b/database/models.py index bd69cba..88bb6ae 100644 --- a/database/models.py +++ b/database/models.py @@ -55,8 +55,9 @@ class User(Base): id: Mapped[int] = mapped_column(primary_key=True) telegram_id: Mapped[int] = mapped_column(BigInteger, unique=True, nullable=False) username: Mapped[Optional[str]] = mapped_column(String(255)) - language_interface: Mapped[str] = mapped_column(String(2), default="ru") # ru/en - learning_language: Mapped[str] = mapped_column(String(2), default="en") # en + language_interface: Mapped[str] = mapped_column(String(2), default="ru") # ru/en/ja - UI language + learning_language: Mapped[str] = mapped_column(String(2), default="en") # en/ja - language being learned + translation_language: Mapped[Optional[str]] = mapped_column(String(2), default=None) # ru/en/ja - translation target (defaults to language_interface if None) level: Mapped[LanguageLevel] = mapped_column(SQLEnum(LanguageLevel), default=LanguageLevel.A1) levels_by_language: Mapped[Optional[dict]] = mapped_column(JSON, default=None) # {"en": "B1", "ja": "N4"} timezone: Mapped[str] = mapped_column(String(50), default="UTC") diff --git a/locales/en.json b/locales/en.json index e379810..4d1c1c5 100644 --- a/locales/en.json +++ b/locales/en.json @@ -202,6 +202,7 @@ "level_prefix": "📊 Level: ", "learning_prefix": "🎯 Learning language: ", "interface_prefix": "🌐 Interface language: ", + "translation_prefix": "💬 Translation language: ", "choose": "Choose what to change:", "close": "❌ Close", "back": "⬅️ Back", @@ -232,6 +233,9 @@ "lang_changed": "✅ Interface language: English", "learning_title": "🎯 Select learning language:\n\n", "learning_changed": "✅ Learning language: {code}", + "translation_title": "💬 Select translation language:\n\n", + "translation_desc": "Words will be translated to this language.\nThis can differ from interface language.", + "translation_changed": "✅ Translation language: {lang_name}", "menu_updated": "Main menu updated ⤵️", "lang_name": { "ru": "🇷🇺 Русский", @@ -290,6 +294,13 @@ "N1": "Fluent - full proficiency in Japanese" } }, + "onboarding": { + "step2_title": "🎯 Which language do you want to learn?", + "step3_title": "💬 Which language to translate words into?", + "complete": "✅ Settings saved!", + "lang_en": "🇬🇧 English", + "lang_ja": "🇯🇵 Japanese" + }, "words": { "generating": "🔄 Generating words for topic '{theme}'...", "generate_failed": "❌ Failed to generate words. Please try again later.", diff --git a/locales/ja.json b/locales/ja.json index 020fdb2..25b3fcc 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -194,6 +194,7 @@ "level_prefix": "📊 レベル: ", "learning_prefix": "🎯 学習言語: ", "interface_prefix": "🌐 インターフェース言語: ", + "translation_prefix": "💬 翻訳言語: ", "choose": "変更したい項目を選択:", "close": "❌ 閉じる", "back": "⬅️ 戻る", @@ -224,6 +225,9 @@ "lang_changed": "✅ インターフェース言語: 日本語", "learning_title": "🎯 学習言語を選択:\n\n", "learning_changed": "✅ 学習言語: {code}", + "translation_title": "💬 翻訳言語を選択:\n\n", + "translation_desc": "単語はこの言語に翻訳されます。\nインターフェース言語と異なる設定が可能です。", + "translation_changed": "✅ 翻訳言語: {lang_name}", "menu_updated": "メインメニューを更新しました ⤵️", "lang_name": { "ru": "🇷🇺 Русский", @@ -282,6 +286,13 @@ "N1": "流暢 - 日本語を完全に習得している" } }, + "onboarding": { + "step2_title": "🎯 どの言語を学びたいですか?", + "step3_title": "💬 どの言語に翻訳しますか?", + "complete": "✅ 設定を保存しました!", + "lang_en": "🇬🇧 英語", + "lang_ja": "🇯🇵 日本語" + }, "words": { "generating": "🔄 テーマ『{theme}』の単語を生成中...", "generate_failed": "❌ 単語の生成に失敗しました。後でもう一度お試しください。", diff --git a/locales/ru.json b/locales/ru.json index fde3b04..5a1dcb2 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -202,6 +202,7 @@ "level_prefix": "📊 Уровень: ", "learning_prefix": "🎯 Язык изучения: ", "interface_prefix": "🌐 Язык интерфейса: ", + "translation_prefix": "💬 Язык перевода: ", "choose": "Выбери, что хочешь изменить:", "close": "❌ Закрыть", "back": "⬅️ Назад", @@ -232,6 +233,9 @@ "lang_changed": "✅ Язык интерфейса: Русский", "learning_title": "🎯 Выбери язык изучения:\n\n", "learning_changed": "✅ Язык изучения: {code}", + "translation_title": "💬 Выбери язык перевода:\n\n", + "translation_desc": "На этот язык будут переводиться слова.\nЭто может отличаться от языка интерфейса.", + "translation_changed": "✅ Язык перевода: {lang_name}", "menu_updated": "Клавиатура обновлена ⤵️", "lang_name": { "ru": "🇷🇺 Русский", @@ -290,6 +294,13 @@ "N1": "Свободный - полное владение японским языком" } }, + "onboarding": { + "step2_title": "🎯 Какой язык хочешь изучать?", + "step3_title": "💬 На какой язык переводить слова?", + "complete": "✅ Настройки сохранены!", + "lang_en": "🇬🇧 Английский", + "lang_ja": "🇯🇵 Японский" + }, "words": { "generating": "🔄 Генерирую подборку слов по теме '{theme}'...", "generate_failed": "❌ Не удалось сгенерировать подборку. Попробуй позже.", diff --git a/migrations/versions/20251207_add_translation_language.py b/migrations/versions/20251207_add_translation_language.py new file mode 100644 index 0000000..fc36828 --- /dev/null +++ b/migrations/versions/20251207_add_translation_language.py @@ -0,0 +1,25 @@ +"""Add translation_language field to users table + +Revision ID: 20251207_translation_language +Revises: 20251206_word_translations +Create Date: 2025-12-07 +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '20251207_translation_language' +down_revision: Union[str, None] = '20251206_word_translations' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column('users', sa.Column('translation_language', sa.String(2), nullable=True)) + + +def downgrade() -> None: + op.drop_column('users', 'translation_language') diff --git a/services/user_service.py b/services/user_service.py index 86f8283..5ba0f86 100644 --- a/services/user_service.py +++ b/services/user_service.py @@ -59,6 +59,23 @@ class UserService: ) return result.scalar_one_or_none() + @staticmethod + async def get_user_by_id(session: AsyncSession, user_id: int) -> Optional[User]: + """ + Получить пользователя по внутреннему ID + + Args: + session: Сессия базы данных + user_id: ID пользователя в БД + + Returns: + Объект пользователя или None + """ + result = await session.execute( + select(User).where(User.id == user_id) + ) + return result.scalar_one_or_none() + @staticmethod async def update_user_level(session: AsyncSession, user_id: int, level: str, language: str = None): """ @@ -120,3 +137,22 @@ class UserService: if user: user.learning_language = language await session.commit() + + @staticmethod + async def update_user_translation_language(session: AsyncSession, user_id: int, language: str): + """ + Обновить язык перевода пользователя + + Args: + session: Сессия базы данных + user_id: ID пользователя + language: Новый язык перевода (ru/en/ja) + """ + result = await session.execute( + select(User).where(User.id == user_id) + ) + user = result.scalar_one_or_none() + + if user: + user.translation_language = language + await session.commit() diff --git a/utils/i18n.py b/utils/i18n.py index 01cd7e5..7559dcd 100644 --- a/utils/i18n.py +++ b/utils/i18n.py @@ -36,6 +36,14 @@ def get_user_lang(user) -> str: return (getattr(user, 'language_interface', None) if user else None) or 'ru' +def get_user_translation_lang(user) -> str: + """Получить язык перевода (translation_language или language_interface как fallback).""" + translation_lang = getattr(user, 'translation_language', None) if user else None + if translation_lang: + return translation_lang + return get_user_lang(user) + + def t(lang: str, key: str, **kwargs) -> str: """Translate key for given lang; fallback to ru and to key itself.