feat(insights): give every category page a metric explainer + normalise keys to english

Two parts to the explainer surface, landed together because they share
the same i18n block and page props.

Coverage: every routed insights category page now passes an
`explainerMetric` prop, so the heading-side `?` glyph is present on all
37 pages instead of 13. The 24 newly covered metrics — body
composition (body water, bone/fat/muscle/lean mass, fat-free mass,
visceral fat), mobility (flights, walking distance/speed/steadiness,
asymmetry, double-support, step length, walking heart rate),
cardiovascular (pulse-wave velocity, vascular age), hearing
(environmental + headphone levels, loud-sound events), respiratory
rate, daylight, blood glucose, and skin temperature — each gain a one
to two sentence layperson definition grounded in standard clinical
references, shipped in all six locales.

Keys: the explainer key segments were a German/English mix
(`blutdruck`, `puls`, `gewicht`, `schlaf`, `stimmung`, `medikamente`
alongside English ones). Rename them to English to match the
naming-convention ADR — `bloodPressure`, `pulse`, `weight`, `sleep`,
`mood`, `medications` — across every locale, and update the
`explainerMetric` prop values that reference them. These are i18n key
segments, not route slugs, so the slug contract is untouched.
This commit is contained in:
Marc-André Bombeck committed 2026-05-31 22:35:02 +02:00
1 parent 8f120cfdef
commit 05097df1ee
37 files changed
+440 -101

No files matched your search

+1
View File
@@ -19,6 +19,7 @@ export default function InsightsLaermereignissePage() {
insightMetric="AUDIO_EXPOSURE_EVENT"
chartKey="audioExposureEvent"
i18nPrefix="insights.audioExposureEvent"
explainerMetric="audioEvents"
color="#ff79c6"
unit=""
yAxisUnit=""
+1
View File
@@ -19,6 +19,7 @@ export default function InsightsBlutzuckerPage() {
insightMetric="BLOOD_GLUCOSE"
chartKey="bloodGlucose"
i18nPrefix="insights.bloodGlucose"
explainerMetric="bloodGlucose"
color="#ff79c6"
unit="mg/dL"
yAxisUnit="mg/dL"
+2 -2
View File
@@ -52,7 +52,7 @@ export default function InsightsBlutdruckPage() {
<SubPageShell
title={t("insights.bloodPressureSectionTitle")}
description={t("insights.subPage.blutdruckDescription")}
explainerMetric="blutdruck"
explainerMetric="bloodPressure"
>
<MetricEmptyState
icon={<HeartPulse className="size-6" />}
@@ -100,7 +100,7 @@ export default function InsightsBlutdruckPage() {
<SubPageShell
title={t("insights.bloodPressureSectionTitle")}
description={t("insights.subPage.blutdruckDescription")}
explainerMetric="blutdruck"
explainerMetric="bloodPressure"
>
<HealthChartDynamic
chartKey="bp"
+1
View File
@@ -19,6 +19,7 @@ export default function InsightsKoerperwasserPage() {
insightMetric="TOTAL_BODY_WATER"
chartKey="totalBodyWater"
i18nPrefix="insights.totalBodyWater"
explainerMetric="bodyWater"
color="#8be9fd"
unit="kg"
yAxisUnit="kg"
+1
View File
@@ -19,6 +19,7 @@ export default function InsightsKnochenmassePage() {
insightMetric="BONE_MASS"
chartKey="boneMass"
i18nPrefix="insights.boneMass"
explainerMetric="boneMass"
color="#ffb86c"
unit="kg"
yAxisUnit="kg"
+1
View File
@@ -19,6 +19,7 @@ export default function InsightsTageslichtPage() {
insightMetric="TIME_IN_DAYLIGHT"
chartKey="timeInDaylight"
i18nPrefix="insights.timeInDaylight"
explainerMetric="daylight"
color="#50fa7b"
unit="min"
yAxisUnit="min"
@@ -19,6 +19,7 @@ export default function InsightsDoppelstandphasePage() {
insightMetric="WALKING_DOUBLE_SUPPORT"
chartKey="walkingDoubleSupport"
i18nPrefix="insights.walkingDoubleSupport"
explainerMetric="doubleSupportTime"
color="#50fa7b"
unit="%"
yAxisUnit="%"
@@ -19,6 +19,7 @@ export default function InsightsLaermbelastungPage() {
insightMetric="AUDIO_EXPOSURE_ENV"
chartKey="audioExposureEnv"
i18nPrefix="insights.audioExposureEnv"
explainerMetric="environmentalAudio"
color="#ff79c6"
unit="dBA"
yAxisUnit="dBA"
+1
View File
@@ -19,6 +19,7 @@ export default function InsightsFettfreieMassePage() {
insightMetric="FAT_FREE_MASS"
chartKey="fatFreeMass"
i18nPrefix="insights.fatFreeMass"
explainerMetric="fatFreeMass"
color="#bd93f9"
unit="kg"
yAxisUnit="kg"
+1
View File
@@ -19,6 +19,7 @@ export default function InsightsFettmassePage() {
insightMetric="FAT_MASS"
chartKey="fatMass"
i18nPrefix="insights.fatMass"
explainerMetric="fatMass"
color="#ffb86c"
unit="kg"
yAxisUnit="kg"
@@ -19,6 +19,7 @@ export default function InsightsStockwerkePage() {
insightMetric="FLIGHTS_CLIMBED"
chartKey="flightsClimbed"
i18nPrefix="insights.flightsClimbed"
explainerMetric="flightsClimbed"
color="#50fa7b"
unit="flights"
yAxisUnit="flights"
@@ -19,6 +19,7 @@ export default function InsightsKopfhoererpegelPage() {
insightMetric="AUDIO_EXPOSURE_HEADPHONE"
chartKey="audioExposureHeadphone"
i18nPrefix="insights.audioExposureHeadphone"
explainerMetric="headphoneAudio"
color="#ff79c6"
unit="dBA"
yAxisUnit="dBA"
+1
View File
@@ -19,6 +19,7 @@ export default function InsightsMagermassePage() {
insightMetric="LEAN_BODY_MASS"
chartKey="leanBodyMass"
i18nPrefix="insights.leanBodyMass"
explainerMetric="leanBodyMass"
color="#bd93f9"
unit="kg"
yAxisUnit="kg"
+3 -3
View File
@@ -114,7 +114,7 @@ export default function InsightsMedikamentePage() {
return (
<SubPageShell
title={t("insights.medicationCompliance")}
explainerMetric="medikamente"
explainerMetric="medications"
>
<div className="flex items-center justify-center py-12">
<Loader2 className="text-primary h-6 w-6 animate-spin motion-reduce:animate-none" />
@@ -130,7 +130,7 @@ export default function InsightsMedikamentePage() {
return (
<SubPageShell
title={t("insights.medicationCompliance")}
explainerMetric="medikamente"
explainerMetric="medications"
>
<EmptyState
icon={<Pill className="size-6" />}
@@ -156,7 +156,7 @@ export default function InsightsMedikamentePage() {
<SubPageShell
title={t("insights.medicationCompliance")}
description={t("insights.subPage.medikamenteDescription")}
explainerMetric="medikamente"
explainerMetric="medications"
>
<div
className={
+2 -2
View File
@@ -76,7 +76,7 @@ export default function InsightsStimmungPage() {
<SubPageShell
title={t("insights.moodSectionTitle")}
description={t("insights.subPage.stimmungDescription")}
explainerMetric="stimmung"
explainerMetric="mood"
>
<MetricEmptyState
icon={<Smile className="size-6" />}
@@ -97,7 +97,7 @@ export default function InsightsStimmungPage() {
<SubPageShell
title={t("insights.moodSectionTitle")}
description={t("insights.subPage.stimmungDescription")}
explainerMetric="stimmung"
explainerMetric="mood"
>
<MoodChart
chartKey="mood"
+1
View File
@@ -19,6 +19,7 @@ export default function InsightsMuskelmassePage() {
insightMetric="MUSCLE_MASS"
chartKey="muscleMass"
i18nPrefix="insights.muscleMass"
explainerMetric="muscleMass"
color="#bd93f9"
unit="kg"
yAxisUnit="kg"
@@ -19,6 +19,7 @@ export default function InsightsPulswellengeschwindigkeitPage() {
insightMetric="PULSE_WAVE_VELOCITY"
chartKey="pulseWaveVelocity"
i18nPrefix="insights.pulseWaveVelocity"
explainerMetric="pulseWaveVelocity"
color="#ff5555"
unit="m/s"
yAxisUnit="m/s"
+2 -2
View File
@@ -62,7 +62,7 @@ export default function InsightsPulsPage() {
<SubPageShell
title={t("insights.pulseSectionTitle")}
description={t("insights.subPage.pulsDescription")}
explainerMetric="puls"
explainerMetric="pulse"
>
<MetricEmptyState
icon={<Heart className="size-6" />}
@@ -113,7 +113,7 @@ export default function InsightsPulsPage() {
<SubPageShell
title={t("insights.pulseSectionTitle")}
description={t("insights.subPage.pulsDescription")}
explainerMetric="puls"
explainerMetric="pulse"
>
<HealthChartDynamic
chartKey="pulse"
@@ -19,6 +19,7 @@ export default function InsightsAtemfrequenzPage() {
insightMetric="RESPIRATORY_RATE"
chartKey="respiratoryRate"
i18nPrefix="insights.respiratoryRate"
explainerMetric="respiratoryRate"
color="#8be9fd"
unit="breaths/min"
yAxisUnit="breaths/min"
@@ -19,6 +19,7 @@ export default function InsightsHauttemperaturPage() {
insightMetric="SKIN_TEMPERATURE"
chartKey="skinTemperature"
i18nPrefix="insights.skinTemperature"
explainerMetric="skinTemperature"
color="#ffb86c"
unit="°C"
yAxisUnit="°C"
+2 -2
View File
@@ -38,7 +38,7 @@ export default function InsightsSchlafPage() {
<SubPageShell
title={t("insights.sleep.title")}
description={t("insights.sleep.description")}
explainerMetric="schlaf"
explainerMetric="sleep"
>
<MetricEmptyState
icon={<Moon className="size-6" />}
@@ -61,7 +61,7 @@ export default function InsightsSchlafPage() {
<SubPageShell
title={t("insights.sleep.title")}
description={t("insights.sleep.description")}
explainerMetric="schlaf"
explainerMetric="sleep"
>
<SleepOverview />
+1
View File
@@ -19,6 +19,7 @@ export default function InsightsSchrittlaengePage() {
insightMetric="WALKING_STEP_LENGTH"
chartKey="walkingStepLength"
i18nPrefix="insights.walkingStepLength"
explainerMetric="stepLength"
color="#50fa7b"
unit="m"
yAxisUnit="m"
+1
View File
@@ -19,6 +19,7 @@ export default function InsightsGefaessalterPage() {
insightMetric="VASCULAR_AGE"
chartKey="vascularAge"
i18nPrefix="insights.vascularAge"
explainerMetric="vascularAge"
color="#ff79c6"
unit="years"
yAxisUnit="years"
+1
View File
@@ -19,6 +19,7 @@ export default function InsightsViszeralfettPage() {
insightMetric="VISCERAL_FAT"
chartKey="visceralFat"
i18nPrefix="insights.visceralFat"
explainerMetric="visceralFat"
color="#ffb86c"
unit=""
yAxisUnit=""
@@ -19,6 +19,7 @@ export default function InsightsGangasymmetriePage() {
insightMetric="WALKING_ASYMMETRY"
chartKey="walkingAsymmetry"
i18nPrefix="insights.walkingAsymmetry"
explainerMetric="walkingAsymmetry"
color="#50fa7b"
unit="%"
yAxisUnit="%"
@@ -19,6 +19,7 @@ export default function InsightsGehstreckePage() {
insightMetric="WALKING_RUNNING_DISTANCE"
chartKey="walkingRunningDistance"
i18nPrefix="insights.walkingRunningDistance"
explainerMetric="walkingDistance"
color="#50fa7b"
unit="km"
yAxisUnit="km"
@@ -19,6 +19,7 @@ export default function InsightsGehpulsPage() {
insightMetric="WALKING_HEART_RATE_AVERAGE"
chartKey="walkingHeartRateAverage"
i18nPrefix="insights.walkingHeartRateAverage"
explainerMetric="walkingHeartRate"
color="#ff5555"
unit="bpm"
yAxisUnit="bpm"
+1
View File
@@ -19,6 +19,7 @@ export default function InsightsGehgeschwindigkeitPage() {
insightMetric="WALKING_SPEED"
chartKey="walkingSpeed"
i18nPrefix="insights.walkingSpeed"
explainerMetric="walkingSpeed"
color="#50fa7b"
unit="km/h"
yAxisUnit="km/h"
@@ -19,6 +19,7 @@ export default function InsightsGangstabilitaetPage() {
insightMetric="WALKING_STEADINESS"
chartKey="walkingSteadiness"
i18nPrefix="insights.walkingSteadiness"
explainerMetric="walkingSteadiness"
color="#50fa7b"
unit="%"
yAxisUnit="%"
+2 -2
View File
@@ -44,7 +44,7 @@ export default function InsightsGewichtPage() {
<SubPageShell
title={t("insights.weightSectionTitle")}
description={t("insights.subPage.gewichtDescription")}
explainerMetric="gewicht"
explainerMetric="weight"
>
<MetricEmptyState
icon={<Scale className="size-6" />}
@@ -74,7 +74,7 @@ export default function InsightsGewichtPage() {
<SubPageShell
title={t("insights.weightSectionTitle")}
description={t("insights.subPage.gewichtDescription")}
explainerMetric="gewicht"
explainerMetric="weight"
>
<HealthChartDynamic
chartKey="weight"
@@ -41,25 +41,52 @@ function KeyProbe({ metric }: { metric: string }) {
);
}
// v1.8.0 — English key segments (ADR-0001). Every routed insights
// category page now passes one of these; the explainer copy ships in
// all six locales.
const METRICS = [
"blutdruck",
"puls",
"gewicht",
"bloodPressure",
"pulse",
"weight",
"bmi",
"schlaf",
"stimmung",
"medikamente",
"sleep",
"mood",
"medications",
"restingHr",
"hrv",
"oxygenSaturation",
"bodyTemperature",
"activeEnergy",
"workouts",
"respiratoryRate",
"bodyWater",
"boneMass",
"fatFreeMass",
"fatMass",
"muscleMass",
"visceralFat",
"leanBodyMass",
"flightsClimbed",
"walkingDistance",
"walkingSteadiness",
"walkingHeartRate",
"walkingAsymmetry",
"doubleSupportTime",
"stepLength",
"walkingSpeed",
"pulseWaveVelocity",
"vascularAge",
"environmentalAudio",
"headphoneAudio",
"audioEvents",
"daylight",
"bloodGlucose",
"skinTemperature",
] as const;
describe("<MetricExplainer>", () => {
it("renders an icon-only trigger button with a metric-specific accessible label (EN)", () => {
const html = ssr(<MetricExplainer metric="blutdruck" />);
const html = ssr(<MetricExplainer metric="bloodPressure" />);
const trigger = html.match(
/<button[^>]*data-slot="metric-explainer-trigger"[^>]*>/,
);
@@ -72,7 +99,7 @@ describe("<MetricExplainer>", () => {
// `min-h-11 min-w-11`; the surrounding heading row keeps its stride
// because `-my-3 -mx-2` swallows the extra reach. Pinned so a future
// refactor can't collapse the hit surface back to the visual chip.
const html = ssr(<MetricExplainer metric="puls" />);
const html = ssr(<MetricExplainer metric="pulse" />);
const trigger = html.match(
/<button[^>]*data-slot="metric-explainer-trigger"[^>]*>/,
);
@@ -94,13 +121,13 @@ describe("<MetricExplainer>", () => {
// the user activates the trigger. Pinning the negative keeps a
// future "open on render" refactor from leaking copy into the
// static markup.
const html = ssr(<MetricExplainer metric="gewicht" />);
const html = ssr(<MetricExplainer metric="weight" />);
expect(html).not.toContain('data-slot="metric-explainer-body"');
expect(html).not.toContain('data-slot="metric-explainer-title"');
});
it("localises the trigger label in German", () => {
const html = ssr(<MetricExplainer metric="schlaf" />, "de");
const html = ssr(<MetricExplainer metric="sleep" />, "de");
const trigger = html.match(
/<button[^>]*data-slot="metric-explainer-trigger"[^>]*>/,
);