The garden learns which language a card is in, and read-aloud stops guessing
Phase 28 (c), the last of the phase. A word met inside a Portuguese document is a Portuguese card: migration 0018 mirrors documents.doc_lang onto vocab_words, set server-side from the ownership lookup capture was already making. Every card still reviews — filtering the queue to the half she is learning would drop the words she actually met. Read-aloud was the larger surprise. detectLang routed Han/kana to Chinese and everything else to en-US, so the zh pair was accidentally right and every Latin pair wrong. doc_lang now reaches the client read-only on the document JSON, and docLang(text, verdict) answers for a passage taken out of it — with the script test still winning, because quoted Chinese must never be spelled out one "Chinese letter" at a time. Claude-Session: https://claude.ai/code/session_01GJHNvirh7Hzhc9RL3HAvz7
This commit is contained in:
@@ -33,8 +33,8 @@ import { Composition } from './Composition'
|
||||
import { RewritePreview, type RewriteStatus } from './RewritePreview'
|
||||
import { planBatch } from './acceptBatch'
|
||||
import { entryId, idAfterRemoval, stepId, type Direction, type Span } from './triage'
|
||||
import { api, type Suggestion, type SuggestionType, type WordInfo } from '../../api/client'
|
||||
import { speak, speechSupported } from '../../audio/speech'
|
||||
import { api, type DocLang, type Suggestion, type SuggestionType, type WordInfo } from '../../api/client'
|
||||
import { docLang as docLocale, speak, speechSupported } from '../../audio/speech'
|
||||
import type { SpellChecker } from '../../hooks/useSpellChecker'
|
||||
import { fromIME } from '../../lib/ime'
|
||||
import type { Segmenter } from '../../lib/segment'
|
||||
@@ -65,6 +65,13 @@ export interface EditorChange {
|
||||
interface Props {
|
||||
// Changing docId reloads the editor with that document's content.
|
||||
docId: string
|
||||
// Which language this document is written in, as the server decided it
|
||||
// ('' | 'en' | 'pair'). Read-aloud is the only thing here that reads it: a
|
||||
// Portuguese selection has to be read by the Portuguese voice, and nothing in
|
||||
// the letters says so. Updated by the server on save, so it trails a language
|
||||
// flip by one auto-save — a passage read in the old voice once is the whole
|
||||
// cost of not blocking the editor on a check.
|
||||
docLang: DocLang
|
||||
initialContent: string
|
||||
onChange: (change: EditorChange) => void
|
||||
// LLM suggestions to highlight; accept/dismiss notify the parent for the API
|
||||
@@ -255,6 +262,7 @@ interface RewriteState {
|
||||
// decoration layer. Hovering a highlight opens its SuggestionCard.
|
||||
export function EditorCore({
|
||||
docId,
|
||||
docLang,
|
||||
initialContent,
|
||||
onChange,
|
||||
suggestions,
|
||||
@@ -1532,8 +1540,10 @@ export function EditorCore({
|
||||
<SelectionBubble
|
||||
style={{ top: selection.top, left: selection.left, transform: 'translateY(calc(-100% - 8px))' }}
|
||||
onRewrite={handleRewrite}
|
||||
onSpeak={speechSupported() ? () => speak(selection.text) : null}
|
||||
onSpeakSlow={speechSupported() ? () => speak(selection.text, undefined, true) : null}
|
||||
onSpeak={speechSupported() ? () => speak(selection.text, docLocale(selection.text, docLang)) : null}
|
||||
onSpeakSlow={
|
||||
speechSupported() ? () => speak(selection.text, docLocale(selection.text, docLang), true) : null
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{rewrite && (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { api, type VocabGrade, type VocabWord } from '../../api/client'
|
||||
import { speak, speechSupported, stopSpeech } from '../../audio/speech'
|
||||
import { docLang as docLocale, speak, speechSupported, stopSpeech } from '../../audio/speech'
|
||||
import { useFocusTrap } from '../../hooks/useFocusTrap'
|
||||
import { usePack, type Line } from '../../i18n'
|
||||
import { JournalView } from './JournalView'
|
||||
@@ -314,7 +314,21 @@ function GardenView({
|
||||
{blossom(w.reps)}
|
||||
</span>
|
||||
<span className="flex min-w-0 flex-1 flex-col">
|
||||
<span className="truncate text-sm font-bold text-plum">{w.word}</span>
|
||||
<span className="flex min-w-0 items-center gap-1.5">
|
||||
<span className="truncate text-sm font-bold text-plum">{w.word}</span>
|
||||
{/* Only a card in her own language is marked. An English
|
||||
garden with a badge on every blossom would be a
|
||||
garden with no badges at all; the marker exists so
|
||||
the rare Portuguese word is legible among them. */}
|
||||
{w.lang === 'pair' && (
|
||||
<span
|
||||
className="shrink-0 rounded-full px-1.5 py-0.5 text-[9px] font-bold lowercase"
|
||||
style={{ background: 'var(--color-surface-alt)', color: 'var(--color-muted)' }}
|
||||
>
|
||||
{t.nativeName}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{(w.gloss || w.definition) && (
|
||||
<span
|
||||
className="truncate text-xs"
|
||||
@@ -354,7 +368,7 @@ function GardenView({
|
||||
{speechSupported() && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => speak(w.word)}
|
||||
onClick={() => speak(w.word, docLocale(w.word, w.lang))}
|
||||
className="rounded-full px-2.5 py-1 text-xs font-semibold"
|
||||
style={{ background: 'var(--color-surface-alt)' }}
|
||||
>
|
||||
@@ -477,7 +491,7 @@ function ReviewSession({
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => speak(card.word)}
|
||||
onClick={() => speak(card.word, docLocale(card.word, card.lang))}
|
||||
aria-label={`Pronounce ${card.word}`}
|
||||
className="flex h-6 w-6 items-center justify-center rounded-full text-xs"
|
||||
style={{ background: 'var(--color-surface)' }}
|
||||
@@ -488,7 +502,7 @@ function ReviewSession({
|
||||
worth hearing stretched out. */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => speak(card.word, undefined, true)}
|
||||
onClick={() => speak(card.word, docLocale(card.word, card.lang), true)}
|
||||
aria-label={`Pronounce ${card.word} slowly`}
|
||||
title={t.garden.readSlowly}
|
||||
className="flex h-6 w-6 items-center justify-center rounded-full text-xs"
|
||||
|
||||
Reference in New Issue
Block a user