feat: JLPT levels for Japanese, custom practice scenarios, UI improvements

- Add separate level systems: CEFR (A1-C2) for European languages, JLPT (N5-N1) for Japanese
- Store levels per language in new `levels_by_language` JSON field
- Add custom scenario option in AI practice mode
- Show action buttons after practice ends (new dialogue, tasks, words)
- Fix level display across all handlers to use correct level system
- Add Alembic migration for levels_by_language field
- Update all locale files (ru, en, ja) with new keys

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-05 14:30:24 +03:00
parent 8bf3504d8d
commit 99deaafcbf
17 changed files with 983 additions and 308 deletions

View File

@@ -31,6 +31,11 @@ def _resolve_key(data: Dict[str, Any], dotted_key: str) -> Any:
return cur
def get_user_lang(user) -> str:
"""Унифицированное получение языка интерфейса пользователя."""
return (getattr(user, 'language_interface', None) if user else None) or 'ru'
def t(lang: str, key: str, **kwargs) -> str:
"""Translate key for given lang; fallback to ru and to key itself.

98
utils/levels.py Normal file
View File

@@ -0,0 +1,98 @@
"""
Утилиты для работы с уровнями языка (CEFR и JLPT)
"""
from database.models import JLPT_LANGUAGES, DEFAULT_CEFR_LEVEL, DEFAULT_JLPT_LEVEL
# Все доступные уровни по системам
CEFR_LEVELS = ["A1", "A2", "B1", "B2", "C1", "C2"]
JLPT_LEVELS = ["N5", "N4", "N3", "N2", "N1"]
def get_level_system(learning_language: str) -> str:
"""Определить систему уровней для языка"""
return "jlpt" if learning_language in JLPT_LANGUAGES else "cefr"
def get_available_levels(learning_language: str) -> list[str]:
"""Получить список доступных уровней для языка"""
if learning_language in JLPT_LANGUAGES:
return JLPT_LEVELS
return CEFR_LEVELS
def get_default_level(learning_language: str) -> str:
"""Получить дефолтный уровень для языка"""
if learning_language in JLPT_LANGUAGES:
return DEFAULT_JLPT_LEVEL
return DEFAULT_CEFR_LEVEL
def get_user_level_for_language(user, language: str = None) -> str:
"""
Получить уровень пользователя для конкретного языка.
Args:
user: Объект пользователя
language: Код языка (если None, берётся learning_language пользователя)
Returns:
Строка уровня (например "B1" или "N4")
"""
if language is None:
language = user.learning_language or "en"
# Пытаемся получить из JSON поля
levels = user.levels_by_language or {}
if language in levels:
return levels[language]
# Fallback на старое поле level (для CEFR языков)
if language not in JLPT_LANGUAGES and user.level:
return user.level.value
# Возвращаем дефолт
return get_default_level(language)
def set_user_level_for_language(user, level: str, language: str = None) -> dict:
"""
Установить уровень пользователя для конкретного языка.
Args:
user: Объект пользователя
level: Уровень (например "B1" или "N4")
language: Код языка (если None, берётся learning_language пользователя)
Returns:
Обновлённый словарь levels_by_language
"""
if language is None:
language = user.learning_language or "en"
# Инициализируем JSON если его нет
if user.levels_by_language is None:
user.levels_by_language = {}
# Копируем для изменения (SQLAlchemy требует новый объект для JSON)
levels = dict(user.levels_by_language)
levels[language] = level
user.levels_by_language = levels
return levels
def get_level_key_for_i18n(learning_language: str, level: str) -> str:
"""
Получить ключ локализации для уровня.
Args:
learning_language: Язык изучения
level: Уровень
Returns:
Ключ для функции t() (например "settings.level.b1" или "settings.jlpt.n4")
"""
if learning_language in JLPT_LANGUAGES:
return f"settings.jlpt.{level.lower()}"
return f"settings.level.{level.lower()}"