refactor(insights): migrate route slugs to english with redirects

Rename every routed Insights sub-page slug from German to English
(`/insights/blutdruck` -> `/insights/blood-pressure`, `gewicht` ->
`weight`, `medikamente` -> `medications`, and the full v1.7.0 metric
tail). UI copy stays localised through `t()` keys; only the internal
slugs change. The slug registry in `src/lib/insights/sub-page-metric.ts`
is the single source of truth, so the tab strip, target summary, and
daily-briefing routing targets follow it.

`next.config.ts` `redirects()` emits a permanent redirect from every
legacy German slug to its English target, including a `:path*` variant
so nested routes survive. Bookmarks and cached PWA navigation keep
resolving.
This commit is contained in:
Marc-André Bombeck committed 2026-05-31 22:12:25 +02:00
1 parent b416cabbf1
commit c4ccd7e297
46 files changed
+244 -180

No files matched your search

@@ -80,7 +80,7 @@ const BP_DATA: TargetsFixture = {
describe("<MetricTargetSummary>", () => {
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("<MetricTargetSummary>", () => {
});
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("<MetricTargetSummary>", () => {
});
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 }],
});
+8 -8
View File
@@ -39,23 +39,23 @@ import type {
* through to `null` and render as a plain row (no link wrap).
*/
const METRIC_HREF: Record<DailyBriefingKeyFinding["sourceMetric"], string | null> = {
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",
};
/**
+36 -36
View File
@@ -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 `<Link>` to a routed sub-page (`/insights/blutdruck` …),
* is now a `<Link>` 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
@@ -67,17 +67,17 @@ interface TargetsResponse {
* component renders nothing for them, so the sub-page collapses cleanly.
*/
const SLUG_TO_TARGET_TYPE: Record<string, string> = {
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;
}
+1 -1
View File
@@ -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
@@ -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