The translate card, pointed the other way, and a call that no longer happens

Phase 28's step (b): both remaining items are about direction, and both had
a wrong answer that looked right.

isTranslation could not simply be read backwards. readsAsEnglish is a
deliberately low bar — Latin letters, not swamped by another script — which
every Portuguese sentence clears as easily as English does, so swapping its
two halves would have called every genuine Portuguese correction inside a
Portuguese document a translation. The flipped direction uses sentenceLang
from doclang.go instead, where English has its own curated marker list and
has to out-evidence the pair language to win. The English-document path is
untouched; reconcilePending carries the verdict to ask the question the
right way round.

The tap-through's whole observable change is a model call that stops
happening. /suggestions/{id}/translate now recovers the explanation's
language by re-running targetFor rather than assuming the pair, which gives
today's answer everywhere except the case that was broken: the Portuguese
writer whose explanation already arrived in Portuguese, previously
round-tripped through the model into Portuguese again. It answers "" there,
and the client's existing `res.translation.trim() || explanation` fallback
seeds the bubble with the explanation itself — no frontend change at all.
It deliberately does not render that explanation into English on the
grounds that English is technically the other half: an unasked-for
rendering into the language she is practising is noise, not a seed.

Tests pin both directions of the detector, with Portuguese-in-Portuguese as
the case the file exists for, plus four handler tests through the real
/check and /translate paths — including the skipped seed asserting the
model was never called, and the learning_pair zh learner whose English
explanation still renders into Chinese.

Left of the phase: (c) the garden's language tagging and read-aloud.

Claude-Session: https://claude.ai/code/session_01GJHNvirh7Hzhc9RL3HAvz7
This commit is contained in:
prosolis
2026-07-28 23:29:50 -07:00
parent 76dede8856
commit 29eb2fe1fc
7 changed files with 318 additions and 36 deletions
+26 -7
View File
@@ -28,16 +28,35 @@ import (
// common words a sentence in that language can hardly avoid and an English
// sentence has no reason to contain.
// isTranslation reports whether this edit is her own language rendered into
// English, rather than a correction to her English. Both halves must hold: the
// quoted span reads as the pair language, and what Petal offers back reads as
// English. The second half matters — a Chinese span rewritten into different
// Chinese is something else entirely, and Petal has no business calling it a
// translation.
func isTranslation(original, replacement, pairLang string) bool {
// isTranslation reports whether this edit is a rendering of one language into
// the other, rather than a correction. Both halves must hold: the quoted span
// reads as one language, and what Petal offers back reads as the other. The
// second half matters — a Chinese span rewritten into different Chinese is
// something else entirely, and Petal has no business calling it a translation.
//
// Which way it points follows the document (Phase 28). In an English document
// the translate card is her language rendered into English — she reached for a
// sentence she couldn't say yet, and Petal said it for her. In a document she
// wrote in her own language the useful card is the mirror image: an English
// sentence she dropped into her Portuguese, rendered into Portuguese. Asking the
// English-document question there would label nothing, and the card would file
// as a correction to prose that was never wrong.
//
// The flipped direction cannot be the same test read backwards. `readsAsEnglish`
// is a low bar on purpose — Latin letters, not swamped by another script — which
// every Portuguese sentence also clears, so using it on the *original* would
// call every genuine Portuguese correction a translation. The flipped test
// instead uses the sentence-level vote from doclang.go, where English has its
// own marker list and has to out-evidence the pair language to win.
func isTranslation(original, replacement, pairLang, docLang string) bool {
if strings.TrimSpace(original) == "" || strings.TrimSpace(replacement) == "" {
return false
}
if normalizeDocLang(docLang) == docLangPair {
p := normalizePairLang(pairLang)
return sentenceLang(original, p) == docLangEnglish &&
sentenceLang(replacement, p) == docLangPair
}
return readsAsPairLang(original, pairLang) && readsAsEnglish(replacement)
}