From 1de72d045eb540338fe6a85fb65e879abaf5d77f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 27 Jun 2026 00:48:29 +0200 Subject: [PATCH] fix(insights): localise the Coach scope pill and clarify the return-to-baseline copy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "the Coach is already on …" pill resolved its metric label through the brand-free English domain vocabulary, so it read in English on every non-English locale. It now prefers the existing localised measurements.type* name for each measurement-backed source, keeping the English phrase only for the gait/audio long tail that rarely becomes a scoped launch. Also rewords the English return-to-baseline line so the day count reads as the in-range run length rather than the time since. --- messages/en.json | 2 +- src/components/insights/coach-metric-scope.ts | 41 ++++++++++++++++++- .../coach-panel/coach-conversation.tsx | 11 +++-- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/messages/en.json b/messages/en.json index 31ff1a2cf..be125f699 100644 --- a/messages/en.json +++ b/messages/en.json @@ -3600,7 +3600,7 @@ } }, "streak": { - "returnLabel": "Your {metric} is back inside your usual range after {count} days — whatever that was, it's passed.", + "returnLabel": "Your {metric} has been back inside your usual range for {count} days now — whatever that was, it's passed.", "daysInside": "{count} days inside", "daysOutside": "{count} days outside" } diff --git a/src/components/insights/coach-metric-scope.ts b/src/components/insights/coach-metric-scope.ts index 6e84c02e9..222dc9b03 100644 --- a/src/components/insights/coach-metric-scope.ts +++ b/src/components/insights/coach-metric-scope.ts @@ -168,7 +168,7 @@ export function scopeSourceFromMetricKey( } /** - * v1.22.0 (A2) — the human label for a launch scope's primary metric, for + * v1.21.2 (A2) — the human label for a launch scope's primary metric, for * the VISIBLE "the Coach is already on …" pill. The caller resolves the * i18n string via `t("insights.coach.scope.metric.")`; this helper * supplies the English domain phrase as the deterministic fallback for any @@ -182,3 +182,42 @@ export function metricScopeLabelFallback( if (!metric) return null; return COACH_SOURCE_DOMAIN_LABEL[metric] ?? (metric as string); } + +/** + * v1.21.2 (A2) — map a launch-scope source onto the EXISTING localised + * `measurements.type*` metric-name key. The scope pill reads in the user's + * own language for every measurement-backed source, instead of the English + * `COACH_SOURCE_DOMAIN_LABEL` fallback that only carries the brand-free + * domain phrase. Sources without a measurement name (mood, the gait / audio + * long tail) keep that English fallback — they almost never become a scoped + * launch. Returns the key, or null when the source has no localised name. + */ +const SCOPE_SOURCE_METRIC_LABEL_KEY: Partial> = + { + bp: "measurements.typeBloodPressure", + weight: "measurements.typeWeight", + pulse: "measurements.typePulse", + hrv: "measurements.typeHeartRateVariability", + resting_hr: "measurements.typeRestingHeartRate", + respiratory_rate: "measurements.typeRespiratoryRate", + spo2: "measurements.typeOxygenSaturation", + bmi: "measurements.typeBodyMassIndex", + body_temp: "measurements.typeBodyTemperature", + vo2_max: "measurements.typeVo2Max", + sleep: "measurements.typeSleep", + body_fat: "measurements.typeBodyFat", + fat_mass: "measurements.typeFatMass", + fat_free_mass: "measurements.typeFatFreeMass", + muscle_mass: "measurements.typeMuscleMass", + lean_body_mass: "measurements.typeLeanBodyMass", + bone_mass: "measurements.typeBoneMass", + active_energy: "measurements.typeActiveEnergyBurned", + flights: "measurements.typeFlightsClimbed", + }; + +export function scopeSourceMetricLabelKey( + metric: CoachScopeSource | undefined | null, +): string | null { + if (!metric) return null; + return SCOPE_SOURCE_METRIC_LABEL_KEY[metric] ?? null; +} diff --git a/src/components/insights/coach-panel/coach-conversation.tsx b/src/components/insights/coach-panel/coach-conversation.tsx index 268c15e9a..c50af1cfb 100644 --- a/src/components/insights/coach-panel/coach-conversation.tsx +++ b/src/components/insights/coach-panel/coach-conversation.tsx @@ -20,7 +20,10 @@ import { apiDelete, apiGet } from "@/lib/api/api-fetch"; import type { CoachScope } from "@/lib/ai/coach/types"; import type { CoachLaunchScope } from "@/lib/insights/coach-launch-context"; import type { CoachSeededQuestionDTO } from "@/app/api/insights/coach/seeded-question/route"; -import { metricScopeLabelFallback } from "@/components/insights/coach-metric-scope"; +import { + metricScopeLabelFallback, + scopeSourceMetricLabelKey, +} from "@/components/insights/coach-metric-scope"; import { CoachDrawerBody } from "./coach-drawer-body"; import { CoachHero } from "./coach-hero"; @@ -444,7 +447,7 @@ export function CoachConversation({ pendingQuestions.length === 0 && !pendingAdopt; - // v1.22.0 (A2 + A3) — the visible scope/opener affordance for the hero. + // v1.21.2 (A2 + A3) — the visible scope/opener affordance for the hero. // // A2 (scoped launch): the Coach was opened narrowed to a metric. Make it // visible — a "the Coach is already on " pill plus the data-aware @@ -482,8 +485,10 @@ export function CoachConversation({ // to the brand-free English domain phrase so every source has a label. // Hoisted so both the hero hint (page) and the sources rail (drawer) read // the same string. Null when the launch carried no metric. + const a2LabelKey = scopeSourceMetricLabelKey(a2Metric); const a2MetricLabel = a2Metric ? (tOrNull(`insights.coach.scope.metric.${a2Metric}`) ?? + (a2LabelKey ? tOrNull(a2LabelKey) : null) ?? metricScopeLabelFallback(a2Metric) ?? a2Metric) : null; @@ -787,7 +792,7 @@ export function CoachConversation({ onSourcesOpenChange={setSourcesTrayOpen} sourcesRail={