Let her choose her own pair
Raised by the user, not by the plan: there was no way to change language in the mobile UI. There was no way anywhere. `users.pair_lang` has been readable since Phase 19 and writable by nobody — /api/me was GET-only and Upsert deliberately skips the column — which is also why "no pt-PT account exists yet" has stood through two phases. Nothing could create one. PATCH /api/me answers with the whole user rather than 204, so the client re-reads the pair from the server instead of trusting its own request. One write reaches everything: langpack, Hunspell dictionary, Piper voice, lexicon provider and prompt language all read the column at use time. The server refuses a pair it has no copy for, and auth.shippedPairs is deliberately not internal/llm's list. That one names pairs the prompts can talk about (fr and es, since Phase 19); this one names pairs Petal can render itself in, which needs a langpack. Storing fr today would strand her on Chinese with no way back except a lucky guess at a button she cannot read. The picker sits in the sidebar footer because the sidebar is the mobile drawer — always one tap away. The status bar exists only while a document is open, which is the wrong moment to find the app speaking a language you can't read. Each language names itself, 中文 and Português: the one place bilingual copy would get in the way. Claude-Session: https://claude.ai/code/session_016y6gyuHkQXPiEuW8RGQyua
This commit is contained in:
@@ -296,6 +296,25 @@ Each item independent and small; order within is free (SUGGESTIONS §5–§6).
|
||||
- Tests: `grammarLite.test.ts` (30, every rule in both directions), `invitation.test.ts` (7), `offline_test.go` (the six engine-split cases), `db_test.go` (the 0013 backfill), plus false-friend shape/tone guards in `i18n.test.ts`.
|
||||
- ⚠️ **Not deployed and not seen in a browser.** Same standing gap as Phases 21–22: the pt-PT copy added here is part of the pack a native speaker still has not reviewed.
|
||||
|
||||
### Phase 23 — Choosing her own pair (2026-07-27)
|
||||
Raised by the user, not by the plan: *"I see no way to change my language in the mobile UI."* She was right, and the gap was total — `users.pair_lang` had been readable since Phase 19 and writable by nobody. `/api/me` was GET-only, `Upsert` deliberately skips the column, and no screen anywhere offered the choice. Phases 19–21 built the machinery for a second pair and then left the switch off the wall, which is why ⚠️ *"no pt-PT account exists yet"* stood unresolved through two phases: **nothing could create one.**
|
||||
- [x] **`PATCH /api/me`** (`auth.UpdateMeHandler`) — answers with the whole updated user rather than 204, so the client re-reads the pair from the server instead of assuming its own request took. One write reaches everything: the langpack, the Hunspell dictionary, the Piper voice, the lexicon provider and the prompt language all read `users.pair_lang` at use time.
|
||||
- [x] **The server refuses a pair it has no copy for.** `auth.shippedPairs` is deliberately *not* `internal/llm`'s language list — that one names every pair the **prompts** can talk about (cheap to add; fr and es have been in it since Phase 19), this one names every pair Petal can **render itself in**, which needs a langpack. Storing `fr` today would strand her on Chinese copy with no way back except a lucky guess at a button she cannot read.
|
||||
- [x] **The picker lives in the sidebar footer**, beside her name and the way out — because the sidebar *is* the mobile drawer, and it is the only chrome that is always one tap away on a phone. The status bar was the other candidate and is wrong: it exists only while a document is open, which is exactly the wrong moment to discover the app is speaking a language you can't read.
|
||||
- [x] **Each language names itself** — 中文, Português, and nothing else. The one place in Petal where bilingual copy would actively get in the way: a writer who has landed on the wrong pair cannot read "Portuguese" written in Chinese. The `aria-label` carries the English for a screen reader, which has no such problem.
|
||||
- [x] **No reload.** The pack was already a subscription (Phase 19), and `useSpellChecker` already reloads on `pack.code` while read-aloud already reads `pack().locale` — so the 2.66 MB pt-PT dictionary inflates, the wide alphabet turns on and the voice changes on the tap. Nothing here needed new plumbing; the switch is the only part that was missing.
|
||||
- [x] Tests: `internal/auth/pairlang_test.go` (round-trip and back again — a writer who tries a pair must be able to return; every unshipped code refused with the column unmoved; 400 vs 401 split so a lapsed session still becomes the sign-in overlay). `i18n.test.ts` asserts `shippedPacks()` offers exactly the pairs that have copy, and that every code it offers actually resolves.
|
||||
- Verified: go build/vet, `go test ./...` clean, tsc, vite build, vitest 173/173. **Not deployed and not seen in a browser** — same standing gap as Phases 21–22.
|
||||
|
||||
### Phase 24 (planned) — the fr and es pairs
|
||||
Scope agreed with the user 2026-07-27: *"switcher for Chinese and Portuguese now, plan support for others in a later session or two."* Phase 21 is the groove; the work per language is the same five items, and the order below is the order in which each one stops being a blocker for the next.
|
||||
1. **The langpack** (~450 lines, `web/src/i18n/packs/{fr,es}.ts`). TypeScript names every string a new pack still owes, so this is mechanical to *start* and slow to *finish* — the companion lines, the bedtime proverbs and the false-friend list are written for the pair, not translated from zh. es and fr both have real en-collisions to exploit (*actuellement*/*actually*, *librería*/*library*), so both want the `alsoIn` and false-friend blocks pt-PT proved. Add the code to `auth.shippedPairs` **in the same commit** — the picker and the server's allowlist are two halves of one fact.
|
||||
2. **A native-speaker review.** Standing at ⚠️ for pt-PT since Phase 21 and inherited here; expect a speaker to change the register before the vocabulary.
|
||||
3. **Hunspell dictionaries.** `scripts/build_ptpt_dictionary.py` generalizes — the eager-affix-expansion problem is French's and Spanish's too, and both are Latin-script so `extendedAlphabet` already covers them. Watch the same trap that caught pt: check what the *source* actually is before vendoring it (fr has `hunspell-fr-classique` vs `-moderne` vs `-toutesvariantes`; es is packaged per country).
|
||||
4. **Piper voices.** Phase 21 made this configuration rather than code: a compose service and two `.env` lines per language (`TTS_ENDPOINT_FR`/`TTS_VOICE_FR`). fr and es both have several European voices in Piper's catalogue, and unlike pt-PT the download path is plain ASCII — so this is the cheapest item on the list.
|
||||
5. **Lexicon coverage.** `dict.db` has held all five languages since Phase 20, so both directions should already answer; measure gloss coverage the way pt-PT's 62% was measured before assuming it.
|
||||
Not blockers, and cheap because Phase 19 did them: `internal/llm/lang.go` already carries fr and es, and `grammarLite`'s L1 rules already gate *ter 30 anos* / "I am agree" / "since three years" to pt+fr+es.
|
||||
|
||||
### Later / explicitly not now
|
||||
- Learner-facing Chinese writing (the zh pair's second direction) — own phase with its own spec (SUGGESTIONS §4); only after Phases 19–21 prove the pair model
|
||||
- ~~Spanish pair — gated on DreamDict growing an es dataset~~ **ungated 2026-07-26** (DreamDict added Spanish). Now a normal follow-on pair after pt-PT, alongside fr — see Phases 20/21.
|
||||
|
||||
Reference in New Issue
Block a user