Phase 19: the copy stops being hardcoded Mandarin
Every `中文 · English` string moves out of ~29 components into web/src/i18n: one Pack type, a verbatim zh pack, and two ways to read it — usePack() for components, pack() for the modules that build a line when something happens rather than when something renders. Anything with a value in it is a function on the pack rather than a template at the call site, English pluralisation included: word order isn't universal, and a pack author has to be able to move the number. The roster constants (tones, rewrite styles, export formats, companions) keep only value + emoji, so a label can't drift from its key. On the server, internal/llm/lang.go replaces "Simplified Chinese" in the three prompts that actually name her language. pt-PT is spelled "European Portuguese (pt-PT, never Brazilian Portuguese)" in the prompt itself, and each Lang carries her word for "why" so the tutor prompt still recognises the question when she asks it her way. pair_lang reaches the model through the row-scoped query each handler already ran — the one that proves she owns the document — rather than a second lookup that could disagree with it. Also records Phase 18's deploy: migration 0011 rehearsed against a copy of the live VPS database, then applied for real.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type { WordInfo } from '../../api/client'
|
||||
import { speak, speechSupported } from '../../audio/speech'
|
||||
import { usePack } from '../../i18n'
|
||||
|
||||
// WordCard is the right-click popover for any word: its dictionary definition(s)
|
||||
// on top and tappable synonym pills below. Clicking a synonym replaces the word
|
||||
@@ -20,6 +21,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export function WordCard({ word, info, loading, saved, onToggleSave, style, onReplace }: Props) {
|
||||
const t = usePack()
|
||||
const definitions = info?.definitions ?? []
|
||||
const synonyms = info?.synonyms ?? []
|
||||
const gloss = info?.gloss ?? ''
|
||||
@@ -47,7 +49,7 @@ export function WordCard({ word, info, loading, saved, onToggleSave, style, onRe
|
||||
className="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-bold"
|
||||
style={{ background: 'var(--color-lavender)', color: 'var(--color-plum)' }}
|
||||
>
|
||||
词语 · Word
|
||||
{t.editor.word}
|
||||
</span>
|
||||
<span className="font-bold" style={{ color: 'var(--color-plum)' }}>
|
||||
{word}
|
||||
@@ -59,7 +61,7 @@ export function WordCard({ word, info, loading, saved, onToggleSave, style, onRe
|
||||
onClick={onToggleSave}
|
||||
aria-label={saved ? 'Remove from vocabulary garden' : 'Save to vocabulary garden'}
|
||||
aria-pressed={saved}
|
||||
title={saved ? '已在词汇花园 · In your garden (tap to remove)' : '加入词汇花园 · Save to garden'}
|
||||
title={saved ? t.editor.inGarden : t.editor.saveToGarden}
|
||||
className="flex h-7 w-7 items-center justify-center rounded-full text-sm transition-transform"
|
||||
style={{
|
||||
background: saved ? 'var(--color-accent)' : 'var(--color-surface-alt)',
|
||||
@@ -73,7 +75,7 @@ export function WordCard({ word, info, loading, saved, onToggleSave, style, onRe
|
||||
type="button"
|
||||
onClick={() => speak(word)}
|
||||
aria-label={`Pronounce ${word}`}
|
||||
title="朗读 · Read aloud"
|
||||
title={t.editor.readAloud}
|
||||
className="flex h-7 w-7 items-center justify-center rounded-full text-sm"
|
||||
style={{ background: 'var(--color-surface-alt)', color: 'var(--color-plum)' }}
|
||||
>
|
||||
@@ -111,14 +113,14 @@ export function WordCard({ word, info, loading, saved, onToggleSave, style, onRe
|
||||
style={{ background: 'var(--color-accent)' }}
|
||||
aria-hidden
|
||||
/>
|
||||
查找中… · Looking up…
|
||||
{t.editor.lookingUp}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{definitions.length > 0 && (
|
||||
<div className="mt-3 space-y-2">
|
||||
<p className="text-xs font-bold" style={{ color: 'var(--color-muted)' }}>
|
||||
释义 · Definition
|
||||
{t.editor.definition}
|
||||
</p>
|
||||
<ol className="space-y-1.5">
|
||||
{definitions.map((m, i) => (
|
||||
@@ -143,7 +145,7 @@ export function WordCard({ word, info, loading, saved, onToggleSave, style, onRe
|
||||
{synonyms.length > 0 && (
|
||||
<div className="mt-3">
|
||||
<p className="mb-1.5 text-xs font-bold" style={{ color: 'var(--color-muted)' }}>
|
||||
近义词 · Synonyms <span className="font-normal">(点击替换 · tap to swap)</span>
|
||||
{t.editor.synonyms} <span className="font-normal">({t.editor.tapToSwap})</span>
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{synonyms.map((s) => (
|
||||
@@ -165,7 +167,7 @@ export function WordCard({ word, info, loading, saved, onToggleSave, style, onRe
|
||||
|
||||
{empty && (
|
||||
<p className="mt-3 leading-snug" style={{ color: 'var(--color-muted)' }}>
|
||||
没有找到这个词 · Nothing found for this word
|
||||
{t.editor.nothingFound}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user