From 74bf6005931bc5d34b30df2a5848b3ab5ee3a4a9 Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Mon, 27 Jul 2026 10:51:09 -0700 Subject: [PATCH] Make the dictionary startup line report rows, not capabilities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It logged dictionary.Langs(), which is a compile-time constant of the languages DreamDict *supports*. The database deployed until today supported Spanish and contained none of it, so the line printed a confident "[en fr pt-PT es zh]" over a file where every Spanish lookup came back empty — the exact failure the line exists to catch, reported as success. Contents() counts rows per language instead. For a file somebody has to copy onto the box by hand, "what is in it" is the only question worth asking, and the answer is now en=136615 es=102971 fr=56096 pt-PT=136300 zh=120883. Claude-Session: https://claude.ai/code/session_016y6gyuHkQXPiEuW8RGQyua --- cmd/server/main.go | 2 +- internal/lexicon/dreamdict.go | 37 ++++++++++++++++++++++++++---- internal/lexicon/dreamdict_test.go | 22 ++++++++++++++++++ internal/lexicon/provider.go | 10 ++++++++ 4 files changed, 65 insertions(+), 6 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index 301fbe4..9c677a6 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -96,7 +96,7 @@ func main() { defer dict.Close() lexSet := lexicon.NewSet(dict) if lexSet.HasDreamDict() { - log.Printf("dictionary: DreamDict open at %s (%v)", cfg.DictPath, dict.Langs()) + log.Printf("dictionary: DreamDict open at %s (%s)", cfg.DictPath, lexSet.Contents()) } else { log.Printf("dictionary: no dict.db at %s — English/Chinese only", cfg.DictPath) } diff --git a/internal/lexicon/dreamdict.go b/internal/lexicon/dreamdict.go index 07a985b..eec3d9f 100644 --- a/internal/lexicon/dreamdict.go +++ b/internal/lexicon/dreamdict.go @@ -2,8 +2,10 @@ package lexicon import ( "errors" + "fmt" "io/fs" "os" + "sort" "strings" "unicode/utf8" @@ -56,9 +58,34 @@ func (dd *DreamDict) Close() error { return dd.d.Close() } -// Langs returns the language codes dict.db was built with, so startup can log -// what it actually got rather than what it hoped for. -func (dd *DreamDict) Langs() []string { return dictionary.Langs() } +// Contents reports how many words the open dict.db holds per language, so +// startup can log what it actually got. +// +// It counts rows rather than returning DreamDict's list of supported languages. +// Those are not the same thing and the difference is the whole point: a +// database built before Spanish existed still *supports* Spanish, and a log +// line naming the supported set would have said so cheerfully while every +// Spanish lookup came back empty. Counting rows is the question worth asking of +// a file somebody had to copy onto the box by hand. +func (dd *DreamDict) Contents() string { + counts, err := dd.d.WordCount() + if err != nil { + return "unreadable: " + err.Error() + } + langs := make([]string, 0, len(counts)) + for lang := range counts { + langs = append(langs, lang) + } + sort.Strings(langs) + parts := make([]string, 0, len(langs)) + for _, lang := range langs { + parts = append(parts, fmt.Sprintf("%s=%d", lang, counts[lang])) + } + if len(parts) == 0 { + return "no words" + } + return strings.Join(parts, " ") +} // dreamProvider serves one writer: English lookups from dict.db, glossed into // native. The struct is a value, created per request by [Set.For] — it holds no @@ -180,8 +207,8 @@ const maxGlossSenses = 3 // // A language dict.db was built without simply has no rows, so this returns "" — // which is exactly what an unglossed word returns, and the popover already -// renders that case. Spanish today is precisely this: supported by DreamDict, -// absent from the deployed database until it is rebuilt. +// renders that case. Spanish was precisely this until the database was rebuilt +// with it on 2026-07-27; the code path did not change, the file did. func (p dreamProvider) translate(norm string) (string, error) { for _, c := range candidates(norm) { trs, err := p.dict.d.Equivalents(c, langEN, p.native) diff --git a/internal/lexicon/dreamdict_test.go b/internal/lexicon/dreamdict_test.go index eff5642..5339446 100644 --- a/internal/lexicon/dreamdict_test.go +++ b/internal/lexicon/dreamdict_test.go @@ -519,3 +519,25 @@ func TestHandlerDecodesPunctuatedWords(t *testing.T) { t.Errorf("Word = %q, want the decoded word", res.Word) } } + +func TestContentsCountsRowsNotSupportedLanguages(t *testing.T) { + // The fixture is seeded with English and pt-PT only. DreamDict *supports* + // French, Spanish and Chinese too — and a startup line that reported the + // supported set would have named all five while every French lookup came + // back empty. That is the failure this log line exists to catch, so it must + // count rows. + got := NewSet(openFixture(t)) + summary := got.Contents() + if !strings.Contains(summary, "en=") || !strings.Contains(summary, "pt-PT=") { + t.Errorf("Contents = %q, want the languages the fixture actually holds", summary) + } + for _, absent := range []string{"fr=", "es=", "zh="} { + if strings.Contains(summary, absent) { + t.Errorf("Contents = %q, must not name %q — no rows exist for it", summary, absent) + } + } + // No dictionary at all still has to answer something printable. + if s := NewSet(nil).Contents(); s == "" { + t.Error("Contents with no dictionary must still say something") + } +} diff --git a/internal/lexicon/provider.go b/internal/lexicon/provider.go index 93a7b03..215dfb9 100644 --- a/internal/lexicon/provider.go +++ b/internal/lexicon/provider.go @@ -53,6 +53,16 @@ func NewSet(dream *DreamDict) *Set { // usable. func (s *Set) HasDreamDict() bool { return s.dream != nil } +// Contents describes what the open dict.db actually holds, for the startup log. +// With no dictionary it says so rather than returning an empty string, because +// a blank in a log line is indistinguishable from a bug in the log line. +func (s *Set) Contents() string { + if s.dream == nil { + return "no dict.db — embedded datasets only" + } + return s.dream.Contents() +} + // For returns the provider that should answer lookups for a writer whose pair // language is lang. //