feat(i18n): localize start/help/menu, practice, words, import, reminder, vocabulary, tasks/stats for RU/EN/JA; add JSON-based i18n helper\n\nfeat(lang): support learning/translation languages across AI flows; hide translations with buttons; store examples per lang\n\nfeat(vocab): add source_lang and translation_lang to Vocabulary, unique constraint (user_id, source_lang, word_original); filter /vocabulary by user.learning_language\n\nchore(migrations): add Alembic setup + migration to add vocab lang columns; env.py reads app settings and supports asyncpg URLs\n\nfix(words/import): pass learning_lang + translation_lang everywhere; fix menu themes generation\n\nfeat(settings): add learning language selector; update main menu on language change

This commit is contained in:
2025-12-04 19:40:01 +03:00
parent 6223351ccf
commit 472771229f
22 changed files with 1587 additions and 471 deletions

View File

@@ -5,60 +5,130 @@ from aiogram.fsm.context import FSMContext
from database.db import async_session_maker
from database.models import LanguageLevel
from bot.handlers.start import main_menu_keyboard
from services.user_service import UserService
router = Router()
def _is_en(user) -> bool:
try:
return (getattr(user, 'language_interface', 'ru') or 'ru') == 'en'
except Exception:
return False
def _is_ja(user) -> bool:
try:
return (getattr(user, 'language_interface', 'ru') or 'ru') == 'ja'
except Exception:
return False
def get_settings_keyboard(user) -> InlineKeyboardMarkup:
"""Создать клавиатуру настроек"""
is_en = _is_en(user)
is_ja = _is_ja(user)
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text=f"📊 Уровень: {user.level.value}",
text=(
"📊 Level: " if is_en else ("📊 レベル: " if is_ja else "📊 Уровень: ")
) + f"{user.level.value}",
callback_data="settings_level"
)],
[InlineKeyboardButton(
text=f"🌐 Язык интерфейса: {'🇷🇺 Русский' if user.language_interface == 'ru' else '🇬🇧 English'}",
text=(
"🎯 Learning language: " if is_en else ("🎯 学習言語: " if is_ja else "🎯 Язык изучения: ")
) + (user.learning_language.upper()),
callback_data="settings_learning"
)],
[InlineKeyboardButton(
text=(
"🌐 Interface language: " if is_en else ("🌐 インターフェース言語: " if is_ja else "🌐 Язык интерфейса: ")
) + ("🇬🇧 English" if getattr(user, 'language_interface', 'ru') == 'en' else ("🇯🇵 日本語" if getattr(user, 'language_interface', 'ru') == 'ja' else "🇷🇺 Русский")),
callback_data="settings_language"
)],
[InlineKeyboardButton(
text="❌ Закрыть",
text=("❌ Close" if is_en else ("❌ 閉じる" if is_ja else "❌ Закрыть")),
callback_data="settings_close"
)]
])
return keyboard
def get_level_keyboard() -> InlineKeyboardMarkup:
def get_level_keyboard(user=None) -> InlineKeyboardMarkup:
"""Клавиатура выбора уровня"""
levels = [
("A1 - Начальный", "set_level_A1"),
("A2 - Элементарный", "set_level_A2"),
("B1 - Средний", "set_level_B1"),
("B2 - Выше среднего", "set_level_B2"),
("C1 - Продвинутый", "set_level_C1"),
("C2 - Профессиональный", "set_level_C2"),
]
lang = getattr(user, 'language_interface', 'ru') if user is not None else 'ru'
if lang == 'en':
levels = [
("A1 - Beginner", "set_level_A1"),
("A2 - Elementary", "set_level_A2"),
("B1 - Intermediate", "set_level_B1"),
("B2 - Upper-intermediate", "set_level_B2"),
("C1 - Advanced", "set_level_C1"),
("C2 - Proficient", "set_level_C2"),
]
elif lang == 'ja':
levels = [
("A1 - 初級", "set_level_A1"),
("A2 - 初級(上)", "set_level_A2"),
("B1 - 中級", "set_level_B1"),
("B2 - 中級(上)", "set_level_B2"),
("C1 - 上級", "set_level_C1"),
("C2 - ネイティブ", "set_level_C2"),
]
else:
levels = [
("A1 - Начальный", "set_level_A1"),
("A2 - Элементарный", "set_level_A2"),
("B1 - Средний", "set_level_B1"),
("B2 - Выше среднего", "set_level_B2"),
("C1 - Продвинутый", "set_level_C1"),
("C2 - Профессиональный", "set_level_C2"),
]
keyboard = []
for level_name, callback_data in levels:
keyboard.append([InlineKeyboardButton(text=level_name, callback_data=callback_data)])
keyboard.append([InlineKeyboardButton(text="⬅️ Назад", callback_data="settings_back")])
back_label = "⬅️ Back" if lang == 'en' else ("⬅️ 戻る" if lang == 'ja' else "⬅️ Назад")
keyboard.append([InlineKeyboardButton(text=back_label, callback_data="settings_back")])
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_language_keyboard() -> InlineKeyboardMarkup:
def get_language_keyboard(user=None) -> InlineKeyboardMarkup:
"""Клавиатура выбора языка интерфейса"""
lang = getattr(user, 'language_interface', 'ru') if user is not None else 'ru'
back = "⬅️ Back" if lang == 'en' else ("⬅️ 戻る" if lang == 'ja' else "⬅️ Назад")
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🇷🇺 Русский", callback_data="set_lang_ru")],
[InlineKeyboardButton(text="🇬🇧 English (скоро)", callback_data="set_lang_en")],
[InlineKeyboardButton(text="⬅️ Назад", callback_data="settings_back")]
[InlineKeyboardButton(text="🇬🇧 English", callback_data="set_lang_en")],
[InlineKeyboardButton(text="🇯🇵 日本語", callback_data="set_lang_ja")],
[InlineKeyboardButton(text=back, callback_data="settings_back")]
])
return keyboard
def get_learning_language_keyboard(user=None) -> InlineKeyboardMarkup:
"""Клавиатура выбора языка изучения"""
lang = getattr(user, 'language_interface', 'ru') if user is not None else 'ru'
back = "⬅️ Back" if lang == 'en' else ("⬅️ 戻る" if lang == 'ja' else "⬅️ Назад")
# Пары (код -> подпись)
options = [
("en", "🇬🇧 English" if lang != 'ja' else "🇬🇧 英語"),
("es", "🇪🇸 Spanish" if lang == 'en' else ("🇪🇸 スペイン語" if lang == 'ja' else "🇪🇸 Испанский")),
("de", "🇩🇪 German" if lang == 'en' else ("🇩🇪 ドイツ語" if lang == 'ja' else "🇩🇪 Немецкий")),
("fr", "🇫🇷 French" if lang == 'en' else ("🇫🇷 フランス語" if lang == 'ja' else "🇫🇷 Французский")),
("ja", "🇯🇵 Japanese" if lang == 'en' else ("🇯🇵 日本語" if lang == 'ja' else "🇯🇵 Японский")),
]
keyboard = [[InlineKeyboardButton(text=label, callback_data=f"set_learning_{code}")] for code, label in options]
keyboard.append([InlineKeyboardButton(text=back, callback_data="settings_back")])
return InlineKeyboardMarkup(inline_keyboard=keyboard)
@router.message(Command("settings"))
async def cmd_settings(message: Message):
"""Обработчик команды /settings"""
@@ -69,12 +139,13 @@ async def cmd_settings(message: Message):
await message.answer("Сначала запусти бота командой /start")
return
settings_text = (
"⚙️ <b>Настройки</b>\n\n"
f"📊 Уровень английского: <b>{user.level.value}</b>\n"
f"🌐 Язык интерфейса: <b>{'Русский' if user.language_interface == 'ru' else 'English'}</b>\n\n"
"Выбери, что хочешь изменить:"
)
lang = getattr(user, 'language_interface', 'ru')
title = "⚙️ <b>Settings</b>\n\n" if lang == 'en' else ("⚙️ <b>設定</b>\n\n" if lang == 'ja' else "⚙️ <b>Настройки</b>\n\n")
level_label = "📊 English level: " if lang == 'en' else ("📊 英語レベル: " if lang == 'ja' else "📊 Уровень английского: ")
lang_label = "🌐 Interface language: " if lang == 'en' else ("🌐 インターフェース言語: " if lang == 'ja' else "🌐 Язык интерфейса: ")
lang_value = 'English' if lang == 'en' else ('日本語' if lang == 'ja' else ('Русский' if user.language_interface == 'ru' else 'English'))
footer = "Choose what to change:" if lang == 'en' else ("変更したい項目を選択:" if lang == 'ja' else "Выбери, что хочешь изменить:")
settings_text = f"{title}{level_label}<b>{user.level.value}</b>\n{lang_label}<b>{lang_value}</b>\n\n{footer}"
await message.answer(settings_text, reply_markup=get_settings_keyboard(user))
@@ -82,14 +153,53 @@ async def cmd_settings(message: Message):
@router.callback_query(F.data == "settings_level")
async def settings_level(callback: CallbackQuery):
"""Показать выбор уровня"""
await callback.message.edit_text(
"📊 <b>Выбери свой уровень английского:</b>\n\n"
"<b>A1-A2</b> - Начинающий\n"
"<b>B1-B2</b> - Средний\n"
"<b>C1-C2</b> - Продвинутый\n\n"
"Это влияет на сложность предлагаемых слов и заданий.",
reply_markup=get_level_keyboard()
)
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = getattr(user, 'language_interface', 'ru') if user else 'ru'
title = "📊 <b>Choose your English level:</b>\n\n" if lang == 'en' else ("📊 <b>英語レベルを選択:</b>\n\n" if lang == 'ja' else "📊 <b>Выбери свой уровень английского:</b>\n\n")
body = (
"<b>A1-A2</b> - Beginner\n<b>B1-B2</b> - Intermediate\n<b>C1-C2</b> - Advanced\n\n" if lang == 'en' else (
"<b>A1-A2</b> - 初級\n<b>B1-B2</b> - 中級\n<b>C1-C2</b> - 上級\n\n" if lang == 'ja' else
"<b>A1-A2</b> - Начинающий\n<b>B1-B2</b> - Средний\n<b>C1-C2</b> - Продвинутый\n\n"
))
tail = "This affects difficulty of suggested words and tasks." if lang == 'en' else ("これは提案される単語や課題の難易度に影響します。" if lang == 'ja' else "Это влияет на сложность предлагаемых слов и заданий.")
await callback.message.edit_text(title + body + tail, reply_markup=get_level_keyboard(user))
await callback.answer()
@router.callback_query(F.data == "settings_learning")
async def settings_learning(callback: CallbackQuery):
"""Показать выбор языка изучения"""
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = getattr(user, 'language_interface', 'ru') if user else 'ru'
title = "🎯 <b>Select learning language:</b>\n\n" if lang == 'en' else ("🎯 <b>学習言語を選択:</b>\n\n" if lang == 'ja' else "🎯 <b>Выбери язык изучения:</b>\n\n")
await callback.message.edit_text(title, reply_markup=get_learning_language_keyboard(user))
await callback.answer()
@router.callback_query(F.data.startswith("set_learning_"))
async def set_learning_language(callback: CallbackQuery):
"""Установить язык изучения"""
code = callback.data.split("_")[-1]
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, code)
lang = getattr(user, 'language_interface', 'ru')
if lang == 'en':
text = f"✅ Learning language: <b>{code.upper()}</b>"
back = "⬅️ Back to settings"
elif lang == 'ja':
text = f"✅ 学習言語: <b>{code.upper()}</b>"
back = "⬅️ 設定に戻る"
else:
text = f"✅ Язык изучения: <b>{code.upper()}</b>"
back = "⬅️ К настройкам"
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text=back, callback_data="settings_back")]])
)
await callback.answer()
@@ -105,12 +215,13 @@ async def set_level(callback: CallbackQuery):
# Обновляем уровень
await UserService.update_user_level(session, user.id, LanguageLevel[level_str])
lang = getattr(user, 'language_interface', 'ru')
changed = "✅ Level changed to " if lang == 'en' else ("✅ レベルが変更されました: " if lang == 'ja' else "✅ Уровень изменен на ")
msg = changed + f"<b>{level_str}</b>\n\n" + ("You will now receive words and tasks matching your level!" if lang == 'en' else ("これからレベルに合った単語と課題が出題されます!" if lang == 'ja' else "Теперь ты будешь получать слова и задания, соответствующие твоему уровню!"))
back = "⬅️ Back to settings" if lang == 'en' else ("⬅️ 設定に戻る" if lang == 'ja' else "⬅️ К настройкам")
await callback.message.edit_text(
f"✅ Уровень изменен на <b>{level_str}</b>\n\n"
"Теперь ты будешь получать слова и задания, соответствующие твоему уровню!",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="⬅️ К настройкам", callback_data="settings_back")]
])
msg,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text=back, callback_data="settings_back")]])
)
await callback.answer()
@@ -119,10 +230,14 @@ async def set_level(callback: CallbackQuery):
@router.callback_query(F.data == "settings_language")
async def settings_language(callback: CallbackQuery):
"""Показать выбор языка"""
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = getattr(user, 'language_interface', 'ru') if user else 'ru'
title = "🌐 <b>Select interface language:</b>\n\n" if lang == 'en' else ("🌐 <b>インターフェース言語を選択:</b>\n\n" if lang == 'ja' else "🌐 <b>Выбери язык интерфейса:</b>\n\n")
desc = "This will change the language of bot messages." if lang == 'en' else ("ボットの表示言語が変更されます。" if lang == 'ja' else "Это изменит язык всех сообщений бота.")
await callback.message.edit_text(
"🌐 <b>Выбери язык интерфейса:</b>\n\n"
"Это изменит язык всех сообщений бота.",
reply_markup=get_language_keyboard()
title + desc,
reply_markup=get_language_keyboard(user)
)
await callback.answer()
@@ -130,24 +245,29 @@ async def settings_language(callback: CallbackQuery):
@router.callback_query(F.data.startswith("set_lang_"))
async def set_language(callback: CallbackQuery):
"""Установить язык"""
lang = callback.data.split("_")[-1] # ru или en
if lang == "en":
await callback.answer("Английский интерфейс скоро будет доступен! 🚧", show_alert=True)
return
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)
if lang == 'en':
text = "✅ Interface language: <b>English</b>"
back = "⬅️ Back"
elif lang == 'ja':
text = "✅ インターフェース言語: <b>日本語</b>"
back = "⬅️ 戻る"
else:
text = "✅ Язык интерфейса: <b>Русский</b>"
back = "⬅️ К настройкам"
await callback.message.edit_text(
f"✅ Язык интерфейса: <b>{'Русский' if lang == 'ru' else 'English'}</b>",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="⬅️ К настройкам", callback_data="settings_back")]
])
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text=back, callback_data="settings_back")]])
)
# Обновляем клавиатуру чата на выбранный язык
menu_updated = "Main menu updated ⤵️" if lang == 'en' else ("メインメニューを更新しました ⤵️" if lang == 'ja' else "Клавиатура обновлена ⤵️")
await callback.message.answer(menu_updated, reply_markup=main_menu_keyboard(lang))
await callback.answer()
@@ -159,12 +279,13 @@ async def settings_back(callback: CallbackQuery):
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
if user:
settings_text = (
"⚙️ <b>Настройки</b>\n\n"
f"📊 Уровень английского: <b>{user.level.value}</b>\n"
f"🌐 Язык интерфейса: <b>{'Русский' if user.language_interface == 'ru' else 'English'}</b>\n\n"
"Выбери, что хочешь изменить:"
)
lang = getattr(user, 'language_interface', 'ru')
title = "⚙️ <b>Settings</b>\n\n" if lang == 'en' else ("⚙️ <b>設定</b>\n\n" if lang == 'ja' else "⚙️ <b>Настройки</b>\n\n")
level_label = "📊 English level: " if lang == 'en' else ("📊 英語レベル: " if lang == 'ja' else "📊 Уровень английского: ")
lang_label = "🌐 Interface language: " if lang == 'en' else ("🌐 インターフェース言語: " if lang == 'ja' else "🌐 Язык интерфейса: ")
lang_value = 'English' if user.language_interface == 'en' else ('日本語' if user.language_interface == 'ja' else 'Русский')
footer = "Choose what to change:" if lang == 'en' else ("変更したい項目を選択:" if lang == 'ja' else "Выбери, что хочешь изменить:")
settings_text = f"{title}{level_label}<b>{user.level.value}</b>\n{lang_label}<b>{lang_value}</b>\n\n{footer}"
await callback.message.edit_text(settings_text, reply_markup=get_settings_keyboard(user))