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

+19 -19
View File
@@ -9,30 +9,30 @@ import (
"time"
)
// cloudTimeout ist das Budget für einen einzigen Synthese-Call eines
// Cloud-Providers (Aufbau + Anfrage + vollständige Antwort). Ansagen sind
// kurz; nach Ablauf greift der Fallback auf die nächste Ketten-Stufe.
// cloudTimeout is the budget for a single synthesis call of a cloud
// provider (connect + request + complete response). Announcements are
// short; once it expires the chain falls back to the next tier.
const cloudTimeout = 45 * time.Second
// cloudClient ist der geteilte HTTP-Client der REST-Provider (mit Timeout,
// im Gegensatz zu http.DefaultClient). Bei Ausbleiben einer Antwort greift
// so der Ketten-Fallback statt einer Endlos-Blockade.
// cloudClient is the shared HTTP client of the REST providers (with a
// timeout, unlike http.DefaultClient). If a response never arrives, the
// chain falls back instead of blocking forever.
var cloudClient = &http.Client{Timeout: cloudTimeout}
// maxErrBody begrenzt, wie viel einer Fehler-Antwort in die Fehlermeldung
// übernommen wird (Diagnose ja, Spam nein).
// maxErrBody limits how much of an error response is copied into the error
// message (diagnosis yes, spam no).
const maxErrBody = 200
// postForBytes macht den gemeinsamen Kern der REST-Provider: POST von JSON
// an url mit Headern, erwartet 2xx und liefert die rohen Antwort-Bytes
// (Audio). Alles andere — non-2xx, Netz-Fehler, Timeouts, leere Antworten —
// ist ein klarer Fehler, an dem die Kette in die nächste Stufe fällt.
// postForBytes is the shared core of the REST providers: POST JSON to url
// with headers, expect 2xx and return the raw response bytes (audio).
// Everything else (non-2xx, network errors, timeouts, empty responses) is a
// clear error on which the chain falls back to the next tier.
func postForBytes(ctx context.Context, client *http.Client, providerName, url string, headers map[string]string, body []byte) ([]byte, error) {
if client == nil {
client = cloudClient
}
// Eigenes Zeit-Budget setzen, falls der Aufrufer keins mitgibt — die
// Kette soll auch ohne fremden Deadline-Kontext weiterfallen können.
// Apply our own time budget if the caller does not bring one; the chain
// must be able to fall through even without an external deadline.
if _, hasDeadline := ctx.Deadline(); !hasDeadline {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, cloudTimeout)
@@ -41,7 +41,7 @@ func postForBytes(ctx context.Context, client *http.Client, providerName, url st
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("%s: Anfrage an %s bauen: %w", providerName, url, err)
return nil, fmt.Errorf("%s: building request to %s: %w", providerName, url, err)
}
req.Header.Set("Content-Type", "application/json")
for k, v := range headers {
@@ -50,21 +50,21 @@ func postForBytes(ctx context.Context, client *http.Client, providerName, url st
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("%s: Anfrage an %s fehlgeschlagen (Netz/Timeout?): %w", providerName, url, err)
return nil, fmt.Errorf("%s: request to %s failed (network/timeout?): %w", providerName, url, err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode > 299 {
snippet, _ := io.ReadAll(io.LimitReader(resp.Body, maxErrBody))
return nil, fmt.Errorf("%s: HTTP %d von %s: %s", providerName, resp.StatusCode, url, bytes.TrimSpace(snippet))
return nil, fmt.Errorf("%s: HTTP %d from %s: %s", providerName, resp.StatusCode, url, bytes.TrimSpace(snippet))
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("%s: Antwort von %s lesen: %w", providerName, url, err)
return nil, fmt.Errorf("%s: reading response from %s: %w", providerName, url, err)
}
if len(data) == 0 {
return nil, fmt.Errorf("%s: leere Antwort von %s (0 Bytes Audio)", providerName, url)
return nil, fmt.Errorf("%s: empty response from %s (0 bytes of audio)", providerName, url)
}
return data, nil
}