From 387219682dee4ebc558b77cc239cc208d4399e3a Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Wed, 29 Apr 2026 12:38:35 +0200 Subject: [PATCH] Fix: Topic word labels translate to selected language MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Topics API now accepts lang= parameter. When lang=de, the word labels are translated from English via Kaikki translations: "eye, pupil, iris" → "Auge, Pupille, Iris" Frontend sends searchLang to /topics endpoint and displays display_words (translated) instead of words (English). Co-Authored-By: Claude Opus 4.6 (1M context) --- backend-lehrer/vocabulary/api.py | 41 +++++++++++++++++++++++++++---- studio-v2/app/vocabulary/page.tsx | 8 +++--- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/backend-lehrer/vocabulary/api.py b/backend-lehrer/vocabulary/api.py index 5915784..ff26f20 100644 --- a/backend-lehrer/vocabulary/api.py +++ b/backend-lehrer/vocabulary/api.py @@ -432,11 +432,15 @@ async def api_enrich_images(word_ids: List[str] = None): @router.get("/topics") -async def api_get_topics(q: str = Query("", description="Search topic or word")): +async def api_get_topics( + q: str = Query("", description="Search topic or word"), + lang: str = Query("en", description="Display language for word labels"), +): """Find topics matching a search word. Returns related word lists. If q matches a topic name → returns that topic. If q matches a word in any topic → returns all topics containing that word. + Words are returned with translations if lang != en. """ from vocabulary.db import get_pool pool = await get_pool() @@ -451,10 +455,37 @@ async def api_get_topics(q: str = Query("", description="Search topic or word")) ORDER BY word_count DESC """, f"%{q_lower}%", q_lower) - return { - "topics": [{"topic": r["topic"], "words": list(r["words"]), "word_count": r["word_count"]} for r in rows], - "query": q, - } + # Translate word labels if not English + topics = [] + for r in rows: + en_words = list(r["words"]) + display_words = en_words + if lang != "en": + # Batch-lookup translations from Kaikki + translated = [] + for w in en_words[:20]: # Limit to 20 for speed + tr_row = await conn.fetchrow( + "SELECT translations FROM vocabulary_kaikki WHERE lang = 'en' AND lower(word) = $1 LIMIT 1", + w.lower(), + ) + if tr_row and tr_row["translations"]: + import json as _json + tr = tr_row["translations"] + if isinstance(tr, str): + tr = _json.loads(tr) + tr_text = tr.get(lang, {}).get("text", "") + translated.append(tr_text if tr_text else w) + else: + translated.append(w) + display_words = translated + en_words[20:] + topics.append({ + "topic": r["topic"], + "words": en_words, + "display_words": display_words, + "word_count": r["word_count"], + }) + + return {"topics": topics, "query": q, "lang": lang} class TranslateRequest(BaseModel): diff --git a/studio-v2/app/vocabulary/page.tsx b/studio-v2/app/vocabulary/page.tsx index 2365f61..5bf1e55 100644 --- a/studio-v2/app/vocabulary/page.tsx +++ b/studio-v2/app/vocabulary/page.tsx @@ -38,7 +38,7 @@ export default function VocabularyPage() { const [posFilter, setPosFilter] = useState('') const [diffFilter, setDiffFilter] = useState(0) const [searchLang, setSearchLang] = useState('en') - const [topics, setTopics] = useState<{ topic: string; words: string[]; word_count: number }[]>([]) + const [topics, setTopics] = useState<{ topic: string; words: string[]; display_words?: string[]; word_count: number }[]>([]) const [showTopics, setShowTopics] = useState(false) // Unit builder @@ -89,7 +89,7 @@ export default function VocabularyPage() { // Also search for matching topics if (query.trim()) { - const topicResp = await fetch(`${getApiBase()}/api/vocabulary/topics?q=${encodeURIComponent(query)}`) + const topicResp = await fetch(`${getApiBase()}/api/vocabulary/topics?q=${encodeURIComponent(query)}&lang=${searchLang}`) if (topicResp.ok) { const topicData = await topicResp.json() setTopics(topicData.topics || []) @@ -259,8 +259,8 @@ export default function VocabularyPage() {
- {topic.words.slice(0, 15).map(w => ( - {w} + {(topic.display_words || topic.words).slice(0, 15).map((w: string, i: number) => ( + {w} ))} {topic.words.length > 15 && ( +{topic.words.length - 15}