Word lookups now come from DreamDict's dict.db for every pair but Chinese — opened read-only beside petal.db, no service, nothing over the VPN, because a hover gloss has to answer in milliseconds. `Provider` is the two questions the popover and the tooltip already asked, so the embedded *Lexicon satisfies it with no changes at all; Set.For(lang) is the single place the choice between them is made. The prerequisite in the dreamdict repo turned out to be two things, not one: the module path was unfetchable *and* the query layer sat in internal/, which no other module may import whatever the module is called. Both fixed upstream. The plan's central assumption did not survive the data. It mapped Gloss ← Translate(word, "en", L1) one-to-one; against the real 452 MB database that table answers for 17% of the 2,000 commonest English words into pt-PT. Wiktionary's translation sections are thin in that direction — "ephemeral", "think" and "quickly" have no en→pt-PT row at all. Shared WordNet synsets answer for 61%, so DreamDict gained Equivalents() and Petal glosses through it. Ordering those was wrong in an instructive way too: sorting by frequency glosses "think" as lembrar, "remember", because lembrar is the commoner Portuguese word even though pensar shares six of think's synsets to lembrar's one. Counting sense agreement first asks the right question. The same measurement is why zh stays on ECDICT: DreamDict reaches a Chinese gloss for 53% of those words, ECDICT for nearly all of them. The plan said converge only if quality holds. It didn't, so nothing converged. Two decisions about failure worth keeping. A missing dict.db is not an error — a laptop checkout has never had one — but a present-and-never-imported one is, because that is a half-finished deploy. And a pt-PT writer with no dictionary falls back to the embedded datasets with the gloss suppressed, keeping definitions, synonyms and phonetics rather than blanking the popover: an empty field reads as "not found", the wrong language reads as broken. The new fields surface as an etymology line and a three-band chip. Three, not five: the difficulty score separates "everyday" from "you'll have to explain this" but cannot rank obfuscate against serendipity, and a finer scale would be a confident-looking lie. An unscored word gets no chip. Writing the tests found two bugs first — trimEtymology sliced by byte, which would have emitted invalid UTF-8 for exactly the Greek and Latin etymologies the feature exists for, and its ellipsis path overran its own cap. go build/vet/test, tsc, vite, vitest 96/96 clean; live smoke against the real dict.db with one instance flipped from zh to pt-PT mid-run. Not deployed: go.mod still replaces github.com/prosolis/dreamdict with ../dreamdict, so the Docker build needs the two upstream commits pushed and the replace dropped. The deployed dict.db also predates DreamDict's Spanish data. Claude-Session: https://claude.ai/code/session_016y6gyuHkQXPiEuW8RGQyua
102 lines
4.2 KiB
Go
102 lines
4.2 KiB
Go
package lexicon
|
|
|
|
// A word lookup used to mean exactly one thing: the embedded datasets, which
|
|
// speak English and Mandarin and nothing else. That was fine while Petal had
|
|
// one writer. It stops being fine the moment a pt-PT writer right-clicks a
|
|
// word and gets a Chinese gloss.
|
|
//
|
|
// So the lookup becomes a seam. A [Provider] answers the same two questions the
|
|
// popover and the hover tooltip have always asked; which provider answers them
|
|
// depends on the writer's language pair, and [Set.For] is the only place that
|
|
// decision is made.
|
|
|
|
// Provider answers word lookups for one writer. The embedded datasets and
|
|
// DreamDict both satisfy it, and both treat a word they don't carry as an empty
|
|
// result rather than an error — a miss is an ordinary outcome of looking a word
|
|
// up, not a failure.
|
|
type Provider interface {
|
|
// Lookup returns the full popover payload: gloss, phonetic, definitions,
|
|
// synonyms, and whatever extras the provider carries.
|
|
Lookup(word string) (Result, error)
|
|
// Gloss returns just the writer's-language translation. It is the hover
|
|
// tooltip's fast path and skips everything else.
|
|
Gloss(word string) (GlossResult, error)
|
|
}
|
|
|
|
// LangZh is the one pair language still served by the embedded datasets. Every
|
|
// other pair goes to DreamDict — see [Set.For] for why zh is held back.
|
|
const LangZh = "zh"
|
|
|
|
// langEN is the language DreamDict is asked about for definitions, synonyms and
|
|
// pronunciation. English is always the *target* language of the pair — what
|
|
// varies is the language the gloss is written in.
|
|
const langEN = "en"
|
|
|
|
// Set holds every provider Petal can serve a lookup from and picks between them
|
|
// by pair language. One Set is shared by the whole process: the embedded
|
|
// datasets load once, and dict.db is one read-only handle.
|
|
type Set struct {
|
|
embedded *Lexicon
|
|
// dream is nil when dict.db was not deployed. That is a supported state,
|
|
// not an error — see [Set.For].
|
|
dream *DreamDict
|
|
}
|
|
|
|
// NewSet returns a Set backed by the embedded datasets and, when dream is
|
|
// non-nil, DreamDict. Passing a nil dream is how Petal runs without dict.db.
|
|
func NewSet(dream *DreamDict) *Set {
|
|
return &Set{embedded: New(), dream: dream}
|
|
}
|
|
|
|
// HasDreamDict reports whether a dict.db is open. Only startup logging and
|
|
// tests care; a handler never asks, because [Set.For] always returns something
|
|
// usable.
|
|
func (s *Set) HasDreamDict() bool { return s.dream != nil }
|
|
|
|
// For returns the provider that should answer lookups for a writer whose pair
|
|
// language is lang.
|
|
//
|
|
// Three rules, in order:
|
|
//
|
|
// zh — and an empty code, which is what a pre-Phase-16 row reads as — stays on
|
|
// the embedded ECDICT gloss. Not because DreamDict lacks Chinese (it has
|
|
// CC-CEDICT), but because that path is in daily use by a real writer and the
|
|
// two have not yet been compared on her actual lookups. Switching it is a
|
|
// quality decision, and it hasn't been made.
|
|
//
|
|
// Any other pair goes to DreamDict, which is the only source that has pt-PT,
|
|
// French or Spanish at all.
|
|
//
|
|
// If dict.db was never deployed, a non-zh writer falls back to the embedded
|
|
// datasets with the gloss suppressed. This is the interesting case: the naive
|
|
// "no data" answer would blank the popover entirely, when in fact the English
|
|
// half of it — definitions, synonyms, phonetic — is compiled into the binary
|
|
// and perfectly correct for her. Only the translation is missing, so only the
|
|
// translation goes missing. A failed dictionary deploy costs her the gloss, not
|
|
// the dictionary.
|
|
func (s *Set) For(lang string) Provider {
|
|
if lang == "" || lang == LangZh {
|
|
return s.embedded
|
|
}
|
|
if s.dream != nil {
|
|
return dreamProvider{dict: s.dream, native: lang}
|
|
}
|
|
return glossless{s.embedded}
|
|
}
|
|
|
|
// glossless serves the embedded datasets with the Chinese gloss stripped, for a
|
|
// writer who does not read Chinese. Handing her the zh gloss would be worse
|
|
// than handing her nothing: an empty field reads as "not found", where the
|
|
// wrong language reads as Petal being broken.
|
|
type glossless struct{ inner Provider }
|
|
|
|
func (g glossless) Lookup(word string) (Result, error) {
|
|
res, err := g.inner.Lookup(word)
|
|
res.Gloss = ""
|
|
return res, err
|
|
}
|
|
|
|
func (g glossless) Gloss(word string) (GlossResult, error) {
|
|
return GlossResult{Word: word}, nil
|
|
}
|