docs(insights): document english tile-id contract + naming ADR

Regenerate `docs/api/openapi.yaml` so the `InsightsLayoutBody` enum
carries the canonical English tile ids first, then the
accepted-but-deprecated legacy German ids, with the normalisation +
deprecation spelled out in the schema description. Add ADR 0001 fixing
the convention — localised UI copy, English internal identifiers — and
the non-breaking alias strategy. Add an iOS coordination note covering
the new canonical ids, the continued acceptance of the legacy ids, and
the recommended migration path.
This commit is contained in:
Marc-André Bombeck committed 2026-05-31 22:12:25 +02:00
1 parent 607dd9e5df
commit 262a3ad70a
4 files changed
+214 -12

No files matched your search

@@ -0,0 +1,82 @@
# 0001 — Insights naming convention: localised UI, English identifiers
- Status: accepted
- Date: 2026-05-31
## Context
The Insights surface grew bilingually. The mother page (`/insights`) and its
routed metric sub-pages were keyed by German slugs (`/insights/blutdruck`,
`/insights/gewicht`, …) because German was the primary author voice when the
routed strip first shipped. Over time the surface accumulated a mix: route
folders, the tile-layout enum, and several internal maps were German, while
later additions (`hrv`, `oxygenSaturation`, `restingHr`, `activeEnergy`,
`workouts`) landed in English. The result was an inconsistent identifier space
where a contributor could not predict whether a given key was German or English.
Identifiers leak. A German route slug shows up in URLs, in browser history, in
the OpenAPI contract the iOS client reads, and in every internal map that pivots
on the slug. Mixed-language identifiers make the codebase harder to navigate and
make the public API contract read inconsistently.
Two surfaces also speak these identifiers across a boundary we do not control:
1. **URLs** — bookmarked and cached by the PWA.
2. **The tile-layout contract** — `GET/PUT /api/insights/layout`, which the
native iOS client persists against. The tile ids in that payload are a
client-visible contract.
## Decision
Separate the two naming axes cleanly:
- **UI copy is always localised.** Every visible string resolves through an
i18n `t()` key and is translated across all six locales (`de / en / es / fr /
it / pl`). The user never sees a raw identifier.
- **Internal identifiers are English.** Route slugs (`/insights/<slug>`), the
tile-layout enum (`INSIGHTS_TILE_IDS`), query keys, and the internal
slug→metric / slug→target maps all use established English terms. English is
the lingua franca of the codebase and of the public API contract.
Concretely, v1.8.0 migrates the routed Insights slugs and the tile-layout enum
from German to English (e.g. `blutdruck` → `blood-pressure`, `gewicht` →
`weight`, `medikamente` → `medications`). The full rename table lives in
`src/lib/insights/sub-page-metric.ts` (slugs) and `src/lib/insights-layout.ts`
(tile ids).
## Non-breaking strategy
The migration must not break either client-visible boundary:
- **URLs.** `next.config.ts` `redirects()` emits a `permanent: true` (301)
redirect from every legacy German slug to its English target, including a
`:path*` variant so any nested route survives. Bookmarks and cached PWA
navigation keep resolving.
- **Tile-layout contract.** The layout endpoint keeps **accepting** the legacy
German tile ids on input. The Zod enum is built from
`ACCEPTED_INSIGHTS_TILE_IDS` (canonical English ids *plus* the legacy German
aliases), so a `PUT` carrying old ids validates rather than tripping a 422.
`normalizeInsightsTileId` collapses each legacy id onto its canonical English
replacement; this normalisation runs on **both** the read path
(`resolveInsightsLayout`) and the write path (`serializeInsightsLayout`), so:
- a layout persisted by a `≤ v1.7.x` client surfaces canonical English ids on
`GET` without forcing a re-`PUT`;
- a `PUT` sending legacy ids persists canonical English ids;
- a layout that carries both a legacy and a canonical id for one tile dedupes
to a single canonical entry.
The legacy ids are accepted-but-deprecated. They will be removed from the
accepted set in a future major version once telemetry shows clients have
migrated. The OpenAPI `InsightsLayoutBody` schema documents the canonical ids,
the accepted legacy ids, and the deprecation.
## Consequences
- The Insights identifier space is uniformly English and predictable.
- No client breaks on upgrade: old URLs redirect, old tile ids normalise.
- The legacy-alias map and the redirect table are deliberate, finite debt to be
retired in a future major. Until then they are covered by tests
(`src/app/api/insights/layout/__tests__/route.test.ts`) that assert the
non-breaking round-trip.
- New Insights surfaces follow the convention from the start: English slug,
English tile id, localised `t()` copy.
+36 -8
View File
@@ -2440,17 +2440,27 @@ components:
type: string
enum:
- overview
- blood-pressure
- pulse
- oxygen
- body-temperature
- weight
- bmi
- active-energy
- workouts
- sleep
- resting-pulse
- hrv
- mood
- medications
- blutdruck
- puls
- sauerstoff
- koerpertemperatur
- gewicht
- bmi
- aktive-energie
- workouts
- schlaf
- ruhepuls
- hrv
- stimmung
- medikamente
visible:
@@ -2467,7 +2477,11 @@ components:
- version
- tiles
description: "Per-user Insights tile layout: an ordered list of tiles with a visibility flag. `version` is the layout
schema version; tile ids are a closed enum derived from the server's tile registry."
schema version. Tile ids are a closed enum: the canonical ids are English (matching the routed
`/insights/<slug>` sub-pages). The legacy German ids (blutdruck, puls, sauerstoff, koerpertemperatur, gewicht,
aktive-energie, schlaf, ruhepuls, stimmung, medikamente) remain accepted on input for backward compatibility and
are normalised to their English equivalents before persisting; GET responses always carry the canonical English
ids. The legacy ids are deprecated and will be removed in a future major version."
CoachMessageFeedbackRequest:
type: object
properties:
@@ -5935,17 +5949,27 @@ components:
type: string
enum:
- overview
- blood-pressure
- pulse
- oxygen
- body-temperature
- weight
- bmi
- active-energy
- workouts
- sleep
- resting-pulse
- hrv
- mood
- medications
- blutdruck
- puls
- sauerstoff
- koerpertemperatur
- gewicht
- bmi
- aktive-energie
- workouts
- schlaf
- ruhepuls
- hrv
- stimmung
- medikamente
visible:
@@ -5964,7 +5988,11 @@ components:
- tiles
additionalProperties: false
description: "Per-user Insights tile layout: an ordered list of tiles with a visibility flag. `version` is the layout
schema version; tile ids are a closed enum derived from the server's tile registry."
schema version. Tile ids are a closed enum: the canonical ids are English (matching the routed
`/insights/<slug>` sub-pages). The legacy German ids (blutdruck, puls, sauerstoff, koerpertemperatur, gewicht,
aktive-energie, schlaf, ruhepuls, stimmung, medikamente) remain accepted on input for backward compatibility and
are normalised to their English equivalents before persisting; GET responses always carry the canonical English
ids. The legacy ids are deprecated and will be removed in a future major version."
Medication:
type: object
properties: