Both sections of the advice arrived in the language she is not learning

Reported from the phone: "I have my main language set as Portuguese and I say
I'm learning English but yet Petal presents the Ask Petal advice in both
sections as Portuguese."

Nothing was wrong with targetFor again. A Portuguese document by a Portuguese
writer is explained in Portuguese, which is the whole point of Phase 28. The
card was right. What was wrong was the tap underneath it: /suggestions/{id}
/translate answered "" for exactly that case, on the reasoning that an
unasked-for English rendering of an explanation she can already read is not a
seed but noise. That reasoning had the writer facing the wrong way. She is
learning English. The half she is *practising* is the half worth a tap, and the
bubble sits directly beneath the explanation inside the same card, so answering
"" left her with Portuguese, the same Portuguese again, and no English anywhere
on the card. targetFor's own comment promised the other language stays one tap
away in both directions; only one direction had ever been built.

So the endpoint keeps the one rule it always claimed: render into whichever
half the explanation is not already in. English explanation into her language,
as before; her language into the English she is learning, which is new. Both
ends of that are now parameters — TranslateMessages took the source language
for granted as English because until Phase 28 it always was. The zh prompt is
unchanged byte for byte, which its test still pins.

The client fallback was the same symptom from a different cause and would have
survived the server fix: an empty answer, or an unreachable model, seeded the
bubble with the explanation itself — a verbatim repeat of the line two above
it, which reads as Petal replying in the language the tap was pressed to
escape. With the endpoint always having somewhere to go, empty now means only
that the model didn't answer, so the panel opens with no bubble at all and the
input where she can ask. AskPetal no longer takes the explanation as a prop; it
never needed anything but the id.

The test that pinned the refusal now pins the rendering, and carries the report.

Claude-Session: https://claude.ai/code/session_01KGACAtTPjvZ2PipDZ5qD99
This commit is contained in:
prosolis
2026-08-02 12:03:19 -07:00
parent 6bed33c27e
commit 7fa98d03c7
8 changed files with 88 additions and 59 deletions
+2 -2
View File
@@ -44,7 +44,7 @@ func TestPromptsNameTheWritersLanguage(t *testing.T) {
t.Fatalf("collocation prompt lost its tone guidance:\n%s", collocation)
}
translate := TranslateMessages("Try a shorter sentence here.", pt)[0].Content
translate := TranslateMessages("Try a shorter sentence here.", English, pt)[0].Content
if !strings.Contains(translate, "European Portuguese") || strings.Contains(translate, "Chinese") {
t.Fatalf("translate prompt targets the wrong language:\n%s", translate)
}
@@ -76,7 +76,7 @@ func TestDefaultPairStillReadsAsBefore(t *testing.T) {
if got := CollocationMessages("x", "", EnglishTarget(zh))[0].Content; !strings.Contains(got, "Simplified Chinese (Mandarin) gloss in parentheses") {
t.Fatalf("zh collocation gloss changed:\n%s", got)
}
if got := TranslateMessages("x", zh)[0].Content; !strings.Contains(got, "natural, friendly Simplified Chinese (Mandarin)") {
if got := TranslateMessages("x", English, zh)[0].Content; !strings.Contains(got, "natural, friendly Simplified Chinese (Mandarin)") {
t.Fatalf("zh translate target changed:\n%s", got)
}
if got := AskPetalSystemPrompt("a", "b", "c", "d", "e", zh); !strings.Contains(got, "为什么") {
+19 -13
View File
@@ -329,23 +329,29 @@ func RewriteMessages(text, style string) []Message {
}
// translateSystemPrompt drives the explanation translator: it renders a
// suggestion's English explanation into the writer's own language so an ESL
// reader sees the "why" in her first language. Strict about returning ONLY the
// translation (no quotes, no romanisation, no English echo) so it can drop
// straight into the chat bubble. Kept warm and plain — these are short, friendly
// one-liners.
const translateSystemPrompt = `You are Petal, a warm writing assistant. Translate the English text the user ` +
`sends into natural, friendly %[1]s. It is a short explanation of a writing ` +
`suggestion, written for a native %[1]s speaker learning English.
// suggestion's explanation into the half of the pair the explanation is not
// already in, so the "why" is readable from both sides. Strict about returning
// ONLY the translation (no quotes, no romanisation, no echo of the source) so it
// can drop straight into the chat bubble. Kept warm and plain — these are short,
// friendly one-liners.
//
// Both languages are parameters because neither end is a constant. Until Phase
// 28 the source was always English and the destination always hers; a document
// written in her own language is explained in her own language, and then the tap
// runs the other way, into the English she is practising.
const translateSystemPrompt = `You are Petal, a warm writing assistant. Translate the %[1]s text the user ` +
`sends into natural, friendly %[2]s. It is a short explanation of a writing ` +
`suggestion, written for someone who is learning one of %[1]s and %[2]s and reads the other most easily.
Respond with ONLY the %[1]s translation. No quotation marks, no romanisation, no English, no preamble — ` +
Respond with ONLY the %[2]s translation. No quotation marks, no romanisation, no %[1]s, no preamble — ` +
`just the translated sentence.`
// TranslateMessages builds the message array for translating one short English
// explanation into the writer's own language.
func TranslateMessages(text string, lang Lang) []Message {
// TranslateMessages builds the message array for rendering one short
// explanation out of the language it arrived in and into the other half of the
// writer's pair.
func TranslateMessages(text string, from, to Lang) []Message {
return []Message{
{Role: "system", Content: fmt.Sprintf(translateSystemPrompt, lang.Name)},
{Role: "system", Content: fmt.Sprintf(translateSystemPrompt, from.Name, to.Name)},
{Role: "user", Content: text},
}
}
+7 -6
View File
@@ -4,13 +4,14 @@ import (
"context"
)
// RunTranslate renders a short English explanation into the writer's own
// language. It is a one-shot Complete (the result seeds the Ask Petal bubble),
// kept at a low temperature so the translation is faithful rather than creative. Output is
// trimmed of any stray surrounding quotes the model may add.
func RunTranslate(ctx context.Context, client LLMClient, text string, lang Lang) (string, error) {
// RunTranslate renders a short explanation out of the language it was written
// in and into the other half of the writer's pair. It is a one-shot Complete
// (the result seeds the Ask Petal bubble), kept at a low temperature so the
// translation is faithful rather than creative. Output is trimmed of any stray
// surrounding quotes the model may add.
func RunTranslate(ctx context.Context, client LLMClient, text string, from, to Lang) (string, error) {
out, err := client.Complete(ctx, CompletionRequest{
Messages: TranslateMessages(text, lang),
Messages: TranslateMessages(text, from, to),
MaxTokens: 512,
Temperature: 0.2,
TopP: 0.9,