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.