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

+39 -42
View File
@@ -5,28 +5,26 @@ import (
"strings"
)
// CORS-Tür für POST /speak (Task 06) — und NUR für diesen Endpunkt: Ein
// Browser (z. B. eine spätere LibreChat-Integration) darf genau die Ansage-
// Schnittstelle cross-origin rufen, keine Steuer-Endpunkte. Die Erlaubnis
// kommt ausschließlich aus der Config-Liste server.allowed_origins
// (Env TALK_ALLOWED_ORIGINS): exakte Origins wie
// "https://chat.az-gruppe.com"; der Eintrag "*" erlaubt jede Origin
// ausdrücklich. Bewusster Default: LEER — dann sendet der Daemon gar keine
// CORS-Header und der Browser blockt (Tür zu, bis sie konfiguriert wird).
// CORS door for POST /speak, and ONLY for this endpoint: a browser (e.g. a
// later LibreChat integration) may call exactly the announcement interface
// cross-origin, never the control endpoints. Permission comes exclusively
// from the config list server.allowed_origins (env TALK_ALLOWED_ORIGINS):
// exact origins such as "https://chat.example.com"; the entry "*" explicitly
// allows any origin. Deliberate default: EMPTY. The daemon then sends no
// CORS headers at all and the browser blocks (door closed until configured).
//
// Die Tür ist serverseitig zu, nicht nur browserseitig (originGuard): CORS
// allein verhindert nur das LESEN der Antwort, nicht die Wirkung — ein
// Browser schickt einen POST mit text/plain-Body oder ein Formular ohne
// Preflight ab, und ein Server, der den Body dennoch als JSON liest, hätte
// gesprochen, gemutet oder abgebrochen. Deshalb lehnt talkd jede Anfrage
// mit fremdem Origin-Header ab (403), die nicht der erlaubte Weg über
// /speak ist. Nur der Preflight OPTIONS /speak bleibt für ungelistete
// Origins ein 204 ohne CORS-Header (er hat keine Wirkung; der Browser
// verwirft den geplanten POST). CLI, curl und andere Nicht-Browser-Clients
// senden keinen Origin-Header und sind nicht betroffen.
// The door is closed server-side, not only browser-side (originGuard): CORS
// alone only prevents READING the response, not the effect. A browser sends
// a POST with a text/plain body or a form without preflight, and a server
// that still reads the body as JSON would have spoken, muted or cancelled.
// Therefore talkd rejects every request with a foreign Origin header (403)
// that is not the permitted path via /speak. Only the preflight OPTIONS
// /speak stays a 204 without CORS headers for unlisted origins (it has no
// effect; the browser drops the planned POST). CLI, curl and other
// non-browser clients send no Origin header and are unaffected.
// CORS-Antwort-Header (Namen zentral, damit Preflight und echter POST
// dieselben setzen).
// CORS response headers (names in one place so preflight and the real POST
// set the same ones).
const (
headerAllowOrigin = "Access-Control-Allow-Origin"
headerAllowMethods = "Access-Control-Allow-Methods"
@@ -35,13 +33,13 @@ const (
headerVary = "Vary"
)
// preflightAge ist die Preflight-Cache-Dauer: 10 Minuten reichen für eine
// Chat-Session und halten den OPTIONS-Chatter klein.
// preflightAge is the preflight cache duration: 10 minutes cover a chat
// session and keep the OPTIONS chatter small.
const preflightAge = "600"
// corsACAOLiefert den Wert für Access-Control-Allow-Origin: bei Wildcard-
// Konfiguration das literale "*", sonst die exakt gematchte Origin (die
// identisch mit dem Config-Eintrag ist). Leer heißt: nicht erlaubt.
// corsACAO returns the value for Access-Control-Allow-Origin: the literal
// "*" for a wildcard configuration, otherwise the exactly matched origin
// (identical to the config entry). Empty means: not allowed.
func (h *handler) corsACAO(r *http.Request) string {
origin := r.Header.Get("Origin")
if origin == "" {
@@ -58,10 +56,9 @@ func (h *handler) corsACAO(r *http.Request) string {
return ""
}
// setSpeakCors setzt die CORS-Header auf echten POST-/speak-Antworten,
// wenn die Anfrage-Origin erlaubt ist. Vary: Origin steht immer (auch bei
// geschlossener Tür), damit Caches die Antwort nie fälschlich origin-
// übergreifend wiederverwenden.
// setSpeakCors sets the CORS headers on real POST /speak responses when the
// request origin is allowed. Vary: Origin is always present (even with the
// door closed) so caches never wrongly reuse the response across origins.
func (h *handler) setSpeakCors(w http.ResponseWriter, r *http.Request) {
w.Header().Add(headerVary, "Origin")
if acao := h.corsACAO(r); acao != "" {
@@ -69,11 +66,11 @@ func (h *handler) setSpeakCors(w http.ResponseWriter, r *http.Request) {
}
}
// speakPreflight behandelt OPTIONS /speak (CORS-Preflight): 204 mit den
// Freigabe-Headern bei gelisteter Origin, 204 ohne jegliche CORS-Header
// sonst (ungelistet, fehlend oder Tür komplett zu) — der Browser verwirft
// den eigentlichen Request, während der Daemon für Nicht-Browser-Clients
// erreichbar bleibt.
// speakPreflight handles OPTIONS /speak (CORS preflight): 204 with the
// permission headers for a listed origin, 204 without any CORS headers
// otherwise (unlisted, missing or door completely closed). The browser
// drops the actual request while the daemon stays reachable for
// non-browser clients.
func (h *handler) speakPreflight(w http.ResponseWriter, r *http.Request) {
w.Header().Add(headerVary, "Origin")
if acao := h.corsACAO(r); acao != "" {
@@ -85,13 +82,13 @@ func (h *handler) speakPreflight(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}
// originGuard ist die Middleware vor dem Router: Anfragen mit Origin-Header
// (also aus einem Browser) kommen nur als OPTIONS /speak (Preflight, ohne
// Wirkung) oder als POST /speak von gelisteter Origin durch. Alles andere —
// ungelistete Origin auf /speak, jeder Browser-Zugriff auf Steuer-Endpunkte
// und /mcp — wird mit 403 abgelehnt, bevor ein Body gelesen oder eine
// Wirkung ausgelöst wird. Ohne Origin-Header (CLI, curl, MCP-Clients)
// passiert nichts.
// originGuard is the middleware in front of the router: requests with an
// Origin header (i.e. from a browser) only pass as OPTIONS /speak (preflight,
// no effect) or as POST /speak from a listed origin. Everything else
// (unlisted origin on /speak, any browser access to control endpoints and
// /mcp) is rejected with 403 before a body is read or an effect is
// triggered. Without an Origin header (CLI, curl, MCP clients) nothing
// happens.
func (h *handler) originGuard(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
@@ -106,6 +103,6 @@ func (h *handler) originGuard(next http.Handler) http.Handler {
}
w.Header().Add(headerVary, "Origin")
writeError(w, http.StatusForbidden,
"Origin "+origin+" ist nicht erlaubt (Browser-Zugriff nur auf POST /speak von gelisteten Origins in server.allowed_origins)")
"origin "+origin+" is not allowed (browsers may only POST /speak from origins listed in server.allowed_origins)")
})
}