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

+21 -21
View File
@@ -1,13 +1,13 @@
// Package notify zeigt Desktop-Benachrichtigungen — best effort, über die
// Bordmittel des Betriebssystems, ohne Bibliotheks-Abhängigkeit und ohne
// CGO (dieselbe Linie wie provider/osbuiltin):
// Package notify shows desktop notifications: best effort, through the
// operating system's own tools, without a library dependency and without
// CGO (the same line as provider/osbuiltin):
//
// Linux: notify-send (libnotify; braucht einen Notification-Daemon)
// Linux: notify-send (libnotify; needs a notification daemon)
// macOS: osascript `display notification`
// Windows: PowerShell Toast (Windows.UI.Notifications)
// Windows: PowerShell toast (Windows.UI.Notifications)
//
// Fehlende Werkzeuge sind ein klarer Fehler für den Aufrufer (Protokoll),
// nie ein Abbruch: Eine Benachrichtigung ist Zusatz, kein Vertrag.
// Missing tools are a clear error for the caller (log), never an abort: a
// notification is an extra, not a contract.
package notify
import (
@@ -23,30 +23,30 @@ import (
"unicode/utf16"
)
// Title ist der Titel aller talk-Benachrichtigungen.
// Title is the title of all talk notifications.
const Title = "talk"
// timeout begrenzt den Aufruf des OS-Werkzeugs.
// timeout bounds the call of the OS tool.
const timeout = 10 * time.Second
// Available meldet, ob auf diesem System ein Benachrichtigungs-Weg
// existiert (Binary vorhanden; ob ein Daemon lauscht, zeigt erst Show).
// Available reports whether a notification path exists on this system
// (binary present; whether a daemon listens only Show reveals).
func Available() bool {
_, err := exec.LookPath(tool())
return err == nil
}
// Describe nennt den Weg für `talk doctor`.
// Describe names the path for `talk doctor`.
func Describe() string {
t := tool()
if _, err := exec.LookPath(t); err != nil {
return t + " nicht gefunden"
return t + " not found"
}
switch runtime.GOOS {
case "darwin":
return "osascript (Notification Center)"
case "windows":
return "powershell (Windows Toast)"
return "powershell (Windows toast)"
default:
return "notify-send (libnotify)"
}
@@ -63,8 +63,8 @@ func tool() string {
}
}
// Show zeigt eine Benachrichtigung mit Title und text und wartet auf das
// Ende des OS-Aufrufs (nicht auf das Schließen der Benachrichtigung).
// Show shows a notification with Title and text and waits for the OS call
// to finish (not for the notification to be closed).
func Show(ctx context.Context, text string) error {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
@@ -79,18 +79,18 @@ func Show(ctx context.Context, text string) error {
cmd = exec.CommandContext(ctx, "notify-send", "--app-name="+Title, "--urgency=normal", Title, text)
}
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("notify: %s fehlgeschlagen: %w: %s", tool(), err, bytes.TrimSpace(out))
return fmt.Errorf("notify: %s failed: %w: %s", tool(), err, bytes.TrimSpace(out))
}
return nil
}
// appleString macht aus s ein AppleScript-Stringliteral.
// appleString turns s into an AppleScript string literal.
func appleString(s string) string {
return `"` + strings.NewReplacer(`\`, `\\`, `"`, `\"`).Replace(s) + `"`
}
// toastScript ist der PowerShell-Toast (Windows 10/11); der Text wird
// base64 übergeben, damit es kein Quoting-Problem gibt.
// toastScript is the PowerShell toast (Windows 10/11); the text is passed
// base64-encoded so there is no quoting problem.
func toastScript(text string) string {
return fmt.Sprintf(`$ErrorActionPreference = 'Stop'
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
@@ -103,7 +103,7 @@ $nodes.Item(1).AppendChild($xml.CreateTextNode($msg)) | Out-Null
`, base64.StdEncoding.EncodeToString([]byte(text)), Title, Title)
}
// psEncode kodiert ein Skript für -EncodedCommand (UTF-16LE + Base64).
// psEncode encodes a script for -EncodedCommand (UTF-16LE + base64).
func psEncode(script string) string {
runes := utf16.Encode([]rune(script))
buf := make([]byte, len(runes)*2)
+15 -15
View File
@@ -9,12 +9,12 @@ import (
"testing"
)
// TestShowRuftNotifySend: Auf Linux geht Show über notify-send — hier ein
// Fake im PATH, der seine Argumente protokolliert (echter Prozess, kein
// Mock-Framework). Titel und Text kommen unverändert an.
func TestShowRuftNotifySend(t *testing.T) {
// TestShowCallsNotifySend: on Linux, Show goes through notify-send; here a
// fake in PATH that records its arguments (a real process, no mock
// framework). Title and text arrive unchanged.
func TestShowCallsNotifySend(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("notify-send-Weg gibt es nur auf Linux")
t.Skip("the notify-send path exists only on Linux")
}
dir := t.TempDir()
seen := filepath.Join(dir, "seen.txt")
@@ -25,9 +25,9 @@ func TestShowRuftNotifySend(t *testing.T) {
t.Setenv("PATH", dir)
if !Available() {
t.Fatal("Available = false mit Fake im PATH")
t.Fatal("Available = false with fake in PATH")
}
if err := Show(context.Background(), "Ruhezeit: Build fertig"); err != nil {
if err := Show(context.Background(), "quiet hours: build done"); err != nil {
t.Fatalf("Show: %v", err)
}
b, err := os.ReadFile(seen)
@@ -35,27 +35,27 @@ func TestShowRuftNotifySend(t *testing.T) {
t.Fatal(err)
}
args := strings.Split(strings.TrimSpace(string(b)), "\n")
if args[len(args)-1] != "Ruhezeit: Build fertig" || args[len(args)-2] != Title {
t.Errorf("notify-send-Argumente = %q, want … %q %q", args, Title, "Ruhezeit: Build fertig")
if args[len(args)-1] != "quiet hours: build done" || args[len(args)-2] != Title {
t.Errorf("notify-send arguments = %q, want ... %q %q", args, Title, "quiet hours: build done")
}
}
// TestShowOhneWerkzeug: fehlt das Werkzeug, ist das ein klarer Fehler.
func TestShowOhneWerkzeug(t *testing.T) {
// TestShowWithoutTool: a missing tool is a clear error.
func TestShowWithoutTool(t *testing.T) {
t.Setenv("PATH", t.TempDir())
if Available() {
t.Fatal("Available = true ohne Werkzeug")
t.Fatal("Available = true without tool")
}
if err := Show(context.Background(), "x"); err == nil {
t.Error("Show ohne Werkzeug: kein Fehler")
t.Error("Show without tool: no error")
}
if !strings.Contains(Describe(), "nicht gefunden") {
if !strings.Contains(Describe(), "not found") {
t.Errorf("Describe = %q", Describe())
}
}
func TestAppleString(t *testing.T) {
if got := appleString(`sag "hallo" \ tschüss`); got != `"sag \"hallo\" \\ tschüss"` {
if got := appleString(`say "hello" \ bye`); got != `"say \"hello\" \\ bye"` {
t.Errorf("appleString = %s", got)
}
}