Phase 21: Petal learns to be an English+Portuguese pair

The plan said "Hunspell pt-PT vendored like en-US". Measuring that first is
what saved it: nspell expands affixes eagerly on construction, and European
Portuguese's 1,340 rules over 44,257 stems want over a gigabyte of browser
heap — ~340 MB for the first 12,000 entries, and no return at all after three
minutes on the whole file. So the expansion runs once at build time instead:
1,039,058 forms, 2.66 MB gzipped, read by the same nspell in 842 ms.

The obvious npm package would also have shipped the wrong language. Both
dictionary-pt and dictionary-pt-br carry VERO, the Brazilian word list, so
vendoring by name puts pt-BR spellings behind a pt-PT label — the drift
SUGGESTIONS §3 warns about, arriving through the packaging where no reviewer
can see it. The source is Projecto Natura's, and the build script now asserts
the fault lines (receção in, recepção out) before writing anything.

Spellcheck consults both dictionaries and flags only what both reject, which
is the no-detector answer to a pair with no script boundary. The word card
does the same in the other direction: "data" is a word in both languages, so
Petal shows both readings rather than guessing which she meant.

Writing the tests caught the one real bug — extendedAlphabet was a snapshot
while correct/suggest read live, and her dictionary arrives after English, so
every lookup would have resolved "cora" while the underlines were already
right.

Not done, and not claimed: the pack has not been read by a pt-PT speaker, and
the Piper voice is deferred with the deploy.

Claude-Session: https://claude.ai/code/session_016y6gyuHkQXPiEuW8RGQyua
This commit is contained in:
prosolis
2026-07-27 12:43:02 -07:00
parent 4de83d0da5
commit ccb43e5a4d
22 changed files with 1458 additions and 107 deletions
+15 -4
View File
@@ -8,8 +8,18 @@
// so it belongs to her account rather than to a browser profile.
//
// Everything here is scoped by `lang` as well as by user. That is the language
// of the *dictionary* that flagged the word, not the writer's own language: an
// en-US personal word must not silence a pt-PT flag once the second pair ships.
// of the *dictionary* the word was accepted against, not the writer's own.
//
// Phase 18 justified that key by saying an en-US personal word must not silence
// a pt-PT flag once the second pair shipped. Phase 21 shipped it and the
// justification did not survive: under the both-dictionaries rule
// (SUGGESTIONS.md §3a) a word is only ever flagged when *every* loaded
// dictionary rejected it, so there is no such thing as a pt-PT flag an English
// exception could silence. What the key is actually good for is narrower and
// still worth having — the rows say which dictionary each acceptance was made
// against, so a pair that later loses or gains a dictionary keeps a truthful
// record instead of one merged list of unknown provenance. The browser writes a
// row per loaded dictionary when she accepts a word; see useSpellChecker.
package spell
import (
@@ -24,8 +34,9 @@ import (
"gitea.parodia.dev/drwily/petal/internal/httputil"
)
// DefaultLang is the dictionary assumed when a caller doesn't name one. Only
// en-US ships today; pt-PT arrives with the first Latin pair.
// DefaultLang is the dictionary assumed when a caller doesn't name one. English
// is in every pair, so it is the safe assumption; pt-PT is named explicitly by
// the pt-PT pair's second dictionary.
const DefaultLang = "en"
// MaxWordLen bounds a single entry. A personal dictionary holds words, and a