fix(insights): localise the Coach scope pill and clarify the return-to-baseline copy

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.
This commit is contained in:
Marc-André Bombeck committed 2026-06-27 00:48:29 +02:00
1 parent 388c8b23dd
commit 1de72d045e
3 files changed
+49 -5

No files matched your search

+1 -1
View File
@@ -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"
}
+40 -1
View File
@@ -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.<source>")`; 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<Record<CoachScopeSource, string>> =
{
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;
}
@@ -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 <metric>" 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={
<SourcesRail
// v1.22.0 (A2) — surface the launch scope on the drawer surface;
// v1.21.2 (A2) — surface the launch scope on the drawer surface;
// only set for a fresh, scoped, not-yet-sent conversation so a
// continued thread (its own established scope) shows no stale line.
activeScopeLabel={