refactor: switch the whole project to English

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>
This commit is contained in:
m3ta-chironandClaude Fable 5.1 committed 2026-10-24 11:15:35 +02:00
1 parent 4d30bd95ad
commit 85fa64cfe6
95 files changed
+4707 -4854

No files matched your search

+41 -40
View File
@@ -5,14 +5,14 @@ import (
"time"
)
// lokal baut eine lokale Test-Zeit (Stunde/Minute reichen — Ruhefenster
// arbeiten auf Minuten-Auflösung).
func lokal(hour, minute int) time.Time {
// local builds a local test time (hour and minute suffice; quiet windows
// work at minute resolution).
func local(hour, minute int) time.Time {
return time.Date(2026, 10, 18, hour, minute, 0, 0, time.Local)
}
// TestParseQuietHours: Format HH:MM-HH:MM (überspannt Mitternacht),
// klare Fehler für Müll.
// TestParseQuietHours: format HH:MM-HH:MM (may span midnight), clear errors
// for garbage.
func TestParseQuietHours(t *testing.T) {
cases := []struct {
in string
@@ -23,8 +23,8 @@ func TestParseQuietHours(t *testing.T) {
{in: "18:00-08:00", start: clockTime{18, 0}, end: clockTime{8, 0}},
{in: "9:30-17:45", start: clockTime{9, 30}, end: clockTime{17, 45}},
{in: "22:00-06:00", start: clockTime{22, 0}, end: clockTime{6, 0}},
{in: "08:00-08:00", start: clockTime{8, 0}, end: clockTime{8, 0}}, // deaktiviert, aber gültig
{in: ""}, // Leerstring = deaktiviert, kein Fehler
{in: "08:00-08:00", start: clockTime{8, 0}, end: clockTime{8, 0}}, // disabled but valid
{in: ""}, // empty = disabled, no error
{in: "18:00", bad: true},
{in: "18:00-", bad: true},
{in: "-08:00", bad: true},
@@ -37,12 +37,12 @@ func TestParseQuietHours(t *testing.T) {
q, err := ParseQuietHours(tc.in)
if tc.bad {
if err == nil {
t.Errorf("ParseQuietHours(%q) = %+v, want Fehler", tc.in, q)
t.Errorf("ParseQuietHours(%q) = %+v, want error", tc.in, q)
}
continue
}
if err != nil {
t.Errorf("ParseQuietHours(%q): unerwarteter Fehler %v", tc.in, err)
t.Errorf("ParseQuietHours(%q): unexpected error %v", tc.in, err)
continue
}
if q.Start != tc.start || q.End != tc.end {
@@ -51,69 +51,70 @@ func TestParseQuietHours(t *testing.T) {
}
}
// TestQuietHoursActive: Tagfenster, Nachtfenster über Mitternacht und der
// deaktivierte Nullwert über alle Grenzen (Start inklusiv, End exklusiv).
// TestQuietHoursActive: day window, night window across midnight and the
// disabled zero value across all boundaries (start inclusive, end
// exclusive).
func TestQuietHoursActive(t *testing.T) {
cases := []struct {
fenster string
window string
hour, minute int
want bool
}{
// Tagfenster 09:00-17:00
// day window 09:00-17:00
{"09:00-17:00", 8, 59, false},
{"09:00-17:00", 9, 0, true}, // Start inklusiv
{"09:00-17:00", 9, 0, true}, // start inclusive
{"09:00-17:00", 12, 30, true},
{"09:00-17:00", 16, 59, true},
{"09:00-17:00", 17, 0, false}, // End exklusiv
{"09:00-17:00", 17, 0, false}, // end exclusive
{"09:00-17:00", 23, 0, false},
// Nachtfenster 18:00-08:00 (überspannt Mitternacht)
// night window 18:00-08:00 (spans midnight)
{"18:00-08:00", 17, 59, false},
{"18:00-08:00", 18, 0, true},
{"18:00-08:00", 23, 59, true},
{"18:00-08:00", 0, 0, true}, // über Mitternacht
{"18:00-08:00", 0, 0, true}, // past midnight
{"18:00-08:00", 7, 59, true},
{"18:00-08:00", 8, 0, false}, // End exklusiv
{"18:00-08:00", 8, 0, false}, // end exclusive
{"18:00-08:00", 12, 0, false},
// Zweites Nachtfenster
// another night window
{"22:00-06:00", 3, 0, true},
{"22:00-06:00", 21, 59, false},
// Start == End: deaktiviert
// start == end: disabled
{"08:00-08:00", 8, 0, false},
{"08:00-08:00", 20, 0, false},
}
for _, tc := range cases {
q := mustQuiet(t, tc.fenster)
if got := q.Active(lokal(tc.hour, tc.minute)); got != tc.want {
t.Errorf("%s um %02d:%02d: Active = %v, want %v", tc.fenster, tc.hour, tc.minute, got, tc.want)
q := mustQuiet(t, tc.window)
if got := q.Active(local(tc.hour, tc.minute)); got != tc.want {
t.Errorf("%s at %02d:%02d: Active = %v, want %v", tc.window, tc.hour, tc.minute, got, tc.want)
}
}
}
// TestQuietHoursUntil: Fenster-Ende (heute bzw. morgen bei Nachtfenstern),
// Nullzeit wenn inaktiv.
// TestQuietHoursUntil: window end (today, or tomorrow for night windows),
// zero time when inactive.
func TestQuietHoursUntil(t *testing.T) {
// Tagfenster: Ende heute.
// Day window: ends today.
q := mustQuiet(t, "09:00-17:00")
if want := lokal(17, 0); !q.Until(lokal(12, 0)).Equal(want) {
t.Errorf("Until(12:00) = %v, want %v (heute 17:00)", q.Until(lokal(12, 0)), want)
if want := local(17, 0); !q.Until(local(12, 0)).Equal(want) {
t.Errorf("Until(12:00) = %v, want %v (today 17:00)", q.Until(local(12, 0)), want)
}
if !q.Until(lokal(3, 0)).IsZero() {
t.Error("Until außerhalb des Fensters: want Nullzeit")
if !q.Until(local(3, 0)).IsZero() {
t.Error("Until outside the window: want zero time")
}
// Nachtfenster: ab 18:00 ist das Ende morgen früh.
// Night window: from 18:00 the end is tomorrow morning.
n := mustQuiet(t, "18:00-08:00")
if want := lokal(0, 0).Add(32 * time.Hour); !n.Until(lokal(20, 0)).Equal(want) {
t.Errorf("Until(20:00) = %v, want %v (morgen 08:00)", n.Until(lokal(20, 0)), want)
if want := local(0, 0).Add(32 * time.Hour); !n.Until(local(20, 0)).Equal(want) {
t.Errorf("Until(20:00) = %v, want %v (tomorrow 08:00)", n.Until(local(20, 0)), want)
}
// Nach Mitternacht (00:30) ist das Ende heute 08:00.
if want := lokal(8, 0); !n.Until(lokal(0, 30)).Equal(want) {
t.Errorf("Until(00:30) = %v, want %v (heute 08:00)", n.Until(lokal(0, 30)), want)
// Past midnight (00:30) the end is today 08:00.
if want := local(8, 0); !n.Until(local(0, 30)).Equal(want) {
t.Errorf("Until(00:30) = %v, want %v (today 08:00)", n.Until(local(0, 30)), want)
}
}
// TestQuietHoursStringRoundtrip: String liefert die konfigurierte Form
// zurück und parst identisch; der Nullwert rendert leer.
// TestQuietHoursStringRoundtrip: String returns the configured form and
// parses back identically; the zero value renders empty.
func TestQuietHoursStringRoundtrip(t *testing.T) {
for _, s := range []string{"18:00-08:00", "09:00-17:00", "22:00-06:00"} {
q := mustQuiet(t, s)
@@ -122,10 +123,10 @@ func TestQuietHoursStringRoundtrip(t *testing.T) {
}
q2 := mustQuiet(t, q.String())
if q2 != q {
t.Errorf("Roundtrip von %q: %+v ≠ %+v", s, q2, q)
t.Errorf("round trip of %q: %+v != %+v", s, q2, q)
}
}
if (QuietHours{}).String() != "" {
t.Error("Nullwert-String nicht leer")
t.Error("zero value String is not empty")
}
}