Fix: Topic word labels translate to selected language
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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() {
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{topic.words.slice(0, 15).map(w => (
|
||||
<span key={w} className={`text-xs px-2 py-0.5 rounded-full ${isDark ? 'bg-white/5 text-white/50' : 'bg-slate-100 text-slate-500'}`}>{w}</span>
|
||||
{(topic.display_words || topic.words).slice(0, 15).map((w: string, i: number) => (
|
||||
<span key={i} className={`text-xs px-2 py-0.5 rounded-full ${isDark ? 'bg-white/5 text-white/50' : 'bg-slate-100 text-slate-500'}`}>{w}</span>
|
||||
))}
|
||||
{topic.words.length > 15 && (
|
||||
<span className={`text-xs px-2 py-0.5 ${isDark ? 'text-white/30' : 'text-slate-400'}`}>+{topic.words.length - 15}</span>
|
||||
|
||||
Reference in New Issue
Block a user