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:
@@ -56,27 +56,27 @@ class AIService:
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def translate_word(self, word: str, target_lang: str = "ru") -> Dict:
|
||||
async def translate_word(self, word: str, source_lang: str = "en", translation_lang: str = "ru") -> Dict:
|
||||
"""
|
||||
Перевести слово и получить дополнительную информацию
|
||||
|
||||
Args:
|
||||
word: Слово для перевода
|
||||
target_lang: Язык перевода (по умолчанию русский)
|
||||
source_lang: Язык исходного слова (ISO2)
|
||||
translation_lang: Язык перевода (ISO2)
|
||||
|
||||
Returns:
|
||||
Dict с переводом, транскрипцией и примерами
|
||||
"""
|
||||
prompt = f"""Переведи английское слово/фразу "{word}" на русский язык.
|
||||
prompt = f"""Переведи слово/фразу "{word}" с языка {source_lang} на {translation_lang}.
|
||||
|
||||
Верни ответ строго в формате JSON:
|
||||
{{
|
||||
"word": "{word}",
|
||||
"translation": "перевод",
|
||||
"transcription": "транскрипция в IPA",
|
||||
"word": "исходное слово на {source_lang}",
|
||||
"translation": "перевод на {translation_lang}",
|
||||
"transcription": "транскрипция в IPA (если применимо)",
|
||||
"examples": [
|
||||
{{"en": "пример на английском", "ru": "перевод примера"}},
|
||||
{{"en": "ещё один пример", "ru": "перевод примера"}}
|
||||
{{"{source_lang}": "пример на языке обучения", "{translation_lang}": "перевод примера"}}
|
||||
],
|
||||
"category": "категория слова (работа, еда, путешествия и т.д.)",
|
||||
"difficulty": "уровень сложности (A1/A2/B1/B2/C1/C2)"
|
||||
@@ -85,10 +85,10 @@ class AIService:
|
||||
Важно: верни только JSON, без дополнительного текста."""
|
||||
|
||||
try:
|
||||
logger.info(f"[GPT Request] translate_word: word='{word}', target_lang='{target_lang}'")
|
||||
logger.info(f"[GPT Request] translate_word: word='{word}', source='{source_lang}', to='{translation_lang}'")
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "Ты - помощник для изучения английского языка. Отвечай только в формате JSON."},
|
||||
{"role": "system", "content": "Ты - помощник для изучения языков. Отвечай только в формате JSON."},
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
@@ -161,33 +161,35 @@ class AIService:
|
||||
"score": 0
|
||||
}
|
||||
|
||||
async def generate_fill_in_sentence(self, word: str) -> Dict:
|
||||
async def generate_fill_in_sentence(self, word: str, learning_lang: str = "en", translation_lang: str = "ru") -> Dict:
|
||||
"""
|
||||
Сгенерировать предложение с пропуском для заданного слова
|
||||
|
||||
Args:
|
||||
word: Слово, для которого нужно создать предложение
|
||||
word: Слово (на языке обучения), для которого нужно создать предложение
|
||||
learning_lang: Язык обучения (ISO2)
|
||||
translation_lang: Язык перевода предложения (ISO2)
|
||||
|
||||
Returns:
|
||||
Dict с предложением и правильным ответом
|
||||
"""
|
||||
prompt = f"""Создай предложение на английском языке, используя слово "{word}".
|
||||
prompt = f"""Создай предложение на языке {learning_lang}, используя слово "{word}".
|
||||
Замени это слово на пропуск "___".
|
||||
|
||||
Верни ответ в формате JSON:
|
||||
{{
|
||||
"sentence": "предложение с пропуском ___",
|
||||
"answer": "{word}",
|
||||
"translation": "перевод предложения на русский"
|
||||
"translation": "перевод предложения на {translation_lang}"
|
||||
}}
|
||||
|
||||
Предложение должно быть простым и естественным. Контекст должен четко подсказывать правильное слово."""
|
||||
|
||||
try:
|
||||
logger.info(f"[GPT Request] generate_fill_in_sentence: word='{word}'")
|
||||
logger.info(f"[GPT Request] generate_fill_in_sentence: word='{word}', lang='{learning_lang}', to='{translation_lang}'")
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "Ты - преподаватель английского языка. Создавай простые и понятные упражнения."},
|
||||
{"role": "system", "content": "Ты - преподаватель иностранных языков. Создавай простые и понятные упражнения."},
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
@@ -206,7 +208,7 @@ class AIService:
|
||||
"translation": f"Мне нравится {word} каждый день."
|
||||
}
|
||||
|
||||
async def generate_thematic_words(self, theme: str, level: str = "B1", count: int = 10) -> List[Dict]:
|
||||
async def generate_thematic_words(self, theme: str, level: str = "B1", count: int = 10, learning_lang: str = "en", translation_lang: str = "ru") -> List[Dict]:
|
||||
"""
|
||||
Сгенерировать подборку слов по теме
|
||||
|
||||
@@ -218,17 +220,17 @@ class AIService:
|
||||
Returns:
|
||||
Список словарей с информацией о словах
|
||||
"""
|
||||
prompt = f"""Создай подборку из {count} английских слов по теме "{theme}" для уровня {level}.
|
||||
prompt = f"""Создай подборку из {count} слов на языке {learning_lang} по теме "{theme}" для уровня {level}. Переводы дай на {translation_lang}.
|
||||
|
||||
Верни ответ в формате JSON:
|
||||
{{
|
||||
"theme": "{theme}",
|
||||
"words": [
|
||||
{{
|
||||
"word": "английское слово",
|
||||
"translation": "перевод на русский",
|
||||
"transcription": "транскрипция в IPA",
|
||||
"example": "пример использования на английском"
|
||||
"word": "слово на {learning_lang}",
|
||||
"translation": "перевод на {translation_lang}",
|
||||
"transcription": "транскрипция в IPA (если применимо)",
|
||||
"example": "пример использования на {learning_lang}"
|
||||
}}
|
||||
]
|
||||
}}
|
||||
@@ -240,10 +242,10 @@ class AIService:
|
||||
- Разнообразными (существительные, глаголы, прилагательные)"""
|
||||
|
||||
try:
|
||||
logger.info(f"[GPT Request] generate_thematic_words: theme='{theme}', level='{level}', count={count}")
|
||||
logger.info(f"[GPT Request] generate_thematic_words: theme='{theme}', level='{level}', count={count}, learn='{learning_lang}', to='{translation_lang}'")
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "Ты - преподаватель английского языка. Подбирай полезные и актуальные слова."},
|
||||
{"role": "system", "content": "Ты - преподаватель иностранных языков. Подбирай полезные и актуальные слова."},
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
@@ -259,7 +261,7 @@ class AIService:
|
||||
logger.error(f"[GPT Error] generate_thematic_words: {type(e).__name__}: {str(e)}")
|
||||
return []
|
||||
|
||||
async def extract_words_from_text(self, text: str, level: str = "B1", max_words: int = 15) -> List[Dict]:
|
||||
async def extract_words_from_text(self, text: str, level: str = "B1", max_words: int = 15, learning_lang: str = "en", translation_lang: str = "ru") -> List[Dict]:
|
||||
"""
|
||||
Извлечь ключевые слова из текста для изучения
|
||||
|
||||
@@ -271,7 +273,7 @@ class AIService:
|
||||
Returns:
|
||||
Список словарей с информацией о словах
|
||||
"""
|
||||
prompt = f"""Проанализируй следующий английский текст и извлеки из него до {max_words} самых полезных слов для изучения на уровне {level}.
|
||||
prompt = f"""Проанализируй следующий текст на языке {learning_lang} и извлеки из него до {max_words} самых полезных слов для изучения на уровне {level}. Переводы дай на {translation_lang}.
|
||||
|
||||
Текст:
|
||||
{text}
|
||||
@@ -280,10 +282,10 @@ class AIService:
|
||||
{{
|
||||
"words": [
|
||||
{{
|
||||
"word": "английское слово (в базовой форме)",
|
||||
"translation": "перевод на русский",
|
||||
"transcription": "транскрипция в IPA",
|
||||
"context": "предложение из текста, где используется это слово"
|
||||
"word": "слово на {learning_lang} (в базовой форме)",
|
||||
"translation": "перевод на {translation_lang}",
|
||||
"transcription": "транскрипция в IPA (если применимо)",
|
||||
"context": "предложение из текста на {learning_lang}, где используется это слово"
|
||||
}}
|
||||
]
|
||||
}}
|
||||
@@ -297,10 +299,10 @@ class AIService:
|
||||
|
||||
try:
|
||||
text_preview = text[:100] + "..." if len(text) > 100 else text
|
||||
logger.info(f"[GPT Request] extract_words_from_text: text_length={len(text)}, level='{level}', max_words={max_words}")
|
||||
logger.info(f"[GPT Request] extract_words_from_text: text_length={len(text)}, level='{level}', max_words={max_words}, learn='{learning_lang}', to='{translation_lang}'")
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "Ты - преподаватель английского языка. Помогаешь извлекать полезные слова для изучения из текстов."},
|
||||
{"role": "system", "content": "Ты - преподаватель иностранных языков. Помогаешь извлекать полезные слова для изучения из текстов."},
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
@@ -316,7 +318,7 @@ class AIService:
|
||||
logger.error(f"[GPT Error] extract_words_from_text: {type(e).__name__}: {str(e)}")
|
||||
return []
|
||||
|
||||
async def start_conversation(self, scenario: str, level: str = "B1") -> Dict:
|
||||
async def start_conversation(self, scenario: str, level: str = "B1", learning_lang: str = "en", translation_lang: str = "ru") -> Dict:
|
||||
"""
|
||||
Начать диалоговую практику с AI
|
||||
|
||||
@@ -338,14 +340,14 @@ class AIService:
|
||||
|
||||
scenario_desc = scenarios.get(scenario, "повседневный разговор")
|
||||
|
||||
prompt = f"""Ты - собеседник для практики английского языка уровня {level}.
|
||||
Начни диалог в сценарии: {scenario_desc}.
|
||||
prompt = f"""Ты - собеседник для практики языка {learning_lang} уровня {level}.
|
||||
Начни диалог в сценарии: {scenario_desc} на {learning_lang}.
|
||||
|
||||
Верни ответ в формате JSON:
|
||||
{{
|
||||
"message": "твоя первая реплика на английском",
|
||||
"translation": "перевод на русский",
|
||||
"context": "краткое описание ситуации на русском",
|
||||
"message": "твоя первая реплика на {learning_lang}",
|
||||
"translation": "перевод на {translation_lang}",
|
||||
"context": "краткое описание ситуации на {translation_lang}",
|
||||
"suggestions": ["подсказка 1", "подсказка 2", "подсказка 3"]
|
||||
}}
|
||||
|
||||
@@ -356,10 +358,10 @@ class AIService:
|
||||
- Подсказки должны помочь пользователю ответить"""
|
||||
|
||||
try:
|
||||
logger.info(f"[GPT Request] start_conversation: scenario='{scenario}', level='{level}'")
|
||||
logger.info(f"[GPT Request] start_conversation: scenario='{scenario}', level='{level}', learn='{learning_lang}', to='{translation_lang}'")
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "Ты - дружелюбный собеседник для практики английского. Веди естественный диалог."},
|
||||
{"role": "system", "content": "Ты - дружелюбный собеседник для практики иностранных языков. Веди естественный диалог."},
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
@@ -384,7 +386,9 @@ class AIService:
|
||||
conversation_history: List[Dict],
|
||||
user_message: str,
|
||||
scenario: str,
|
||||
level: str = "B1"
|
||||
level: str = "B1",
|
||||
learning_lang: str = "en",
|
||||
translation_lang: str = "ru"
|
||||
) -> Dict:
|
||||
"""
|
||||
Продолжить диалог и проверить ответ пользователя
|
||||
@@ -404,7 +408,7 @@ class AIService:
|
||||
for msg in conversation_history[-6:] # Последние 6 сообщений
|
||||
])
|
||||
|
||||
prompt = f"""Ты ведешь диалог на английском языке уровня {level} в сценарии "{scenario}".
|
||||
prompt = f"""Ты ведешь диалог на языке {learning_lang} уровня {level} в сценарии "{scenario}".
|
||||
|
||||
История диалога:
|
||||
{history_text}
|
||||
@@ -412,8 +416,8 @@ User: {user_message}
|
||||
|
||||
Верни ответ в формате JSON:
|
||||
{{
|
||||
"response": "твой ответ на английском",
|
||||
"translation": "перевод твоего ответа на русский",
|
||||
"response": "твой ответ на {learning_lang}",
|
||||
"translation": "перевод твоего ответа на {translation_lang}",
|
||||
"feedback": {{
|
||||
"has_errors": true/false,
|
||||
"corrections": "исправления ошибок пользователя (если есть)",
|
||||
@@ -429,11 +433,11 @@ User: {user_message}
|
||||
- Используй лексику уровня {level}"""
|
||||
|
||||
try:
|
||||
logger.info(f"[GPT Request] continue_conversation: scenario='{scenario}', level='{level}', history_length={len(conversation_history)}")
|
||||
logger.info(f"[GPT Request] continue_conversation: scenario='{scenario}', level='{level}', history_length={len(conversation_history)}, learn='{learning_lang}', to='{translation_lang}'")
|
||||
|
||||
# Формируем сообщения для API
|
||||
messages = [
|
||||
{"role": "system", "content": f"Ты - дружелюбный собеседник для практики английского языка уровня {level}. Веди естественный диалог и помогай исправлять ошибки."}
|
||||
{"role": "system", "content": f"Ты - дружелюбный собеседник для практики языка {learning_lang} уровня {level}. Веди естественный диалог и помогай исправлять ошибки."}
|
||||
]
|
||||
|
||||
# Добавляем историю
|
||||
|
||||
@@ -75,7 +75,9 @@ class TaskService:
|
||||
async def generate_mixed_tasks(
|
||||
session: AsyncSession,
|
||||
user_id: int,
|
||||
count: int = 5
|
||||
count: int = 5,
|
||||
learning_lang: str = 'en',
|
||||
translation_lang: str = 'ru'
|
||||
) -> List[Dict]:
|
||||
"""
|
||||
Генерация заданий разных типов (переводы + заполнение пропусков)
|
||||
@@ -109,23 +111,31 @@ class TaskService:
|
||||
task_type = random.choice(['translate', 'fill_in'])
|
||||
|
||||
if task_type == 'translate':
|
||||
# Задание на перевод
|
||||
direction = random.choice(['en_to_ru', 'ru_to_en'])
|
||||
# Задание на перевод между языком обучения и языком перевода
|
||||
direction = random.choice(['learn_to_tr', 'tr_to_learn'])
|
||||
|
||||
if direction == 'en_to_ru':
|
||||
# Локализация фразы "Переведи слово"
|
||||
if translation_lang == 'en':
|
||||
prompt = "Translate the word:"
|
||||
elif translation_lang == 'ja':
|
||||
prompt = "単語を訳してください:"
|
||||
else:
|
||||
prompt = "Переведи слово:"
|
||||
|
||||
if direction == 'learn_to_tr':
|
||||
task = {
|
||||
'type': 'translate_to_ru',
|
||||
'type': f'translate_to_{translation_lang}',
|
||||
'word_id': word.id,
|
||||
'question': f"Переведи слово: <b>{word.word_original}</b>",
|
||||
'question': f"{prompt} <b>{word.word_original}</b>",
|
||||
'word': word.word_original,
|
||||
'correct_answer': word.word_translation,
|
||||
'transcription': word.transcription
|
||||
}
|
||||
else:
|
||||
task = {
|
||||
'type': 'translate_to_en',
|
||||
'type': f'translate_to_{learning_lang}',
|
||||
'word_id': word.id,
|
||||
'question': f"Переведи слово: <b>{word.word_translation}</b>",
|
||||
'question': f"{prompt} <b>{word.word_translation}</b>",
|
||||
'word': word.word_translation,
|
||||
'correct_answer': word.word_original,
|
||||
'transcription': word.transcription
|
||||
@@ -133,13 +143,25 @@ class TaskService:
|
||||
else:
|
||||
# Задание на заполнение пропуска
|
||||
# Генерируем предложение с пропуском через AI
|
||||
sentence_data = await ai_service.generate_fill_in_sentence(word.word_original)
|
||||
sentence_data = await ai_service.generate_fill_in_sentence(
|
||||
word.word_original,
|
||||
learning_lang=learning_lang,
|
||||
translation_lang=translation_lang
|
||||
)
|
||||
|
||||
# Локализация заголовка
|
||||
if translation_lang == 'en':
|
||||
fill_title = "Fill in the blank in the sentence:"
|
||||
elif translation_lang == 'ja':
|
||||
fill_title = "文の空欄を埋めてください:"
|
||||
else:
|
||||
fill_title = "Заполни пропуск в предложении:"
|
||||
|
||||
task = {
|
||||
'type': 'fill_in',
|
||||
'word_id': word.id,
|
||||
'question': (
|
||||
f"Заполни пропуск в предложении:\n\n"
|
||||
f"{fill_title}\n\n"
|
||||
f"<b>{sentence_data['sentence']}</b>\n\n"
|
||||
f"<i>{sentence_data.get('translation', '')}</i>"
|
||||
),
|
||||
|
||||
@@ -95,3 +95,22 @@ class UserService:
|
||||
if user:
|
||||
user.language_interface = language
|
||||
await session.commit()
|
||||
|
||||
@staticmethod
|
||||
async def update_user_learning_language(session: AsyncSession, user_id: int, language: str):
|
||||
"""
|
||||
Обновить язык изучения пользователя
|
||||
|
||||
Args:
|
||||
session: Сессия базы данных
|
||||
user_id: ID пользователя
|
||||
language: Новый язык изучения (ISO2)
|
||||
"""
|
||||
result = await session.execute(
|
||||
select(User).where(User.id == user_id)
|
||||
)
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if user:
|
||||
user.learning_language = language
|
||||
await session.commit()
|
||||
|
||||
@@ -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