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

@@ -2,6 +2,7 @@ from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from database.models import Vocabulary, WordSource, LanguageLevel
from typing import List, Optional
import re
class VocabularyService:
@@ -13,6 +14,8 @@ class VocabularyService:
user_id: int,
word_original: str,
word_translation: str,
source_lang: Optional[str] = None,
translation_lang: Optional[str] = None,
transcription: Optional[str] = None,
examples: Optional[dict] = None,
category: Optional[str] = None,
@@ -50,6 +53,8 @@ class VocabularyService:
user_id=user_id,
word_original=word_original,
word_translation=word_translation,
source_lang=source_lang,
translation_lang=translation_lang,
transcription=transcription,
examples=examples,
category=category,
@@ -65,7 +70,27 @@ class VocabularyService:
return new_word
@staticmethod
async def get_user_words(session: AsyncSession, user_id: int, limit: int = 50) -> List[Vocabulary]:
@staticmethod
def _is_japanese(text: str) -> bool:
if not text:
return False
return re.search(r"[\u3040-\u30FF\u3400-\u4DBF\u4E00-\u9FFF]", text) is not None
@staticmethod
def _filter_by_learning_lang(words: List[Vocabulary], learning_lang: Optional[str]) -> List[Vocabulary]:
if not learning_lang:
return words
# Если в БД указан source_lang фильтруем по нему.
with_lang = [w for w in words if getattr(w, 'source_lang', None)]
if with_lang:
return [w for w in words if (w.source_lang or '').lower() == learning_lang.lower()]
# Фолбэк-эвристика для японского, если язык не сохранён
if learning_lang.lower() == 'ja':
return [w for w in words if VocabularyService._is_japanese(w.word_original)]
return [w for w in words if not VocabularyService._is_japanese(w.word_original)]
@staticmethod
async def get_user_words(session: AsyncSession, user_id: int, limit: int = 50, learning_lang: Optional[str] = None) -> List[Vocabulary]:
"""
Получить все слова пользователя
@@ -81,12 +106,13 @@ class VocabularyService:
select(Vocabulary)
.where(Vocabulary.user_id == user_id)
.order_by(Vocabulary.created_at.desc())
.limit(limit)
)
return list(result.scalars().all())
words = list(result.scalars().all())
words = VocabularyService._filter_by_learning_lang(words, learning_lang)
return words[:limit]
@staticmethod
async def get_words_count(session: AsyncSession, user_id: int) -> int:
async def get_words_count(session: AsyncSession, user_id: int, learning_lang: Optional[str] = None) -> int:
"""
Получить количество слов в словаре пользователя
@@ -100,7 +126,9 @@ class VocabularyService:
result = await session.execute(
select(Vocabulary).where(Vocabulary.user_id == user_id)
)
return len(list(result.scalars().all()))
words = list(result.scalars().all())
words = VocabularyService._filter_by_learning_lang(words, learning_lang)
return len(words)
@staticmethod
async def find_word(session: AsyncSession, user_id: int, word: str) -> Optional[Vocabulary]: