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:
@@ -174,9 +174,63 @@ func (p dreamProvider) Lookup(word string) (Result, error) {
|
||||
}
|
||||
res.Etymology = trimEtymology(ety)
|
||||
|
||||
if res.Reverse, err = p.reverse(norm); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// reverse reads the token as a word of the writer's own language, and returns
|
||||
// nil when it isn't one — which is the answer for almost every word she looks
|
||||
// up, since she is writing English.
|
||||
//
|
||||
// The English de-inflection walk is deliberately *not* applied here. [candidates]
|
||||
// knows about -s, -ed and -ing; running it over Portuguese would turn "vinhas"
|
||||
// into "vinha" by an English rule that happens to be right and "cantava" into
|
||||
// nothing by rules that are simply irrelevant. dict.db stores headwords, so an
|
||||
// inflected Portuguese form finds nothing and the card shows only the English
|
||||
// reading — the same outcome as today, rather than a confidently wrong one.
|
||||
func (p dreamProvider) reverse(norm string) (*Reverse, error) {
|
||||
back, err := p.dict.d.Equivalents(norm, p.native, langEN)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defs, err := p.dict.d.Define(norm, p.native)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(back) == 0 && len(defs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rev := &Reverse{Lang: p.native}
|
||||
if len(back) > maxGlossSenses {
|
||||
back = back[:maxGlossSenses]
|
||||
}
|
||||
rev.Gloss = strings.Join(back, "; ")
|
||||
for _, d := range defs {
|
||||
rev.Definitions = append(rev.Definitions, Meaning{PartOfSpeech: d.POS, Definition: d.Gloss})
|
||||
if len(rev.Definitions) >= maxReverseDefinitions {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
prons, err := p.dict.d.Pronunciation(norm, p.native)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rev.Phonetic = pickIPA(prons)
|
||||
|
||||
return rev, nil
|
||||
}
|
||||
|
||||
// maxReverseDefinitions is smaller than [maxDefinitions]: the reverse reading is
|
||||
// the second half of a card that already has an English one, and it is there to
|
||||
// say "this is also a Portuguese word, and here is what it means" rather than to
|
||||
// be a dictionary entry in its own right.
|
||||
const maxReverseDefinitions = 2
|
||||
|
||||
// Gloss returns the writer's-language translation alone — the hover tooltip's
|
||||
// fast path, one indexed query per candidate form and nothing else.
|
||||
func (p dreamProvider) Gloss(word string) (GlossResult, error) {
|
||||
@@ -188,7 +242,21 @@ func (p dreamProvider) Gloss(word string) (GlossResult, error) {
|
||||
if err != nil {
|
||||
return GlossResult{}, err
|
||||
}
|
||||
return GlossResult{Word: word, Gloss: gloss}, nil
|
||||
res := GlossResult{Word: word, Gloss: gloss}
|
||||
|
||||
// The tooltip carries only the reverse *gloss*, not the whole reading: it is
|
||||
// a one-line bubble under a resting pointer, and the popover is one click
|
||||
// away for anyone who wants the rest.
|
||||
back, err := p.dict.d.Equivalents(norm, p.native, langEN)
|
||||
if err != nil {
|
||||
return GlossResult{}, err
|
||||
}
|
||||
if len(back) > maxGlossSenses {
|
||||
back = back[:maxGlossSenses]
|
||||
}
|
||||
res.Reverse = strings.Join(back, "; ")
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// maxGlossSenses caps how many translations are strung together. One is often
|
||||
|
||||
Reference in New Issue
Block a user