Give every locale its own strings on the non-prompt surfaces

Four locales received English on surfaces whose translations were
already complete, because the resolving code was a de/en binary.

- Medication reminder push and the Telegram inline-keyboard labels
  admitted only de/en, so es/fr/it/pl got the English template on every
  channel. Route the resolver through a shared coerceLocale().
- The Coach's trailing "reply now in X" clause said English for any
  non-German locale, overriding the correct system-prompt directive.
  Read the language name from a shared map instead.
- Coach refusal copy was a de/en constant pair, answering four locales
  in English at the one turn the Coach is declining to help. Move the
  copy into the bundles; the EN/DE constants now derive from them.
- The sleep-stage chart formatted axis ticks as en-US for four locales;
  the cycle calendar and the device-score tile followed the *browser*
  locale rather than the app locale.
- Three components carried a byte-identical duration formatter that
  branched on German. Lift it to one key-driven helper.

Polish needs a third plural tier: 2-4 takes a "few" form a two-form
scheme cannot express, so "2 godzin temu" read as "2 of-hours ago".
Add a pluralKey() helper driven by Intl.PluralRules and a Few tier to
the relative-time and stale-hint keys. The five locales whose rules
have no "few" category resolve exactly as before by construction, and
their Few values mirror Other. Teach the reverse-coverage guard to
expand a pluralKey() base into its three tiers.
This commit is contained in:
Marc-André Bombeck committed 2026-07-19 06:02:27 +02:00
1 parent 624ce3292c
commit c23f2545cc
24 files changed
+543 -143

No files matched your search

+5 -5
View File
@@ -39,6 +39,8 @@ import { requireAssistantSurface } from "@/lib/feature-flags";
import { requireModuleEnabled } from "@/lib/modules/gate";
import { resolveServerLocale } from "@/lib/i18n/server-locale";
import { getServerTranslator } from "@/lib/i18n/server-translator";
import { localeLanguageNames as LANGUAGE_NAMES } from "@/lib/i18n/config";
import {
AllProvidersFailedError,
runStreamingRawCompletionWithFallback,
@@ -386,9 +388,7 @@ async function handleChatRequest(request: NextRequest): Promise<Response> {
message,
refusalText:
replayed.message ??
(locale === "de"
? "Eine frühere Nachricht in dieser Unterhaltung enthält Anweisungen, die meine Vorgaben überschreiben sollen. Beginne bitte eine neue Unterhaltung."
: "An earlier message in this conversation contains wording that overrides my instructions. Please start a new conversation."),
getServerTranslator(locale).t("coach.refusal.conversationPoisoned"),
});
}
} else {
@@ -622,7 +622,7 @@ ${groundingBlock}`
CONVERSATION
${transcript}
Reply now as the assistant, in ${locale === "de" ? "German" : "English"}.`;
Reply now as the assistant, in ${LANGUAGE_NAMES[locale]}.`;
// v1.18.7 (SENIOR-DEV HIGH) — atomically RESERVE the day's budget before
// the provider call. The reservation increments the day's total by the
@@ -727,7 +727,7 @@ Reply now as the assistant, in ${locale === "de" ? "German" : "English"}.`;
CONVERSATION
${transcript}
Reply now as the assistant, in ${locale === "de" ? "German" : "English"}. Fetch any figures you cite with the tools first.`,
Reply now as the assistant, in ${LANGUAGE_NAMES[locale]}. Fetch any figures you cite with the tools first.`,
},
];
const loop = await runCoachToolLoop({