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:
@@ -96,6 +96,24 @@ func writeFixture(t *testing.T, seeded bool) string {
|
||||
exec(`INSERT INTO word_synsets (word_id, synset_id, source) VALUES
|
||||
(5, 1, 'wordnet'), (6, 1, 'omw'), (7, 1, 'omw')`)
|
||||
|
||||
// "data" is the collision the Latin pairs create and the zh pair never did:
|
||||
// a real English word and a real Portuguese one, spelled identically and
|
||||
// meaning different things. There is no honest way to look at it in a mixed
|
||||
// document and know which was meant, so Petal shows both readings.
|
||||
exec(`INSERT INTO words (id, word, lang, pos, frequency) VALUES
|
||||
(8, 'data', 'en', 'noun', 800),
|
||||
(9, 'data', 'pt-PT', 'noun', 700),
|
||||
(10, 'date', 'en', 'noun', 750)`)
|
||||
exec(`INSERT INTO definitions (word_id, pos, gloss, source, priority) VALUES
|
||||
(8, 'noun', 'facts collected for reference', 'wordnet', 10),
|
||||
(9, 'noun', 'dia do mês', 'wiktionary', 20),
|
||||
(9, 'noun', 'momento no tempo', 'wiktionary', 30),
|
||||
(9, 'noun', 'um terceiro sentido', 'wiktionary', 40)`)
|
||||
exec(`INSERT INTO translations (word_id, translation, target_lang, source) VALUES
|
||||
(9, 'date', 'en', 'kaikki')`)
|
||||
exec(`INSERT INTO pronunciations (word_id, format, value, source) VALUES
|
||||
(9, 'ipa', '/ˈdatɐ/', 'wiktionary')`)
|
||||
|
||||
exec(`INSERT INTO etymology (word_id, text, source) VALUES
|
||||
(1, 'From Medieval Latin ephemerus, from Ancient Greek ἐφήμερος (ephḗmeros, "lasting only a day"), from ἐπί (epí, "upon") and ἡμέρα (hēméra, "day"). The sense of transience is attested in English from the late sixteenth century onwards.', 'wiktionary')`)
|
||||
|
||||
@@ -541,3 +559,106 @@ func TestContentsCountsRowsNotSupportedLanguages(t *testing.T) {
|
||||
t.Error("Contents with no dictionary must still say something")
|
||||
}
|
||||
}
|
||||
|
||||
// The Latin+Latin wrinkle (SUGGESTIONS.md §3a). An English+Chinese pair never
|
||||
// had to decide which language a word was in — the script decided. An
|
||||
// English+Portuguese pair has no script boundary, and "data", "sale", "comum"
|
||||
// and "tarde" are real words on both sides of it. Petal asks both directions
|
||||
// and shows whatever answers, which needs no language detector and therefore
|
||||
// cannot be wrong about somebody's writing.
|
||||
func TestDreamLookupShowsBothReadingsOnACollision(t *testing.T) {
|
||||
p := dreamProvider{dict: openFixture(t), native: "pt-PT"}
|
||||
res, err := p.Lookup("data")
|
||||
if err != nil {
|
||||
t.Fatalf("Lookup: %v", err)
|
||||
}
|
||||
|
||||
// The English reading is unchanged and still leads.
|
||||
if len(res.Definitions) == 0 || res.Definitions[0].Definition != "facts collected for reference" {
|
||||
t.Fatalf("Definitions = %v, want the English sense first", res.Definitions)
|
||||
}
|
||||
|
||||
if res.Reverse == nil {
|
||||
t.Fatal("Reverse = nil; a word that exists in both languages must carry both readings")
|
||||
}
|
||||
if res.Reverse.Lang != "pt-PT" {
|
||||
t.Errorf("Reverse.Lang = %q, want the writer's language", res.Reverse.Lang)
|
||||
}
|
||||
if res.Reverse.Gloss != "date" {
|
||||
t.Errorf("Reverse.Gloss = %q, want the English meaning of the Portuguese word", res.Reverse.Gloss)
|
||||
}
|
||||
if res.Reverse.Phonetic != "ˈdatɐ" {
|
||||
t.Errorf("Reverse.Phonetic = %q, want the Portuguese IPA without slashes", res.Reverse.Phonetic)
|
||||
}
|
||||
// The reverse reading is a footnote on a card that already has an English
|
||||
// half, so it is capped harder than the main entry.
|
||||
if len(res.Reverse.Definitions) != maxReverseDefinitions {
|
||||
t.Fatalf("Reverse.Definitions = %d, want %d", len(res.Reverse.Definitions), maxReverseDefinitions)
|
||||
}
|
||||
if res.Reverse.Definitions[0].Definition != "dia do mês" {
|
||||
t.Errorf("Reverse.Definitions[0] = %q, want the Portuguese sense",
|
||||
res.Reverse.Definitions[0].Definition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDreamLookupHasNoReverseForAnEnglishOnlyWord(t *testing.T) {
|
||||
// Which is almost every word she looks up: she is writing English. A
|
||||
// second block under every card would make the collision case invisible.
|
||||
p := dreamProvider{dict: openFixture(t), native: "pt-PT"}
|
||||
res, err := p.Lookup("ephemeral")
|
||||
if err != nil {
|
||||
t.Fatalf("Lookup: %v", err)
|
||||
}
|
||||
if res.Reverse != nil {
|
||||
t.Fatalf("Reverse = %+v, want none for a word that is only English", res.Reverse)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDreamGlossCarriesTheReverseReading(t *testing.T) {
|
||||
// The hover tooltip takes the same both-directions rule in one line less
|
||||
// space: the reverse *gloss* only, never the definitions.
|
||||
p := dreamProvider{dict: openFixture(t), native: "pt-PT"}
|
||||
|
||||
g, err := p.Gloss("data")
|
||||
if err != nil {
|
||||
t.Fatalf("Gloss: %v", err)
|
||||
}
|
||||
if g.Reverse != "date" {
|
||||
t.Errorf("Gloss.Reverse = %q, want the English meaning of the Portuguese word", g.Reverse)
|
||||
}
|
||||
|
||||
g, err = p.Gloss("ephemeral")
|
||||
if err != nil {
|
||||
t.Fatalf("Gloss: %v", err)
|
||||
}
|
||||
if g.Reverse != "" {
|
||||
t.Errorf("Gloss.Reverse = %q, want none for an English-only word", g.Reverse)
|
||||
}
|
||||
if g.Gloss != "efémero; passageiro" {
|
||||
t.Errorf("Gloss = %q, want the forward gloss untouched", g.Gloss)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReverseIsSilentForTheEmbeddedProviders(t *testing.T) {
|
||||
// The zh pair has no collisions and no DreamDict, and a writer with no
|
||||
// dict.db at all falls through to `glossless`. Neither may start emitting a
|
||||
// reverse block: the card would then claim a Chinese reading of an English
|
||||
// word, which is worse than saying nothing.
|
||||
set := NewSet(nil)
|
||||
|
||||
res, err := set.For(LangZh).Lookup("river")
|
||||
if err != nil {
|
||||
t.Fatalf("Lookup: %v", err)
|
||||
}
|
||||
if res.Reverse != nil {
|
||||
t.Errorf("embedded Reverse = %+v, want none", res.Reverse)
|
||||
}
|
||||
|
||||
res, err = set.For("pt-PT").Lookup("river")
|
||||
if err != nil {
|
||||
t.Fatalf("Lookup: %v", err)
|
||||
}
|
||||
if res.Reverse != nil {
|
||||
t.Errorf("glossless Reverse = %+v, want none", res.Reverse)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user