talk is going to be published. Code comments, CLI output, error messages,
daemon log lines, MCP descriptions and results, test names and assertions,
README, spec, skill, build scripts and flake are now English. Only the
spoken announcement texts stay bilingual: the wrap result and the test
sentence are German or English depending on the detected language.
Language fallback is now configurable: `lang = "en" | "de"` (env
TALK_LANG, default en) is used when detection cannot tell (numbers, single
words, ties) and for talk's own announcements. langdetect.Detect takes the
fallback as an argument instead of hard-coding German. `talk setup` asks for
it; `talk doctor` and `talk status` show it.
Vocabulary pinned across packages so the process-boundary tests observe
stable strings: chain log lines ("chain: tier X skipped (not available)",
"falling back to the next tier", "spoken via X"), queue lines ("discarded:
quiet hours ... active", "replaced by toast", "max_age"), CLI notes
("speaking directly", "daemon started at", "no daemon", "nothing to cancel"),
status block with aligned English labels, MCP messages, origin guard 403.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
83 lines
3.0 KiB
Go
83 lines
3.0 KiB
Go
// Package langdetect guesses whether an announcement is German or English.
|
||
// It is a pure function over two small word lists, deliberately without any
|
||
// ML: announcements are one or two sentences, and the only question is which
|
||
// voice should read them.
|
||
package langdetect
|
||
|
||
import (
|
||
"strings"
|
||
"unicode"
|
||
)
|
||
|
||
// Detect returns "de" or "en" for text. Umlauts and ß count as strong German
|
||
// signals; otherwise common-word hits decide. English wins only with strictly
|
||
// more English than German hits; ties and undecidable text (empty, numbers,
|
||
// punctuation) return fallback, which callers take from the configured
|
||
// default language.
|
||
func Detect(text, fallback string) string {
|
||
var de, en int
|
||
for _, r := range text {
|
||
switch r {
|
||
case 'ä', 'ö', 'ü', 'ß', 'Ä', 'Ö', 'Ü':
|
||
de++
|
||
}
|
||
}
|
||
|
||
for _, word := range strings.FieldsFunc(text, func(r rune) bool {
|
||
return !unicode.IsLetter(r) && r != '\'' && r != '’'
|
||
}) {
|
||
word = strings.ToLower(word)
|
||
if _, ok := germanWords[word]; ok {
|
||
de++
|
||
}
|
||
if _, ok := englishWords[word]; ok {
|
||
en++
|
||
}
|
||
}
|
||
|
||
switch {
|
||
case en > de:
|
||
return "en"
|
||
case de > en:
|
||
return "de"
|
||
default:
|
||
return fallback
|
||
}
|
||
}
|
||
|
||
// germanWords are common German words that do not occur in English
|
||
// (ambiguous ones such as "was" and "in" are left out on purpose).
|
||
var germanWords = map[string]struct{}{
|
||
"der": {}, "die": {}, "das": {}, "und": {}, "nicht": {}, "ist": {},
|
||
"ein": {}, "eine": {}, "einen": {}, "einem": {}, "einer": {},
|
||
"mit": {}, "für": {}, "auf": {}, "von": {}, "vom": {}, "zum": {},
|
||
"zur": {}, "den": {}, "dem": {}, "des": {}, "wir": {}, "ihr": {},
|
||
"sie": {}, "auch": {}, "als": {}, "wie": {}, "bei": {},
|
||
"aus": {}, "nach": {}, "noch": {}, "nur": {}, "schon": {},
|
||
"wenn": {}, "dann": {}, "aber": {}, "oder": {}, "doch": {},
|
||
"mehr": {}, "sehr": {}, "über": {}, "unter": {}, "jetzt": {},
|
||
"hier": {}, "heute": {}, "morgen": {}, "danke": {}, "bitte": {},
|
||
"erfolgreich": {}, "fehlgeschlagen": {}, "fertig": {},
|
||
"gestartet": {}, "beendet": {}, "abgeschlossen": {}, "warten": {},
|
||
"läuft": {}, "kannst": {}, "muss": {}, "soll": {},
|
||
"kein": {}, "keine": {}, "wieder": {}, "werden": {}, "wurde": {},
|
||
"wurden": {}, "haben": {}, "geht": {}, "machen": {},
|
||
}
|
||
|
||
// englishWords are common English words that do not occur in German.
|
||
var englishWords = map[string]struct{}{
|
||
"the": {}, "is": {}, "not": {}, "and": {}, "with": {}, "for": {},
|
||
"on": {}, "of": {}, "to": {}, "from": {}, "by": {}, "at": {},
|
||
"or": {}, "but": {}, "if": {}, "then": {}, "when": {}, "how": {},
|
||
"what": {}, "why": {}, "who": {}, "done": {}, "failed": {},
|
||
"finished": {}, "started": {}, "completed": {}, "error": {},
|
||
"warning": {}, "thanks": {}, "you": {}, "your": {},
|
||
"we": {}, "it": {}, "this": {}, "that": {}, "these": {},
|
||
"those": {}, "were": {}, "are": {}, "be": {},
|
||
"been": {}, "has": {}, "have": {}, "had": {}, "can": {},
|
||
"will": {}, "would": {}, "should": {}, "could": {}, "just": {},
|
||
"only": {}, "still": {}, "already": {}, "now": {}, "today": {},
|
||
"tomorrow": {}, "waiting": {}, "running": {}, "please": {},
|
||
"needs": {}, "need": {}, "succeeded": {},
|
||
}
|