diff --git a/next.config.ts b/next.config.ts
index c37098a14..7440d153a 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -53,6 +53,65 @@ const nextConfig: NextConfig = {
// from memory instead of paying a full reload. See
// `src/lib/http/cache-headers.ts` for the typed constant reused by
// route handlers that opt into the same posture.
+ // v1.8.0 — the routed Insights sub-pages migrated from German to
+ // English slugs (`/insights/blutdruck` → `/insights/blood-pressure`,
+ // …). Every legacy German URL 301-redirects to its English target so
+ // bookmarks, the PWA's cached navigation, and any external link keep
+ // resolving. The redirect set is exhaustive and matches the rename
+ // table in `docs/adr/0001-insights-naming-convention.md`; the slug
+ // registry itself lives in `src/lib/insights/sub-page-metric.ts`.
+ // `bmi`, `hrv`, and `workouts` were already English and need no entry.
+ async redirects() {
+ const insightsSlugRenames: Array<[string, string]> = [
+ ["blutdruck", "blood-pressure"],
+ ["puls", "pulse"],
+ ["sauerstoff", "oxygen"],
+ ["koerpertemperatur", "body-temperature"],
+ ["atemfrequenz", "respiratory-rate"],
+ ["gewicht", "weight"],
+ ["koerperwasser", "body-water"],
+ ["knochenmasse", "bone-mass"],
+ ["fettfreie-masse", "fat-free-mass"],
+ ["fettmasse", "fat-mass"],
+ ["muskelmasse", "muscle-mass"],
+ ["viszeralfett", "visceral-fat"],
+ ["magermasse", "lean-body-mass"],
+ ["aktive-energie", "active-energy"],
+ ["stockwerke", "flights-climbed"],
+ ["gehstrecke", "walking-distance"],
+ ["gangstabilitaet", "walking-steadiness"],
+ ["gehpuls", "walking-heart-rate"],
+ ["gangasymmetrie", "walking-asymmetry"],
+ ["doppelstandphase", "double-support-time"],
+ ["schrittlaenge", "step-length"],
+ ["gehgeschwindigkeit", "walking-speed"],
+ ["schlaf", "sleep"],
+ ["ruhepuls", "resting-pulse"],
+ ["pulswellengeschwindigkeit", "pulse-wave-velocity"],
+ ["gefaessalter", "vascular-age"],
+ ["laermbelastung", "environmental-audio"],
+ ["kopfhoererpegel", "headphone-audio"],
+ ["laermereignisse", "audio-events"],
+ ["tageslicht", "daylight"],
+ ["blutzucker", "blood-glucose"],
+ ["hauttemperatur", "skin-temperature"],
+ ["stimmung", "mood"],
+ ["medikamente", "medications"],
+ ];
+ return insightsSlugRenames.flatMap(([from, to]) => [
+ {
+ source: `/insights/${from}`,
+ destination: `/insights/${to}`,
+ permanent: true,
+ },
+ // Preserve any nested path (e.g. a future `/insights/sleep/2026-05-31`).
+ {
+ source: `/insights/${from}/:path*`,
+ destination: `/insights/${to}/:path*`,
+ permanent: true,
+ },
+ ]);
+ },
async headers() {
return [
{
diff --git a/src/app/api/insights/glp1-timeline/route.ts b/src/app/api/insights/glp1-timeline/route.ts
index 26f291608..45d18cb43 100644
--- a/src/app/api/insights/glp1-timeline/route.ts
+++ b/src/app/api/insights/glp1-timeline/route.ts
@@ -4,7 +4,7 @@
* Returns a chronological merge of every GLP-1-relevant event the user
* has logged: dose changes, injections (with site if recorded),
* inventory events, and side-effect-tagged mood entries from the
- * trailing 90 days. Used by /insights/medikamente's TherapyTimeline
+ * trailing 90 days. Used by /insights/medications' TherapyTimeline
* component. Web-only users without a GLP-1 medication get
* `hasGlp1: false` so the component hides cleanly.
*/
diff --git a/src/app/insights/aktive-energie/page.tsx b/src/app/insights/active-energy/page.tsx
similarity index 96%
rename from src/app/insights/aktive-energie/page.tsx
rename to src/app/insights/active-energy/page.tsx
index 4ce571b3c..1fcf9dc01 100644
--- a/src/app/insights/aktive-energie/page.tsx
+++ b/src/app/insights/active-energy/page.tsx
@@ -5,7 +5,7 @@ import { Flame } from "lucide-react";
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
/**
- * v1.4.32 — `/insights/aktive-energie`.
+ * v1.4.32 — `/insights/active-energy`.
*
* Active-energy-burned sub-page. The MeasurementType is cumulative
* (kcal per day, per R-A Option A); the v1.4.30 server-side daily-
diff --git a/src/app/insights/laermereignisse/page.tsx b/src/app/insights/audio-events/page.tsx
similarity index 95%
rename from src/app/insights/laermereignisse/page.tsx
rename to src/app/insights/audio-events/page.tsx
index 458822f9c..3fcf04442 100644
--- a/src/app/insights/laermereignisse/page.tsx
+++ b/src/app/insights/audio-events/page.tsx
@@ -5,7 +5,7 @@ import { Volume2 } from "lucide-react";
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
/**
- * v1.7.0 — `/insights/laermereignisse`.
+ * v1.7.0 — `/insights/audio-events`.
*
* AUDIO_EXPOSURE_EVENT sub-page. Reuses the generic HealthKitMetricPage scaffold;
* the chart daily-aggregates through the rollup read path. Empty-state
diff --git a/src/app/insights/blutzucker/page.tsx b/src/app/insights/blood-glucose/page.tsx
similarity index 95%
rename from src/app/insights/blutzucker/page.tsx
rename to src/app/insights/blood-glucose/page.tsx
index 504e9c5ff..2ede714d6 100644
--- a/src/app/insights/blutzucker/page.tsx
+++ b/src/app/insights/blood-glucose/page.tsx
@@ -5,7 +5,7 @@ import { Droplet } from "lucide-react";
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
/**
- * v1.7.0 — `/insights/blutzucker`.
+ * v1.7.0 — `/insights/blood-glucose`.
*
* BLOOD_GLUCOSE sub-page. Reuses the generic HealthKitMetricPage scaffold;
* the chart daily-aggregates through the rollup read path. Empty-state
diff --git a/src/app/insights/blutdruck/page.tsx b/src/app/insights/blood-pressure/page.tsx
similarity index 97%
rename from src/app/insights/blutdruck/page.tsx
rename to src/app/insights/blood-pressure/page.tsx
index e8762e3e7..dc7b4eee1 100644
--- a/src/app/insights/blutdruck/page.tsx
+++ b/src/app/insights/blood-pressure/page.tsx
@@ -18,7 +18,7 @@ import { SubPageShell } from "@/components/insights/sub-page-shell";
import { getBpTargets } from "@/lib/analytics/bp-targets";
/**
- * v1.4.25 W4 — `/insights/blutdruck`.
+ * v1.4.25 W4 — `/insights/blood-pressure`.
*
* Routed Blood-Pressure sub-page. The mother page hosts the overview
* (hero + briefing + advisor); this page concentrates on BP-only depth:
@@ -114,7 +114,7 @@ export default function InsightsBlutdruckPage() {
userTimezone={user?.timezone}
/>
-
+
{
const medicationSummary = medicationSummaryById.get(med.id);
// UI-H3 — route every medication-list row (medications page
- // + insights/medikamente) through ``
+ // + insights/medications) through ``
// so the surface stays one shape: `{name} {dose}` on line 1
// + outline category badge on line 2. The streak chip rides
// the `stateBadges` slot so a strong streak still surfaces.
@@ -243,7 +243,7 @@ export default function InsightsMedikamentePage() {
})}
-
+
-
+
-
+
-
+
{/*
v1.4.28 (BK-UI-StatusSchlaf) — the six sibling sub-pages all
diff --git a/src/app/insights/schrittlaenge/page.tsx b/src/app/insights/step-length/page.tsx
similarity index 95%
rename from src/app/insights/schrittlaenge/page.tsx
rename to src/app/insights/step-length/page.tsx
index aecb395b7..276610342 100644
--- a/src/app/insights/schrittlaenge/page.tsx
+++ b/src/app/insights/step-length/page.tsx
@@ -5,7 +5,7 @@ import { Footprints } from "lucide-react";
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
/**
- * v1.7.0 — `/insights/schrittlaenge`.
+ * v1.7.0 — `/insights/step-length`.
*
* WALKING_STEP_LENGTH sub-page. Reuses the generic HealthKitMetricPage scaffold;
* the chart daily-aggregates through the rollup read path. Empty-state
diff --git a/src/app/insights/gefaessalter/page.tsx b/src/app/insights/vascular-age/page.tsx
similarity index 95%
rename from src/app/insights/gefaessalter/page.tsx
rename to src/app/insights/vascular-age/page.tsx
index 939f14ca1..397271e11 100644
--- a/src/app/insights/gefaessalter/page.tsx
+++ b/src/app/insights/vascular-age/page.tsx
@@ -5,7 +5,7 @@ import { HeartPulse } from "lucide-react";
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
/**
- * v1.7.0 — `/insights/gefaessalter`.
+ * v1.7.0 — `/insights/vascular-age`.
*
* VASCULAR_AGE sub-page. Reuses the generic HealthKitMetricPage scaffold;
* the chart daily-aggregates through the rollup read path. Empty-state
diff --git a/src/app/insights/viszeralfett/page.tsx b/src/app/insights/visceral-fat/page.tsx
similarity index 95%
rename from src/app/insights/viszeralfett/page.tsx
rename to src/app/insights/visceral-fat/page.tsx
index bc1bf20d4..3fb062cff 100644
--- a/src/app/insights/viszeralfett/page.tsx
+++ b/src/app/insights/visceral-fat/page.tsx
@@ -5,7 +5,7 @@ import { Droplets } from "lucide-react";
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
/**
- * v1.7.0 — `/insights/viszeralfett`.
+ * v1.7.0 — `/insights/visceral-fat`.
*
* VISCERAL_FAT sub-page. Reuses the generic HealthKitMetricPage scaffold;
* the chart daily-aggregates through the rollup read path. Empty-state
diff --git a/src/app/insights/gangasymmetrie/page.tsx b/src/app/insights/walking-asymmetry/page.tsx
similarity index 95%
rename from src/app/insights/gangasymmetrie/page.tsx
rename to src/app/insights/walking-asymmetry/page.tsx
index 5b6ecb548..7182a8e8d 100644
--- a/src/app/insights/gangasymmetrie/page.tsx
+++ b/src/app/insights/walking-asymmetry/page.tsx
@@ -5,7 +5,7 @@ import { Footprints } from "lucide-react";
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
/**
- * v1.7.0 — `/insights/gangasymmetrie`.
+ * v1.7.0 — `/insights/walking-asymmetry`.
*
* WALKING_ASYMMETRY sub-page. Reuses the generic HealthKitMetricPage scaffold;
* the chart daily-aggregates through the rollup read path. Empty-state
diff --git a/src/app/insights/gehstrecke/page.tsx b/src/app/insights/walking-distance/page.tsx
similarity index 96%
rename from src/app/insights/gehstrecke/page.tsx
rename to src/app/insights/walking-distance/page.tsx
index f43bf76c8..6280cc0a8 100644
--- a/src/app/insights/gehstrecke/page.tsx
+++ b/src/app/insights/walking-distance/page.tsx
@@ -5,7 +5,7 @@ import { Footprints } from "lucide-react";
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
/**
- * v1.7.0 — `/insights/gehstrecke`.
+ * v1.7.0 — `/insights/walking-distance`.
*
* WALKING_RUNNING_DISTANCE sub-page. Reuses the generic HealthKitMetricPage scaffold;
* the chart daily-aggregates through the rollup read path. Empty-state
diff --git a/src/app/insights/gehpuls/page.tsx b/src/app/insights/walking-heart-rate/page.tsx
similarity index 95%
rename from src/app/insights/gehpuls/page.tsx
rename to src/app/insights/walking-heart-rate/page.tsx
index 1f150cb8c..c7c442671 100644
--- a/src/app/insights/gehpuls/page.tsx
+++ b/src/app/insights/walking-heart-rate/page.tsx
@@ -5,7 +5,7 @@ import { HeartPulse } from "lucide-react";
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
/**
- * v1.7.0 — `/insights/gehpuls`.
+ * v1.7.0 — `/insights/walking-heart-rate`.
*
* WALKING_HEART_RATE_AVERAGE sub-page. Reuses the generic HealthKitMetricPage scaffold;
* the chart daily-aggregates through the rollup read path. Empty-state
diff --git a/src/app/insights/gehgeschwindigkeit/page.tsx b/src/app/insights/walking-speed/page.tsx
similarity index 95%
rename from src/app/insights/gehgeschwindigkeit/page.tsx
rename to src/app/insights/walking-speed/page.tsx
index 0602540a0..c4fc56b9e 100644
--- a/src/app/insights/gehgeschwindigkeit/page.tsx
+++ b/src/app/insights/walking-speed/page.tsx
@@ -5,7 +5,7 @@ import { Gauge } from "lucide-react";
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
/**
- * v1.7.0 — `/insights/gehgeschwindigkeit`.
+ * v1.7.0 — `/insights/walking-speed`.
*
* WALKING_SPEED sub-page. Reuses the generic HealthKitMetricPage scaffold;
* the chart daily-aggregates through the rollup read path. Empty-state
diff --git a/src/app/insights/gangstabilitaet/page.tsx b/src/app/insights/walking-steadiness/page.tsx
similarity index 95%
rename from src/app/insights/gangstabilitaet/page.tsx
rename to src/app/insights/walking-steadiness/page.tsx
index c3e88c47d..fed1ed7ea 100644
--- a/src/app/insights/gangstabilitaet/page.tsx
+++ b/src/app/insights/walking-steadiness/page.tsx
@@ -5,7 +5,7 @@ import { Gauge } from "lucide-react";
import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
/**
- * v1.7.0 — `/insights/gangstabilitaet`.
+ * v1.7.0 — `/insights/walking-steadiness`.
*
* WALKING_STEADINESS sub-page. Reuses the generic HealthKitMetricPage scaffold;
* the chart daily-aggregates through the rollup read path. Empty-state
diff --git a/src/app/insights/gewicht/page.tsx b/src/app/insights/weight/page.tsx
similarity index 97%
rename from src/app/insights/gewicht/page.tsx
rename to src/app/insights/weight/page.tsx
index 44405ee3d..5a4b01055 100644
--- a/src/app/insights/gewicht/page.tsx
+++ b/src/app/insights/weight/page.tsx
@@ -18,7 +18,7 @@ import { SubPageShell } from "@/components/insights/sub-page-shell";
import { buildWeightBandsFromHeight } from "@/lib/analytics/value-bands";
/**
- * v1.4.25 W4 — `/insights/gewicht`.
+ * v1.4.25 W4 — `/insights/weight`.
*
* Routed Weight sub-page. Renders the weight chart with the user's
* height-derived green/orange/red bands plus the per-section AI
@@ -87,7 +87,7 @@ export default function InsightsGewichtPage() {
userTimezone={user?.timezone}
/>
-
+
", () => {
it("renders the numeric range + in-target share for a simple metric", () => {
- const html = renderWith("gewicht", WEIGHT_DATA);
+ const html = renderWith("weight", WEIGHT_DATA);
// Trailing `.0` trimmed, single-decimal kept.
expect(html).toContain("Target: 60.1–80.9 kg");
// round(18/24*100) = 75
@@ -88,19 +88,19 @@ describe("", () => {
});
it("stitches the systolic + diastolic bands for blood pressure", () => {
- const html = renderWith("blutdruck", BP_DATA);
+ const html = renderWith("blood-pressure", BP_DATA);
expect(html).toContain("Target: 120–129 / 70–79 mmHg");
expect(html).toContain("50% of logged days within target");
});
it("renders the adjust-target link to /targets", () => {
- const html = renderWith("gewicht", WEIGHT_DATA);
+ const html = renderWith("weight", WEIGHT_DATA);
expect(html).toContain('href="/targets"');
expect(html).toContain("Adjust target range");
});
it("suppresses the share when the route flags insufficient data", () => {
- const html = renderWith("gewicht", {
+ const html = renderWith("weight", {
...WEIGHT_DATA,
targets: [{ ...WEIGHT_DATA.targets[0], insufficientData: true }],
});
@@ -114,12 +114,12 @@ describe("", () => {
});
it("renders nothing before the payload resolves", () => {
- const html = renderWith("gewicht", undefined);
+ const html = renderWith("weight", undefined);
expect(html).toBe("");
});
it("renders nothing when the matching target has no range", () => {
- const html = renderWith("gewicht", {
+ const html = renderWith("weight", {
...WEIGHT_DATA,
targets: [{ ...WEIGHT_DATA.targets[0], range: null }],
});
diff --git a/src/components/insights/daily-briefing.tsx b/src/components/insights/daily-briefing.tsx
index 846e99a9d..7c83dfc13 100644
--- a/src/components/insights/daily-briefing.tsx
+++ b/src/components/insights/daily-briefing.tsx
@@ -39,23 +39,23 @@ import type {
* through to `null` and render as a plain row (no link wrap).
*/
const METRIC_HREF: Record = {
- bp: "/insights/blutdruck",
- weight: "/insights/gewicht",
- pulse: "/insights/puls",
- mood: "/insights/stimmung",
- compliance: "/insights/medikamente",
+ bp: "/insights/blood-pressure",
+ weight: "/insights/weight",
+ pulse: "/insights/pulse",
+ mood: "/insights/mood",
+ compliance: "/insights/medications",
// Apple Health / GLP-1 additive metrics — no dedicated sub-page yet.
// They render as static rows; v1.4.28 can wire each as they ship.
hrv: null,
- sleep: "/insights/schlaf",
+ sleep: "/insights/sleep",
resting_hr: null,
steps: null,
active_energy: null,
flights: null,
distance: null,
- vo2_max: "/insights/puls",
+ vo2_max: "/insights/pulse",
body_temp: null,
- glp1_plateau: "/insights/medikamente",
+ glp1_plateau: "/insights/medications",
};
/**
diff --git a/src/components/insights/insights-tab-strip.tsx b/src/components/insights/insights-tab-strip.tsx
index e1c6af69f..77613690f 100644
--- a/src/components/insights/insights-tab-strip.tsx
+++ b/src/components/insights/insights-tab-strip.tsx
@@ -31,7 +31,7 @@ import {
* v1.4.25 W4 — routed tab strip for `/insights`.
*
* Behaviour evolved from the v1.4.25 W3 scroll-anchor version: each pill
- * is now a `` to a routed sub-page (`/insights/blutdruck` …),
+ * is now a `` to a routed sub-page (`/insights/blood-pressure` …),
* `usePathname()` decides the active pill, and the strip is mounted in
* `src/app/insights/layout.tsx` so it persists across navigation
* without re-rendering. The CoachDrawer is NOT mounted in this layout —
@@ -116,47 +116,47 @@ const SUB_PAGE_TABS: Record<
{ labelKey: string; metric: InsightMetric }
> = {
// ── vitals ──
- blutdruck: {
+ "blood-pressure": {
labelKey: "insights.navBloodPressure",
metric: "BLOOD_PRESSURE_SYS",
},
- puls: { labelKey: "insights.navPulse", metric: "PULSE" },
- sauerstoff: {
+ pulse: { labelKey: "insights.navPulse", metric: "PULSE" },
+ oxygen: {
labelKey: "insights.navOxygenSaturation",
metric: "OXYGEN_SATURATION",
},
- koerpertemperatur: {
+ "body-temperature": {
labelKey: "insights.navBodyTemperature",
metric: "BODY_TEMPERATURE",
},
- atemfrequenz: {
+ "respiratory-rate": {
labelKey: "insights.navRespiratoryRate",
metric: "RESPIRATORY_RATE",
},
// ── body composition ──
- gewicht: { labelKey: "insights.navWeight", metric: "WEIGHT" },
+ weight: { labelKey: "insights.navWeight", metric: "WEIGHT" },
bmi: { labelKey: "insights.navBmi", metric: "BMI" },
- koerperwasser: {
+ "body-water": {
labelKey: "insights.navTotalBodyWater",
metric: "TOTAL_BODY_WATER",
},
- knochenmasse: { labelKey: "insights.navBoneMass", metric: "BONE_MASS" },
- "fettfreie-masse": {
+ "bone-mass": { labelKey: "insights.navBoneMass", metric: "BONE_MASS" },
+ "fat-free-mass": {
labelKey: "insights.navFatFreeMass",
metric: "FAT_FREE_MASS",
},
- fettmasse: { labelKey: "insights.navFatMass", metric: "FAT_MASS" },
- muskelmasse: { labelKey: "insights.navMuscleMass", metric: "MUSCLE_MASS" },
- viszeralfett: {
+ "fat-mass": { labelKey: "insights.navFatMass", metric: "FAT_MASS" },
+ "muscle-mass": { labelKey: "insights.navMuscleMass", metric: "MUSCLE_MASS" },
+ "visceral-fat": {
labelKey: "insights.navVisceralFat",
metric: "VISCERAL_FAT",
},
- magermasse: {
+ "lean-body-mass": {
labelKey: "insights.navLeanBodyMass",
metric: "LEAN_BODY_MASS",
},
// ── activity ──
- "aktive-energie": {
+ "active-energy": {
labelKey: "insights.navActiveEnergy",
metric: "ACTIVE_ENERGY_BURNED",
},
@@ -164,85 +164,85 @@ const SUB_PAGE_TABS: Record<
// availability helper reads `inputs.hasWorkouts` rather than a
// `summaries[…].count`.
workouts: { labelKey: "insights.navWorkouts", metric: "WORKOUTS" },
- stockwerke: {
+ "flights-climbed": {
labelKey: "insights.navFlightsClimbed",
metric: "FLIGHTS_CLIMBED",
},
- gehstrecke: {
+ "walking-distance": {
labelKey: "insights.navWalkingRunningDistance",
metric: "WALKING_RUNNING_DISTANCE",
},
- gangstabilitaet: {
+ "walking-steadiness": {
labelKey: "insights.navWalkingSteadiness",
metric: "WALKING_STEADINESS",
},
- gehpuls: {
+ "walking-heart-rate": {
labelKey: "insights.navWalkingHeartRateAverage",
metric: "WALKING_HEART_RATE_AVERAGE",
},
- gangasymmetrie: {
+ "walking-asymmetry": {
labelKey: "insights.navWalkingAsymmetry",
metric: "WALKING_ASYMMETRY",
},
- doppelstandphase: {
+ "double-support-time": {
labelKey: "insights.navWalkingDoubleSupport",
metric: "WALKING_DOUBLE_SUPPORT",
},
- schrittlaenge: {
+ "step-length": {
labelKey: "insights.navWalkingStepLength",
metric: "WALKING_STEP_LENGTH",
},
- gehgeschwindigkeit: {
+ "walking-speed": {
labelKey: "insights.navWalkingSpeed",
metric: "WALKING_SPEED",
},
// ── sleep ──
- schlaf: { labelKey: "insights.navSleep", metric: "SLEEP_DURATION" },
+ sleep: { labelKey: "insights.navSleep", metric: "SLEEP_DURATION" },
// ── cardiovascular ──
- ruhepuls: {
+ "resting-pulse": {
labelKey: "insights.navRestingHr",
metric: "RESTING_HEART_RATE",
},
hrv: { labelKey: "insights.navHrv", metric: "HEART_RATE_VARIABILITY" },
- pulswellengeschwindigkeit: {
+ "pulse-wave-velocity": {
labelKey: "insights.navPulseWaveVelocity",
metric: "PULSE_WAVE_VELOCITY",
},
- gefaessalter: {
+ "vascular-age": {
labelKey: "insights.navVascularAge",
metric: "VASCULAR_AGE",
},
// ── hearing ──
- laermbelastung: {
+ "environmental-audio": {
labelKey: "insights.navAudioExposureEnv",
metric: "AUDIO_EXPOSURE_ENV",
},
- kopfhoererpegel: {
+ "headphone-audio": {
labelKey: "insights.navAudioExposureHeadphone",
metric: "AUDIO_EXPOSURE_HEADPHONE",
},
- laermereignisse: {
+ "audio-events": {
labelKey: "insights.navAudioExposureEvent",
metric: "AUDIO_EXPOSURE_EVENT",
},
// ── environment ──
- tageslicht: {
+ daylight: {
labelKey: "insights.navTimeInDaylight",
metric: "TIME_IN_DAYLIGHT",
},
// ── metabolic ──
- blutzucker: {
+ "blood-glucose": {
labelKey: "insights.navBloodGlucose",
metric: "BLOOD_GLUCOSE",
},
- hauttemperatur: {
+ "skin-temperature": {
labelKey: "insights.navSkinTemperature",
metric: "SKIN_TEMPERATURE",
},
// ── mood ──
- stimmung: { labelKey: "insights.navMood", metric: "MOOD" },
+ mood: { labelKey: "insights.navMood", metric: "MOOD" },
// ── events ──
- medikamente: { labelKey: "insights.navMedication", metric: "MEDICATION" },
+ medications: { labelKey: "insights.navMedication", metric: "MEDICATION" },
};
/**
@@ -409,7 +409,7 @@ function InsightsTabStripImpl({
if (tab.kind === "link") {
// The overview pill matches the mother page exactly; the
// sub-page pills match a prefix so future nested routes
- // (e.g. `/insights/schlaf/2026-05-11`) still highlight the
+ // (e.g. `/insights/sleep/2026-05-11`) still highlight the
// parent tab.
const isActive =
tab.href === INSIGHTS_OVERVIEW_PATH
diff --git a/src/components/insights/metric-target-summary.tsx b/src/components/insights/metric-target-summary.tsx
index a5328b290..d1b5fe0bb 100644
--- a/src/components/insights/metric-target-summary.tsx
+++ b/src/components/insights/metric-target-summary.tsx
@@ -67,17 +67,17 @@ interface TargetsResponse {
* component renders nothing for them, so the sub-page collapses cleanly.
*/
const SLUG_TO_TARGET_TYPE: Record = {
- blutdruck: "BLOOD_PRESSURE",
- gewicht: "WEIGHT",
+ "blood-pressure": "BLOOD_PRESSURE",
+ weight: "WEIGHT",
bmi: "BMI",
- puls: "PULSE",
- schlaf: "SLEEP_DURATION",
- stimmung: "MOOD_SCORE",
- medikamente: "MEDICATION_COMPLIANCE",
+ pulse: "PULSE",
+ sleep: "SLEEP_DURATION",
+ mood: "MOOD_SCORE",
+ medications: "MEDICATION_COMPLIANCE",
};
interface MetricTargetSummaryProps {
- /** Insights category slug, e.g. `"blutdruck"`. */
+ /** Insights category slug, e.g. `"blood-pressure"`. */
slug: string;
}
diff --git a/src/components/insights/therapy-timeline.tsx b/src/components/insights/therapy-timeline.tsx
index afcecb39a..4e4a765ab 100644
--- a/src/components/insights/therapy-timeline.tsx
+++ b/src/components/insights/therapy-timeline.tsx
@@ -11,7 +11,7 @@ import {
} from "@/lib/medications/injection-sites";
/**
- * v1.4.25 W4d — therapy-timeline view for /insights/medikamente.
+ * v1.4.25 W4d — therapy-timeline view for /insights/medications.
*
* Renders a chronological list of GLP-1 therapy events: dose changes,
* injections (with site if recorded), inventory events, side-effect tag
diff --git a/src/components/insights/vo2-max-chart-row.tsx b/src/components/insights/vo2-max-chart-row.tsx
index fc980d154..5b87da9c9 100644
--- a/src/components/insights/vo2-max-chart-row.tsx
+++ b/src/components/insights/vo2-max-chart-row.tsx
@@ -9,7 +9,7 @@ import { cn } from "@/lib/utils";
import { HealthChartDynamic } from "@/components/charts/health-chart-dynamic";
/**
- * v1.4.25 W16a — VO2 max chart-row card for `/insights/puls`.
+ * v1.4.25 W16a — VO2 max chart-row card for `/insights/pulse`.
*
* The dashboard already carries an opt-in VO2 max trend tile (W8d.5);
* this is the deeper Insights surface. Renders one HealthChart for the
diff --git a/src/lib/dashboard-layout.ts b/src/lib/dashboard-layout.ts
index ab7afc3b9..d393bbfdd 100644
--- a/src/lib/dashboard-layout.ts
+++ b/src/lib/dashboard-layout.ts
@@ -176,7 +176,7 @@ export const CHART_OVERLAY_KEYS = [
"medications",
"sleep",
"steps",
- // v1.4.25 W16a — VO2 max chart-row on /insights/puls. Independent
+ // v1.4.25 W16a — VO2 max chart-row on /insights/pulse. Independent
// overlay-prefs slot so the cardio sub-page's VO2 chart-cog state
// doesn't bleed into the pulse chart sitting directly above it.
"vo2Max",
diff --git a/src/lib/feature-flags/__tests__/coach-cascade.test.tsx b/src/lib/feature-flags/__tests__/coach-cascade.test.tsx
index 0238743c8..75f067465 100644
--- a/src/lib/feature-flags/__tests__/coach-cascade.test.tsx
+++ b/src/lib/feature-flags/__tests__/coach-cascade.test.tsx
@@ -261,7 +261,7 @@ describe("Coach disable cascade invariant", () => {
// directly. Cross-cut gates on sub-pages (`/targets` page drawer,
// `` per-card CTA) sit OUTSIDE the fixture and are owned
// by sibling invariants. A future contributor who lands a new Coach
- // mount on (e.g.) `/insights/blutdruck/page.tsx` would not trip the
+ // mount on (e.g.) `/insights/blood-pressure/page.tsx` would not trip the
// surface-count check at all — the new site is on a sub-page and
// would silently leak.
//
diff --git a/src/lib/insights/coach-launch-context.tsx b/src/lib/insights/coach-launch-context.tsx
index 2b0aacdae..1adfa9824 100644
--- a/src/lib/insights/coach-launch-context.tsx
+++ b/src/lib/insights/coach-launch-context.tsx
@@ -16,7 +16,7 @@ import type { CoachScopeSource } from "@/lib/ai/coach/types";
*
* Until v1.4.27 the Coach drawer was mounted inline on
* `/insights/page.tsx` body, which meant navigating to a routed
- * sub-page (`/insights/blutdruck`, etc.) unmounted the drawer and
+ * sub-page (`/insights/blood-pressure`, etc.) unmounted the drawer and
* removed every entry point to the Coach. The MA3 audit + Decision F
* promoted the drawer up to `/insights/layout.tsx` so every routed
* Insights surface can launch it.
diff --git a/src/lib/insights/sub-page-metric.ts b/src/lib/insights/sub-page-metric.ts
index 3709eb1c2..8b9a2be53 100644
--- a/src/lib/insights/sub-page-metric.ts
+++ b/src/lib/insights/sub-page-metric.ts
@@ -5,19 +5,24 @@
* trends + advisor). Each named slug below is a sub-route that focuses
* one metric. Charts on the sub-pages still use the dashboard chart-cog
* surface — see `chartKey` strings in `src/lib/dashboard-layout.ts`
- * (`CHART_OVERLAY_KEYS`). The slugs themselves are German because the
- * product surface is bilingual and German is the primary author voice.
+ * (`CHART_OVERLAY_KEYS`).
*
- * `medikamente` carries no `MeasurementType[]` because medication
+ * v1.8.0 — the slugs are English. They were German through v1.7.x; the
+ * naming-convention ADR (`docs/adr/0001-insights-naming-convention.md`)
+ * settled the surface: UI copy stays localised (resolved through `t()`
+ * keys), but internal identifiers — route slugs, tile ids, query keys —
+ * are English. Legacy `/insights/` URLs 301-redirect to their
+ * English target via `next.config.ts` `redirects()`.
+ *
+ * `medications` carries no `MeasurementType[]` because medication
* compliance is event-driven (`MedicationIntakeEvent` rows) rather than
* time-series `Measurement` rows; the page fetches that data via the
* existing `/api/insights/comprehensive` consumer.
*
- * `bmi` lists both `WEIGHT` + `BODY_HEIGHT` because BMI is derived
- * client-side — `WEIGHT / (heightCm/100)^2` — and the chart sets
- * `valueMode="bmi"` on ``. `BODY_HEIGHT` is informational
- * only (we don't fetch a height series; the user's height lives on the
- * profile).
+ * `bmi` lists `WEIGHT` because BMI is derived client-side —
+ * `WEIGHT / (heightCm/100)^2` — and the chart sets `valueMode="bmi"` on
+ * ``. The user's height lives on the profile, so there is
+ * no separate height series to fetch.
*/
/**
* Single source of truth for the routed insights sub-pages. The slug
@@ -36,66 +41,66 @@
// so the five pills collapse behind one "Vitalwerte" / "Vitals" parent
// pill with a popover sub-list. The `group: "vitals"` field below
// drives that grouping; the canonical order inside the popover follows
-// the strip order. The earlier inline pills (`puls`, `blutdruck`,
-// `gewicht`, `bmi`, `schlaf`, `stimmung`, `medikamente`, `workouts`)
-// stay flat — they predate v1.4.32 and the maintainer's audit kept
-// them as direct entries.
+// the strip order. The earlier inline pills (`pulse`, `blood-pressure`,
+// `weight`, `bmi`, `sleep`, `mood`, `medications`, `workouts`) stay
+// flat — they predate v1.4.32 and the maintainer's audit kept them as
+// direct entries.
const SUB_PAGE_METRIC = {
// ── vitals ──
- blutdruck: ["BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA", "PULSE"],
- puls: ["PULSE"],
- sauerstoff: ["OXYGEN_SATURATION"],
- koerpertemperatur: ["BODY_TEMPERATURE"],
+ "blood-pressure": ["BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA", "PULSE"],
+ pulse: ["PULSE"],
+ oxygen: ["OXYGEN_SATURATION"],
+ "body-temperature": ["BODY_TEMPERATURE"],
// v1.7.0 — respiratory rate joins the vitals cluster.
- atemfrequenz: ["RESPIRATORY_RATE"],
+ "respiratory-rate": ["RESPIRATORY_RATE"],
// ── body composition ──
- gewicht: ["WEIGHT"],
+ weight: ["WEIGHT"],
// BMI is derived from WEIGHT + profile height (no separate series).
bmi: ["WEIGHT"],
// v1.7.0 — Withings / Apple body-composition tail.
- koerperwasser: ["TOTAL_BODY_WATER"],
- knochenmasse: ["BONE_MASS"],
- "fettfreie-masse": ["FAT_FREE_MASS"],
- fettmasse: ["FAT_MASS"],
- muskelmasse: ["MUSCLE_MASS"],
- viszeralfett: ["VISCERAL_FAT"],
- magermasse: ["LEAN_BODY_MASS"],
+ "body-water": ["TOTAL_BODY_WATER"],
+ "bone-mass": ["BONE_MASS"],
+ "fat-free-mass": ["FAT_FREE_MASS"],
+ "fat-mass": ["FAT_MASS"],
+ "muscle-mass": ["MUSCLE_MASS"],
+ "visceral-fat": ["VISCERAL_FAT"],
+ "lean-body-mass": ["LEAN_BODY_MASS"],
// ── activity ──
- "aktive-energie": ["ACTIVE_ENERGY_BURNED"],
+ "active-energy": ["ACTIVE_ENERGY_BURNED"],
// v1.4.32 — workouts surface; the page reads `Workout` rows directly
// through `useWorkouts()` rather than the `summaries` map, so the
// metric list stays empty.
workouts: [],
// v1.7.0 — activity + Apple-Health Mobility cluster.
- stockwerke: ["FLIGHTS_CLIMBED"],
- gehstrecke: ["WALKING_RUNNING_DISTANCE"],
- gangstabilitaet: ["WALKING_STEADINESS"],
- gehpuls: ["WALKING_HEART_RATE_AVERAGE"],
- gangasymmetrie: ["WALKING_ASYMMETRY"],
- doppelstandphase: ["WALKING_DOUBLE_SUPPORT"],
- schrittlaenge: ["WALKING_STEP_LENGTH"],
- gehgeschwindigkeit: ["WALKING_SPEED"],
+ "flights-climbed": ["FLIGHTS_CLIMBED"],
+ "walking-distance": ["WALKING_RUNNING_DISTANCE"],
+ "walking-steadiness": ["WALKING_STEADINESS"],
+ "walking-heart-rate": ["WALKING_HEART_RATE_AVERAGE"],
+ "walking-asymmetry": ["WALKING_ASYMMETRY"],
+ "double-support-time": ["WALKING_DOUBLE_SUPPORT"],
+ "step-length": ["WALKING_STEP_LENGTH"],
+ "walking-speed": ["WALKING_SPEED"],
// ── sleep ──
- schlaf: ["SLEEP_DURATION"],
+ sleep: ["SLEEP_DURATION"],
// ── cardiovascular ──
- ruhepuls: ["RESTING_HEART_RATE"],
+ "resting-pulse": ["RESTING_HEART_RATE"],
hrv: ["HEART_RATE_VARIABILITY"],
// v1.7.0 — Withings cardiovascular risk markers.
- pulswellengeschwindigkeit: ["PULSE_WAVE_VELOCITY"],
- gefaessalter: ["VASCULAR_AGE"],
+ "pulse-wave-velocity": ["PULSE_WAVE_VELOCITY"],
+ "vascular-age": ["VASCULAR_AGE"],
// ── hearing (v1.7.0) — audio-exposure cluster ──
- laermbelastung: ["AUDIO_EXPOSURE_ENV"],
- kopfhoererpegel: ["AUDIO_EXPOSURE_HEADPHONE"],
- laermereignisse: ["AUDIO_EXPOSURE_EVENT"],
+ "environmental-audio": ["AUDIO_EXPOSURE_ENV"],
+ "headphone-audio": ["AUDIO_EXPOSURE_HEADPHONE"],
+ "audio-events": ["AUDIO_EXPOSURE_EVENT"],
// ── environment (v1.7.0) ──
- tageslicht: ["TIME_IN_DAYLIGHT"],
+ daylight: ["TIME_IN_DAYLIGHT"],
// ── metabolic (v1.7.0) ──
- blutzucker: ["BLOOD_GLUCOSE"],
- hauttemperatur: ["SKIN_TEMPERATURE"],
+ "blood-glucose": ["BLOOD_GLUCOSE"],
+ "skin-temperature": ["SKIN_TEMPERATURE"],
// ── mood ──
- stimmung: ["MOOD"],
+ mood: ["MOOD"],
// ── events (medication adherence is event-driven; no measurement series) ──
- medikamente: [],
+ medications: [],
} as const satisfies Record;
export type SubPageSlug = keyof typeof SUB_PAGE_METRIC;
@@ -112,7 +117,7 @@ export const SUB_PAGE_SLUGS = Object.keys(SUB_PAGE_METRIC) as SubPageSlug[];
*/
// v1.7.0 — the new metric clusters each collapse behind a parent pill
// so the strip stays scannable as the sub-page count grows. The flat
-// pills (puls, blutdruck, gewicht, bmi, schlaf, stimmung, medikamente,
+// pills (pulse, blood-pressure, weight, bmi, sleep, mood, medications,
// workouts) stay direct entries as before.
export type SubPageGroup =
| "vitals"
@@ -126,40 +131,40 @@ export type SubPageGroup =
export const SUB_PAGE_GROUP: Partial> = {
// vitals
hrv: "vitals",
- ruhepuls: "vitals",
- sauerstoff: "vitals",
- koerpertemperatur: "vitals",
- "aktive-energie": "vitals",
- atemfrequenz: "vitals",
+ "resting-pulse": "vitals",
+ oxygen: "vitals",
+ "body-temperature": "vitals",
+ "active-energy": "vitals",
+ "respiratory-rate": "vitals",
// body composition (v1.7.0)
- koerperwasser: "body",
- knochenmasse: "body",
- "fettfreie-masse": "body",
- fettmasse: "body",
- muskelmasse: "body",
- viszeralfett: "body",
- magermasse: "body",
+ "body-water": "body",
+ "bone-mass": "body",
+ "fat-free-mass": "body",
+ "fat-mass": "body",
+ "muscle-mass": "body",
+ "visceral-fat": "body",
+ "lean-body-mass": "body",
// activity / mobility (v1.7.0)
- stockwerke: "activity",
- gehstrecke: "activity",
- gangstabilitaet: "activity",
- gehpuls: "activity",
- gangasymmetrie: "activity",
- doppelstandphase: "activity",
- schrittlaenge: "activity",
- gehgeschwindigkeit: "activity",
+ "flights-climbed": "activity",
+ "walking-distance": "activity",
+ "walking-steadiness": "activity",
+ "walking-heart-rate": "activity",
+ "walking-asymmetry": "activity",
+ "double-support-time": "activity",
+ "step-length": "activity",
+ "walking-speed": "activity",
// cardiovascular (v1.7.0)
- pulswellengeschwindigkeit: "cardiovascular",
- gefaessalter: "cardiovascular",
+ "pulse-wave-velocity": "cardiovascular",
+ "vascular-age": "cardiovascular",
// hearing (v1.7.0)
- laermbelastung: "hearing",
- kopfhoererpegel: "hearing",
- laermereignisse: "hearing",
+ "environmental-audio": "hearing",
+ "headphone-audio": "hearing",
+ "audio-events": "hearing",
// environment (v1.7.0)
- tageslicht: "environment",
+ daylight: "environment",
// metabolic (v1.7.0)
- blutzucker: "metabolic",
- hauttemperatur: "metabolic",
+ "blood-glucose": "metabolic",
+ "skin-temperature": "metabolic",
};
/**