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,16 +1,16 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import type { SaveStatus } from '../../hooks/useAutoSave'
|
||||
import {
|
||||
BEDTIME,
|
||||
BREAKS,
|
||||
ENCOURAGEMENTS,
|
||||
ERRORS,
|
||||
GREETING,
|
||||
MILESTONES,
|
||||
TIPS,
|
||||
WELCOME_BACK,
|
||||
bedtime,
|
||||
breaks,
|
||||
encouragements,
|
||||
errors,
|
||||
greeting,
|
||||
milestoneLine,
|
||||
pick,
|
||||
tips,
|
||||
welcomeBack,
|
||||
type Line,
|
||||
} from './tips'
|
||||
import { analyzeProse } from './prose'
|
||||
@@ -60,11 +60,11 @@ const HOVER_GRACE_MS = 3_000 // lingers this long after she stops hovering
|
||||
const now = () => Date.now()
|
||||
|
||||
// Reading time for a bubble: a base floor by tone, stretched by the combined
|
||||
// length of the Mandarin + English lines so denser advice stays up long enough
|
||||
// length of the native + English lines so denser advice stays up long enough
|
||||
// to actually finish reading.
|
||||
function readBubbleMs(b: Bubble): number {
|
||||
const base = b.tone === 'cheer' ? CHEER_MS : b.tone === 'bedtime' ? BUBBLE_MS + 4_000 : BUBBLE_MS
|
||||
const chars = b.zh.length + b.en.length
|
||||
const chars = b.native.length + b.en.length
|
||||
return Math.min(MAX_BUBBLE_MS, base + chars * READ_MS_PER_CHAR)
|
||||
}
|
||||
|
||||
@@ -138,9 +138,9 @@ export function useCompanion({ wordCount, saveStatus, llmDown, editTick, acceptT
|
||||
if (hint) {
|
||||
lastRule.current = hint.rule
|
||||
recentHints.current = [hint.id, ...recentHints.current].slice(0, 8)
|
||||
return { zh: hint.zh, en: hint.en, tone: 'tip' }
|
||||
return { native: hint.native, en: hint.en, tone: 'tip' }
|
||||
}
|
||||
return { ...pick(TIPS), tone: 'tip' }
|
||||
return { ...pick(tips()), tone: 'tip' }
|
||||
}, [])
|
||||
|
||||
const dismiss = useCallback(() => {
|
||||
@@ -167,7 +167,7 @@ export function useCompanion({ wordCount, saveStatus, llmDown, editTick, acceptT
|
||||
|
||||
// Opening hello (once), after a short beat so it doesn't race the first paint.
|
||||
useEffect(() => {
|
||||
const id = setTimeout(() => say({ ...GREETING, tone: 'tip' }), 1200)
|
||||
const id = setTimeout(() => say({ ...greeting(), tone: 'tip' }), 1200)
|
||||
return () => clearTimeout(id)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
@@ -183,7 +183,7 @@ export function useCompanion({ wordCount, saveStatus, llmDown, editTick, acceptT
|
||||
if (sleeping.current) {
|
||||
sleeping.current = false
|
||||
sessionStart.current = now() // a fresh stretch starts on return
|
||||
say({ ...WELCOME_BACK, tone: 'cheer' })
|
||||
say({ ...welcomeBack(), tone: 'cheer' })
|
||||
} else if (mood === 'sleeping') {
|
||||
setMood('idle')
|
||||
}
|
||||
@@ -197,7 +197,7 @@ export function useCompanion({ wordCount, saveStatus, llmDown, editTick, acceptT
|
||||
firstAccept.current = false
|
||||
return
|
||||
}
|
||||
say({ ...pick(ENCOURAGEMENTS), tone: 'cheer' }, { celebrate: true })
|
||||
say({ ...pick(encouragements()), tone: 'cheer' }, { celebrate: true })
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [acceptTick])
|
||||
|
||||
@@ -229,7 +229,7 @@ export function useCompanion({ wordCount, saveStatus, llmDown, editTick, acceptT
|
||||
return
|
||||
}
|
||||
lastError.current = t
|
||||
say({ ...pick(ERRORS), tone: 'error' }, { sound: 'error' })
|
||||
say({ ...pick(errors()), tone: 'error' }, { sound: 'error' })
|
||||
}, [say])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -246,7 +246,7 @@ export function useCompanion({ wordCount, saveStatus, llmDown, editTick, acceptT
|
||||
// Occasional gentle cheer on a successful save (kept rare so it isn't noise).
|
||||
useEffect(() => {
|
||||
if (saveStatus === 'saved' && Math.random() < 0.18) {
|
||||
say({ ...pick(ENCOURAGEMENTS), tone: 'cheer' }, { proactive: true })
|
||||
say({ ...pick(encouragements()), tone: 'cheer' }, { proactive: true })
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [saveStatus])
|
||||
@@ -267,7 +267,7 @@ export function useCompanion({ wordCount, saveStatus, llmDown, editTick, acceptT
|
||||
if (t - sessionStart.current > BREAK_MS && t - lastBreak.current > BREAK_MS) {
|
||||
lastBreak.current = t
|
||||
sessionStart.current = t
|
||||
say({ ...pick(BREAKS), tone: 'break' }, { proactive: true })
|
||||
say({ ...pick(breaks()), tone: 'break' }, { proactive: true })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -276,7 +276,7 @@ export function useCompanion({ wordCount, saveStatus, llmDown, editTick, acceptT
|
||||
// returned if she's away/napping).
|
||||
if (isBedtime() && t - lastBedtime.current > BEDTIME_GAP) {
|
||||
lastBedtime.current = t
|
||||
say({ ...pick(BEDTIME), tone: 'bedtime' }, { proactive: true })
|
||||
say({ ...pick(bedtime()), tone: 'bedtime' }, { proactive: true })
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user