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:
@@ -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]:
|
||||
|
||||
Reference in New Issue
Block a user