feat: add translation language setting & onboarding flow
- Add separate translation_language setting (independent from interface language) - Implement 3-step onboarding for new users: 1. Choose interface language 2. Choose learning language 3. Choose translation language - Fix localization issues when using callback.message (user_id from state) - Add UserService.get_user_by_id() method - Add get_user_translation_lang() helper in i18n - Update all handlers to use correct translation language - Add localization keys for onboarding (ru/en/ja) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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):
|
||||
"""Вернуться к настройкам"""
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"✅ Добавлено слов: <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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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: <b>English</b>",
|
||||
"learning_title": "🎯 <b>Select learning language:</b>\n\n",
|
||||
"learning_changed": "✅ Learning language: <b>{code}</b>",
|
||||
"translation_title": "💬 <b>Select translation language:</b>\n\n",
|
||||
"translation_desc": "Words will be translated to this language.\nThis can differ from interface language.",
|
||||
"translation_changed": "✅ Translation language: <b>{lang_name}</b>",
|
||||
"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.",
|
||||
|
||||
@@ -194,6 +194,7 @@
|
||||
"level_prefix": "📊 レベル: ",
|
||||
"learning_prefix": "🎯 学習言語: ",
|
||||
"interface_prefix": "🌐 インターフェース言語: ",
|
||||
"translation_prefix": "💬 翻訳言語: ",
|
||||
"choose": "変更したい項目を選択:",
|
||||
"close": "❌ 閉じる",
|
||||
"back": "⬅️ 戻る",
|
||||
@@ -224,6 +225,9 @@
|
||||
"lang_changed": "✅ インターフェース言語: <b>日本語</b>",
|
||||
"learning_title": "🎯 <b>学習言語を選択:</b>\n\n",
|
||||
"learning_changed": "✅ 学習言語: <b>{code}</b>",
|
||||
"translation_title": "💬 <b>翻訳言語を選択:</b>\n\n",
|
||||
"translation_desc": "単語はこの言語に翻訳されます。\nインターフェース言語と異なる設定が可能です。",
|
||||
"translation_changed": "✅ 翻訳言語: <b>{lang_name}</b>",
|
||||
"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": "❌ 単語の生成に失敗しました。後でもう一度お試しください。",
|
||||
|
||||
@@ -202,6 +202,7 @@
|
||||
"level_prefix": "📊 Уровень: ",
|
||||
"learning_prefix": "🎯 Язык изучения: ",
|
||||
"interface_prefix": "🌐 Язык интерфейса: ",
|
||||
"translation_prefix": "💬 Язык перевода: ",
|
||||
"choose": "Выбери, что хочешь изменить:",
|
||||
"close": "❌ Закрыть",
|
||||
"back": "⬅️ Назад",
|
||||
@@ -232,6 +233,9 @@
|
||||
"lang_changed": "✅ Язык интерфейса: <b>Русский</b>",
|
||||
"learning_title": "🎯 <b>Выбери язык изучения:</b>\n\n",
|
||||
"learning_changed": "✅ Язык изучения: <b>{code}</b>",
|
||||
"translation_title": "💬 <b>Выбери язык перевода:</b>\n\n",
|
||||
"translation_desc": "На этот язык будут переводиться слова.\nЭто может отличаться от языка интерфейса.",
|
||||
"translation_changed": "✅ Язык перевода: <b>{lang_name}</b>",
|
||||
"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": "❌ Не удалось сгенерировать подборку. Попробуй позже.",
|
||||
|
||||
25
migrations/versions/20251207_add_translation_language.py
Normal file
25
migrations/versions/20251207_add_translation_language.py
Normal file
@@ -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')
|
||||
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user