diff --git a/CHANGELOG.md b/CHANGELOG.md
index 902c31222..01e58b020 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,26 @@
## [Unreleased]
+## [1.32.36] — 2026-07-25
+
+Nothing here changes what you see. This release is about the checks that run
+before a release goes out.
+
+A recurring shape of mistake in this project is a half-built feature: something
+is stored, or sent to the app, and the part that was supposed to use it never
+gets written. Each end works on its own, so nothing complains. Three checks now
+run on every build and refuse that. One walks the fields the app receives about
+your account and insists each one is actually read somewhere. One compares the
+measurement table against what a backup writes out, so a new column cannot go
+missing from your backup file. The third fixes a check that had gone quiet: it
+was meant to find translation entries no screen uses any more, but one line of
+it had been waving through several thousand entries at once for a long time
+without anyone being able to tell.
+
+With that check honest again, 149 leftover translations turned out to belong to
+screens that were rebuilt or removed some time ago. They are gone from all six
+languages. Nothing that is on screen today was touched.
+
## [1.32.35] — 2026-07-25
The doctor report stops asking for your practice name every time.
diff --git a/CLAUDE.md b/CLAUDE.md
index e529b0e80..196234aaa 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -89,6 +89,7 @@ The architecture map in `.planning/codebase/arch.md` walks each layer with file:
- **Every API route wraps in `apiHandler`, with three documented exceptions.** The MCP OAuth routes (`mcp/oauth/{authorize,token,register}`) use `withBackgroundEvent` instead, because OAuth requires RFC-format error bodies that the standard envelope cannot express. They carry the same protections by hand — rate limit, body cap, kill switch, mandatory PKCE-S256, redirect-URI allowlist. Every other route wraps. Every body-accepting route runs Zod `safeParse` and returns 422 via `returnAllZodIssues` (multi-issue envelope sanitised against echoed input through `sanitiseZodIssues`).
- **`userId` is always narrowed from session or Bearer.** No route accepts `userId` as a body field — it comes from `requireAuth()` and feeds the Prisma `where`. The batch endpoints (`/api/measurements/batch`, `/api/mood-entries/bulk`, `/api/medications/intake/bulk`, `/api/workouts/batch`) don't even declare a `userId` field in their Zod schemas.
- **No mass assignment.** Every `prisma.X.{create,update}({ data: ... })` builds its `data` object field-by-field from `parsed.data`, never by spreading the parsed object whole. `tokens.create` builds `permissions` from a hardcoded literal, never from request input.
+- **A two-ended change carries both ends.** Adding a column, a payload field, or a user preference without its consumer is the failure mode this repo keeps rediscovering: the column ships with the schema change, the reader is "the follow-up", and nothing in the gate notices because every other check proves one end. So the consumer lands in the same release branch, or the schema comment says why it cannot: `/// @internal: ` for something deliberately never surfaced, `/// @pending-consumer(#issue)` for a consumer that is genuinely still coming. `pnpm dlx tsx scripts/audit-column-readers.ts` sweeps for unannotated write-only columns and re-lists every `@pending-consumer` marker so a follow-up that never happened surfaces at the next release audit. Run it in the deep-audit step; it is a review tool with a grep-shaped matcher, deliberately not a gate. The two questions that can be answered exactly are gates instead — `account-payload-consumer-guard.test.ts` (every `/api/auth/me` field has a client reader) and `measurement-backup-completeness.test.ts` (every `Measurement` column reaches the backup).
- **Bearer scope is fail-closed.** `requireAuth()` with no argument admits a cookie session or a wildcard (`["*"]`) token only — a narrow-scope token is refused with 403 and an `undeclared_scope` audit row. A route that means to accept a narrow token names the scope: `requireAuth(FHIR_READ_SCOPE)`. Adding a route therefore cannot silently widen an existing token's reach, because the default arm denies. Only the login / passkey / refresh paths mint `["*"]`; every user-facing mint is narrow, and narrow now means narrow. Two surfaces resolve a Bearer token outside `requireAuth` — `/api/ingest/medication` and `/mcp` — and that set is frozen by a structural test (`src/__tests__/bearer-scope-enforcement-guard.test.ts`).
- **`requireAdmin()` is cookie-only.** Bearer tokens — even with `["*"]` scope — cannot reach admin endpoints. This is a structural boundary in `requireAdmin()` (`src/lib/api-handler.ts`), not a runtime check that future code can soften.
- **Raw SQL is parameter-bound or whitelist-spliced.** Every `$queryRaw` uses tagged-template parameters. Every `$queryRawUnsafe` / `$executeRawUnsafe` either passes positional `$N` placeholders with an argument array OR splices a value already asserted against a closed enum + regex (see the whitelist-splice block in `src/lib/rollups/measurement-rollups.ts`, around the `$queryRawUnsafe` calls). Inline comments document the whitelist at the splice point.
diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml
index e9884298b..25b8a043c 100644
--- a/docs/api/openapi.yaml
+++ b/docs/api/openapi.yaml
@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: HealthLog API
- version: 1.32.35
+ version: 1.32.36
description: >-
Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest.
diff --git a/messages/de.json b/messages/de.json
index 554c832be..bfaf09acd 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -901,7 +901,6 @@
"emptyDescription": "Lege Medikamente, Dosierungen und Einnahmefenster an, um die Therapietreue zu erfassen.",
"loadFailed": "Medikamente konnten nicht geladen werden",
"retryLoad": "Erneut laden",
- "name": "Name",
"inactive": "Pausiert",
"intakeHistory": "Einnahme-Verlauf",
"openDetailPage": "Medikament-Detailseite öffnen",
@@ -1204,8 +1203,6 @@
"dosesPerUnitLabel": "Dosen pro Einheit",
"dosesPerUnitPlaceholder": "z. B. 4",
"dosesPerUnitHint": "Dosen pro Pen / Ampulle — leer lassen, wenn kein Bestand getrackt wird.",
- "injectionSiteToggle": "Injektionsstellen-Rotation",
- "injectionSiteHint": "Optionaler Rotationshelfer. Die Stelle jeder Dosis wird beim Eintragen der Einnahme erfasst.",
"short": "Dosis",
"unitsPerDoseLabel": "Einheiten pro Dosis",
"unitsPerDoseHint": "z. B. 2 Tabletten à 2 mg für eine 4-mg-Dosis – oder ½ Tablette bei geteilter Dosis"
@@ -1256,7 +1253,6 @@
"remindersDescription": "Push, Telegram oder ntfy — je nach deinen Einstellungen.",
"noChannelHint": "Erinnerungen brauchen einen Benachrichtigungskanal — aktuell ist keiner aktiv.",
"noChannelHintLink": "Benachrichtigungen einrichten",
- "multiScheduleNote": "Dieses Medikament hat mehrere Zeitpläne. Den Detail-Editor öffnen wir in einer späteren Version.",
"short": "Fertig"
}
},
@@ -2122,7 +2118,6 @@
"emptyDescription": "Insights erscheinen, sobald du ein paar Messwerte erfasst hast. Beginne mit Gewicht oder Blutdruck.",
"emptyAddMeasurement": "Messwert erfassen",
"editMode": {
- "open": "Anpassen",
"done": "Fertig",
"reset": "Zurücksetzen",
"title": "Übersicht anpassen",
@@ -2149,16 +2144,6 @@
"assessmentPreparing": "Einschätzung wird vorbereitet…",
"noProviderConfigured": "Verbinde einen KI-Anbieter, um hier eine Einschätzung zu deinen Werten zu sehen.",
"noProviderAction": "Zu den KI-Einstellungen",
- "startAnalysis": "Einschätzung starten",
- "refreshAnalysis": "Einschätzung aktualisieren",
- "keyTakeaway": "Das Wichtigste",
- "findingsTitle": "Befunde",
- "heroFindingPositive": "Wichtigster Befund",
- "heroFindingNeutral": "Wichtigster Befund",
- "heroFindingAttention": "Beachten",
- "heroFindingWarning": "Wichtigster Alarm",
- "correlationsTitle": "Zusammenhänge",
- "recommendationsTitle": "Empfehlungen",
"personalRecord": {
"badge": "PR",
"tooltip": "Persönlicher Rekord: {value} {unit} am {date}"
@@ -2166,53 +2151,33 @@
"recommendation": {
"source": "Quelle",
"viewSource": "Leitlinie in neuem Tab öffnen",
- "rationale": "Warum diese Empfehlung",
"rationaleWindow": "Fenster",
"rationaleComparedTo": "Verglichen mit",
"rationaleDeviation": "Abweichung",
"rationaleExpand": "Begründung anzeigen",
"rationaleCollapse": "Begründung ausblenden",
- "legacyPayloadCta": "Insights aktualisiert — Neugenerierung lädt die neuen Erklärfunktionen",
"feedbackHelpful": "Hilfreich",
"feedbackNotHelpful": "Nicht hilfreich",
"feedbackThanks": "Danke für dein Feedback",
- "feedbackConfirmed": "Feedback gespeichert",
"feedbackAlreadyRated": "Bereits bewertet",
"feedbackError": "Dein Feedback konnte nicht gespeichert werden. Bitte versuche es erneut.",
- "confidence": "Vertrauen",
"confidenceAria": "Vertrauen: {value} von 100",
- "confidenceHigh": "Hohes Vertrauen",
- "confidenceMedium": "Mittleres Vertrauen",
"confidenceLow": "Geringes Vertrauen — wenige Datenpunkte",
"confidenceDraft": "Entwurf"
},
- "dataFoundation": "Datengrundlage",
- "dataGaps": "Datenlücken:",
- "lastUpdated": "Zuletzt aktualisiert",
- "confidenceHoch": "Hoch",
- "confidenceMittel": "Mittel",
- "confidenceGering": "Gering",
"loadError": "Auswertungen konnten nicht geladen werden",
"heroPersonalBaseline": "Basierend auf deinen letzten 90 Tagen",
- "heroRegenerate": "Neu generieren",
"heroRegenerating": "Wird generiert",
"regenerateAnalysis": "Einschätzung neu erstellen",
"regenerateSuccess": "Einschätzung wurde neu erstellt",
- "warmStarted": "Einschätzungen werden im Hintergrund erstellt",
"narrativeTitle": "Rückblick",
"narrativeWeek": "Diese Woche",
"narrativeMonth": "Dieser Monat",
- "narrativePreparing": "Deine Zusammenfassung wird erstellt…",
"narrativeUpdating": "Wird aktualisiert…",
- "narrativeProvenanceLabel": "Wie das zustande kam",
- "narrativeProvenanceMethod": "Eine Zusammenfassung des Zeitraums in einfacher Sprache, ausschließlich aus deinen erfassten Werten und den Zusammenhängen, die die statistische Mehrfachvergleichskorrektur überstanden haben. Beschreibend, nie eine Ursache.",
- "narrativeProvenanceMetrics": "Basierend auf: {metrics}",
- "narrativeProvenanceWindow": "Zeitfenster: {from} bis {to}",
"heroFallbackSubtitle": "Täglicher Blick auf deine Trends — direkt aus den Zahlen, die du protokollierst.",
"heroGreetingMorning": "Guten Morgen",
"heroGreetingAfternoon": "Guten Tag",
"heroGreetingEvening": "Guten Abend",
- "heroComingSoonTooltip": "Kommt bald",
"heroActionAskCoach": "Coach fragen",
"healthScore": {
"label": "Gesundheitsscore",
@@ -2225,14 +2190,12 @@
"componentMood": "Stimmung",
"componentCompliance": "Therapietreue",
"deltaExplainer": {
- "trigger": "Was bedeutet diese Veränderung?",
"title": "So wird die Veränderung berechnet",
"description": "Veränderung gegenüber letzter Woche: {delta}",
"body": "Der Score kombiniert Blutdruck, Gewicht, Stimmung und Therapietreue. Wir vergleichen den heutigen Stand mit demselben Zeitpunkt vor einer Woche. Erfasse eine frische Messung oder einen Stimmungs-Check, um den Wert nach oben zu schubsen."
},
"provenance": {
"toggle": "Zusammensetzung",
- "weightLabel": "Anteil",
"mixedBanner": "Einige Komponenten kombinieren mehrere Quellen. Die Aufschlüsselung zeigt den aktuellsten Wert je Komponente.",
"footnote": "Anteile sind nach verfügbaren Daten neu skaliert.",
"asOfLabel": "Stand {date}",
@@ -2296,10 +2259,6 @@
"viewDetail": "{metric} im Detail"
},
"vo2Max": {
- "statLatest": "Aktuell",
- "statMin": "Min",
- "statMax": "Max",
- "empty": "Noch keine VO₂-Max-Daten — synchronisiere Apple Health, um diese Ansicht zu füllen.",
"cardioLinkTitle": "Cardio-Fitness (VO₂ max)",
"cardioLinkBody": "VO₂ max ist das zentrale Maß für die aerobe Fitness. Das vollständige Diagramm und die Einschätzung findest du auf der eigenen Seite.",
"cardioLinkCta": "Cardio-Fitness öffnen"
@@ -2764,125 +2723,68 @@
"gewichtDescription": "Gewichtsverlauf gegen das aus deiner Größe abgeleitete gesunde Band, mit Einschätzung.",
"pulsDescription": "Pulsverlauf gegenüber dem persönlichen Karvonen-Zielband, mit Einschätzung.",
"stimmungDescription": "Tägliche Stimmungswerte im Zeitverlauf mit einer textbasierten Einschätzung.",
- "stimmungEmptyTitle": "Erste Stimmung erfassen",
- "stimmungEmptyDescription": "Stimmungs-Insights erscheinen, sobald du einige Einträge protokolliert hast. Tippe unten, um zu beginnen.",
- "stimmungEmptyAction": "Stimmungs-Tagebuch öffnen",
"medikamenteDescription": "Einnahme-Kalender pro Medikament plus eine textbasierte Einschätzung.",
- "medikamenteEmptyTitle": "Erstes Medikament hinzufügen",
- "medikamenteEmptyDescription": "Therapietreue-Insights brauchen mindestens ein erfasstes Medikament.",
- "medikamenteEmptyAction": "Medikamente öffnen",
"bmiDescription": "BMI aus deinem Gewicht und deiner Größe abgeleitet, mit Einschätzung.",
"target": {
"adjustLink": "Zielbereich anpassen"
},
"explainer": {
- "trigger": "Was ist {title}?",
- "bloodPressureTitle": "Blutdruck",
"bloodPressureBody": "Der Blutdruck ist die Kraft, die das Blut auf die Gefäßwände ausübt, angegeben als systolisch zu diastolisch (Spitzenwert beim Herzschlag über dem Ruhewert dazwischen). Dauerhaft hohe Werte belasten Herz und Gefäße auf Dauer. Als allgemeine Orientierung gilt nach europäischer Leitlinie (ESH 2023) unter 120/80 als optimal und ab 140/90 als Bluthochdruck; die US-Leitlinie (ACC/AHA 2017) setzt mit 130–139/80–89 (Stadium 1) eine strengere Grenze. Einordnung immer aus einer ruhigen, sitzenden Messung, nie aus einem einzelnen Wert.",
- "pulseTitle": "Puls",
"pulseBody": "Der Puls gibt an, wie oft das Herz pro Minute schlägt. Eine Einzelmessung spiegelt den Moment wider – in Ruhe liegt er meist bei 60–100 Schlägen pro Minute und steigt natürlicherweise bei Bewegung, Stress oder Koffein.",
- "weightTitle": "Gewicht",
"weightBody": "Das Körpergewicht ist eines der einfachsten Signale für die langfristige Energiebilanz und die allgemeine Gesundheit. Der Trend über Wochen zählt weit mehr als ein einzelner Wert, der mit Flüssigkeit, Ernährung und Tageszeit schwankt.",
- "bmiTitle": "Body-Mass-Index (BMI)",
"bmiBody": "Der BMI ist dein Gewicht geteilt durch deine Körpergröße zum Quadrat und ordnet dich einer groben Gewichtskategorie zu. Er ist ein Orientierungswert, keine Diagnose, und unterscheidet nicht zwischen Muskeln und Fett.",
- "sleepTitle": "Schlaf",
"sleepBody": "Die Schlafdauer ist die tatsächlich geschlafene Zeit pro Nacht, aufgeteilt in Leicht-, Tief- und REM-Schlaf. Die meisten Erwachsenen brauchen etwa sieben bis neun Stunden; gleichmäßige Menge und Qualität unterstützen Erholung, Stimmung und Stoffwechsel.",
- "moodTitle": "Stimmung",
"moodBody": "Die Stimmungserfassung hält auf einer einfachen Skala fest, wie du dich fühlst. Regelmäßiges Protokollieren zeigt Muster – wie Schlaf, Aktivität oder Medikamente mit dem Befinden zusammenhängen –, die ein einzelner Tag nie offenbart.",
- "medicationsTitle": "Medikamenten-Therapietreue",
"medicationsBody": "Die Therapietreue zeigt, wie zuverlässig du deine Medikamente wie verordnet einnimmst, hier als Anteil der erwarteten Dosen, die du erfasst hast. Eine gleichmäßige Einnahme entscheidet oft darüber, ob eine Therapie wie beabsichtigt wirkt.",
- "restingHrTitle": "Ruhepuls",
"restingHrBody": "Der Ruhepuls ist die Herzfrequenz in völliger Ruhe, meist über Nacht gemessen. Ein niedriger Ruhepuls deutet in der Regel auf eine bessere Herz-Kreislauf-Fitness hin, ein steigender Trend kann ein frühes Zeichen für Stress oder Krankheit sein.",
- "hrvTitle": "Herzfrequenzvariabilität (HRV)",
"hrvBody": "Die HRV ist die kleine Schwankung der Zeit zwischen aufeinanderfolgenden Herzschlägen. Eine höhere Variabilität spiegelt meist ein gut erholtes, anpassungsfähiges Nervensystem wider, ein anhaltender Rückgang kann auf Erschöpfung, Stress oder mangelnde Erholung hinweisen.",
- "oxygenSaturationTitle": "Sauerstoffsättigung (SpO₂)",
"oxygenSaturationBody": "Die SpO₂ ist der Prozentsatz der roten Blutkörperchen, die Sauerstoff transportieren. Als allgemeine Orientierung sind Werte von 95–100 % in Ruhe normal (StatPearls/NIH 2023); Werte dauerhaft unter 90 % verdienen Aufmerksamkeit, kurze nächtliche Abfälle sind häufig und über ~98 % bringt nichts. Optische Sensoren können die wahre Sättigung bei dunklerer Haut überschätzen (US FDA 2024) — ein einzelner normnaher Wert ist daher keine Entwarnung; bei Sorge warm und ruhig erneut messen.",
- "bodyTemperatureTitle": "Körpertemperatur",
"bodyTemperatureBody": "Die Körpertemperatur spiegelt deinen Kernwärmezustand wider, normalerweise um 37 °C mit kleinen Tagesschwankungen. Ein anhaltender Anstieg kann auf eine Infektion hindeuten; nächtliche Messungen am Handgelenk erfassen vor allem relative Änderungen, keinen klinischen Wert.",
- "activeEnergyTitle": "Aktive Energie",
"activeEnergyBody": "Die aktive Energie sind die Kalorien, die du durch Bewegung verbrennst, zusätzlich zum Grundumsatz in Ruhe. Sie ist ein direktes Maß dafür, wie körperlich aktiv dein Tag war.",
- "workoutsTitle": "Workouts",
"workoutsBody": "Workouts sind die strukturierten Trainingseinheiten, die deine Geräte mit Art, Dauer und Energie aufzeichnen. Ihre Erfassung zeigt, wie sich regelmäßiges Training über Wochen summiert und wie es mit deinen anderen Gesundheitssignalen zusammenhängt.",
- "respiratoryRateTitle": "Atemfrequenz",
"respiratoryRateBody": "Die Atemfrequenz ist die Anzahl der Atemzüge pro Minute. In Ruhe atmen die meisten Erwachsenen etwa zwölf- bis zwanzigmal pro Minute; ein anhaltender Anstieg kann mit Krankheit, Anstrengung oder Stress einhergehen.",
- "bodyWaterTitle": "Gesamtkörperwasser",
"bodyWaterBody": "Das Gesamtkörperwasser ist der Anteil deines Körpergewichts, der aus Flüssigkeit in und um die Zellen besteht. Es schwankt mit Flüssigkeitshaushalt, Ernährung und Aktivität, daher sagt der Trend über Wochen mehr aus als ein einzelner Wert.",
- "boneMassTitle": "Knochenmasse",
"boneMassBody": "Die Knochenmasse ist das geschätzte Gewicht der Mineralien in deinem Skelett. Waagengestützte Schätzungen sind ungefähr, doch ein gleichbleibender oder steigender Trend unterstützt die langfristige Knochenstärke.",
- "fatFreeMassTitle": "Fettfreie Masse",
"fatFreeMassBody": "Die fettfreie Masse ist alles in deinem Körper, das kein Fett ist — Muskeln, Knochen, Organe und Wasser. Sie zu halten oder aufzubauen, während das Gewicht sinkt, deutet meist auf eine gesündere Körperzusammensetzung hin.",
- "fatMassTitle": "Fettmasse",
"fatMassBody": "Die Fettmasse ist das Gesamtgewicht des Körperfetts. Etwas Fett ist für Hormone und Energiespeicher unverzichtbar, doch zu viel — besonders am Bauch — erhöht das langfristige Gesundheitsrisiko (WHO 2024).",
- "muscleMassTitle": "Muskelmasse",
"muscleMassBody": "Die Muskelmasse ist das geschätzte Gewicht der Muskeln in deinem Körper. Mehr Muskeln unterstützen Kraft, Stoffwechsel und Beweglichkeit und müssen mit zunehmendem Alter aktiv erhalten werden.",
- "visceralFatTitle": "Viszeralfett",
"visceralFatBody": "Viszeralfett ist das Fett, das tief im Bauchraum um die Organe gespeichert wird. Anders als das Unterhautfett ist ein hoher Wert eng mit Herzkrankheiten und Typ-2-Diabetes verbunden (AHA, Circulation 2021), daher ist niedriger meist besser. Die Skala zeigt einen einheitenlosen Wert (etwa 1–12 gilt als gesunder Bereich, ab 13 als erhöht); das ist eine andere Skala als die bildgebenden cm²-Schwellenwerte, die in der Klinik verwendet werden, und nicht mit ihnen austauschbar.",
- "leanBodyMassTitle": "Magermasse",
"leanBodyMassBody": "Die Magermasse ist dein Gesamtgewicht abzüglich des Körperfetts — Muskeln, Knochen, Organe und Wasser zusammen. Sie neben dem Gewicht zu verfolgen zeigt, ob Veränderungen aus Muskeln oder aus Fett stammen.",
- "flightsClimbedTitle": "Etagen gestiegen",
"flightsClimbedBody": "Die gestiegenen Etagen zählen die Stockwerke, die du täglich nach oben gehst, wobei eine Etage etwa drei Metern Höhe entspricht. Es ist ein einfacher Marker für Alltagsaktivität und Beinarbeit.",
- "walkingDistanceTitle": "Geh- + Laufstrecke",
"walkingDistanceBody": "Die Geh- und Laufstrecke ist die Strecke, die du täglich zu Fuß zurücklegst. Sie ergänzt die Schrittzahl, indem sie zeigt, wie weit du dich bewegt hast, nicht nur wie oft du getreten bist.",
- "walkingSteadinessTitle": "Gehstabilität",
"walkingSteadinessBody": "Die Gehstabilität schätzt, wie stabil und ausgewogen dein Gangbild ist, eingeteilt von OK bis niedrig. Ein niedriger Wert deutet auf ein höheres Sturzrisiko hin und sollte ärztlich besprochen werden.",
- "walkingHeartRateTitle": "Herzfrequenz beim Gehen",
"walkingHeartRateBody": "Die Herzfrequenz beim Gehen ist dein durchschnittlicher Puls bei einem gleichmäßigen Spaziergang. Ein über die Zeit niedrigerer Wert spiegelt bei gleicher Anstrengung oft eine bessere Herz-Kreislauf-Fitness wider.",
- "walkingAsymmetryTitle": "Gangasymmetrie",
"walkingAsymmetryBody": "Die Gangasymmetrie ist der Anteil der Zeit, in der deine Schritte zwischen linkem und rechtem Bein ungleichmäßig sind. Ein höherer Prozentsatz kann auf Schmerzen, eine Verletzung oder ein Gangproblem hindeuten, das näher betrachtet werden sollte.",
- "doubleSupportTimeTitle": "Doppelunterstützungszeit",
"doubleSupportTimeBody": "Die Doppelunterstützungszeit ist der Anteil jedes Gehzyklus, in dem beide Füße gleichzeitig den Boden berühren. Ein höherer Prozentsatz bedeutet meist ein vorsichtigeres, weniger stabiles Gangbild.",
- "stepLengthTitle": "Schrittlänge",
"stepLengthBody": "Die Schrittlänge ist der durchschnittliche Abstand von einem Fußauftritt zum nächsten. Eine über die Zeit kürzere Schrittlänge kann Müdigkeit, Vorsicht oder eine veränderte Beweglichkeit widerspiegeln.",
- "walkingSpeedTitle": "Gehgeschwindigkeit",
"walkingSpeedBody": "Die Gehgeschwindigkeit ist, wie schnell du dich auf ebenem Untergrund fortbewegst. Sie ist ein gut untersuchter Marker für allgemeine Gesundheit und Beweglichkeit, und ein gleichbleibendes oder steigendes Tempo ist ein gutes Zeichen.",
- "pulseWaveVelocityTitle": "Pulswellengeschwindigkeit",
"pulseWaveVelocityBody": "Die Pulswellengeschwindigkeit misst, wie schnell die Druckwelle jedes Herzschlags durch deine Arterien läuft. Eine schnellere Welle weist auf steifere Arterien hin, was mit einem höheren Herz-Kreislauf-Risiko verbunden ist.",
- "vascularAgeTitle": "Gefäßalter",
"vascularAgeBody": "Das Gefäßalter schätzt anhand der Arteriensteifigkeit, wie alt sich deine Arterien im Vergleich zu deinem tatsächlichen Alter verhalten. Ein Gefäßalter unter dem echten Alter deutet auf gesündere Blutgefäße hin.",
- "environmentalAudioTitle": "Umgebungslautstärke",
"environmentalAudioBody": "Die Umgebungslautstärke erfasst die Lautstärke deiner Umwelt in Dezibel. Anhaltende Belastung über etwa 70 dB kann das Gehör mit der Zeit schädigen, daher sind niedrigere Tagesmittel sicherer.",
- "headphoneAudioTitle": "Kopfhörerlautstärke",
"headphoneAudioBody": "Die Kopfhörerlautstärke erfasst in Dezibel, wie laut du über Ohrhörer oder Kopfhörer hörst. Die Lautstärke niedrig zu halten und Pausen einzulegen schützt dein Gehör langfristig.",
- "audioEventsTitle": "Laute Geräuschereignisse",
"audioEventsBody": "Laute Geräuschereignisse markieren Momente, in denen der Lärm um dich herum ein Niveau erreicht hat, das bei ausreichender Belastung das Gehör schädigen kann. Häufige Ereignisse sind ein Hinweis, eine ruhigere Umgebung oder Gehörschutz zu suchen.",
- "daylightTitle": "Zeit im Tageslicht",
"daylightBody": "Die Zeit im Tageslicht ist, wie lange du dich täglich im Freien im Licht aufhältst. Regelmäßiges Tageslicht unterstützt einen gesunden Schlaf-Wach-Rhythmus und die Stimmung (CDC 2024), und Zeit im Freien ist mit der Augengesundheit verbunden.",
- "bloodGlucoseTitle": "Blutzucker",
"bloodGlucoseBody": "Der Blutzucker ist die Zuckermenge in deinem Blut, die wichtigste Energiequelle des Körpers. Er steigt nach dem Essen und pendelt sich zwischen den Mahlzeiten ein; dauerhaft hohe Werte können auf Diabetes oder Prädiabetes hinweisen.",
- "skinTemperatureTitle": "Hauttemperatur",
"skinTemperatureBody": "Die Hauttemperatur ist hier die kleine nächtliche Abweichung deiner Handgelenkstemperatur von deinem eigenen Ausgangswert, keine absolute Fiebermessung. Veränderungen können Schlaf, Umgebung, Krankheit oder Zyklusphase widerspiegeln.",
- "cardioFitnessTitle": "Cardiofitness (VO₂max)",
"cardioFitnessBody": "VO₂max gibt an, wie viel Sauerstoff dein Körper bei maximaler Anstrengung verwerten kann — der beste Laborindex der Ausdauerfitness, den Wearables indirekt schätzen. Als allgemeine Orientierung: Fitness ist ein abgestufter Prädiktor der Überlebensrate — jeder zusätzliche MET (etwa 3,5 ml/kg/min) geht mit rund 10–25 % geringerer Sterblichkeit einher (AHA, Circulation 2016). Ein höherer oder steigender Trend ist ein starkes Zeichen für eine gute Herz-Kreislauf-Gesundheit.",
- "cardioRecoveryTitle": "Erholungspuls",
"cardioRecoveryBody": "Der Erholungspuls gibt an, um wie viele Schläge pro Minute dein Puls in der ersten Minute nach dem Ende einer anstrengenden Belastung fällt. Ein größerer Abfall bedeutet, dass sich dein Herz schneller beruhigt — ein Zeichen guter Fitness.",
- "wristTemperatureTitle": "Handgelenktemperatur",
"wristTemperatureBody": "Die Handgelenktemperatur wird im Schlaf gemessen und als Abweichung von deinem persönlichen Ausgangswert angegeben, nicht als absoluter Wert. Ein gleichmäßiger Wert ist normal, ein anhaltender Anstieg kann auf Krankheit, intensives Training oder eine Zyklusveränderung hindeuten.",
- "fallsTitle": "Stürze",
"fallsBody": "Hier werden die von deinem Wearable erkannten schweren Stürze gezählt. Null ist das Ziel; eine steigende Zahl ist ein Hinweis auf ein erhöhtes Sturzrisiko, das du — besonders im höheren Alter — mit einer Ärztin oder einem Arzt besprechen solltest (CDC STEADI 2024).",
- "sixMinuteWalkTitle": "6-Minuten-Gehstrecke",
"sixMinuteWalkBody": "Dies ist eine Schätzung, wie weit du in sechs Minuten gehen könntest — ein etablierter klinischer Test für Mobilität und kardiopulmonale Ausdauer. Der Wearable-Wert ist ein Näherungswert, nicht der überwachte klinische Test; aussagekräftig ist der gleichbleibende oder steigende Trend.",
- "stairAscentSpeedTitle": "Treppensteig-Tempo",
"stairAscentSpeedBody": "Dies gibt an, wie schnell du eine Treppe hinaufgehst. Es ist ein empfindlicher Marker für Beinkraft und Mobilität — ein gleichbleibendes oder steigendes Tempo ist gut, ein sinkendes solltest du im Blick behalten.",
- "stairDescentSpeedTitle": "Treppab-Tempo",
"stairDescentSpeedBody": "Dies gibt an, wie schnell du eine Treppe hinabgehst. Zusammen mit dem Steig-Tempo zeigt es Beinkontrolle und Gleichgewicht — ein gleichmäßiges Tempo ist gut, ein sinkendes solltest du im Blick behalten.",
- "breathingDisturbancesTitle": "Atemaussetzer im Schlaf",
"breathingDisturbancesBody": "Ein Wearable kann schätzen, wie oft deine Atmung im Schlaf unterbrochen war. Ein niedriger Index ist besser; ein als erhöht eingestufter Wert — vor allem über viele Nächte — sollte mit einer Ärztin oder einem Arzt besprochen werden. Es ist ein Screening-Näherungswert, keine Schlafapnoe-Diagnose.",
- "stepsTitle": "Schritte",
"stepsBody": "Die Schrittzahl ist die Gesamtzahl der Schritte, die du täglich gehst. Sie ist das einfachste Maß für Alltagsbewegung, und eine stetige Gewohnheit zählt mehr als ein einzelner großer Tag. Als allgemeine Orientierung fällt das Sterblichkeitsrisiko deutlich Richtung etwa 8.000 Schritte pro Tag, mit weiterem Nutzen bis rund 12.000 (Saint-Maurice JAMA 2020); bei älteren Menschen liegt der optimale Wert niedriger. Die „10.000“ ist ein Werbeslogan, keine Forschung.",
"readinessBody": "Die Tagesform ist ein einzelner Tageswert dafür, wie bereit dein Körper für den Tag ist — er verbindet Ruhepuls, Herzfrequenzvariabilität, Schlaf und die Beständigkeit deiner letzten Nächte. Es ist ein Geräte-Näherungswert, der an deinem eigenen Ausgangswert gemessen wird, keine klinische Messung, und zwischen Geräten nicht vergleichbar. Ein höherer Wert heißt erholt und belastbar; ein niedrigerer ist ein sanfter Hinweis, einen Gang zurückzuschalten.",
"sleepScoreBody": "Dein Schlafscore fasst eine ganze Nacht in einer Zahl zusammen — aus Dauer, Effizienz, Regelmäßigkeit und der Verteilung von Leicht-, Tief- und REM-Schlaf. Achte eher auf das Muster über Wochen als auf einen perfekten Wert in einer einzelnen Nacht.",
"recoveryBody": "Die Erholung zeigt, wie gut sich dein Körper regeneriert hat — gestützt auf nächtliche Signale wie Herzfrequenzvariabilität und Ruhepuls. Es ist ein geräteabgeleiteter Näherungswert an deinem eigenen Ausgangswert, keine klinische Messung, und zwischen Geräten nicht vergleichbar. Ein hoher Wert ist grünes Licht, mehr zu geben; ein niedrigerer deutet an, dass dein System noch aufbaut.",
"strainBody": "Die Belastung erfasst, wie viel kardiovaskuläre und körperliche Last du deinem Körper zugemutet hast — von Workouts bis zu einem aktiven Tag auf den Beinen. Es ist ein geräteabgeleiteter Index, gemessen an deinem eigenen üblichen Aufwand, keine klinische Messung, und der Kennwert ist zwischen Geräten nicht vergleichbar.",
"stressBody": "Stress ist hier eine Geräte-Heuristik aus Mustern der Herzfrequenzvariabilität — keine Cortisol-Messung und nicht, wie du dich bewusst fühlst, und auch kein klinischer Stress-Score. Lies ihn an deinem eigenen Ausgangswert; zwischen Geräten ist er nicht vergleichbar. Ein niedrigerer Wert deutet auf einen ruhigen, gut regulierten Zustand hin; ein höherer ist eine Einladung, Erholung einzuplanen.",
- "recoveryPageTitle": "Erholung",
"recoveryPageBody": "Die Erholung fasst die nächtlichen Signale deines Wearables zusammen – Recharge, Belastung, Load sowie deine Herzfrequenz- und Energiewerte des Tages – und zeigt, wie gut sich dein Körper erholt hat. Achte eher auf das Muster über mehrere Tage als auf einen einzelnen Wert; ein schwächerer Wert ist eine Einladung, kürzerzutreten, kein Alarm.",
- "pulsePressureTitle": "Pulsdruck",
"pulsePressureBody": "Der Pulsdruck ist die Differenz zwischen deinem systolischen und diastolischen Wert (systolisch minus diastolisch). Als allgemeine Orientierung sind etwa 40 mmHg in Ruhe typisch; eine große Differenz über rund 60 mmHg im mittleren Alter kann auf versteifte Arterien hinweisen, eine kleine Differenz unter etwa 25 mmHg kann mit niedrigem Schlagvolumen einhergehen (StatPearls/NIH 2023). Ein Gesprächsanlass, keine Diagnose.",
- "meanArterialPressureTitle": "Mittlerer arterieller Druck",
"meanArterialPressureBody": "Der mittlere arterielle Druck ist der durchschnittliche Druck, den deine Organe über einen Herzschlag sehen – grob der diastolische Wert plus ein Drittel der Differenz zum systolischen. Als allgemeine Orientierung liegt ein Ruhewert bei etwa 70–100 mmHg (StatPearls/NIH 2023). Die oft genannte Grenze von ~60 mmHg ist ein Konzept der Akutmedizin, kein Alarm für die Selbstmessung.",
"cycleEducationBody": "Dein Zyklus durchläuft vier hormongesteuerte Phasen, und seine Länge und Regelmäßigkeit sind ein gut zugängliches Fenster auf die allgemeine Gesundheit. Als allgemeine Orientierung dauern Zyklen bei Erwachsenen typischerweise 21–35 Tage (klinische Referenzen nennen 24–38), mit einer Blutung von rund fünf Tagen (ACOG 2015). Die Lutealphase ist der stabile Anker, daher schätzt man den Eisprung am besten durch Rückrechnen von der nächsten erwarteten Periode – nie über einen festen „Tag 14“. Ein vorhergesagtes fruchtbares Fenster ist eine Schätzung, die mehrere erfasste Zyklen braucht und ungefähr bleibt, und die Basaltemperatur bestätigt den Eisprung im Nachhinein, statt ihn vorherzusagen. Dies ist keine Verhütungsmethode.",
- "sleepHygieneBody": "Einige evidenzbasierte Gewohnheiten unterstützen den Schlaf: gleichbleibende Zubett- und Aufstehzeiten auch am Wochenende, helles Morgenlicht und gedämpfte Abende, ein kühles, dunkles und ruhiges Zimmer sowie der Verzicht auf Koffein am Nachmittag, späten Alkohol und große späte Mahlzeiten (CDC 2024). Regelmäßigkeit sagt die Gesundheit mindestens so stark voraus wie die Dauer, und die Gesamtschlafzeit eines Wearables ist weit verlässlicher als seine Aufteilung in Tief-, Leicht- und REM-Schlaf.",
- "referenceGuidance": "Allgemeine Orientierung, keine medizinische Beratung. Referenzbereiche sind Populationswerte, die sich mit Alter, Geschlecht und Erkrankungen verschieben – dein eigener Verlauf über die Zeit führt die Einordnung."
+ "sleepHygieneBody": "Einige evidenzbasierte Gewohnheiten unterstützen den Schlaf: gleichbleibende Zubett- und Aufstehzeiten auch am Wochenende, helles Morgenlicht und gedämpfte Abende, ein kühles, dunkles und ruhiges Zimmer sowie der Verzicht auf Koffein am Nachmittag, späten Alkohol und große späte Mahlzeiten (CDC 2024). Regelmäßigkeit sagt die Gesundheit mindestens so stark voraus wie die Dauer, und die Gesamtschlafzeit eines Wearables ist weit verlässlicher als seine Aufteilung in Tief-, Leicht- und REM-Schlaf."
},
"bpDerived": {
"pulsePressure": "Der Pulsdruck (die Differenz zwischen deinen Werten) beträgt etwa {value} mmHg – rund 40 ist in Ruhe typisch.",
@@ -2916,16 +2818,6 @@
"compositionTitle": "Phasen-Verteilung",
"compositionSubtitle": "Letzte {nights} Nächte",
"compositionAriaLabel": "Schlafphasen-Verteilung über {nights} Nächte",
- "hypnogram": {
- "title": "Letzte Nacht",
- "subtitle": "{asleep} geschlafen",
- "ariaLabel": "Schlafphasen-Verlauf der letzten Nacht: {asleep} geschlafen, {awakenings} Wachphasen",
- "asleepTotal": "Geschlafen {value}",
- "inBedTotal": "Im Bett {value}",
- "awakenings": "{count} Wachphasen",
- "source": "Quelle: {source}",
- "estimateNote": "Die Phasen-Zeiten sind geschätzt — diese Quelle liefert Summen je Phase, keinen gemessenen Verlauf."
- },
"sourceDiscrepancy": {
"tooltipTitle": "Quellen weichen ab"
},
@@ -2980,7 +2872,6 @@
"loadError": "Dein Schlafrhythmus konnte gerade nicht geladen werden. Er erscheint wieder, sobald die Verbindung steht."
}
},
- "correlations": "Korrelationen",
"medicationCompliance": "Therapietreue",
"dayStreak": "{count} Tage Serie",
"dayStreakOne": "{count} Tag Serie",
@@ -2989,19 +2880,12 @@
"taken": "genommen",
"skipped": "übersprungen",
"missed": "verpasst",
- "generalStatusBadge": {
- "good": "Gut",
- "watch": "Beobachten",
- "critical": "Kritisch"
- },
"assessmentTitle": "Einschätzung",
"restMode": {
"title": "Ruhemodus",
"body": "Eine Krankheits- oder Beschwerdephase ist aktiv, daher spiegelt dies eine Erholungsphase wider. Dein Score wird dafür nicht gesenkt.",
"bodySince": "Seit {since} ist eine Krankheits- oder Beschwerdephase aktiv, daher spiegelt dies eine Erholungsphase wider. Dein Score wird dafür nicht gesenkt."
},
- "assessmentShowMore": "Mehr anzeigen",
- "assessmentShowLess": "Weniger anzeigen",
"bloodPressureSectionTitle": "Blutdruck",
"weightSectionTitle": "Gewicht",
"pulseSectionTitle": "Puls",
@@ -3009,8 +2893,6 @@
"complianceNoData": "Keine Daten zur Therapietreue verfügbar",
"generate": "Neue Analyse erstellen",
"moodSectionTitle": "Stimmung",
- "confidence": "Konfidenz",
- "cached": "Gecacht",
"emptyState": {
"bloodPressure": {
"title": "Noch keine Blutdruckwerte",
@@ -3059,11 +2941,6 @@
"title": "Noch keine Workouts",
"description": "Öffne die Apple-Health-App auf dem iPhone, damit Workouts hier synchronisiert werden. Withings-Aktivitätsimport pflegt Geh- und Radtouren mit ein."
},
- "row": {
- "duration": "Dauer",
- "distance": "Distanz",
- "energy": "Aktive Energie"
- },
"detail": {
"backToList": "Zurück zu Workouts",
"statsTitle": "Werte",
@@ -3076,13 +2953,9 @@
"statsStepCount": "Schritte",
"statsElevation": "Höhenmeter",
"statsPace": "Pace",
- "statsSource": "Quelle",
"routeTitle": "Strecke",
- "routeUnavailable": "Für dieses Workout liegt keine GPS-Strecke vor.",
"hrChartTitle": "Herzfrequenz",
- "hrChartUnavailable": "Sekundengenaue Herzfrequenz-Werte werden für dieses Workout nicht gespeichert.",
"notFound": "Workout nicht gefunden.",
- "loading": "Workout wird geladen…",
"hrFromPulse": "Aus deinen Herzfrequenzdaten rund um diese Einheit",
"hrAxisMinutes": "Min.",
"sportAverageLabel": "Dein aktueller Durchschnitt:",
@@ -3554,7 +3427,6 @@
"title": "Was auffällt",
"subheading": "Entdeckte Zusammenhänge",
"footer": "Herausgefiltert aus {tested} Tag-zu-Tag-Paaren, die über all deine Werte getestet wurden, nach der Korrektur für Fehlentdeckungen.",
- "empty": "Bisher hat kein Stimmungszusammenhang die statistische Hürde genommen. Logge weiter.",
"disclaimer": "Statistisch gefiltert, aber weiterhin beobachtend – ein Muster, das einen Blick wert ist, keine Ursache.",
"moodUp": "Höheres {factor} geht in deinen Daten tendenziell mit besserer Stimmung am Folgetag einher.",
"moodDown": "Höheres {factor} geht in deinen Daten tendenziell mit schlechterer Stimmung am Folgetag einher.",
@@ -3933,8 +3805,6 @@
},
"briefing": {
"memory": {
- "recall": "Zuletzt: {text}",
- "forward": "Mit Blick nach vorn: {text}",
"forwardWatch": "{metric} ist im Blick zu behalten.",
"forwardHolding": "Im Moment ist alles stabil."
}
@@ -3948,9 +3818,7 @@
}
},
"streak": {
- "returnLabel": "Dein {metric} ist seit {count} Tagen wieder in deinem üblichen Bereich — was es auch war, es ist vorbei.",
- "daysInside": "{count} Tage drinnen",
- "daysOutside": "{count} Tage draußen"
+ "returnLabel": "Dein {metric} ist seit {count} Tagen wieder in deinem üblichen Bereich — was es auch war, es ist vorbei."
},
"updatedTodayAt": "Heute aktualisiert, {time}",
"updatedYesterday": "Gestern aktualisiert",
@@ -4833,17 +4701,10 @@
"passwordMismatch": "Neue Passwörter stimmen nicht überein",
"passwordUpdated": "Passwort erfolgreich geändert",
"passkeys": "Passkeys",
- "registeredPasskeys": "Registrierte Passkeys",
"passkeysDescription": "Passkeys ermöglichen sicheres Anmelden ohne Passwort.",
"passkeyName": "Name",
- "passkeyDevice": "Gerät",
- "passkeyBackup": "Backup",
- "passkeyCreated": "Erstellt",
- "passkeyActions": "Aktionen",
"noPasskeys": "Keine Passkeys registriert.",
"addPasskey": "Passkey hinzufügen",
- "passkeyAdded": "Passkey erfolgreich hinzugefügt!",
- "passkeyOptionsError": "Konnte Passkey-Optionen nicht laden",
"passkeyRegistrationFailed": "Fehler bei Passkey-Registrierung",
"passkeyRegistrationCancelled": "Passkey-Registrierung abgebrochen",
"passkeyNotSupported": "Dein Browser oder Gerät unterstützt Passkeys noch nicht. Bitte einen anderen Browser oder ein anderes Gerät verwenden.",
@@ -4892,7 +4753,6 @@
"webPushSubscribed": "Push-Benachrichtigungen aktiviert!",
"webPushUnsubscribed": "Push-Benachrichtigungen deaktiviert.",
"webPushSubscribeFailed": "Aktivierung fehlgeschlagen",
- "webPushActive": "Aktiv",
"kiInsightsDescription": "Optional: Hinterlege deinen OpenAI-Key für tägliche, automatische Auswertungen in den Insights.",
"codexConnected": "ChatGPT erfolgreich verbunden! Insights sind jetzt aktiv.",
"codexDisconnected": "ChatGPT-Verbindung getrennt.",
@@ -4930,7 +4790,6 @@
"savedShort": "(gespeichert)",
"saveCta": "Speichern",
"saved": "Gespeichert",
- "saveFailed": "Speichern fehlgeschlagen",
"errorGeneric": "Fehler",
"providerChain": {
"types": {
@@ -5066,11 +4925,9 @@
"hint": "Wird als zusätzlicher Kontext an Coach und Tagesbriefing übergeben. Leer lassen, um nichts mitzusenden.",
"profileHint": "Alter und Geschlecht stammen aus deinem Profil und werden automatisch mitgegeben.",
"freeTextLabel": "Über mich (Freitext)",
- "conditionsLabel": "Chronische Erkrankungen",
"conditionsPlaceholder": "Z. B. Bluthochdruck, Typ-2-Diabetes…",
"allergiesLabel": "Allergien & Unverträglichkeiten",
"allergiesPlaceholder": "Z. B. Laktoseintoleranz, Penicillin-Allergie…",
- "focusLabel": "Worauf soll der Coach achten?",
"focusPlaceholder": "Z. B. behalte meinen Blutdruck im Blick, motiviere mich zu mehr Bewegung…",
"completeness": "{answered} von {total} beantwortet",
"questionsTitle": "Der Coach hat Rückfragen",
@@ -5148,8 +5005,6 @@
"token": "Der Token-Austausch mit Withings ist fehlgeschlagen. Prüfe Client-ID und Client-Secret und versuche es erneut.",
"generic": "Unbekannter Fehler bei der Verbindung. Starte die Verbindung bitte erneut."
},
- "integrationsSourcesHint": "Liefern mehrere Quellen dieselbe Metrik, entscheidet die Quellen-Priorität, welcher Wert zählt.",
- "integrationsSourcesHintLink": "Quellen-Priorität öffnen",
"withingsSynchronize": "Synchronisieren",
"withingsDisconnect": "Trennen",
"withingsDisconnectTitle": "Withings-Verbindung trennen?",
@@ -5340,10 +5195,7 @@
"apiEndpointPath": "Pfad",
"apiEndpointAuth": "Authentifizierung",
"apiEndpointExample": "Body-Beispiel",
- "collapse": "Einklappen",
- "expand": "Ausklappen",
"dangerZone": "Alle Daten löschen",
- "dangerZoneTitle": "Gefahrenzone",
"dangerZoneDescription": "Löscht alle deine Gesundheitsdaten und Integrationen. Dein Nutzerkonto bleibt erhalten.",
"dangerZoneConfirm": "Alle Daten löschen?",
"dangerZoneConfirmDescription": "Dieser Vorgang löscht alle deine Gesundheitsdaten und Integrationen unwiderruflich. Dein Nutzerkonto bleibt erhalten.",
@@ -5363,7 +5215,6 @@
"about": {
"version": "App-Version",
"gitSha": "Build",
- "builtAt": "Erstellt {time}",
"builtAtLabel": "Erstellt",
"license": "Lizenz",
"repository": "Quellcode",
@@ -5481,8 +5332,6 @@
"resourceTypes": "FHIR-Ressourcentypen",
"create": "Link erstellen",
"tokenCreated": "Freigabe-Link erstellt",
- "tokenOnce": "Kopiere diesen Link jetzt — der Token wird nur einmal angezeigt und lässt sich nicht wiederherstellen.",
- "copy": "Link kopieren",
"copied": "In die Zwischenablage kopiert.",
"activeTitle": "Aktive Links",
"noActive": "Keine aktiven Freigabe-Links.",
@@ -5496,7 +5345,6 @@
"statusExpired": "Abgelaufen",
"inactiveTitle": "Widerrufen und abgelaufen ({count})",
"shownOnce": "Nur einmal sichtbar. Erfasse QR-Code und Passphrase jetzt — beide lassen sich später nicht wiederherstellen.",
- "qrLabel": "QR-Code",
"qrAlt": "QR-Code mit Freigabelink und Passphrase",
"linkLabel": "Link",
"passphraseLabel": "Passphrase",
@@ -5555,7 +5403,6 @@
"polar": "Polar",
"polarDescription": "Verbinde dein Polar-Konto, um Nightly-Recharge-Erholung, Schlafphasen, tägliche Aktivität und Trainingseinheiten zu synchronisieren.",
"polarViewData": "Schlaf & Erholung ansehen",
- "polarHelp": "Melde dich bei Polar an, um den Zugriff zu erlauben. Daten werden stündlich im Hintergrund abgerufen.",
"polarCredentials": "API-Zugangsdaten",
"polarClientId": "Client ID",
"polarClientSecret": "Client Secret",
@@ -5585,7 +5432,6 @@
"oura": "Oura",
"ouraDescription": "Verbinde dein Oura-Konto, um Readiness, Schlafphasen, Herzfrequenzvariabilität und tägliche Aktivität zu synchronisieren.",
"ouraViewData": "Schlaf & Erholung ansehen",
- "ouraHelp": "Melde dich bei Oura an, um den Zugriff zu erlauben. Daten werden stündlich im Hintergrund abgerufen.",
"ouraCredentials": "API-Zugangsdaten",
"ouraClientId": "Client ID",
"ouraClientSecret": "Client Secret",
@@ -7476,7 +7322,6 @@
"measurementReminders": {
"sectionTitle": "Vorsorge",
"sectionDescription": "Erinnerungen, was wann und wo zu messen oder zu prüfen ist.",
- "addButton": "Erinnerung hinzufügen",
"markDone": "Erledigt",
"delete": "Löschen",
"deleteConfirmTitle": "Erinnerung löschen?",
diff --git a/messages/en.json b/messages/en.json
index fcf3e7d0a..6a16a1816 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -891,7 +891,6 @@
"emptyDescription": "Add medications, dosages, and reminder windows to track adherence.",
"loadFailed": "Could not load medications",
"retryLoad": "Retry",
- "name": "Name",
"inactive": "Paused",
"intakeHistory": "Intake history",
"openDetailPage": "Open medication detail page",
@@ -1194,8 +1193,6 @@
"dosesPerUnitLabel": "Doses per unit",
"dosesPerUnitPlaceholder": "e.g. 4",
"dosesPerUnitHint": "Doses per pen / vial — leave empty for no inventory tracking.",
- "injectionSiteToggle": "Injection-site rotation",
- "injectionSiteHint": "Optional rotation guide. The site for each dose is recorded when you log the intake.",
"short": "Dose",
"unitsPerDoseLabel": "Units per dose",
"unitsPerDoseHint": "e.g. 2 tablets of 2 mg for a 4 mg dose, or ½ a tablet for a split dose"
@@ -1246,7 +1243,6 @@
"remindersDescription": "Push, Telegram or ntfy — based on your settings.",
"noChannelHint": "Reminders need a notification channel — none is active right now.",
"noChannelHintLink": "Set up notifications",
- "multiScheduleNote": "This medication has multiple schedules. The detailed editor returns in a later release.",
"short": "Done"
}
},
@@ -2114,7 +2110,6 @@
"emptyDescription": "Insights light up once you've logged a handful of measurements. Start with weight or blood pressure.",
"emptyAddMeasurement": "Add a measurement",
"editMode": {
- "open": "Customize",
"done": "Done",
"reset": "Reset",
"title": "Customize overview",
@@ -2139,16 +2134,6 @@
"bmiEmptyAction": "Open account settings",
"noProviderConfigured": "Connect an AI provider to see an assessment of your readings here.",
"noProviderAction": "Open AI settings",
- "startAnalysis": "Start assessment",
- "refreshAnalysis": "Refresh assessment",
- "keyTakeaway": "Key Takeaway",
- "findingsTitle": "Findings",
- "heroFindingPositive": "Top finding",
- "heroFindingNeutral": "Top finding",
- "heroFindingAttention": "Worth attention",
- "heroFindingWarning": "Top alert",
- "correlationsTitle": "Correlations",
- "recommendationsTitle": "Recommendations",
"personalRecord": {
"badge": "PR",
"tooltip": "Personal record: {value} {unit} on {date}"
@@ -2156,53 +2141,33 @@
"recommendation": {
"source": "Source",
"viewSource": "Open guideline in a new tab",
- "rationale": "Why this recommendation",
"rationaleWindow": "Window",
"rationaleComparedTo": "Compared to",
"rationaleDeviation": "Deviation",
"rationaleExpand": "Show why",
"rationaleCollapse": "Hide why",
- "legacyPayloadCta": "Insights updated — regenerate for new explainability features",
"feedbackHelpful": "Helpful",
"feedbackNotHelpful": "Not helpful",
"feedbackThanks": "Thanks for your feedback",
- "feedbackConfirmed": "Feedback recorded",
"feedbackAlreadyRated": "Already rated",
"feedbackError": "Could not save your feedback. Please try again.",
- "confidence": "Confidence",
"confidenceAria": "Confidence: {value} of 100",
- "confidenceHigh": "High confidence",
- "confidenceMedium": "Moderate confidence",
"confidenceLow": "Low confidence — based on limited data",
"confidenceDraft": "Draft"
},
- "dataFoundation": "Data coverage",
- "dataGaps": "Data gaps:",
- "lastUpdated": "Last updated",
- "confidenceHoch": "High",
- "confidenceMittel": "Medium",
- "confidenceGering": "Low",
"loadError": "Failed to load insights",
"heroPersonalBaseline": "Based on your last 90 days",
- "heroRegenerate": "Regenerate",
"heroRegenerating": "Regenerating",
"regenerateAnalysis": "Re-run assessment",
"regenerateSuccess": "Assessment refreshed",
- "warmStarted": "Assessments are being prepared in the background",
"narrativeTitle": "In review",
"narrativeWeek": "This week",
"narrativeMonth": "This month",
- "narrativePreparing": "Preparing your summary…",
"narrativeUpdating": "Updating…",
- "narrativeProvenanceLabel": "How this was put together",
- "narrativeProvenanceMethod": "A plain-language summary of the period, drawn only from your logged numbers and the associations that survived statistical multiple-comparison control. Descriptive, never a cause.",
- "narrativeProvenanceMetrics": "Based on: {metrics}",
- "narrativeProvenanceWindow": "Window: {from} to {to}",
"heroFallbackSubtitle": "A daily read of your trends, drawn straight from the numbers you've logged.",
"heroGreetingMorning": "Good morning",
"heroGreetingAfternoon": "Good afternoon",
"heroGreetingEvening": "Good evening",
- "heroComingSoonTooltip": "Coming soon",
"heroActionAskCoach": "Ask the coach",
"healthScore": {
"label": "Health Score",
@@ -2215,14 +2180,12 @@
"componentMood": "Mood",
"componentCompliance": "Adherence",
"deltaExplainer": {
- "trigger": "What does this delta mean?",
"title": "How the delta is computed",
"description": "Change vs last week: {delta}",
"body": "The score combines BP, weight, mood and medication adherence. We compare today's score with the same time last week. Log a fresh reading or a mood check to nudge it up."
},
"provenance": {
"toggle": "Driven by",
- "weightLabel": "share",
"mixedBanner": "Some components blend multiple sources. The breakdown shows the most recent value per component.",
"footnote": "Weights are rescaled to the data that's actually available.",
"asOfLabel": "as of {date}",
@@ -2286,10 +2249,6 @@
"viewDetail": "View {metric} details"
},
"vo2Max": {
- "statLatest": "Latest",
- "statMin": "Min",
- "statMax": "Max",
- "empty": "No VO₂ max data yet — sync Apple Health to populate this view.",
"cardioLinkTitle": "Cardio fitness (VO₂ max)",
"cardioLinkBody": "VO₂ max is the headline measure of aerobic fitness. The full chart and assessment live on its own page.",
"cardioLinkCta": "Open cardio fitness"
@@ -2754,125 +2713,68 @@
"gewichtDescription": "Weight trend mapped against your height-derived healthy band, plus a written assessment.",
"pulsDescription": "Pulse trend against the personalised Karvonen target band, plus a written assessment.",
"stimmungDescription": "Daily mood scores over time with a written assessment.",
- "stimmungEmptyTitle": "Log your first mood",
- "stimmungEmptyDescription": "Mood insights light up once you've logged a few entries. Tap below to get started.",
- "stimmungEmptyAction": "Open mood journal",
"medikamenteDescription": "Per-medication adherence calendars and a written assessment.",
- "medikamenteEmptyTitle": "Add your first medication",
- "medikamenteEmptyDescription": "Adherence insights need at least one tracked medication.",
- "medikamenteEmptyAction": "Open medications",
"bmiDescription": "BMI derived from your weight and height, with a written assessment.",
"target": {
"adjustLink": "Adjust target range"
},
"explainer": {
- "trigger": "What is {title}?",
- "bloodPressureTitle": "Blood pressure",
"bloodPressureBody": "Blood pressure is the force your blood exerts on the artery walls, written as systolic over diastolic (the peak during a heartbeat over the resting value between beats). Consistently high readings strain the heart and blood vessels over time. As general guidance, European guidance (ESH 2023) calls under 120/80 optimal and 140/90 or above hypertension; US guidance (ACC/AHA 2017) sets a stricter line, labelling 130–139/80–89 as Stage 1. Categorise from a calm seated reading, never a single number.",
- "pulseTitle": "Pulse",
"pulseBody": "Your pulse is how many times your heart beats per minute. A spot reading reflects what you were doing in the moment — at rest it is typically 60–100 beats per minute, and it rises naturally with movement, stress, or caffeine.",
- "weightTitle": "Weight",
"weightBody": "Body weight is one of the simplest signals of long-term energy balance and overall health. The trend over weeks matters far more than any single reading, which fluctuates with hydration, food, and time of day.",
- "bmiTitle": "Body mass index (BMI)",
"bmiBody": "BMI is your weight divided by your height squared, giving a single number that places you in a broad weight category. It is a population-level screen, not a diagnosis, and does not distinguish muscle from fat.",
- "sleepTitle": "Sleep",
"sleepBody": "Sleep duration is how long you actually slept across the night, split into light, deep, and REM stages. Most adults need roughly seven to nine hours; both consistent quantity and quality support recovery, mood, and metabolism.",
- "moodTitle": "Mood",
"moodBody": "Mood tracking records how you feel over time on a simple scale. Logging it regularly reveals patterns — how sleep, activity, or medication line up with how you feel — that a single day never shows.",
- "medicationsTitle": "Medication adherence",
"medicationsBody": "Adherence is how reliably you take your medication as prescribed, shown here as the share of expected doses you actually logged. Steady intake is often what makes a treatment work as intended.",
- "restingHrTitle": "Resting heart rate",
"restingHrBody": "Resting heart rate is your heart rate when you are completely at rest, usually measured overnight — distinct from your pulse during activity. A lower resting rate generally points to better cardiovascular fitness, and a rising trend can be an early sign of stress or illness. As general guidance, a normal adult resting rate is 60–100 beats per minute (AHA 2024); a trained heart can sit benignly at 40–60.",
- "hrvTitle": "Heart rate variability (HRV)",
"hrvBody": "HRV is the tiny variation in time between consecutive heartbeats. Higher variability usually reflects a well-recovered, adaptable nervous system, while a sustained drop can signal fatigue, stress, or insufficient recovery.",
- "oxygenSaturationTitle": "Oxygen saturation (SpO₂)",
"oxygenSaturationBody": "SpO₂ is the percentage of your red blood cells carrying oxygen. As general guidance, readings of 95–100% are normal at rest (StatPearls/NIH 2023); values that stay below 90% warrant attention, though brief nightly dips are common and above ~98% adds no benefit. Optical readers can over-read true saturation in people with darker skin (US FDA 2024), so a single near-normal reading is not a clearance — retake when warm and still if concerned.",
- "bodyTemperatureTitle": "Body temperature",
"bodyTemperatureBody": "Body temperature reflects your core thermal state. As general guidance, the population mean is about 36.6 °C — not 37.0 °C — with a normal oral range of roughly 35.7–37.4 °C and a fever from 38.0 °C (J Gen Intern Med 2019). It swings about 0.5 °C across the day, and sites differ by up to ~1 °C, so tag where you measured. Overnight wrist readings track relative change, not a clinical fever value.",
- "activeEnergyTitle": "Active energy",
"activeEnergyBody": "Active energy is the calories you burn through movement, on top of the baseline your body uses at rest. It is a direct measure of how physically active your day was.",
- "workoutsTitle": "Workouts",
"workoutsBody": "Workouts are the structured exercise sessions your devices record, with type, duration, and energy. Tracking them shows how regular training accumulates over weeks and how it relates to your other health signals.",
- "respiratoryRateTitle": "Respiratory rate",
"respiratoryRateBody": "Respiratory rate is the number of breaths you take per minute. As general guidance, at rest most adults breathe 12–20 times a minute (American Lung Association); it is meaningful only truly at rest or asleep. A sustained rise of about +3 above your own nightly baseline can be an early illness signal — clinical escalation cutoffs are hospital tools, not self-alarms.",
- "bodyWaterTitle": "Total body water",
"bodyWaterBody": "Total body water is the share of your body weight made up of fluid, inside and around your cells. It shifts with hydration, food, and activity, so the trend over weeks tells you more than any single reading.",
- "boneMassTitle": "Bone mass",
"boneMassBody": "Bone mass is the estimated weight of the mineral in your skeleton. Scale-based estimates are approximate, but a steady or rising trend supports long-term bone strength.",
- "fatFreeMassTitle": "Fat-free mass",
"fatFreeMassBody": "Fat-free mass is everything in your body that is not fat — muscle, bone, organs, and water. Holding or building it while losing weight usually signals healthier body composition.",
- "fatMassTitle": "Fat mass",
"fatMassBody": "Fat mass is the total weight of fat in your body. Some fat is essential for hormones and energy storage, but too much — especially around the abdomen — raises long-term health risk (WHO 2024).",
- "muscleMassTitle": "Muscle mass",
"muscleMassBody": "Muscle mass is the estimated weight of the muscle in your body. More muscle supports strength, metabolism, and mobility, and it tends to need active effort to preserve as you age.",
- "visceralFatTitle": "Visceral fat",
"visceralFatBody": "Visceral fat is the fat stored deep in the abdomen around your organs. Unlike fat under the skin, a high level is closely linked to heart disease and type 2 diabetes (AHA, Circulation 2021), so lower is generally better. The scale shows a unitless rating (about 1–12 is the healthy range, 13 and up elevated); this is a separate scale from the imaging cm² thresholds clinicians use and is not interchangeable with them.",
- "leanBodyMassTitle": "Lean body mass",
"leanBodyMassBody": "Lean body mass is your total weight minus body fat — muscle, bone, organs, and water combined. Tracking it alongside weight shows whether changes come from muscle or from fat.",
- "flightsClimbedTitle": "Flights climbed",
"flightsClimbedBody": "Flights climbed counts the floors you go up each day, with one flight roughly ten feet of elevation. It is a simple marker of incidental activity and lower-body effort.",
- "walkingDistanceTitle": "Walking + running distance",
"walkingDistanceBody": "Walking and running distance is the ground you cover on foot each day. It complements step count by reflecting how far you moved, not just how often you stepped.",
- "walkingSteadinessTitle": "Walking steadiness",
"walkingSteadinessBody": "Walking steadiness estimates how stable and balanced your gait is, classed from OK to low. A lower level points to a higher chance of a fall and is worth discussing with a clinician.",
- "walkingHeartRateTitle": "Walking heart rate",
"walkingHeartRateBody": "Walking heart rate is your average pulse during a steady walk. A lower walking rate over time often reflects improving cardiovascular fitness for the same effort.",
- "walkingAsymmetryTitle": "Walking asymmetry",
"walkingAsymmetryBody": "Walking asymmetry is the share of time your steps are uneven between the left and right leg. A higher percentage can point to pain, injury, or a gait problem worth looking into.",
- "doubleSupportTimeTitle": "Double support time",
"doubleSupportTimeBody": "Double support time is the share of each walking cycle when both feet touch the ground at once. A higher percentage usually means a more cautious, less stable gait.",
- "stepLengthTitle": "Step length",
"stepLengthBody": "Step length is the average distance from one footfall to the next. A shorter step over time can reflect fatigue, caution, or a change in mobility.",
- "walkingSpeedTitle": "Walking speed",
"walkingSpeedBody": "Walking speed is how fast you move on a level walk. It is a well-studied marker of overall health and mobility, and a steady or rising pace is a good sign.",
- "pulseWaveVelocityTitle": "Pulse wave velocity",
"pulseWaveVelocityBody": "Pulse wave velocity measures how fast the pressure wave from each heartbeat travels along your arteries. A faster wave indicates stiffer arteries, which is linked to higher cardiovascular risk. As general guidance, European clinical reference flags carotid-femoral velocity above 10 m/s (ESC/ESH 2018) — but a consumer estimate is a proxy, not interchangeable with that clinical measurement.",
- "vascularAgeTitle": "Vascular age",
"vascularAgeBody": "Vascular age estimates how old your arteries behave compared with your actual age, based on arterial stiffness. A vascular age below your real age suggests healthier blood vessels.",
- "environmentalAudioTitle": "Environmental sound levels",
"environmentalAudioBody": "Environmental sound levels track the loudness of the world around you in decibels. Prolonged exposure above about 70 dB can damage hearing over time, so lower daily averages are safer.",
- "headphoneAudioTitle": "Headphone audio levels",
"headphoneAudioBody": "Headphone audio levels track how loudly you listen through earbuds or headphones, in decibels. Keeping the volume down and taking breaks protects your hearing over the long run.",
- "audioEventsTitle": "Loud sound events",
"audioEventsBody": "Loud sound events flag moments when noise around you reached a level that can harm hearing with enough exposure. Frequent events are a cue to seek quieter surroundings or hearing protection.",
- "daylightTitle": "Time in daylight",
"daylightBody": "Time in daylight is how long you spend in outdoor light each day. Regular daylight supports a healthy sleep-wake rhythm and mood (CDC 2024), and time outdoors is linked to eye health.",
- "bloodGlucoseTitle": "Blood glucose",
"bloodGlucoseBody": "Blood glucose is the amount of sugar in your blood, the body's main energy source. It rises after eating and settles between meals. As general guidance for adults without diabetes, a fasting reading under 100 mg/dL is normal, 100–125 is prediabetes, and 126 or above falls in the diabetes range (ADA 2024). A single high reading does not diagnose — a clinician confirms with a repeat test, and people managing diabetes use different, clinician-set targets.",
- "skinTemperatureTitle": "Skin temperature",
"skinTemperatureBody": "Skin temperature here is the small nightly change in your wrist temperature from your own baseline, not an absolute fever reading. Shifts can track sleep, environment, illness, or menstrual-cycle phase.",
- "cardioFitnessTitle": "Cardio fitness (VO₂ max)",
"cardioFitnessBody": "VO₂ max is how much oxygen your body can use at peak effort — the best lab index of aerobic fitness, which wearables estimate indirectly. As general guidance, fitness is a graded predictor of survival: each one-MET (about 3.5 mL/kg/min) higher value associates with roughly 10–25% lower mortality (AHA, Circulation 2016). A higher or rising trend is a strong sign of cardiovascular health.",
- "cardioRecoveryTitle": "Cardio recovery",
"cardioRecoveryBody": "Cardio recovery is how many beats per minute your heart rate falls in the first minute after you stop hard exercise. A bigger drop means your heart settles down faster, which is a sign of good fitness.",
- "wristTemperatureTitle": "Wrist temperature",
"wristTemperatureBody": "Wrist temperature is measured while you sleep and reported as the difference from your personal baseline, not an absolute value. A steady reading is normal, while a sustained rise can hint at illness, intense training, or a menstrual-cycle change.",
- "fallsTitle": "Falls",
"fallsBody": "This counts the hard falls your wearable detects. Zero is the goal; a rising count is a mobility-risk signal worth raising with a clinician, especially for older adults (CDC STEADI 2024).",
- "sixMinuteWalkTitle": "Six-minute walk distance",
"sixMinuteWalkBody": "This is an estimate of how far you could walk in six minutes — a long-standing clinical test of mobility and cardiopulmonary endurance. The wearable figure is a proxy, not the supervised clinical test; a steady or rising trend is the useful read.",
- "stairAscentSpeedTitle": "Stair ascent speed",
"stairAscentSpeedBody": "This is how fast you climb a flight of stairs. It is a sensitive marker of leg strength and mobility — a steady or rising pace is a good sign, and a falling one is worth watching.",
- "stairDescentSpeedTitle": "Stair descent speed",
"stairDescentSpeedBody": "This is how fast you go down a flight of stairs. Together with ascent speed it tracks leg control and balance — a steady pace is a good sign, and a falling one is worth watching.",
- "breathingDisturbancesTitle": "Sleep breathing disturbances",
"breathingDisturbancesBody": "A wearable can estimate how often your breathing was interrupted during sleep. A lower index is better; a level flagged as elevated, especially across many nights, is worth raising with a clinician. It is a screening proxy, not a sleep-apnea diagnosis.",
- "stepsTitle": "Steps",
"stepsBody": "Step count is the total number of steps you take each day. It is the simplest measure of everyday movement, and a steady habit matters more than any single big day. As general guidance, mortality risk falls steeply toward about 8,000 steps a day with continued benefit through roughly 12,000 (Saint-Maurice JAMA 2020); the optimal count is lower for older adults. The '10,000' figure is a marketing slogan, not research.",
"readinessBody": "Readiness is a single daily read on how prepared your body is to take on the day, blending resting heart rate, heart-rate variability, sleep, and how steady your recent nights have been. It is a device proxy benchmarked against your own baseline, not a clinical measure, and scores are not comparable across devices. A higher score means recovered and resourced; a lower one is a gentle cue to ease off.",
"sleepScoreBody": "Your sleep score distils a whole night into one number — duration, efficiency, regularity, and the balance of light, deep, and REM. Watch the pattern across weeks rather than chasing a perfect score on any single night.",
"recoveryBody": "Recovery reflects how well your body has bounced back, drawing on overnight signals like heart-rate variability and resting heart rate. It is a device-derived proxy read against your own baseline, not a clinical measure, and is not comparable across devices. A strong score is a green light to push; a softer one suggests your system is still rebuilding.",
"strainBody": "Strain captures how much cardiovascular and physical load you have put on your body, from workouts to an active day on your feet. It is a device-derived index judged against your own typical effort, not a clinical measure, and the headline number is not comparable across devices.",
"stressBody": "Stress here is a device heuristic derived from heart-rate-variability patterns, not a cortisol measurement or how you consciously feel — and not a clinical stress score. Read it against your own baseline; it is not comparable across devices. A lower score points to a calm, well-regulated state; a higher one is an invitation to build in recovery.",
- "recoveryPageTitle": "Recovery",
"recoveryPageBody": "Recovery brings together the overnight signals your wearable records — recharge, strain, load and your day's heart-rate and energy totals — to show how well your body has bounced back. Read the pattern across days rather than any single number; a softer reading is an invitation to ease off, not an alarm.",
- "pulsePressureTitle": "Pulse pressure",
"pulsePressureBody": "Pulse pressure is the gap between your systolic and diastolic readings (systolic minus diastolic). As general guidance, about 40 mmHg is typical at rest; a wide gap above roughly 60 mmHg in middle age can reflect stiffening arteries, while a narrow gap under about 25 mmHg can accompany low stroke volume (StatPearls/NIH 2023). It is a discussion prompt, not a diagnosis.",
- "meanArterialPressureTitle": "Mean arterial pressure",
"meanArterialPressureBody": "Mean arterial pressure is the average pressure your organs see across a heartbeat, roughly the diastolic plus a third of the gap to systolic. As general guidance, a resting value sits around 70–100 mmHg (StatPearls/NIH 2023). The often-cited ~60 mmHg floor is an acute-care concept, not a consumer self-alarm.",
"cycleEducationBody": "Your cycle runs through four phases driven by shifting hormones, and its length and regularity are an accessible window on overall health. As general guidance, adult cycles typically run 21–35 days (clinical references cite 24–38), with bleeding around five days (ACOG 2015). The luteal phase is the steady anchor, so ovulation is best estimated by counting back from your next expected period — never a fixed 'day 14'. A predicted fertile window is an estimate that needs several recorded cycles and stays approximate, and basal body temperature confirms ovulation after the fact rather than predicting it. This is not a contraceptive method.",
- "sleepHygieneBody": "A few evidence-based habits support sleep: keep consistent bed and wake times including weekends, get bright morning daylight and dim your evenings, keep the room cool, dark and quiet, and avoid afternoon caffeine, late alcohol and large late meals (CDC 2024). Regularity predicts health at least as strongly as duration, and a wearable's total sleep time is far more trustworthy than its deep, light and REM split.",
- "referenceGuidance": "General guidance, not medical advice. Reference ranges are population anchors that shift with your age, sex and health conditions — your own trend over time leads the read."
+ "sleepHygieneBody": "A few evidence-based habits support sleep: keep consistent bed and wake times including weekends, get bright morning daylight and dim your evenings, keep the room cool, dark and quiet, and avoid afternoon caffeine, late alcohol and large late meals (CDC 2024). Regularity predicts health at least as strongly as duration, and a wearable's total sleep time is far more trustworthy than its deep, light and REM split."
},
"bpDerived": {
"pulsePressure": "Pulse pressure (the gap between your readings) is about {value} mmHg — roughly 40 is typical at rest.",
@@ -2906,16 +2808,6 @@
"compositionTitle": "Stage composition",
"compositionSubtitle": "Last {nights} nights",
"compositionAriaLabel": "Sleep stage composition over {nights} nights",
- "hypnogram": {
- "title": "Last night",
- "subtitle": "{asleep} asleep",
- "ariaLabel": "Sleep stage progression for last night: {asleep} asleep, {awakenings} awakenings",
- "asleepTotal": "Asleep {value}",
- "inBedTotal": "In bed {value}",
- "awakenings": "{count} awakenings",
- "source": "Source: {source}",
- "estimateNote": "Stage timing is estimated — this source reports totals per stage, not a measured timeline."
- },
"sourceDiscrepancy": {
"tooltipTitle": "Sources disagree"
},
@@ -2970,7 +2862,6 @@
"loadError": "Your sleep rhythm couldn't load just now. It'll reappear once the connection settles."
}
},
- "correlations": "Correlations",
"medicationCompliance": "Medication adherence",
"dayStreak": "{count} day streak",
"dayStreakOne": "{count} day streak",
@@ -2979,19 +2870,12 @@
"taken": "taken",
"skipped": "skipped",
"missed": "missed",
- "generalStatusBadge": {
- "good": "Good",
- "watch": "Watch",
- "critical": "Critical"
- },
"assessmentTitle": "Assessment",
"restMode": {
"title": "Rest Mode",
"body": "An illness or condition episode is active, so this reflects a recovery window. Your score isn't lowered for it.",
"bodySince": "An illness or condition episode has been active since {since}, so this reflects a recovery window. Your score isn't lowered for it."
},
- "assessmentShowMore": "Show more",
- "assessmentShowLess": "Show less",
"bloodPressureSectionTitle": "Blood Pressure",
"weightSectionTitle": "Weight",
"pulseSectionTitle": "Pulse",
@@ -2999,8 +2883,6 @@
"complianceNoData": "No adherence data available.",
"generate": "Generate new analysis",
"moodSectionTitle": "Mood",
- "confidence": "Confidence",
- "cached": "Cached",
"emptyState": {
"bloodPressure": {
"title": "No blood pressure entries yet",
@@ -3049,11 +2931,6 @@
"title": "No workouts yet",
"description": "Open the Apple Health app on your iPhone to sync workouts here. Withings activity import keeps walking and cycling sessions flowing in too."
},
- "row": {
- "duration": "Duration",
- "distance": "Distance",
- "energy": "Active energy"
- },
"detail": {
"backToList": "Back to workouts",
"statsTitle": "Stats",
@@ -3066,13 +2943,9 @@
"statsStepCount": "Steps",
"statsElevation": "Elevation",
"statsPace": "Pace",
- "statsSource": "Source",
"routeTitle": "Route",
- "routeUnavailable": "No GPS route recorded for this workout.",
"hrChartTitle": "Heart rate",
- "hrChartUnavailable": "Per-second heart-rate samples are not stored for this workout.",
"notFound": "Workout not found.",
- "loading": "Loading workout…",
"hrFromPulse": "From your heart-rate data around this session",
"hrAxisMinutes": "min",
"sportAverageLabel": "Your recent average:",
@@ -3544,7 +3417,6 @@
"title": "What stands out",
"subheading": "Discovered relationships",
"footer": "Surfaced from {tested} day-to-day pairs tested across all your metrics, after the false-discovery correction.",
- "empty": "No mood relationships cleared the statistical bar yet. Keep logging.",
"disclaimer": "Statistically filtered, but still observational — a pattern worth watching, not a cause.",
"moodUp": "Higher {factor} tends to go with higher next-day mood in your data.",
"moodDown": "Higher {factor} tends to go with lower next-day mood in your data.",
@@ -3923,8 +3795,6 @@
},
"briefing": {
"memory": {
- "recall": "Earlier, {text}",
- "forward": "Looking ahead: {text}",
"forwardWatch": "Worth keeping an eye on {metric}.",
"forwardHolding": "Everything's holding steady for now."
}
@@ -3938,9 +3808,7 @@
}
},
"streak": {
- "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"
+ "returnLabel": "Your {metric} has been back inside your usual range for {count} days now — whatever that was, it's passed."
},
"updatedTodayAt": "Updated today, {time}",
"updatedYesterday": "Updated yesterday",
@@ -4823,17 +4691,10 @@
"passwordMismatch": "New passwords do not match",
"passwordUpdated": "Password updated successfully",
"passkeys": "Passkeys",
- "registeredPasskeys": "Registered Passkeys",
"passkeysDescription": "Passkeys allow secure login without a password.",
"passkeyName": "Name",
- "passkeyDevice": "Device",
- "passkeyBackup": "Backup",
- "passkeyCreated": "Created",
- "passkeyActions": "Actions",
"noPasskeys": "No passkeys registered.",
"addPasskey": "Add passkey",
- "passkeyAdded": "Passkey added successfully!",
- "passkeyOptionsError": "Could not load passkey options",
"passkeyRegistrationFailed": "Passkey registration failed",
"passkeyRegistrationCancelled": "Passkey registration cancelled",
"passkeyNotSupported": "Your browser or device doesn't support passkeys yet. Try a different browser or device.",
@@ -4882,7 +4743,6 @@
"webPushSubscribed": "Push notifications enabled!",
"webPushUnsubscribed": "Push notifications disabled.",
"webPushSubscribeFailed": "Activation failed",
- "webPushActive": "Active",
"kiInsightsDescription": "Optional: Save your OpenAI key for daily, automatic evaluations in Insights.",
"codexConnected": "ChatGPT connected successfully! Insights are now active.",
"codexDisconnected": "ChatGPT connection removed.",
@@ -4920,7 +4780,6 @@
"savedShort": "(saved)",
"saveCta": "Save",
"saved": "Saved",
- "saveFailed": "Save failed",
"errorGeneric": "Error",
"providerChain": {
"types": {
@@ -5056,11 +4915,9 @@
"hint": "Passed to the Coach and the daily briefing as additional context. Leave empty to send nothing.",
"profileHint": "Age and gender come from your profile and are included automatically.",
"freeTextLabel": "About me (free text)",
- "conditionsLabel": "Chronic conditions",
"conditionsPlaceholder": "E.g. hypertension, type 2 diabetes…",
"allergiesLabel": "Allergies & intolerances",
"allergiesPlaceholder": "E.g. lactose intolerance, penicillin allergy…",
- "focusLabel": "What should the Coach watch?",
"focusPlaceholder": "E.g. keep an eye on my blood pressure, motivate me to move more…",
"completeness": "{answered} of {total} answered",
"questionsTitle": "The Coach has follow-up questions",
@@ -5138,8 +4995,6 @@
"token": "The token exchange with Withings failed. Check the client ID and client secret and try again.",
"generic": "Unknown error while connecting. Please start the connection again."
},
- "integrationsSourcesHint": "When several sources report the same metric, the source priority decides which value counts.",
- "integrationsSourcesHintLink": "Open source priority",
"withingsSynchronize": "Synchronize",
"withingsDisconnect": "Disconnect",
"withingsDisconnectTitle": "Disconnect Withings?",
@@ -5330,10 +5185,7 @@
"apiEndpointPath": "Path",
"apiEndpointAuth": "Authentication",
"apiEndpointExample": "Body example",
- "collapse": "Collapse",
- "expand": "Expand",
"dangerZone": "Delete All Data",
- "dangerZoneTitle": "Danger zone",
"dangerZoneDescription": "Deletes all your health data and integrations. Your user account will be preserved.",
"dangerZoneConfirm": "Delete all data?",
"dangerZoneConfirmDescription": "This will permanently delete all your health data and integrations. Your user account will be preserved.",
@@ -5353,7 +5205,6 @@
"about": {
"version": "App version",
"gitSha": "Build",
- "builtAt": "Built {time}",
"builtAtLabel": "Built",
"license": "License",
"repository": "Source code",
@@ -5471,8 +5322,6 @@
"resourceTypes": "FHIR resource types",
"create": "Create link",
"tokenCreated": "Share link created",
- "tokenOnce": "Copy this link now — the token is shown only once and cannot be recovered.",
- "copy": "Copy link",
"copied": "Copied to clipboard.",
"activeTitle": "Active links",
"noActive": "No active share links.",
@@ -5486,7 +5335,6 @@
"statusExpired": "Expired",
"inactiveTitle": "Revoked and expired ({count})",
"shownOnce": "Shown once only. Capture the QR code and passphrase now — neither can be recovered later.",
- "qrLabel": "QR code",
"qrAlt": "QR code containing the share link and passphrase",
"linkLabel": "Link",
"passphraseLabel": "Passphrase",
@@ -5545,7 +5393,6 @@
"polar": "Polar",
"polarDescription": "Connect your Polar account to sync Nightly Recharge recovery, sleep stages, daily activity, and training sessions.",
"polarViewData": "View your sleep & recovery",
- "polarHelp": "Sign in with Polar to grant access. Data is pulled in the background once an hour.",
"polarCredentials": "API Credentials",
"polarClientId": "Client ID",
"polarClientSecret": "Client Secret",
@@ -5575,7 +5422,6 @@
"oura": "Oura",
"ouraDescription": "Connect your Oura account to sync readiness, sleep stages, heart-rate variability, and daily activity.",
"ouraViewData": "View your sleep & recovery",
- "ouraHelp": "Sign in with Oura to grant access. Data is pulled in the background once an hour.",
"ouraCredentials": "API Credentials",
"ouraClientId": "Client ID",
"ouraClientSecret": "Client Secret",
@@ -7466,7 +7312,6 @@
"measurementReminders": {
"sectionTitle": "Checkups",
"sectionDescription": "Reminders for what to measure or check, when, and where.",
- "addButton": "Add reminder",
"markDone": "Done",
"delete": "Delete",
"deleteConfirmTitle": "Delete reminder?",
diff --git a/messages/es.json b/messages/es.json
index 9867f5444..61b9bbad9 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -903,7 +903,6 @@
"emptyDescription": "Crea medicamentos, dosis y franjas horarias para registrar el cumplimiento.",
"loadFailed": "No se han podido cargar los medicamentos",
"retryLoad": "Reintentar",
- "name": "Nombre",
"inactive": "En pausa",
"intakeHistory": "Historial de tomas",
"openDetailPage": "Abrir la página de detalle del medicamento",
@@ -1206,8 +1205,6 @@
"dosesPerUnitLabel": "Dosis por unidad",
"dosesPerUnitPlaceholder": "p. ej. 4",
"dosesPerUnitHint": "Dosis por pluma / vial — déjalo vacío para no controlar el inventario.",
- "injectionSiteToggle": "Rotación del sitio de inyección",
- "injectionSiteHint": "Guía de rotación opcional. El sitio de cada dosis se registra al anotar la toma.",
"short": "Dosis",
"unitsPerDoseLabel": "Unidades por dosis",
"unitsPerDoseHint": "p. ej., 2 comprimidos de 2 mg para una dosis de 4 mg, o ½ comprimido para una dosis partida"
@@ -1258,7 +1255,6 @@
"remindersDescription": "Push, Telegram o ntfy — según tus ajustes.",
"noChannelHint": "Los recordatorios necesitan un canal de notificaciones — ahora mismo no hay ninguno activo.",
"noChannelHintLink": "Configurar notificaciones",
- "multiScheduleNote": "Este medicamento tiene varios horarios. El editor detallado volverá en una versión posterior.",
"short": "Listo"
}
},
@@ -2126,7 +2122,6 @@
"emptyDescription": "Los insights aparecen en cuanto registres algunas mediciones. Empieza por peso o tensión arterial.",
"emptyAddMeasurement": "Registrar medición",
"editMode": {
- "open": "Personalizar",
"done": "Listo",
"reset": "Restablecer",
"title": "Personalizar resumen",
@@ -2151,16 +2146,6 @@
"bmiEmptyAction": "Abrir ajustes de cuenta",
"noProviderConfigured": "Conecta un proveedor de IA para ver aquí una valoración de tus lecturas.",
"noProviderAction": "Abrir ajustes de IA",
- "startAnalysis": "Iniciar valoración",
- "refreshAnalysis": "Actualizar valoración",
- "keyTakeaway": "Lo más importante",
- "findingsTitle": "Hallazgos",
- "heroFindingPositive": "Hallazgo principal",
- "heroFindingNeutral": "Hallazgo principal",
- "heroFindingAttention": "A tener en cuenta",
- "heroFindingWarning": "Alerta principal",
- "correlationsTitle": "Correlaciones",
- "recommendationsTitle": "Recomendaciones",
"personalRecord": {
"badge": "PR",
"tooltip": "Récord personal: {value} {unit} el {date}"
@@ -2168,53 +2153,33 @@
"recommendation": {
"source": "Fuente",
"viewSource": "Abrir guía en una pestaña nueva",
- "rationale": "Por qué esta recomendación",
"rationaleWindow": "Ventana",
"rationaleComparedTo": "Comparado con",
"rationaleDeviation": "Desviación",
"rationaleExpand": "Mostrar justificación",
"rationaleCollapse": "Ocultar justificación",
- "legacyPayloadCta": "Insights actualizados — vuelve a generar para activar las nuevas explicaciones",
"feedbackHelpful": "Útil",
"feedbackNotHelpful": "No útil",
"feedbackThanks": "Gracias por tu opinión",
- "feedbackConfirmed": "Opinión guardada",
"feedbackAlreadyRated": "Ya valorado",
"feedbackError": "No se pudo guardar tu opinión. Inténtalo de nuevo.",
- "confidence": "Confianza",
"confidenceAria": "Confianza: {value} de 100",
- "confidenceHigh": "Confianza alta",
- "confidenceMedium": "Confianza media",
"confidenceLow": "Confianza baja — pocos datos",
"confidenceDraft": "Borrador"
},
- "dataFoundation": "Base de datos",
- "dataGaps": "Vacíos de datos:",
- "lastUpdated": "Última actualización",
- "confidenceHoch": "Alta",
- "confidenceMittel": "Media",
- "confidenceGering": "Baja",
"loadError": "No se han podido cargar los análisis",
"heroPersonalBaseline": "Basado en tus últimos 90 días",
- "heroRegenerate": "Regenerar",
"heroRegenerating": "Generando",
"regenerateAnalysis": "Volver a generar la valoración",
"regenerateSuccess": "Valoración regenerada",
- "warmStarted": "Las valoraciones se están preparando en segundo plano",
"narrativeTitle": "Resumen",
"narrativeWeek": "Esta semana",
"narrativeMonth": "Este mes",
- "narrativePreparing": "Preparando tu resumen…",
"narrativeUpdating": "Actualizando…",
- "narrativeProvenanceLabel": "Cómo se elaboró esto",
- "narrativeProvenanceMethod": "Un resumen del período en lenguaje sencillo, basado únicamente en tus valores registrados y en las asociaciones que superaron el control estadístico de comparaciones múltiples. Descriptivo, nunca una causa.",
- "narrativeProvenanceMetrics": "Basado en: {metrics}",
- "narrativeProvenanceWindow": "Ventana: del {from} al {to}",
"heroFallbackSubtitle": "Una mirada diaria a tus tendencias — directamente de los datos que registras.",
"heroGreetingMorning": "Buenos días",
"heroGreetingAfternoon": "Buenas tardes",
"heroGreetingEvening": "Buenas noches",
- "heroComingSoonTooltip": "Próximamente",
"heroActionAskCoach": "Preguntar al coach",
"healthScore": {
"label": "Puntuación de salud",
@@ -2227,14 +2192,12 @@
"componentMood": "Estado de ánimo",
"componentCompliance": "Cumplimiento",
"deltaExplainer": {
- "trigger": "¿Qué significa este cambio?",
"title": "Cómo se calcula el cambio",
"description": "Cambio respecto a la semana pasada: {delta}",
"body": "La puntuación combina PA, peso, ánimo y adherencia. Comparamos la puntuación de hoy con la del mismo momento de la semana anterior. Registra una medición nueva o una nota de ánimo para empujarla hacia arriba."
},
"provenance": {
"toggle": "Compuesto por",
- "weightLabel": "peso",
"mixedBanner": "Algunos componentes combinan varias fuentes. El desglose muestra el valor más reciente por componente.",
"footnote": "Los pesos se reescalan según los datos realmente disponibles.",
"asOfLabel": "a fecha de {date}",
@@ -2298,10 +2261,6 @@
"viewDetail": "Ver detalles de {metric}"
},
"vo2Max": {
- "statLatest": "Actual",
- "statMin": "Mín",
- "statMax": "Máx",
- "empty": "Aún no hay datos de VO₂ máx — sincroniza Apple Health para llenar esta vista.",
"cardioLinkTitle": "Forma cardiovascular (VO₂ máx)",
"cardioLinkBody": "El VO₂ máx es la medida principal de la forma aeróbica. El gráfico completo y la evaluación están en su propia página.",
"cardioLinkCta": "Abrir forma cardiovascular"
@@ -2766,125 +2725,68 @@
"gewichtDescription": "Evolución del peso frente a la banda saludable derivada de tu altura, con valoración.",
"pulsDescription": "Resting pulse vs. the personalised Karvonen target band, plus a written assessment.",
"stimmungDescription": "Valores diarios de estado de ánimo en el tiempo, con una valoración en texto.",
- "stimmungEmptyTitle": "Registrar primer estado de ánimo",
- "stimmungEmptyDescription": "Los insights de ánimo aparecen en cuanto registres unas cuantas entradas. Toca abajo para empezar.",
- "stimmungEmptyAction": "Abrir diario de ánimo",
"medikamenteDescription": "Calendario de tomas por medicamento, más una valoración en texto.",
- "medikamenteEmptyTitle": "Añadir primer medicamento",
- "medikamenteEmptyDescription": "Los insights de adherencia necesitan al menos un medicamento registrado.",
- "medikamenteEmptyAction": "Abrir medicamentos",
"bmiDescription": "IMC derivado de tu peso y altura, con valoración.",
"target": {
"adjustLink": "Ajustar el rango objetivo"
},
"explainer": {
- "trigger": "¿Qué es {title}?",
- "bloodPressureTitle": "Presión arterial",
"bloodPressureBody": "La presión arterial es la fuerza que ejerce la sangre sobre las paredes de las arterias, expresada como sistólica sobre diastólica (el pico durante un latido sobre el valor en reposo entre latidos). Los valores altos de forma constante sobrecargan el corazón y los vasos con el tiempo.",
- "pulseTitle": "Pulso",
"pulseBody": "El pulso es cuántas veces late el corazón por minuto. Una medición puntual refleja lo que hacías en ese momento: en reposo suele ser de 60 a 100 latidos por minuto y sube de forma natural con el movimiento, el estrés o la cafeína.",
- "weightTitle": "Peso",
"weightBody": "El peso corporal es una de las señales más simples del balance energético a largo plazo y de la salud general. La tendencia a lo largo de las semanas importa mucho más que cualquier lectura individual, que varía con la hidratación, la comida y la hora del día.",
- "bmiTitle": "Índice de masa corporal (IMC)",
"bmiBody": "El IMC es tu peso dividido por tu altura al cuadrado, un único número que te sitúa en una categoría amplia de peso. Es un cribado poblacional, no un diagnóstico, y no distingue el músculo de la grasa.",
- "sleepTitle": "Sueño",
"sleepBody": "La duración del sueño es cuánto dormiste realmente durante la noche, dividido en fases ligera, profunda y REM. La mayoría de los adultos necesita entre siete y nueve horas; tanto la cantidad como la calidad constantes favorecen la recuperación, el ánimo y el metabolismo.",
- "moodTitle": "Estado de ánimo",
"moodBody": "El registro del estado de ánimo anota cómo te sientes con el tiempo en una escala sencilla. Anotarlo con regularidad revela patrones —cómo el sueño, la actividad o la medicación se relacionan con cómo te sientes— que un solo día nunca muestra.",
- "medicationsTitle": "Adherencia a la medicación",
"medicationsBody": "La adherencia es la fiabilidad con la que tomas tu medicación según lo prescrito, aquí como la proporción de dosis previstas que registraste. Una toma constante suele ser lo que hace que un tratamiento funcione como se pretende.",
- "restingHrTitle": "Frecuencia cardíaca en reposo",
"restingHrBody": "La frecuencia cardíaca en reposo es tu ritmo cardíaco cuando estás completamente en reposo, normalmente medido durante la noche. Una frecuencia más baja suele indicar mejor forma cardiovascular, y una tendencia al alza puede ser una señal temprana de estrés o enfermedad.",
- "hrvTitle": "Variabilidad de la frecuencia cardíaca (VFC)",
"hrvBody": "La VFC es la pequeña variación en el tiempo entre latidos consecutivos. Una mayor variabilidad suele reflejar un sistema nervioso bien recuperado y adaptable, mientras que una caída sostenida puede señalar fatiga, estrés o recuperación insuficiente.",
- "oxygenSaturationTitle": "Saturación de oxígeno (SpO₂)",
"oxygenSaturationBody": "La SpO₂ es el porcentaje de glóbulos rojos que transportan oxígeno. Los valores de 95 a 100 % son normales en reposo; los valores que se mantienen por debajo del 90 % merecen atención, aunque las bajadas breves nocturnas son frecuentes.",
- "bodyTemperatureTitle": "Temperatura corporal",
"bodyTemperatureBody": "La temperatura corporal refleja tu estado térmico central, normalmente en torno a 37 °C con pequeñas variaciones diarias. Un aumento sostenido puede indicar una infección, mientras que las lecturas nocturnas en la muñeca reflejan sobre todo cambios relativos, no un valor clínico.",
- "activeEnergyTitle": "Energía activa",
"activeEnergyBody": "La energía activa son las calorías que quemas con el movimiento, además del consumo basal en reposo. Es una medida directa de lo físicamente activo que fue tu día.",
- "workoutsTitle": "Entrenamientos",
"workoutsBody": "Los entrenamientos son las sesiones de ejercicio estructurado que registran tus dispositivos, con tipo, duración y energía. Registrarlos muestra cómo el entrenamiento regular se acumula a lo largo de las semanas y cómo se relaciona con tus otras señales de salud.",
- "respiratoryRateTitle": "Frecuencia respiratoria",
"respiratoryRateBody": "La frecuencia respiratoria es el número de respiraciones que haces por minuto. En reposo, la mayoría de los adultos respiran entre doce y veinte veces por minuto; un aumento sostenido puede acompañar a una enfermedad, el esfuerzo o el estrés.",
- "bodyWaterTitle": "Agua corporal total",
"bodyWaterBody": "El agua corporal total es la parte de tu peso compuesta por líquido, dentro y alrededor de las células. Varía con la hidratación, la comida y la actividad, así que la tendencia a lo largo de semanas dice más que una sola medición.",
- "boneMassTitle": "Masa ósea",
"boneMassBody": "La masa ósea es el peso estimado del mineral de tu esqueleto. Las estimaciones de la báscula son aproximadas, pero una tendencia estable o al alza favorece la fortaleza ósea a largo plazo.",
- "fatFreeMassTitle": "Masa libre de grasa",
"fatFreeMassBody": "La masa libre de grasa es todo lo que en tu cuerpo no es grasa: músculo, hueso, órganos y agua. Mantenerla o aumentarla mientras pierdes peso suele indicar una composición corporal más sana.",
- "fatMassTitle": "Masa grasa",
"fatMassBody": "La masa grasa es el peso total de la grasa de tu cuerpo. Algo de grasa es esencial para las hormonas y la reserva de energía, pero un exceso —sobre todo en el abdomen— eleva el riesgo para la salud a largo plazo (WHO 2024).",
- "muscleMassTitle": "Masa muscular",
"muscleMassBody": "La masa muscular es el peso estimado del músculo de tu cuerpo. Más músculo favorece la fuerza, el metabolismo y la movilidad, y suele requerir esfuerzo activo para conservarse con la edad.",
- "visceralFatTitle": "Grasa visceral",
"visceralFatBody": "La grasa visceral es la grasa almacenada en lo profundo del abdomen, alrededor de los órganos. A diferencia de la grasa bajo la piel, un nivel alto se asocia estrechamente con enfermedad cardíaca y diabetes tipo 2 (AHA, Circulation 2021), por lo que cuanto más baja, mejor. La escala muestra una puntuación sin unidades (aproximadamente 1–12 es el rango saludable, 13 o más se considera elevado); es una escala distinta de los umbrales en cm² por imagen que usan los profesionales clínicos y no es intercambiable con ellos.",
- "leanBodyMassTitle": "Masa magra corporal",
"leanBodyMassBody": "La masa magra corporal es tu peso total menos la grasa: músculo, hueso, órganos y agua en conjunto. Seguirla junto al peso muestra si los cambios vienen del músculo o de la grasa.",
- "flightsClimbedTitle": "Pisos subidos",
"flightsClimbedBody": "Los pisos subidos cuentan las plantas que subes cada día, con un piso equivalente a unos tres metros de altura. Es un indicador sencillo de la actividad cotidiana y el esfuerzo del tren inferior.",
- "walkingDistanceTitle": "Distancia a pie y corriendo",
"walkingDistanceBody": "La distancia a pie y corriendo es el terreno que recorres caminando cada día. Complementa el recuento de pasos al reflejar cuánto te moviste, no solo con qué frecuencia diste pasos.",
- "walkingSteadinessTitle": "Estabilidad al caminar",
"walkingSteadinessBody": "La estabilidad al caminar estima cuán estable y equilibrada es tu marcha, clasificada de correcta a baja. Un nivel bajo indica mayor probabilidad de caída y conviene comentarlo con un profesional.",
- "walkingHeartRateTitle": "Frecuencia cardíaca al caminar",
"walkingHeartRateBody": "La frecuencia cardíaca al caminar es tu pulso medio durante una caminata constante. Un valor más bajo con el tiempo suele reflejar una mejor forma cardiovascular para el mismo esfuerzo.",
- "walkingAsymmetryTitle": "Asimetría al caminar",
"walkingAsymmetryBody": "La asimetría al caminar es la proporción de tiempo en que tus pasos son desiguales entre la pierna izquierda y la derecha. Un porcentaje más alto puede indicar dolor, lesión o un problema de marcha que conviene revisar.",
- "doubleSupportTimeTitle": "Tiempo de doble apoyo",
"doubleSupportTimeBody": "El tiempo de doble apoyo es la proporción de cada ciclo de marcha en que ambos pies tocan el suelo a la vez. Un porcentaje más alto suele indicar una marcha más cautelosa y menos estable.",
- "stepLengthTitle": "Longitud de zancada",
"stepLengthBody": "La longitud de zancada es la distancia media de una pisada a la siguiente. Una zancada más corta con el tiempo puede reflejar fatiga, cautela o un cambio en la movilidad.",
- "walkingSpeedTitle": "Velocidad al caminar",
"walkingSpeedBody": "La velocidad al caminar es lo rápido que te mueves en terreno llano. Es un marcador bien estudiado de la salud general y la movilidad, y un ritmo estable o al alza es buena señal.",
- "pulseWaveVelocityTitle": "Velocidad de la onda de pulso",
"pulseWaveVelocityBody": "La velocidad de la onda de pulso mide la rapidez con que la onda de presión de cada latido viaja por tus arterias. Una onda más rápida indica arterias más rígidas, lo que se asocia a mayor riesgo cardiovascular.",
- "vascularAgeTitle": "Edad vascular",
"vascularAgeBody": "La edad vascular estima qué edad aparentan tus arterias en comparación con tu edad real, según la rigidez arterial. Una edad vascular por debajo de la real sugiere vasos sanguíneos más sanos.",
- "environmentalAudioTitle": "Niveles de sonido ambiental",
"environmentalAudioBody": "Los niveles de sonido ambiental registran el volumen del entorno en decibelios. La exposición prolongada por encima de unos 70 dB puede dañar la audición con el tiempo, así que los promedios diarios más bajos son más seguros.",
- "headphoneAudioTitle": "Niveles de audio con auriculares",
"headphoneAudioBody": "Los niveles de audio con auriculares registran en decibelios el volumen al que escuchas con auriculares. Bajar el volumen y hacer pausas protege tu audición a largo plazo.",
- "audioEventsTitle": "Eventos de sonido fuerte",
"audioEventsBody": "Los eventos de sonido fuerte señalan momentos en que el ruido a tu alrededor alcanzó un nivel que puede dañar la audición con suficiente exposición. Los eventos frecuentes son una señal para buscar un entorno más silencioso o protección auditiva.",
- "daylightTitle": "Tiempo a la luz del día",
"daylightBody": "El tiempo a la luz del día es cuánto pasas bajo luz exterior cada día. La luz diurna regular favorece un ritmo sueño-vigilia y un estado de ánimo saludables (CDC 2024), y el tiempo al aire libre se asocia con la salud ocular.",
- "bloodGlucoseTitle": "Glucosa en sangre",
"bloodGlucoseBody": "La glucosa en sangre es la cantidad de azúcar en tu sangre, la principal fuente de energía del cuerpo. Sube tras comer y se estabiliza entre comidas; valores que se mantienen altos con el tiempo pueden indicar diabetes o prediabetes.",
- "skinTemperatureTitle": "Temperatura de la piel",
"skinTemperatureBody": "La temperatura de la piel aquí es el pequeño cambio nocturno de la temperatura de tu muñeca respecto a tu propio valor de referencia, no una medición absoluta de fiebre. Las variaciones pueden seguir el sueño, el entorno, una enfermedad o la fase del ciclo menstrual.",
- "cardioFitnessTitle": "Condición cardiovascular (VO₂ máx)",
"cardioFitnessBody": "El VO₂ máx es la cantidad de oxígeno que tu cuerpo puede usar al máximo esfuerzo: el mejor índice de laboratorio de la forma aeróbica, que los wearables estiman de forma indirecta. Como orientación general, la forma física es un predictor gradual de supervivencia: cada MET adicional (unos 3,5 ml/kg/min) se asocia a una mortalidad entre un 10 % y un 25 % menor (AHA, Circulation 2016). Una tendencia más alta o en aumento es una fuerte señal de salud cardiovascular.",
- "cardioRecoveryTitle": "Recuperación cardiaca",
"cardioRecoveryBody": "La recuperación cardiaca es cuántas pulsaciones por minuto baja tu frecuencia cardiaca en el primer minuto tras parar un ejercicio intenso. Una caída mayor significa que tu corazón se calma más rápido, señal de buena forma física.",
- "wristTemperatureTitle": "Temperatura de muñeca",
"wristTemperatureBody": "La temperatura de muñeca se mide mientras duermes y se indica como la diferencia respecto a tu valor de referencia, no como un valor absoluto. Un valor estable es normal, mientras que una subida sostenida puede indicar enfermedad, entrenamiento intenso o un cambio del ciclo.",
- "fallsTitle": "Caídas",
"fallsBody": "Esto cuenta las caídas fuertes que detecta tu wearable. El objetivo es cero; un número creciente es una señal de riesgo de movilidad que conviene comentar con un profesional, sobre todo en personas mayores (CDC STEADI 2024).",
- "sixMinuteWalkTitle": "Distancia en marcha de 6 minutos",
"sixMinuteWalkBody": "Es una estimación de cuánto podrías caminar en seis minutos, una prueba clínica clásica de movilidad y resistencia cardiopulmonar. El valor del wearable es una aproximación, no la prueba clínica supervisada; lo útil es la tendencia estable o en aumento.",
- "stairAscentSpeedTitle": "Velocidad subiendo escaleras",
"stairAscentSpeedBody": "Es lo rápido que subes un tramo de escaleras. Es un marcador sensible de fuerza de piernas y movilidad: un ritmo estable o en aumento es buena señal y uno que baja conviene vigilar.",
- "stairDescentSpeedTitle": "Velocidad bajando escaleras",
"stairDescentSpeedBody": "Es lo rápido que bajas un tramo de escaleras. Junto con la velocidad de subida refleja el control de piernas y el equilibrio: un ritmo estable es buena señal y uno que baja conviene vigilar.",
- "breathingDisturbancesTitle": "Alteraciones respiratorias del sueño",
"breathingDisturbancesBody": "Un wearable puede estimar con qué frecuencia se interrumpió tu respiración mientras dormías. Un índice más bajo es mejor; un nivel marcado como elevado, sobre todo durante muchas noches, conviene comentarlo con un profesional. Es una aproximación de cribado, no un diagnóstico de apnea del sueño.",
- "stepsTitle": "Pasos",
"stepsBody": "El recuento de pasos es el número total de pasos que das cada día. Es la medida más sencilla del movimiento cotidiano: un recuento diario más alto suele reflejar un estilo de vida más activo, y un hábito constante importa más que un único día destacado.",
"readinessBody": "La preparación es una lectura diaria de lo listo que está tu cuerpo para afrontar el día, combinando la frecuencia cardíaca en reposo, la variabilidad de la frecuencia cardíaca, el sueño y la regularidad de tus últimas noches. Una puntuación más alta indica que tu organismo se ve recuperado y con recursos; una más baja es un aviso suave para bajar el ritmo.",
"sleepScoreBody": "Tu puntuación de sueño resume toda una noche en un solo número — duración, eficiencia, regularidad y el equilibrio entre sueño ligero, profundo y REM. Fíjate en el patrón a lo largo de las semanas en lugar de perseguir una puntuación perfecta una sola noche.",
"recoveryBody": "La recuperación refleja lo bien que tu cuerpo se ha repuesto, a partir de señales nocturnas como la variabilidad de la frecuencia cardíaca y la frecuencia en reposo. Una puntuación alta es luz verde para exigirte; una más baja sugiere que tu organismo aún se está reconstruyendo.",
"strainBody": "El esfuerzo capta cuánta carga cardiovascular y física le has impuesto a tu cuerpo, desde los entrenamientos hasta un día activo de pie. Cuando ya hay suficiente historial, se valora frente a tu propio esfuerzo habitual, no frente a un estándar ajeno.",
"stressBody": "El estrés aquí es una estimación de la carga fisiológica sobre tu sistema nervioso, leída sobre todo a partir de patrones de variabilidad de la frecuencia cardíaca y no de cómo te sientes conscientemente. Una puntuación más baja apunta a un estado calmado y bien regulado; una más alta es una invitación a incorporar recuperación.",
- "recoveryPageTitle": "Recuperación",
"recoveryPageBody": "La recuperación reúne las señales nocturnas que registra tu wearable —recarga, esfuerzo, carga y los totales de frecuencia cardiaca y energía del día— para mostrar cómo se ha recuperado tu cuerpo. Fíjate en el patrón a lo largo de los días más que en un solo número; un valor más bajo es una invitación a bajar el ritmo, no una alarma.",
- "pulsePressureTitle": "Presión de pulso",
"pulsePressureBody": "La presión de pulso es la diferencia entre tu valor sistólico y diastólico (sistólico menos diastólico). Como orientación general, unos 40 mmHg es lo típico en reposo; una diferencia amplia por encima de unos 60 mmHg en la mediana edad puede reflejar arterias más rígidas, mientras que una diferencia estrecha por debajo de unos 25 mmHg puede acompañar a un bajo volumen sistólico (StatPearls/NIH 2023). Es un motivo de conversación, no un diagnóstico.",
- "meanArterialPressureTitle": "Presión arterial media",
"meanArterialPressureBody": "La presión arterial media es la presión promedio que ven tus órganos a lo largo de un latido, aproximadamente la diastólica más un tercio de la diferencia con la sistólica. Como orientación general, un valor en reposo ronda los 70–100 mmHg (StatPearls/NIH 2023). El umbral de ~60 mmHg que se suele citar es un concepto de cuidados agudos, no una alarma para el autocontrol.",
"cycleEducationBody": "Tu ciclo atraviesa cuatro fases impulsadas por hormonas, y su duración y regularidad son una ventana accesible a la salud general. Como orientación general, los ciclos en personas adultas suelen durar 21–35 días (las referencias clínicas citan 24–38), con un sangrado de unos cinco días (ACOG 2015). La fase lútea es el ancla estable, así que la ovulación se estima mejor contando hacia atrás desde tu próxima regla prevista, nunca con un «día 14» fijo. Una ventana fértil predicha es una estimación que necesita varios ciclos registrados y sigue siendo aproximada, y la temperatura basal confirma la ovulación a posteriori en lugar de predecirla. No es un método anticonceptivo.",
- "sleepHygieneBody": "Algunos hábitos con respaldo de evidencia favorecen el sueño: mantener horarios constantes de acostarse y levantarse incluso los fines de semana, recibir luz brillante por la mañana y atenuar las noches, una habitación fresca, oscura y silenciosa, y evitar la cafeína por la tarde, el alcohol tardío y las comidas copiosas a última hora (CDC 2024). La regularidad predice la salud al menos tanto como la duración, y el tiempo total de sueño de un wearable es mucho más fiable que su reparto entre sueño profundo, ligero y REM.",
- "referenceGuidance": "Orientación general, no consejo médico. Los rangos de referencia son valores poblacionales que cambian con tu edad, sexo y condiciones de salud; tu propia tendencia a lo largo del tiempo guía la lectura."
+ "sleepHygieneBody": "Algunos hábitos con respaldo de evidencia favorecen el sueño: mantener horarios constantes de acostarse y levantarse incluso los fines de semana, recibir luz brillante por la mañana y atenuar las noches, una habitación fresca, oscura y silenciosa, y evitar la cafeína por la tarde, el alcohol tardío y las comidas copiosas a última hora (CDC 2024). La regularidad predice la salud al menos tanto como la duración, y el tiempo total de sueño de un wearable es mucho más fiable que su reparto entre sueño profundo, ligero y REM."
},
"bpDerived": {
"pulsePressure": "La presión de pulso (la diferencia entre tus valores) es de unos {value} mmHg; alrededor de 40 es lo típico en reposo.",
@@ -2918,16 +2820,6 @@
"compositionTitle": "Distribución de fases",
"compositionSubtitle": "Últimas {nights} noches",
"compositionAriaLabel": "Distribución de fases del sueño en {nights} noches",
- "hypnogram": {
- "title": "Última noche",
- "subtitle": "{asleep} dormido",
- "ariaLabel": "Progresión de las fases del sueño de la última noche: {asleep} dormido, {awakenings} despertares",
- "asleepTotal": "Dormido {value}",
- "inBedTotal": "En la cama {value}",
- "awakenings": "{count} despertares",
- "source": "Fuente: {source}",
- "estimateNote": "Los tiempos de cada fase son estimados: esta fuente informa totales por fase, no una cronología medida."
- },
"sourceDiscrepancy": {
"tooltipTitle": "Las fuentes difieren"
},
@@ -2982,7 +2874,6 @@
"loadError": "Tu ritmo de sueño no pudo cargarse ahora mismo. Reaparecerá en cuanto se estabilice la conexión."
}
},
- "correlations": "Correlaciones",
"medicationCompliance": "Cumplimiento de medicación",
"dayStreak": "{count} días de racha",
"dayStreakOne": "{count} día de racha",
@@ -2991,19 +2882,12 @@
"taken": "Tomada",
"skipped": "Omitida",
"missed": "Perdida",
- "generalStatusBadge": {
- "good": "Bien",
- "watch": "Observar",
- "critical": "Crítico"
- },
"assessmentTitle": "Valoración",
"restMode": {
"title": "Modo descanso",
"body": "Hay un episodio de enfermedad o afección activo, por lo que esto refleja un periodo de recuperación. Tu puntuación no baja por ello.",
"bodySince": "Hay un episodio de enfermedad o afección activo desde el {since}, por lo que esto refleja un periodo de recuperación. Tu puntuación no baja por ello."
},
- "assessmentShowMore": "Mostrar más",
- "assessmentShowLess": "Mostrar menos",
"bloodPressureSectionTitle": "Presión arterial",
"weightSectionTitle": "Peso",
"pulseSectionTitle": "Pulso",
@@ -3011,8 +2895,6 @@
"complianceNoData": "Sin datos de cumplimiento disponibles",
"generate": "Generar análisis nuevo",
"moodSectionTitle": "Estado de ánimo",
- "confidence": "Confianza",
- "cached": "En caché",
"emptyState": {
"bloodPressure": {
"title": "Aún no hay registros de presión arterial",
@@ -3061,11 +2943,6 @@
"title": "Aún no hay entrenamientos",
"description": "Abre la app Apple Salud en el iPhone para sincronizar tus entrenamientos. La importación de actividad de Withings también incorpora caminatas y rutas en bici."
},
- "row": {
- "duration": "Duración",
- "distance": "Distancia",
- "energy": "Energía activa"
- },
"detail": {
"backToList": "Volver a entrenamientos",
"statsTitle": "Estadísticas",
@@ -3078,13 +2955,9 @@
"statsStepCount": "Pasos",
"statsElevation": "Desnivel",
"statsPace": "Ritmo",
- "statsSource": "Fuente",
"routeTitle": "Ruta",
- "routeUnavailable": "No se registró una ruta GPS para este entrenamiento.",
"hrChartTitle": "Frecuencia cardíaca",
- "hrChartUnavailable": "Las muestras de frecuencia cardíaca por segundo no se almacenan para este entrenamiento.",
"notFound": "Entrenamiento no encontrado.",
- "loading": "Cargando entrenamiento…",
"hrFromPulse": "A partir de tus datos de frecuencia cardíaca en torno a esta sesión",
"hrAxisMinutes": "min",
"sportAverageLabel": "Tu promedio reciente:",
@@ -3556,7 +3429,6 @@
"title": "Lo que destaca",
"subheading": "Relaciones descubiertas",
"footer": "Extraídas de {tested} pares de un día a otro probados en todas tus métricas, tras la corrección de falsos descubrimientos.",
- "empty": "Todavía ninguna relación de ánimo superó el umbral estadístico. Sigue registrando.",
"disclaimer": "Filtrado estadísticamente, pero aún observacional: un patrón que vale la pena observar, no una causa.",
"moodUp": "Un mayor {factor} suele coincidir con mejor ánimo al día siguiente en tus datos.",
"moodDown": "Un mayor {factor} suele coincidir con peor ánimo al día siguiente en tus datos.",
@@ -3935,8 +3807,6 @@
},
"briefing": {
"memory": {
- "recall": "Antes, {text}",
- "forward": "Mirando hacia adelante: {text}",
"forwardWatch": "Conviene vigilar {metric}.",
"forwardHolding": "Por ahora todo se mantiene estable."
}
@@ -3950,9 +3820,7 @@
}
},
"streak": {
- "returnLabel": "Tu {metric} ha vuelto a tu rango habitual tras {count} días — fuera lo que fuera, ya pasó.",
- "daysInside": "{count} días dentro",
- "daysOutside": "{count} días fuera"
+ "returnLabel": "Tu {metric} ha vuelto a tu rango habitual tras {count} días — fuera lo que fuera, ya pasó."
},
"updatedTodayAt": "Actualizado hoy, {time}",
"updatedYesterday": "Actualizado ayer",
@@ -4835,17 +4703,10 @@
"passwordMismatch": "Las nuevas contraseñas no coinciden",
"passwordUpdated": "Contraseña actualizada correctamente",
"passkeys": "Passkeys",
- "registeredPasskeys": "Claves de acceso registradas",
"passkeysDescription": "Las claves de acceso permiten iniciar sesión de forma segura sin contraseña.",
"passkeyName": "Nombre",
- "passkeyDevice": "Dispositivo",
- "passkeyBackup": "Backup",
- "passkeyCreated": "Creada",
- "passkeyActions": "Acciones",
"noPasskeys": "No hay claves de acceso registradas.",
"addPasskey": "Añadir clave de acceso",
- "passkeyAdded": "¡Clave de acceso añadida correctamente!",
- "passkeyOptionsError": "No se pudieron cargar las opciones de la clave de acceso",
"passkeyRegistrationFailed": "Falló el registro de la clave de acceso",
"passkeyRegistrationCancelled": "Registro de la clave de acceso cancelado",
"passkeyNotSupported": "Tu navegador o dispositivo aún no admite claves de acceso. Prueba con otro navegador o dispositivo.",
@@ -4894,7 +4755,6 @@
"webPushSubscribed": "¡Notificaciones push activadas!",
"webPushUnsubscribed": "Notificaciones push desactivadas.",
"webPushSubscribeFailed": "Falló la activación",
- "webPushActive": "Activo",
"kiInsightsDescription": "Opcional: guarda tu clave de OpenAI para análisis diarios y automáticos en Insights.",
"codexConnected": "¡ChatGPT conectado correctamente! Insights ya está activo.",
"codexDisconnected": "Conexión con ChatGPT eliminada.",
@@ -4932,7 +4792,6 @@
"savedShort": "(guardada)",
"saveCta": "Guardar",
"saved": "Guardado",
- "saveFailed": "Falló el guardado",
"errorGeneric": "Error",
"providerChain": {
"types": {
@@ -5068,11 +4927,9 @@
"hint": "Se envía al Coach y al informe diario como contexto adicional. Déjalo vacío para no enviar nada.",
"profileHint": "La edad y el género provienen de tu perfil y se incluyen automáticamente.",
"freeTextLabel": "Sobre mí (texto libre)",
- "conditionsLabel": "Enfermedades crónicas",
"conditionsPlaceholder": "P. ej. hipertensión, diabetes tipo 2…",
"allergiesLabel": "Alergias e intolerancias",
"allergiesPlaceholder": "P. ej. intolerancia a la lactosa, alergia a la penicilina…",
- "focusLabel": "¿En qué debería fijarse el Coach?",
"focusPlaceholder": "P. ej. vigila mi tensión arterial, motívame a moverme más…",
"completeness": "{answered} de {total} respondidas",
"questionsTitle": "El Coach tiene preguntas",
@@ -5150,8 +5007,6 @@
"token": "El intercambio de tokens con Withings falló. Comprueba el Client ID y el Client Secret e inténtalo de nuevo.",
"generic": "Error desconocido al conectar. Inicia la conexión de nuevo."
},
- "integrationsSourcesHint": "Si varias fuentes aportan la misma métrica, la prioridad de fuentes decide qué valor cuenta.",
- "integrationsSourcesHintLink": "Abrir prioridad de fuentes",
"withingsSynchronize": "Sincronizar",
"withingsDisconnect": "Desconectar",
"withingsDisconnectTitle": "¿Desconectar Withings?",
@@ -5342,10 +5197,7 @@
"apiEndpointPath": "Ruta",
"apiEndpointAuth": "Autenticación",
"apiEndpointExample": "Ejemplo de cuerpo",
- "collapse": "Contraer",
- "expand": "Expandir",
"dangerZone": "Eliminar todos los datos",
- "dangerZoneTitle": "Zona de peligro",
"dangerZoneDescription": "Elimina todos tus datos de salud e integraciones. Tu cuenta de usuario se conservará.",
"dangerZoneConfirm": "¿Eliminar todos los datos?",
"dangerZoneConfirmDescription": "Esto eliminará de forma permanente todos tus datos de salud e integraciones. Tu cuenta de usuario se conservará.",
@@ -5365,7 +5217,6 @@
"about": {
"version": "Versión de la app",
"gitSha": "Build",
- "builtAt": "Compilado {time}",
"builtAtLabel": "Compilado",
"license": "Licencia",
"repository": "Código fuente",
@@ -5483,8 +5334,6 @@
"resourceTypes": "Tipos de recurso FHIR",
"create": "Crear enlace",
"tokenCreated": "Enlace para compartir creado",
- "tokenOnce": "Copia este enlace ahora — el token se muestra una sola vez y no se puede recuperar.",
- "copy": "Copiar enlace",
"copied": "Copiado al portapapeles.",
"activeTitle": "Enlaces activos",
"noActive": "No hay enlaces para compartir activos.",
@@ -5498,7 +5347,6 @@
"statusExpired": "Caducado",
"inactiveTitle": "Revocados y caducados ({count})",
"shownOnce": "Se muestra una sola vez. Guarde ahora el código QR y la frase de acceso — no se pueden recuperar después.",
- "qrLabel": "Código QR",
"qrAlt": "Código QR con el enlace para compartir y la frase de acceso",
"linkLabel": "Enlace",
"passphraseLabel": "Frase de acceso",
@@ -5557,7 +5405,6 @@
"polar": "Polar",
"polarDescription": "Conecta tu cuenta de Polar para sincronizar la recuperación Nightly Recharge, fases de sueño, actividad diaria y sesiones de entrenamiento.",
"polarViewData": "Ver tu sueño y recuperación",
- "polarHelp": "Inicia sesión en Polar para conceder acceso. Los datos se obtienen en segundo plano una vez por hora.",
"polarCredentials": "Credenciales de API",
"polarClientId": "Client ID",
"polarClientSecret": "Client Secret",
@@ -5587,7 +5434,6 @@
"oura": "Oura",
"ouraDescription": "Conecta tu cuenta de Oura para sincronizar disposición, fases de sueño, variabilidad cardíaca y actividad diaria.",
"ouraViewData": "Ver tu sueño y recuperación",
- "ouraHelp": "Inicia sesión en Oura para conceder acceso. Los datos se obtienen en segundo plano una vez por hora.",
"ouraCredentials": "Credenciales de API",
"ouraClientId": "Client ID",
"ouraClientSecret": "Client Secret",
@@ -7478,7 +7324,6 @@
"measurementReminders": {
"sectionTitle": "Revisiones",
"sectionDescription": "Recordatorios de qué medir o revisar, cuándo y dónde.",
- "addButton": "Añadir recordatorio",
"markDone": "Hecho",
"delete": "Eliminar",
"deleteConfirmTitle": "¿Eliminar recordatorio?",
diff --git a/messages/fr.json b/messages/fr.json
index fca3bdb2c..73294c491 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -905,7 +905,6 @@
"emptyDescription": "Crée des médicaments, des doses et des fenêtres horaires pour suivre l’observance.",
"loadFailed": "Impossible de charger les médicaments",
"retryLoad": "Réessayer",
- "name": "Nom",
"inactive": "En pause",
"intakeHistory": "Historique des prises",
"openDetailPage": "Ouvrir la page de détail du médicament",
@@ -1208,8 +1207,6 @@
"dosesPerUnitLabel": "Doses par unité",
"dosesPerUnitPlaceholder": "p. ex. 4",
"dosesPerUnitHint": "Doses par stylo / flacon — laisser vide pour ne pas suivre le stock.",
- "injectionSiteToggle": "Rotation des sites d'injection",
- "injectionSiteHint": "Guide de rotation facultatif. Le site de chaque dose est enregistré lors de la saisie de la prise.",
"short": "Dose",
"unitsPerDoseLabel": "Unités par dose",
"unitsPerDoseHint": "p. ex. 2 comprimés de 2 mg pour une dose de 4 mg, ou ½ comprimé pour une dose fractionnée"
@@ -1260,7 +1257,6 @@
"remindersDescription": "Push, Telegram ou ntfy — selon vos réglages.",
"noChannelHint": "Les rappels nécessitent un canal de notification — aucun n'est actif pour le moment.",
"noChannelHintLink": "Configurer les notifications",
- "multiScheduleNote": "Ce médicament a plusieurs horaires. L’éditeur détaillé reviendra dans une version ultérieure.",
"short": "Fini"
}
},
@@ -2128,7 +2124,6 @@
"emptyDescription": "Les insights apparaissent dès que tu auras saisi quelques mesures. Commence par le poids ou la tension.",
"emptyAddMeasurement": "Saisir une mesure",
"editMode": {
- "open": "Personnaliser",
"done": "Terminé",
"reset": "Réinitialiser",
"title": "Personnaliser l’aperçu",
@@ -2153,16 +2148,6 @@
"bmiEmptyAction": "Ouvrir les paramètres du compte",
"noProviderConfigured": "Connecte un fournisseur d'IA pour voir ici une évaluation de tes mesures.",
"noProviderAction": "Ouvrir les réglages IA",
- "startAnalysis": "Lancer l’évaluation",
- "refreshAnalysis": "Actualiser l’évaluation",
- "keyTakeaway": "L’essentiel",
- "findingsTitle": "Constats",
- "heroFindingPositive": "Constat principal",
- "heroFindingNeutral": "Constat principal",
- "heroFindingAttention": "À surveiller",
- "heroFindingWarning": "Alerte principale",
- "correlationsTitle": "Corrélations",
- "recommendationsTitle": "Recommandations",
"personalRecord": {
"badge": "PR",
"tooltip": "Record personnel : {value} {unit} le {date}"
@@ -2170,53 +2155,33 @@
"recommendation": {
"source": "Source",
"viewSource": "Ouvrir la recommandation dans un nouvel onglet",
- "rationale": "Pourquoi cette recommandation",
"rationaleWindow": "Fenêtre",
"rationaleComparedTo": "Comparé à",
"rationaleDeviation": "Écart",
"rationaleExpand": "Afficher la justification",
"rationaleCollapse": "Masquer la justification",
- "legacyPayloadCta": "Insights mis à jour — regénère pour activer les nouvelles explications",
"feedbackHelpful": "Utile",
"feedbackNotHelpful": "Peu utile",
"feedbackThanks": "Merci pour ton retour",
- "feedbackConfirmed": "Retour enregistré",
"feedbackAlreadyRated": "Déjà noté",
"feedbackError": "Impossible d’enregistrer votre retour. Réessayez.",
- "confidence": "Confiance",
"confidenceAria": "Confiance : {value} sur 100",
- "confidenceHigh": "Confiance élevée",
- "confidenceMedium": "Confiance moyenne",
"confidenceLow": "Confiance faible — peu de points de données",
"confidenceDraft": "Brouillon"
},
- "dataFoundation": "Base de données",
- "dataGaps": "Lacunes :",
- "lastUpdated": "Dernière mise à jour",
- "confidenceHoch": "Élevée",
- "confidenceMittel": "Moyenne",
- "confidenceGering": "Faible",
"loadError": "Impossible de charger les analyses",
"heroPersonalBaseline": "Basé sur tes 90 derniers jours",
- "heroRegenerate": "Régénérer",
"heroRegenerating": "Génération en cours",
"regenerateAnalysis": "Relancer l’évaluation",
"regenerateSuccess": "Évaluation regénérée",
- "warmStarted": "Les évaluations sont en cours de préparation en arrière-plan",
"narrativeTitle": "Rétrospective",
"narrativeWeek": "Cette semaine",
"narrativeMonth": "Ce mois-ci",
- "narrativePreparing": "Préparation de votre résumé…",
"narrativeUpdating": "Mise à jour…",
- "narrativeProvenanceLabel": "Comment cela a été établi",
- "narrativeProvenanceMethod": "Un résumé de la période en langage clair, établi uniquement à partir de vos valeurs enregistrées et des associations ayant survécu au contrôle statistique des comparaisons multiples. Descriptif, jamais une cause.",
- "narrativeProvenanceMetrics": "Basé sur : {metrics}",
- "narrativeProvenanceWindow": "Fenêtre : du {from} au {to}",
"heroFallbackSubtitle": "Un coup d’œil quotidien sur tes tendances — directement issu des chiffres que tu saisis.",
"heroGreetingMorning": "Bonjour",
"heroGreetingAfternoon": "Bonjour",
"heroGreetingEvening": "Bonsoir",
- "heroComingSoonTooltip": "Bientôt disponible",
"heroActionAskCoach": "Demander au coach",
"healthScore": {
"label": "Score de santé",
@@ -2229,14 +2194,12 @@
"componentMood": "Humeur",
"componentCompliance": "Observance",
"deltaExplainer": {
- "trigger": "Que signifie cet écart ?",
"title": "Comment l'écart est calculé",
"description": "Écart par rapport à la semaine dernière : {delta}",
"body": "Le score combine TA, poids, humeur et observance. Nous comparons le score d'aujourd'hui à celui de la semaine précédente à la même période. Enregistre une nouvelle mesure ou une note d'humeur pour le faire remonter."
},
"provenance": {
"toggle": "Composé de",
- "weightLabel": "part",
"mixedBanner": "Certains composants combinent plusieurs sources. La ventilation affiche la valeur la plus récente par composant.",
"footnote": "Les pondérations sont recalibrées en fonction des données disponibles.",
"asOfLabel": "au {date}",
@@ -2300,10 +2263,6 @@
"viewDetail": "Voir le détail de {metric}"
},
"vo2Max": {
- "statLatest": "Actuel",
- "statMin": "Min",
- "statMax": "Max",
- "empty": "Pas encore de données VO₂ max — synchronise Apple Health pour remplir cette vue.",
"cardioLinkTitle": "Condition cardio (VO₂ max)",
"cardioLinkBody": "Le VO₂ max est la mesure de référence de la condition aérobie. Le graphique complet et l’évaluation se trouvent sur leur propre page.",
"cardioLinkCta": "Ouvrir la condition cardio"
@@ -2768,125 +2727,68 @@
"gewichtDescription": "Évolution du poids face à la bande saine dérivée de ta taille, avec évaluation.",
"pulsDescription": "Resting pulse vs. the personalised Karvonen target band, plus a written assessment.",
"stimmungDescription": "Valeurs d’humeur quotidiennes dans le temps, avec une évaluation textuelle.",
- "stimmungEmptyTitle": "Saisir la première humeur",
- "stimmungEmptyDescription": "Les insights d’humeur apparaissent dès que tu auras saisi quelques entrées. Appuie ci-dessous pour démarrer.",
- "stimmungEmptyAction": "Ouvrir le journal d’humeur",
"medikamenteDescription": "Calendrier des prises par médicament, ainsi qu’une évaluation textuelle.",
- "medikamenteEmptyTitle": "Ajouter le premier médicament",
- "medikamenteEmptyDescription": "Les insights d’observance ont besoin d’au moins un médicament enregistré.",
- "medikamenteEmptyAction": "Ouvrir les médicaments",
"bmiDescription": "IMC dérivé de ton poids et de ta taille, avec évaluation.",
"target": {
"adjustLink": "Ajuster la plage cible"
},
"explainer": {
- "trigger": "Qu'est-ce que {title} ?",
- "bloodPressureTitle": "Tension artérielle",
"bloodPressureBody": "La tension artérielle est la force exercée par le sang sur les parois des artères, exprimée en systolique sur diastolique (le pic lors d'un battement sur la valeur de repos entre les battements). Des valeurs durablement élevées sollicitent le cœur et les vaisseaux à la longue.",
- "pulseTitle": "Pouls",
"pulseBody": "Le pouls correspond au nombre de battements du cœur par minute. Une mesure ponctuelle reflète ce que vous faisiez à l'instant : au repos, il est généralement de 60 à 100 battements par minute et augmente naturellement avec le mouvement, le stress ou la caféine.",
- "weightTitle": "Poids",
"weightBody": "Le poids corporel est l'un des signaux les plus simples de l'équilibre énergétique à long terme et de la santé globale. La tendance sur plusieurs semaines compte bien plus qu'une mesure isolée, qui varie avec l'hydratation, l'alimentation et l'heure de la journée.",
- "bmiTitle": "Indice de masse corporelle (IMC)",
"bmiBody": "L'IMC correspond à votre poids divisé par votre taille au carré, un seul chiffre qui vous situe dans une large catégorie de poids. C'est un dépistage de population, pas un diagnostic, et il ne distingue pas le muscle de la graisse.",
- "sleepTitle": "Sommeil",
"sleepBody": "La durée du sommeil est le temps réellement dormi au cours de la nuit, réparti entre sommeil léger, profond et paradoxal. La plupart des adultes ont besoin d'environ sept à neuf heures ; une quantité et une qualité régulières soutiennent la récupération, l'humeur et le métabolisme.",
- "moodTitle": "Humeur",
"moodBody": "Le suivi de l'humeur consigne ce que vous ressentez au fil du temps sur une échelle simple. Le noter régulièrement révèle des tendances — comment le sommeil, l'activité ou les médicaments s'alignent sur votre ressenti — qu'une seule journée ne montre jamais.",
- "medicationsTitle": "Observance du traitement",
"medicationsBody": "L'observance mesure la fiabilité avec laquelle vous prenez votre traitement comme prescrit, ici sous forme de la part des doses attendues que vous avez enregistrées. Une prise régulière est souvent ce qui permet à un traitement d'agir comme prévu.",
- "restingHrTitle": "Fréquence cardiaque au repos",
"restingHrBody": "La fréquence cardiaque au repos est votre rythme cardiaque lorsque vous êtes totalement au repos, généralement mesuré la nuit. Une fréquence plus basse indique souvent une meilleure forme cardiovasculaire, et une tendance à la hausse peut être un signe précoce de stress ou de maladie.",
- "hrvTitle": "Variabilité de la fréquence cardiaque (VFC)",
"hrvBody": "La VFC est la petite variation du temps entre des battements consécutifs. Une variabilité plus élevée reflète généralement un système nerveux bien récupéré et adaptable, tandis qu'une baisse durable peut signaler de la fatigue, du stress ou une récupération insuffisante.",
- "oxygenSaturationTitle": "Saturation en oxygène (SpO₂)",
"oxygenSaturationBody": "La SpO₂ est le pourcentage de globules rouges transportant de l'oxygène. Des valeurs de 95 à 100 % sont normales au repos ; des valeurs qui restent sous 90 % méritent attention, bien que de brèves baisses nocturnes soient fréquentes.",
- "bodyTemperatureTitle": "Température corporelle",
"bodyTemperatureBody": "La température corporelle reflète votre état thermique central, normalement autour de 37 °C avec de petites variations quotidiennes. Une hausse durable peut indiquer une infection, tandis que les mesures nocturnes au poignet suivent surtout des variations relatives plutôt qu'une valeur clinique.",
- "activeEnergyTitle": "Énergie active",
"activeEnergyBody": "L'énergie active correspond aux calories que vous brûlez par le mouvement, en plus de la dépense de base au repos. C'est une mesure directe du niveau d'activité physique de votre journée.",
- "workoutsTitle": "Entraînements",
"workoutsBody": "Les entraînements sont les séances d'exercice structurées que vos appareils enregistrent, avec le type, la durée et l'énergie. Les suivre montre comment un entraînement régulier s'accumule au fil des semaines et comment il se relie à vos autres signaux de santé.",
- "respiratoryRateTitle": "Fréquence respiratoire",
"respiratoryRateBody": "La fréquence respiratoire est le nombre de respirations que vous prenez par minute. Au repos, la plupart des adultes respirent environ douze à vingt fois par minute ; une hausse durable peut accompagner une maladie, un effort ou du stress.",
- "bodyWaterTitle": "Eau corporelle totale",
"bodyWaterBody": "L'eau corporelle totale est la part de votre poids constituée de liquide, à l'intérieur et autour des cellules. Elle varie avec l'hydratation, l'alimentation et l'activité, si bien que la tendance sur plusieurs semaines en dit plus qu'une seule mesure.",
- "boneMassTitle": "Masse osseuse",
"boneMassBody": "La masse osseuse est le poids estimé des minéraux de votre squelette. Les estimations sur balance sont approximatives, mais une tendance stable ou en hausse soutient la solidité osseuse à long terme.",
- "fatFreeMassTitle": "Masse non grasse",
"fatFreeMassBody": "La masse non grasse est tout ce qui, dans votre corps, n'est pas de la graisse : muscle, os, organes et eau. La maintenir ou la développer tout en perdant du poids signale en général une composition corporelle plus saine.",
- "fatMassTitle": "Masse grasse",
"fatMassBody": "La masse grasse est le poids total de la graisse de votre corps. Une part de graisse est indispensable aux hormones et au stockage d'énergie, mais un excès — surtout au niveau de l'abdomen — augmente le risque pour la santé à long terme (WHO 2024).",
- "muscleMassTitle": "Masse musculaire",
"muscleMassBody": "La masse musculaire est le poids estimé des muscles de votre corps. Plus de muscle soutient la force, le métabolisme et la mobilité, et demande généralement un effort actif pour être préservé avec l'âge.",
- "visceralFatTitle": "Graisse viscérale",
"visceralFatBody": "La graisse viscérale est la graisse stockée au plus profond de l'abdomen, autour des organes. Contrairement à la graisse sous la peau, un niveau élevé est étroitement lié aux maladies cardiaques et au diabète de type 2 (AHA, Circulation 2021) : plus elle est basse, mieux c'est. L'échelle indique une valeur sans unité (environ 1 à 12 correspond à la plage saine, 13 et plus étant élevé) ; il s'agit d'une échelle distincte des seuils en cm² obtenus par imagerie qu'utilisent les cliniciens, et elle n'est pas interchangeable avec eux.",
- "leanBodyMassTitle": "Masse maigre",
"leanBodyMassBody": "La masse maigre est votre poids total moins la graisse corporelle : muscle, os, organes et eau réunis. La suivre en parallèle du poids montre si les variations viennent du muscle ou de la graisse.",
- "flightsClimbedTitle": "Étages montés",
"flightsClimbedBody": "Les étages montés comptent les niveaux que vous gravissez chaque jour, un étage correspondant à environ trois mètres de dénivelé. C'est un indicateur simple de l'activité du quotidien et de l'effort des jambes.",
- "walkingDistanceTitle": "Distance à pied et en course",
"walkingDistanceBody": "La distance à pied et en course est le trajet que vous parcourez à pied chaque jour. Elle complète le nombre de pas en reflétant la distance parcourue, pas seulement la fréquence des pas.",
- "walkingSteadinessTitle": "Stabilité de la marche",
"walkingSteadinessBody": "La stabilité de la marche estime à quel point votre démarche est stable et équilibrée, classée d'acceptable à faible. Un niveau bas indique un risque de chute plus élevé et mérite d'être évoqué avec un professionnel de santé.",
- "walkingHeartRateTitle": "Fréquence cardiaque à la marche",
"walkingHeartRateBody": "La fréquence cardiaque à la marche est votre pouls moyen lors d'une marche régulière. Une valeur plus basse au fil du temps reflète souvent une meilleure forme cardiovasculaire pour le même effort.",
- "walkingAsymmetryTitle": "Asymétrie de la marche",
"walkingAsymmetryBody": "L'asymétrie de la marche est la part du temps où vos pas sont inégaux entre la jambe gauche et la droite. Un pourcentage plus élevé peut signaler une douleur, une blessure ou un trouble de la marche à examiner.",
- "doubleSupportTimeTitle": "Temps de double appui",
"doubleSupportTimeBody": "Le temps de double appui est la part de chaque cycle de marche où les deux pieds touchent le sol en même temps. Un pourcentage plus élevé traduit en général une démarche plus prudente et moins stable.",
- "stepLengthTitle": "Longueur de pas",
"stepLengthBody": "La longueur de pas est la distance moyenne d'une foulée à la suivante. Un pas plus court au fil du temps peut traduire de la fatigue, de la prudence ou un changement de mobilité.",
- "walkingSpeedTitle": "Vitesse de marche",
"walkingSpeedBody": "La vitesse de marche est la rapidité de votre déplacement sur terrain plat. C'est un marqueur bien étudié de la santé générale et de la mobilité, et une allure stable ou en hausse est bon signe.",
- "pulseWaveVelocityTitle": "Vitesse de l'onde de pouls",
"pulseWaveVelocityBody": "La vitesse de l'onde de pouls mesure la rapidité avec laquelle l'onde de pression de chaque battement parcourt vos artères. Une onde plus rapide indique des artères plus rigides, liées à un risque cardiovasculaire accru.",
- "vascularAgeTitle": "Âge vasculaire",
"vascularAgeBody": "L'âge vasculaire estime, d'après la rigidité artérielle, l'âge apparent de vos artères par rapport à votre âge réel. Un âge vasculaire inférieur à votre âge réel suggère des vaisseaux sanguins en meilleure santé.",
- "environmentalAudioTitle": "Niveaux sonores ambiants",
"environmentalAudioBody": "Les niveaux sonores ambiants suivent le volume de votre environnement en décibels. Une exposition prolongée au-delà d'environ 70 dB peut endommager l'audition avec le temps : des moyennes journalières plus basses sont plus sûres.",
- "headphoneAudioTitle": "Niveaux audio au casque",
"headphoneAudioBody": "Les niveaux audio au casque suivent, en décibels, le volume auquel vous écoutez avec des écouteurs ou un casque. Baisser le volume et faire des pauses protège votre audition sur le long terme.",
- "audioEventsTitle": "Événements sonores forts",
"audioEventsBody": "Les événements sonores forts signalent les moments où le bruit autour de vous a atteint un niveau pouvant nuire à l'audition en cas d'exposition suffisante. Des événements fréquents invitent à rechercher un environnement plus calme ou une protection auditive.",
- "daylightTitle": "Temps à la lumière du jour",
"daylightBody": "Le temps à la lumière du jour correspond à la durée passée en lumière extérieure chaque jour. Une lumière naturelle régulière soutient un rythme veille-sommeil et une humeur sains (CDC 2024), et le temps passé dehors est lié à la santé oculaire.",
- "bloodGlucoseTitle": "Glycémie",
"bloodGlucoseBody": "La glycémie est la quantité de sucre dans votre sang, principale source d'énergie de l'organisme. Elle augmente après les repas et se stabilise entre eux ; des valeurs durablement élevées peuvent signaler un diabète ou un prédiabète.",
- "skinTemperatureTitle": "Température cutanée",
"skinTemperatureBody": "La température cutanée correspond ici à la petite variation nocturne de la température de votre poignet par rapport à votre propre référence, et non à une mesure absolue de fièvre. Les variations peuvent suivre le sommeil, l'environnement, une maladie ou la phase du cycle menstruel.",
- "cardioFitnessTitle": "Capacité cardio (VO₂ max)",
"cardioFitnessBody": "Le VO₂ max correspond à la quantité d'oxygène que votre corps peut utiliser à l'effort maximal — le meilleur indice de laboratoire de la forme aérobie, que les wearables estiment de façon indirecte. À titre indicatif, la condition physique est un prédicteur graduel de survie : chaque MET supplémentaire (environ 3,5 ml/kg/min) est associé à une mortalité inférieure d'environ 10 à 25 % (AHA, Circulation 2016). Une tendance plus élevée ou en hausse est un fort signe de santé cardiovasculaire.",
- "cardioRecoveryTitle": "Récupération cardiaque",
"cardioRecoveryBody": "La récupération cardiaque correspond au nombre de battements par minute dont votre fréquence cardiaque diminue dans la première minute après l'arrêt d'un effort intense. Une baisse plus forte signifie que votre cœur se calme plus vite, signe d'une bonne condition physique.",
- "wristTemperatureTitle": "Température du poignet",
"wristTemperatureBody": "La température du poignet est mesurée pendant votre sommeil et indiquée comme l'écart par rapport à votre valeur de référence, et non comme une valeur absolue. Une valeur stable est normale, tandis qu'une hausse durable peut signaler une maladie, un entraînement intense ou un changement de cycle.",
- "fallsTitle": "Chutes",
"fallsBody": "Ceci compte les chutes lourdes détectées par votre wearable. L'objectif est zéro ; un nombre en hausse est un signal de risque de mobilité à évoquer avec un professionnel, surtout pour les personnes âgées (CDC STEADI 2024).",
- "sixMinuteWalkTitle": "Distance de marche de 6 minutes",
"sixMinuteWalkBody": "C'est une estimation de la distance que vous pourriez parcourir en six minutes, un test clinique classique de mobilité et d'endurance cardiopulmonaire. La valeur du wearable est une approximation, pas le test clinique encadré ; c'est la tendance stable ou en hausse qui compte.",
- "stairAscentSpeedTitle": "Vitesse en montée d'escaliers",
"stairAscentSpeedBody": "C'est la vitesse à laquelle vous montez une volée de marches. C'est un marqueur sensible de la force des jambes et de la mobilité : un rythme stable ou en hausse est bon signe, un rythme en baisse mérite attention.",
- "stairDescentSpeedTitle": "Vitesse en descente d'escaliers",
"stairDescentSpeedBody": "C'est la vitesse à laquelle vous descendez une volée de marches. Avec la vitesse de montée, elle reflète le contrôle des jambes et l'équilibre : un rythme stable est bon signe, un rythme en baisse mérite attention.",
- "breathingDisturbancesTitle": "Troubles respiratoires du sommeil",
"breathingDisturbancesBody": "Un wearable peut estimer à quelle fréquence votre respiration a été interrompue pendant le sommeil. Un indice plus bas est préférable ; un niveau signalé comme élevé, surtout sur plusieurs nuits, mérite d'être évoqué avec un professionnel. C'est une approximation de dépistage, pas un diagnostic d'apnée du sommeil.",
- "stepsTitle": "Pas",
"stepsBody": "Le nombre de pas correspond au total de pas que vous faites chaque jour. C'est la mesure la plus simple de l'activité quotidienne : un total journalier plus élevé reflète généralement un mode de vie plus actif, et une habitude régulière compte plus qu'une seule grosse journée.",
"readinessBody": "La forme du jour est une lecture quotidienne unique de la disposition de votre corps à affronter la journée, mêlant fréquence cardiaque au repos, variabilité de la fréquence cardiaque, sommeil et régularité de vos dernières nuits. Un score élevé signifie que votre organisme paraît récupéré et disponible ; un score plus bas est une invitation douce à lever le pied.",
"sleepScoreBody": "Votre score de sommeil résume toute une nuit en un seul chiffre — durée, efficacité, régularité des horaires et équilibre entre sommeil léger, profond et paradoxal. Observez la tendance sur plusieurs semaines plutôt que de courir après un score parfait sur une seule nuit.",
"recoveryBody": "La récupération reflète la qualité du rétablissement de votre corps, à partir de signaux nocturnes comme la variabilité de la fréquence cardiaque et la fréquence au repos. Un score élevé est un feu vert pour pousser ; un score plus bas suggère que votre organisme se reconstruit encore.",
"strainBody": "La charge mesure l'effort cardiovasculaire et physique que vous avez imposé à votre corps, des entraînements à une journée active debout. Dès qu'il y a assez d'historique, elle est évaluée par rapport à votre propre effort habituel, pas à un standard extérieur.",
"stressBody": "Le stress est ici une estimation de la charge physiologique sur votre système nerveux, lue surtout à partir des schémas de variabilité de la fréquence cardiaque et non de ce que vous ressentez consciemment. Un score plus bas indique un état calme et bien régulé ; un score plus élevé invite à prévoir de la récupération.",
- "recoveryPageTitle": "Récupération",
"recoveryPageBody": "La récupération rassemble les signaux nocturnes enregistrés par votre wearable — recharge, effort, charge ainsi que vos totaux de fréquence cardiaque et d'énergie du jour — pour montrer à quel point votre corps a récupéré. Observez la tendance sur plusieurs jours plutôt qu'un seul chiffre ; une valeur plus faible invite à lever le pied, ce n'est pas une alarme.",
- "pulsePressureTitle": "Pression pulsée",
"pulsePressureBody": "La pression pulsée est l'écart entre votre valeur systolique et diastolique (systolique moins diastolique). À titre indicatif général, environ 40 mmHg est typique au repos ; un écart large au-dessus d'environ 60 mmHg à l'âge moyen peut refléter des artères rigidifiées, tandis qu'un écart étroit en dessous d'environ 25 mmHg peut accompagner un faible volume d'éjection (StatPearls/NIH 2023). C'est un sujet de discussion, pas un diagnostic.",
- "meanArterialPressureTitle": "Pression artérielle moyenne",
"meanArterialPressureBody": "La pression artérielle moyenne est la pression moyenne que voient vos organes sur un battement, soit grosso modo la diastolique plus un tiers de l'écart avec la systolique. À titre indicatif général, une valeur au repos se situe autour de 70–100 mmHg (StatPearls/NIH 2023). Le seuil de ~60 mmHg souvent cité est un concept de soins aigus, pas une alarme pour l'autosurveillance.",
"cycleEducationBody": "Votre cycle traverse quatre phases pilotées par les hormones, et sa durée et sa régularité sont une fenêtre accessible sur la santé globale. À titre indicatif général, les cycles chez l'adulte durent typiquement 21–35 jours (les références cliniques citent 24–38), avec des règles d'environ cinq jours (ACOG 2015). La phase lutéale est l'ancrage stable, donc l'ovulation s'estime au mieux en comptant à rebours depuis vos prochaines règles prévues, jamais à un « jour 14 » fixe. Une fenêtre fertile prédite est une estimation qui demande plusieurs cycles enregistrés et reste approximative, et la température basale confirme l'ovulation après coup plutôt qu'elle ne la prédit. Ce n'est pas une méthode de contraception.",
- "sleepHygieneBody": "Quelques habitudes fondées sur les preuves favorisent le sommeil : des heures de coucher et de lever régulières, week-ends compris, une lumière vive le matin et des soirées tamisées, une chambre fraîche, sombre et calme, et éviter la caféine l'après-midi, l'alcool tardif et les repas copieux tard le soir (CDC 2024). La régularité prédit la santé au moins autant que la durée, et le temps de sommeil total d'un wearable est bien plus fiable que sa répartition entre sommeil profond, léger et paradoxal.",
- "referenceGuidance": "Indication générale, pas un avis médical. Les plages de référence sont des repères de population qui évoluent avec votre âge, votre sexe et vos conditions de santé ; votre propre tendance dans le temps guide la lecture."
+ "sleepHygieneBody": "Quelques habitudes fondées sur les preuves favorisent le sommeil : des heures de coucher et de lever régulières, week-ends compris, une lumière vive le matin et des soirées tamisées, une chambre fraîche, sombre et calme, et éviter la caféine l'après-midi, l'alcool tardif et les repas copieux tard le soir (CDC 2024). La régularité prédit la santé au moins autant que la durée, et le temps de sommeil total d'un wearable est bien plus fiable que sa répartition entre sommeil profond, léger et paradoxal."
},
"bpDerived": {
"pulsePressure": "La pression pulsée (l'écart entre vos valeurs) est d'environ {value} mmHg ; environ 40 est typique au repos.",
@@ -2920,16 +2822,6 @@
"compositionTitle": "Répartition des phases",
"compositionSubtitle": "{nights} dernières nuits",
"compositionAriaLabel": "Répartition des phases de sommeil sur {nights} nuits",
- "hypnogram": {
- "title": "La nuit dernière",
- "subtitle": "{asleep} de sommeil",
- "ariaLabel": "Progression des phases de sommeil de la nuit dernière : {asleep} de sommeil, {awakenings} réveils",
- "asleepTotal": "Sommeil {value}",
- "inBedTotal": "Au lit {value}",
- "awakenings": "{count} réveils",
- "source": "Source : {source}",
- "estimateNote": "Les durées des phases sont estimées : cette source indique des totaux par phase, pas une chronologie mesurée."
- },
"sourceDiscrepancy": {
"tooltipTitle": "Les sources divergent"
},
@@ -2984,7 +2876,6 @@
"loadError": "Votre rythme de sommeil n'a pas pu se charger pour l'instant. Il réapparaîtra dès que la connexion sera rétablie."
}
},
- "correlations": "Corrélations",
"medicationCompliance": "Observance des médicaments",
"dayStreak": "{count} jours de série",
"dayStreakOne": "{count} jour de série",
@@ -2993,19 +2884,12 @@
"taken": "Pris",
"skipped": "Sauté",
"missed": "Manqué",
- "generalStatusBadge": {
- "good": "Bien",
- "watch": "Surveiller",
- "critical": "Critique"
- },
"assessmentTitle": "Évaluation",
"restMode": {
"title": "Mode repos",
"body": "Un épisode de maladie ou d'affection est en cours, ceci reflète donc une période de récupération. Votre score n'en est pas pénalisé.",
"bodySince": "Un épisode de maladie ou d'affection est en cours depuis le {since}, ceci reflète donc une période de récupération. Votre score n'en est pas pénalisé."
},
- "assessmentShowMore": "Afficher plus",
- "assessmentShowLess": "Afficher moins",
"bloodPressureSectionTitle": "Tension artérielle",
"weightSectionTitle": "Poids",
"pulseSectionTitle": "Pouls",
@@ -3013,8 +2897,6 @@
"complianceNoData": "Aucune donnée d’observance disponible",
"generate": "Générer une nouvelle analyse",
"moodSectionTitle": "Humeur",
- "confidence": "Confiance",
- "cached": "En cache",
"emptyState": {
"bloodPressure": {
"title": "Aucune mesure de tension artérielle",
@@ -3063,11 +2945,6 @@
"title": "Aucun entraînement",
"description": "Ouvre l'app Apple Santé sur ton iPhone pour synchroniser tes entraînements. L'import d'activité Withings alimente également la marche et le vélo."
},
- "row": {
- "duration": "Durée",
- "distance": "Distance",
- "energy": "Énergie active"
- },
"detail": {
"backToList": "Retour aux entraînements",
"statsTitle": "Statistiques",
@@ -3080,13 +2957,9 @@
"statsStepCount": "Pas",
"statsElevation": "Dénivelé",
"statsPace": "Allure",
- "statsSource": "Source",
"routeTitle": "Parcours",
- "routeUnavailable": "Aucun parcours GPS enregistré pour cet entraînement.",
"hrChartTitle": "Fréquence cardiaque",
- "hrChartUnavailable": "Les échantillons de fréquence cardiaque à la seconde ne sont pas stockés pour cet entraînement.",
"notFound": "Entraînement introuvable.",
- "loading": "Chargement de l'entraînement…",
"hrFromPulse": "À partir de vos données de fréquence cardiaque autour de cette séance",
"hrAxisMinutes": "min",
"sportAverageLabel": "Votre moyenne récente :",
@@ -3558,7 +3431,6 @@
"title": "Ce qui ressort",
"subheading": "Relations découvertes",
"footer": "Issues de {tested} paires d'un jour à l'autre testées sur l'ensemble de vos mesures, après correction des fausses découvertes.",
- "empty": "Aucune relation d'humeur n'a encore franchi le seuil statistique. Continuez à enregistrer.",
"disclaimer": "Filtré statistiquement, mais toujours observationnel : une tendance à surveiller, pas une cause.",
"moodUp": "Un {factor} plus élevé tend à coïncider avec une meilleure humeur le lendemain dans vos données.",
"moodDown": "Un {factor} plus élevé tend à coïncider avec une moins bonne humeur le lendemain dans vos données.",
@@ -3937,8 +3809,6 @@
},
"briefing": {
"memory": {
- "recall": "Plus tôt, {text}",
- "forward": "En regardant vers l'avenir : {text}",
"forwardWatch": "{metric} est à surveiller.",
"forwardHolding": "Pour l'instant, tout reste stable."
}
@@ -3952,9 +3822,7 @@
}
},
"streak": {
- "returnLabel": "Ton {metric} est de nouveau dans ta plage habituelle après {count} jours — quoi que c'était, c'est passé.",
- "daysInside": "{count} jours dans la plage",
- "daysOutside": "{count} jours hors plage"
+ "returnLabel": "Ton {metric} est de nouveau dans ta plage habituelle après {count} jours — quoi que c'était, c'est passé."
},
"updatedTodayAt": "Mis à jour aujourd'hui, {time}",
"updatedYesterday": "Mis à jour hier",
@@ -4837,17 +4705,10 @@
"passwordMismatch": "Les nouveaux mots de passe ne correspondent pas",
"passwordUpdated": "Mot de passe mis à jour",
"passkeys": "Passkeys",
- "registeredPasskeys": "Clés d’accès enregistrées",
"passkeysDescription": "Les clés d’accès permettent une connexion sécurisée sans mot de passe.",
"passkeyName": "Nom",
- "passkeyDevice": "Appareil",
- "passkeyBackup": "Backup",
- "passkeyCreated": "Créée",
- "passkeyActions": "Actions",
"noPasskeys": "Aucune clé d’accès enregistrée.",
"addPasskey": "Ajouter une clé d’accès",
- "passkeyAdded": "Clé d’accès ajoutée !",
- "passkeyOptionsError": "Impossible de charger les options de clé d’accès",
"passkeyRegistrationFailed": "Échec de l’enregistrement de la clé d’accès",
"passkeyRegistrationCancelled": "Enregistrement de la clé d’accès annulé",
"passkeyNotSupported": "Votre navigateur ou appareil ne prend pas encore en charge les clés d’accès. Essayez un autre navigateur ou appareil.",
@@ -4896,7 +4757,6 @@
"webPushSubscribed": "Notifications push activées !",
"webPushUnsubscribed": "Notifications push désactivées.",
"webPushSubscribeFailed": "Échec de l’activation",
- "webPushActive": "Actif",
"kiInsightsDescription": "Facultatif : enregistrez votre clé OpenAI pour des analyses quotidiennes et automatiques dans Insights.",
"codexConnected": "ChatGPT connecté ! Insights est maintenant actif.",
"codexDisconnected": "Connexion à ChatGPT supprimée.",
@@ -4934,7 +4794,6 @@
"savedShort": "(enregistrée)",
"saveCta": "Enregistrer",
"saved": "Enregistré",
- "saveFailed": "Échec de l’enregistrement",
"errorGeneric": "Erreur",
"providerChain": {
"types": {
@@ -5070,11 +4929,9 @@
"hint": "Transmis au Coach et au briefing quotidien comme contexte supplémentaire. Laissez vide pour ne rien envoyer.",
"profileHint": "L'âge et le genre proviennent de votre profil et sont inclus automatiquement.",
"freeTextLabel": "À propos de moi (texte libre)",
- "conditionsLabel": "Maladies chroniques",
"conditionsPlaceholder": "P. ex. hypertension, diabète de type 2…",
"allergiesLabel": "Allergies et intolérances",
"allergiesPlaceholder": "P. ex. intolérance au lactose, allergie à la pénicilline…",
- "focusLabel": "À quoi le Coach doit-il veiller ?",
"focusPlaceholder": "P. ex. surveille ma tension, motive-moi à bouger davantage…",
"completeness": "{answered} sur {total} renseignés",
"questionsTitle": "Le Coach a des questions",
@@ -5152,8 +5009,6 @@
"token": "L'échange de jetons avec Withings a échoué. Vérifiez le Client ID et le Client Secret, puis réessayez.",
"generic": "Erreur inconnue lors de la connexion. Relancez la connexion."
},
- "integrationsSourcesHint": "Quand plusieurs sources fournissent la même métrique, la priorité des sources décide quelle valeur compte.",
- "integrationsSourcesHintLink": "Ouvrir la priorité des sources",
"withingsSynchronize": "Synchroniser",
"withingsDisconnect": "Déconnecter",
"withingsDisconnectTitle": "Déconnecter Withings ?",
@@ -5344,10 +5199,7 @@
"apiEndpointPath": "Chemin",
"apiEndpointAuth": "Authentification",
"apiEndpointExample": "Exemple de corps",
- "collapse": "Réduire",
- "expand": "Développer",
"dangerZone": "Supprimer toutes les données",
- "dangerZoneTitle": "Zone à risque",
"dangerZoneDescription": "Supprime toutes vos données de santé et intégrations. Votre compte utilisateur sera conservé.",
"dangerZoneConfirm": "Supprimer toutes les données ?",
"dangerZoneConfirmDescription": "Cette action supprimera définitivement toutes vos données de santé et intégrations. Votre compte utilisateur sera conservé.",
@@ -5367,7 +5219,6 @@
"about": {
"version": "Version de l’app",
"gitSha": "Build",
- "builtAt": "Compilé {time}",
"builtAtLabel": "Compilé",
"license": "Licence",
"repository": "Code source",
@@ -5485,8 +5336,6 @@
"resourceTypes": "Types de ressource FHIR",
"create": "Créer le lien",
"tokenCreated": "Lien de partage créé",
- "tokenOnce": "Copiez ce lien maintenant — le jeton n'est affiché qu'une fois et ne peut pas être récupéré.",
- "copy": "Copier le lien",
"copied": "Copié dans le presse-papiers.",
"activeTitle": "Liens actifs",
"noActive": "Aucun lien de partage actif.",
@@ -5500,7 +5349,6 @@
"statusExpired": "Expiré",
"inactiveTitle": "Révoqués et expirés ({count})",
"shownOnce": "Affiché une seule fois. Enregistrez le code QR et la phrase secrète maintenant — ils ne pourront pas être récupérés ensuite.",
- "qrLabel": "Code QR",
"qrAlt": "Code QR contenant le lien de partage et la phrase secrète",
"linkLabel": "Lien",
"passphraseLabel": "Phrase secrète",
@@ -5559,7 +5407,6 @@
"polar": "Polar",
"polarDescription": "Connecte ton compte Polar pour synchroniser la récupération Nightly Recharge, les phases de sommeil, l'activité et les séances d'entraînement.",
"polarViewData": "Voir votre sommeil et récupération",
- "polarHelp": "Connectez-vous à Polar pour accorder l'accès. Les données sont récupérées en arrière-plan une fois par heure.",
"polarCredentials": "Identifiants API",
"polarClientId": "Client ID",
"polarClientSecret": "Client Secret",
@@ -5589,7 +5436,6 @@
"oura": "Oura",
"ouraDescription": "Connecte ton compte Oura pour synchroniser readiness, phases de sommeil, variabilité cardiaque et activité quotidienne.",
"ouraViewData": "Voir votre sommeil et récupération",
- "ouraHelp": "Connectez-vous à Oura pour accorder l'accès. Les données sont récupérées en arrière-plan une fois par heure.",
"ouraCredentials": "Identifiants API",
"ouraClientId": "Client ID",
"ouraClientSecret": "Client Secret",
@@ -7480,7 +7326,6 @@
"measurementReminders": {
"sectionTitle": "Examens",
"sectionDescription": "Rappels de ce qu'il faut mesurer ou vérifier, quand et où.",
- "addButton": "Ajouter un rappel",
"markDone": "Terminé",
"delete": "Supprimer",
"deleteConfirmTitle": "Supprimer le rappel ?",
diff --git a/messages/it.json b/messages/it.json
index 1b15c4507..ec0ae461c 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -895,7 +895,6 @@
"emptyDescription": "Crea farmaci, dosi e fasce orarie per registrare l’aderenza.",
"loadFailed": "Impossibile caricare i farmaci",
"retryLoad": "Riprova",
- "name": "Nome",
"inactive": "In pausa",
"intakeHistory": "Cronologia assunzioni",
"openDetailPage": "Apri la pagina di dettaglio del farmaco",
@@ -1198,8 +1197,6 @@
"dosesPerUnitLabel": "Dosi per unità",
"dosesPerUnitPlaceholder": "es. 4",
"dosesPerUnitHint": "Dosi per penna / fiala — lascia vuoto per non tracciare le scorte.",
- "injectionSiteToggle": "Rotazione del sito di iniezione",
- "injectionSiteHint": "Guida alla rotazione facoltativa. Il sito di ogni dose viene registrato al momento della registrazione dell'assunzione.",
"short": "Dose",
"unitsPerDoseLabel": "Unità per dose",
"unitsPerDoseHint": "ad es. 2 compresse da 2 mg per una dose da 4 mg, oppure ½ compressa per una dose divisa"
@@ -1250,7 +1247,6 @@
"remindersDescription": "Push, Telegram o ntfy — in base alle tue impostazioni.",
"noChannelHint": "I promemoria richiedono un canale di notifica — al momento nessuno è attivo.",
"noChannelHintLink": "Configura le notifiche",
- "multiScheduleNote": "Questo farmaco ha più orari. L’editor dettagliato tornerà in una versione successiva.",
"short": "Fatto"
}
},
@@ -2118,7 +2114,6 @@
"emptyDescription": "Gli insights compaiono appena registri qualche misurazione. Inizia da peso o pressione.",
"emptyAddMeasurement": "Registra misurazione",
"editMode": {
- "open": "Personalizza",
"done": "Fine",
"reset": "Reimposta",
"title": "Personalizza panoramica",
@@ -2143,16 +2138,6 @@
"bmiEmptyAction": "Apri impostazioni account",
"noProviderConfigured": "Collega un provider IA per vedere qui una valutazione delle tue misurazioni.",
"noProviderAction": "Apri impostazioni IA",
- "startAnalysis": "Avvia valutazione",
- "refreshAnalysis": "Aggiorna valutazione",
- "keyTakeaway": "Ciò che conta",
- "findingsTitle": "Riscontri",
- "heroFindingPositive": "Riscontro principale",
- "heroFindingNeutral": "Riscontro principale",
- "heroFindingAttention": "Da tenere d’occhio",
- "heroFindingWarning": "Avviso principale",
- "correlationsTitle": "Correlazioni",
- "recommendationsTitle": "Raccomandazioni",
"personalRecord": {
"badge": "PR",
"tooltip": "Record personale: {value} {unit} il {date}"
@@ -2160,53 +2145,33 @@
"recommendation": {
"source": "Fonte",
"viewSource": "Apri la linea guida in una nuova scheda",
- "rationale": "Perché questo consiglio",
"rationaleWindow": "Finestra",
"rationaleComparedTo": "Confrontato con",
"rationaleDeviation": "Scostamento",
"rationaleExpand": "Mostra motivazione",
"rationaleCollapse": "Nascondi motivazione",
- "legacyPayloadCta": "Insights aggiornati — rigenera per attivare le nuove spiegazioni",
"feedbackHelpful": "Utile",
"feedbackNotHelpful": "Poco utile",
"feedbackThanks": "Grazie per il riscontro",
- "feedbackConfirmed": "Feedback salvato",
"feedbackAlreadyRated": "Già valutato",
"feedbackError": "Impossibile salvare il tuo riscontro. Riprova.",
- "confidence": "Confidenza",
"confidenceAria": "Confidenza: {value} su 100",
- "confidenceHigh": "Confidenza alta",
- "confidenceMedium": "Confidenza media",
"confidenceLow": "Confidenza bassa — pochi dati",
"confidenceDraft": "Bozza"
},
- "dataFoundation": "Base dati",
- "dataGaps": "Lacune dati:",
- "lastUpdated": "Ultimo aggiornamento",
- "confidenceHoch": "Alta",
- "confidenceMittel": "Media",
- "confidenceGering": "Bassa",
"loadError": "Impossibile caricare le analisi",
"heroPersonalBaseline": "Basato sugli ultimi 90 giorni",
- "heroRegenerate": "Rigenera",
"heroRegenerating": "Generazione in corso",
"regenerateAnalysis": "Rigenera la valutazione",
"regenerateSuccess": "Valutazione rigenerata",
- "warmStarted": "Le valutazioni vengono preparate in background",
"narrativeTitle": "Riepilogo",
"narrativeWeek": "Questa settimana",
"narrativeMonth": "Questo mese",
- "narrativePreparing": "Preparazione del riepilogo…",
"narrativeUpdating": "Aggiornamento…",
- "narrativeProvenanceLabel": "Come è stato realizzato",
- "narrativeProvenanceMethod": "Un riepilogo del periodo in linguaggio semplice, basato esclusivamente sui valori registrati e sulle associazioni che hanno superato il controllo statistico dei confronti multipli. Descrittivo, mai una causa.",
- "narrativeProvenanceMetrics": "Basato su: {metrics}",
- "narrativeProvenanceWindow": "Finestra: dal {from} al {to}",
"heroFallbackSubtitle": "Uno sguardo quotidiano ai tuoi trend — direttamente dai numeri che registri.",
"heroGreetingMorning": "Buongiorno",
"heroGreetingAfternoon": "Buon pomeriggio",
"heroGreetingEvening": "Buonasera",
- "heroComingSoonTooltip": "In arrivo",
"heroActionAskCoach": "Chiedi al coach",
"healthScore": {
"label": "Punteggio di salute",
@@ -2219,14 +2184,12 @@
"componentMood": "Umore",
"componentCompliance": "Aderenza",
"deltaExplainer": {
- "trigger": "Cosa significa questa variazione?",
"title": "Come si calcola la variazione",
"description": "Variazione rispetto alla settimana scorsa: {delta}",
"body": "Il punteggio combina PA, peso, umore e aderenza. Confrontiamo il punteggio di oggi con quello dello stesso momento della settimana scorsa. Registra una nuova misurazione o una nota di umore per spingerlo verso l'alto."
},
"provenance": {
"toggle": "Composto da",
- "weightLabel": "peso",
"mixedBanner": "Alcuni componenti combinano più fonti. Il dettaglio mostra il valore più recente per componente.",
"footnote": "I pesi sono ricalcolati in base ai dati effettivamente disponibili.",
"asOfLabel": "al {date}",
@@ -2290,10 +2253,6 @@
"viewDetail": "Vedi i dettagli di {metric}"
},
"vo2Max": {
- "statLatest": "Attuale",
- "statMin": "Min",
- "statMax": "Max",
- "empty": "Ancora nessun dato VO₂ max — sincronizza Apple Health per riempire questa vista.",
"cardioLinkTitle": "Forma cardio (VO₂ max)",
"cardioLinkBody": "Il VO₂ max è la misura principale della forma aerobica. Il grafico completo e la valutazione sono nella loro pagina dedicata.",
"cardioLinkCta": "Apri forma cardio"
@@ -2758,125 +2717,68 @@
"gewichtDescription": "Andamento del peso rispetto alla fascia sana derivata dalla tua altezza, con valutazione.",
"pulsDescription": "Resting pulse vs. the personalised Karvonen target band, plus a written assessment.",
"stimmungDescription": "Valori giornalieri di umore nel tempo, con una valutazione testuale.",
- "stimmungEmptyTitle": "Registra il primo umore",
- "stimmungEmptyDescription": "Gli insights dell’umore compaiono appena registri qualche voce. Tocca sotto per iniziare.",
- "stimmungEmptyAction": "Apri diario umore",
"medikamenteDescription": "Calendario delle assunzioni per farmaco e una valutazione testuale.",
- "medikamenteEmptyTitle": "Aggiungi il primo farmaco",
- "medikamenteEmptyDescription": "Gli insights di aderenza richiedono almeno un farmaco registrato.",
- "medikamenteEmptyAction": "Apri farmaci",
"bmiDescription": "BMI derivato dal tuo peso e dalla tua altezza, con valutazione.",
"target": {
"adjustLink": "Modifica l'intervallo obiettivo"
},
"explainer": {
- "trigger": "Che cos'è {title}?",
- "bloodPressureTitle": "Pressione arteriosa",
"bloodPressureBody": "La pressione arteriosa è la forza che il sangue esercita sulle pareti delle arterie, indicata come sistolica su diastolica (il picco durante un battito sul valore a riposo tra i battiti). Valori costantemente alti affaticano il cuore e i vasi nel tempo.",
- "pulseTitle": "Polso",
"pulseBody": "Il polso indica quante volte il cuore batte al minuto. Una misurazione singola riflette ciò che stavi facendo in quel momento: a riposo è di solito tra 60 e 100 battiti al minuto e aumenta naturalmente con il movimento, lo stress o la caffeina.",
- "weightTitle": "Peso",
"weightBody": "Il peso corporeo è uno dei segnali più semplici dell'equilibrio energetico a lungo termine e della salute generale. L'andamento nelle settimane conta molto più di una singola misurazione, che varia con l'idratazione, il cibo e l'ora del giorno.",
- "bmiTitle": "Indice di massa corporea (IMC)",
"bmiBody": "L'IMC è il peso diviso per l'altezza al quadrato, un singolo numero che ti colloca in un'ampia categoria di peso. È uno screening di popolazione, non una diagnosi, e non distingue il muscolo dal grasso.",
- "sleepTitle": "Sonno",
"sleepBody": "La durata del sonno è quanto hai effettivamente dormito nella notte, suddivisa in fasi leggera, profonda e REM. La maggior parte degli adulti necessita di circa sette-nove ore; quantità e qualità costanti favoriscono recupero, umore e metabolismo.",
- "moodTitle": "Umore",
"moodBody": "Il monitoraggio dell'umore registra come ti senti nel tempo su una scala semplice. Annotarlo con regolarità rivela schemi — come sonno, attività o farmaci si allineano a come ti senti — che un singolo giorno non mostra mai.",
- "medicationsTitle": "Aderenza alla terapia",
"medicationsBody": "L'aderenza misura quanto regolarmente assumi i farmaci come prescritto, qui come quota delle dosi previste che hai registrato. Un'assunzione costante è spesso ciò che fa funzionare una terapia come previsto.",
- "restingHrTitle": "Frequenza cardiaca a riposo",
"restingHrBody": "La frequenza cardiaca a riposo è il battito quando sei completamente a riposo, di solito misurato durante la notte. Una frequenza più bassa indica in genere una migliore forma cardiovascolare, mentre un andamento in aumento può essere un segnale precoce di stress o malattia.",
- "hrvTitle": "Variabilità della frequenza cardiaca (HRV)",
"hrvBody": "L'HRV è la piccola variazione del tempo tra battiti consecutivi. Una variabilità più alta riflette di solito un sistema nervoso ben recuperato e adattabile, mentre un calo prolungato può segnalare affaticamento, stress o recupero insufficiente.",
- "oxygenSaturationTitle": "Saturazione di ossigeno (SpO₂)",
"oxygenSaturationBody": "La SpO₂ è la percentuale di globuli rossi che trasportano ossigeno. Valori tra 95 e 100% sono normali a riposo; valori che restano sotto il 90% meritano attenzione, anche se brevi cali notturni sono comuni.",
- "bodyTemperatureTitle": "Temperatura corporea",
"bodyTemperatureBody": "La temperatura corporea riflette il tuo stato termico centrale, normalmente intorno ai 37 °C con piccole oscillazioni giornaliere. Un aumento prolungato può indicare un'infezione, mentre le misurazioni notturne al polso seguono soprattutto variazioni relative, non un valore clinico.",
- "activeEnergyTitle": "Energia attiva",
"activeEnergyBody": "L'energia attiva sono le calorie che bruci con il movimento, oltre al consumo di base a riposo. È una misura diretta di quanto è stata fisicamente attiva la tua giornata.",
- "workoutsTitle": "Allenamenti",
"workoutsBody": "Gli allenamenti sono le sessioni di esercizio strutturato che i tuoi dispositivi registrano, con tipo, durata ed energia. Tracciarli mostra come l'allenamento regolare si accumula nel corso delle settimane e come si collega agli altri segnali di salute.",
- "respiratoryRateTitle": "Frequenza respiratoria",
"respiratoryRateBody": "La frequenza respiratoria è il numero di respiri che fai al minuto. A riposo la maggior parte degli adulti respira circa dodici-venti volte al minuto; un aumento prolungato può accompagnare malattia, sforzo o stress.",
- "bodyWaterTitle": "Acqua corporea totale",
"bodyWaterBody": "L'acqua corporea totale è la quota del tuo peso costituita da liquido, dentro e intorno alle cellule. Varia con l'idratazione, l'alimentazione e l'attività, quindi l'andamento nelle settimane dice più di una singola misura.",
- "boneMassTitle": "Massa ossea",
"boneMassBody": "La massa ossea è il peso stimato dei minerali del tuo scheletro. Le stime da bilancia sono approssimative, ma un andamento stabile o in aumento sostiene la robustezza ossea a lungo termine.",
- "fatFreeMassTitle": "Massa magra",
"fatFreeMassBody": "La massa magra è tutto ciò che nel corpo non è grasso: muscoli, ossa, organi e acqua. Mantenerla o aumentarla mentre si perde peso indica di solito una composizione corporea più sana.",
- "fatMassTitle": "Massa grassa",
"fatMassBody": "La massa grassa è il peso totale del grasso nel corpo. Un po' di grasso è essenziale per gli ormoni e le riserve di energia, ma un eccesso — soprattutto sull'addome — aumenta il rischio per la salute a lungo termine (WHO 2024).",
- "muscleMassTitle": "Massa muscolare",
"muscleMassBody": "La massa muscolare è il peso stimato dei muscoli del corpo. Più muscolo sostiene forza, metabolismo e mobilità, e di solito richiede impegno attivo per essere mantenuto con l'età.",
- "visceralFatTitle": "Grasso viscerale",
"visceralFatBody": "Il grasso viscerale è il grasso depositato in profondità nell'addome, intorno agli organi. A differenza del grasso sottocutaneo, un livello elevato è strettamente legato a malattie cardiache e diabete di tipo 2 (AHA, Circulation 2021), quindi più basso è in genere meglio. La scala mostra un valore senza unità di misura (circa 1–12 è l'intervallo salutare, 13 e oltre è elevato); si tratta di una scala distinta dalle soglie in cm² ottenute per immagini che i clinici utilizzano, e non è intercambiabile con esse.",
- "leanBodyMassTitle": "Massa magra corporea",
"leanBodyMassBody": "La massa magra corporea è il peso totale meno il grasso: muscoli, ossa, organi e acqua insieme. Seguirla accanto al peso mostra se i cambiamenti derivano dal muscolo o dal grasso.",
- "flightsClimbedTitle": "Piani saliti",
"flightsClimbedBody": "I piani saliti contano i livelli che sali ogni giorno, dove un piano corrisponde a circa tre metri di dislivello. È un indicatore semplice dell'attività quotidiana e dello sforzo degli arti inferiori.",
- "walkingDistanceTitle": "Distanza a piedi e di corsa",
"walkingDistanceBody": "La distanza a piedi e di corsa è il percorso che copri a piedi ogni giorno. Integra il conteggio dei passi mostrando quanto ti sei mosso, non solo quanto spesso hai mosso il passo.",
- "walkingSteadinessTitle": "Stabilità del cammino",
"walkingSteadinessBody": "La stabilità del cammino stima quanto è stabile ed equilibrata la tua andatura, classificata da buona a bassa. Un livello basso indica una maggiore probabilità di caduta ed è opportuno parlarne con un medico.",
- "walkingHeartRateTitle": "Frequenza cardiaca in camminata",
"walkingHeartRateBody": "La frequenza cardiaca in camminata è il battito medio durante una camminata costante. Un valore più basso nel tempo riflette spesso una migliore forma cardiovascolare a parità di sforzo.",
- "walkingAsymmetryTitle": "Asimmetria del cammino",
"walkingAsymmetryBody": "L'asimmetria del cammino è la quota di tempo in cui i tuoi passi sono diseguali tra gamba sinistra e destra. Una percentuale più alta può indicare dolore, un infortunio o un problema di andatura da approfondire.",
- "doubleSupportTimeTitle": "Tempo di doppio appoggio",
"doubleSupportTimeBody": "Il tempo di doppio appoggio è la quota di ogni ciclo del passo in cui entrambi i piedi toccano terra contemporaneamente. Una percentuale più alta indica di solito un'andatura più prudente e meno stabile.",
- "stepLengthTitle": "Lunghezza del passo",
"stepLengthBody": "La lunghezza del passo è la distanza media da un appoggio del piede al successivo. Un passo più corto nel tempo può riflettere stanchezza, prudenza o un cambiamento nella mobilità.",
- "walkingSpeedTitle": "Velocità del cammino",
"walkingSpeedBody": "La velocità del cammino è quanto rapidamente ti muovi in piano. È un marcatore ben studiato della salute generale e della mobilità, e un ritmo stabile o in aumento è un buon segno.",
- "pulseWaveVelocityTitle": "Velocità dell'onda di polso",
"pulseWaveVelocityBody": "La velocità dell'onda di polso misura la rapidità con cui l'onda di pressione di ogni battito percorre le arterie. Un'onda più veloce indica arterie più rigide, associate a un rischio cardiovascolare più alto.",
- "vascularAgeTitle": "Età vascolare",
"vascularAgeBody": "L'età vascolare stima quanto si comportano vecchie le tue arterie rispetto alla tua età reale, in base alla rigidità arteriosa. Un'età vascolare inferiore a quella reale suggerisce vasi sanguigni più sani.",
- "environmentalAudioTitle": "Livelli sonori ambientali",
"environmentalAudioBody": "I livelli sonori ambientali rilevano il volume dell'ambiente intorno a te in decibel. Un'esposizione prolungata sopra i 70 dB circa può danneggiare l'udito nel tempo, quindi medie giornaliere più basse sono più sicure.",
- "headphoneAudioTitle": "Livelli audio in cuffia",
"headphoneAudioBody": "I livelli audio in cuffia rilevano in decibel quanto forte ascolti con auricolari o cuffie. Tenere basso il volume e fare pause protegge l'udito nel lungo periodo.",
- "audioEventsTitle": "Eventi sonori intensi",
"audioEventsBody": "Gli eventi sonori intensi segnalano i momenti in cui il rumore intorno a te ha raggiunto un livello che può danneggiare l'udito con esposizione sufficiente. Eventi frequenti sono un invito a cercare un ambiente più silenzioso o protezioni acustiche.",
- "daylightTitle": "Tempo alla luce del giorno",
"daylightBody": "Il tempo alla luce del giorno è quanto trascorri sotto la luce esterna ogni giorno. La luce diurna regolare sostiene un sano ritmo sonno-veglia e l'umore (CDC 2024), e il tempo all'aperto è collegato alla salute degli occhi.",
- "bloodGlucoseTitle": "Glicemia",
"bloodGlucoseBody": "La glicemia è la quantità di zucchero nel sangue, la principale fonte di energia del corpo. Sale dopo i pasti e si stabilizza tra un pasto e l'altro; valori che restano alti nel tempo possono indicare diabete o prediabete.",
- "skinTemperatureTitle": "Temperatura cutanea",
"skinTemperatureBody": "La temperatura cutanea qui è la piccola variazione notturna della temperatura del polso rispetto al tuo valore di riferimento, non una misura assoluta della febbre. Le variazioni possono seguire sonno, ambiente, malattia o fase del ciclo mestruale.",
- "cardioFitnessTitle": "Forma cardio (VO₂ max)",
"cardioFitnessBody": "Il VO₂ max indica quanto ossigeno il tuo corpo riesce a utilizzare al massimo sforzo: il miglior indice di laboratorio della forma aerobica, che i wearable stimano in modo indiretto. Come orientamento generale, la forma fisica è un predittore graduale della sopravvivenza: ogni MET in più (circa 3,5 ml/kg/min) si associa a una mortalità inferiore di circa il 10–25 % (AHA, Circulation 2016). Una tendenza più alta o in aumento è un forte segno di salute cardiovascolare.",
- "cardioRecoveryTitle": "Recupero cardiaco",
"cardioRecoveryBody": "Il recupero cardiaco è di quanti battiti al minuto cala la frequenza cardiaca nel primo minuto dopo aver interrotto uno sforzo intenso. Un calo maggiore significa che il cuore si calma più in fretta, segno di buona forma fisica.",
- "wristTemperatureTitle": "Temperatura del polso",
"wristTemperatureBody": "La temperatura del polso viene misurata mentre dormi ed è indicata come differenza dal tuo valore di riferimento, non come valore assoluto. Un valore stabile è normale, mentre un aumento prolungato può indicare malattia, allenamento intenso o un cambiamento del ciclo.",
- "fallsTitle": "Cadute",
"fallsBody": "Qui vengono contate le cadute forti rilevate dal tuo wearable. L'obiettivo è zero; un numero in aumento è un segnale di rischio di mobilità da segnalare a un medico, soprattutto per le persone anziane (CDC STEADI 2024).",
- "sixMinuteWalkTitle": "Distanza nel cammino di 6 minuti",
"sixMinuteWalkBody": "È una stima di quanto potresti camminare in sei minuti, un classico test clinico di mobilità e resistenza cardiopolmonare. Il valore del wearable è un'approssimazione, non il test clinico supervisionato; ciò che conta è la tendenza stabile o in aumento.",
- "stairAscentSpeedTitle": "Velocità in salita scale",
"stairAscentSpeedBody": "È la velocità con cui sali una rampa di scale. È un marcatore sensibile di forza delle gambe e mobilità: un ritmo stabile o in aumento è un buon segno, mentre uno in calo va tenuto d'occhio.",
- "stairDescentSpeedTitle": "Velocità in discesa scale",
"stairDescentSpeedBody": "È la velocità con cui scendi una rampa di scale. Insieme alla velocità in salita riflette il controllo delle gambe e l'equilibrio: un ritmo stabile è un buon segno, mentre uno in calo va tenuto d'occhio.",
- "breathingDisturbancesTitle": "Disturbi respiratori nel sonno",
"breathingDisturbancesBody": "Un wearable può stimare quanto spesso la respirazione si è interrotta durante il sonno. Un indice più basso è migliore; un livello segnalato come elevato, soprattutto su molte notti, andrebbe segnalato a un medico. È un'approssimazione di screening, non una diagnosi di apnea del sonno.",
- "stepsTitle": "Passi",
"stepsBody": "Il conteggio dei passi è il numero totale di passi che fai ogni giorno. È la misura più semplice del movimento quotidiano: un conteggio giornaliero più alto riflette di solito uno stile di vita più attivo, e un'abitudine costante conta più di un singolo giorno intenso.",
"readinessBody": "La prontezza è una lettura giornaliera di quanto il tuo corpo sia pronto ad affrontare la giornata, combinando frequenza cardiaca a riposo, variabilità della frequenza cardiaca, sonno e regolarità delle ultime notti. Un punteggio più alto indica che il tuo organismo appare recuperato e pieno di risorse; uno più basso è un invito gentile a rallentare.",
"sleepScoreBody": "Il tuo punteggio del sonno riassume un'intera notte in un solo numero — durata, efficienza, regolarità degli orari ed equilibrio tra sonno leggero, profondo e REM. Osserva l'andamento nell'arco delle settimane invece di rincorrere un punteggio perfetto in una singola notte.",
"recoveryBody": "Il recupero riflette quanto bene il tuo corpo si è ripreso, basandosi su segnali notturni come la variabilità della frequenza cardiaca e la frequenza a riposo. Un punteggio alto è il via libera per spingere; uno più basso suggerisce che il tuo organismo si sta ancora ricostruendo.",
"strainBody": "Lo sforzo misura quanto carico cardiovascolare e fisico hai imposto al tuo corpo, dagli allenamenti a una giornata attiva in piedi. Quando c'è abbastanza storico, viene valutato rispetto al tuo sforzo abituale, non rispetto a uno standard esterno.",
"stressBody": "Lo stress qui è una stima del carico fisiologico sul tuo sistema nervoso, letto soprattutto dagli schemi di variabilità della frequenza cardiaca e non da come ti senti consapevolmente. Un punteggio più basso indica uno stato calmo e ben regolato; uno più alto è un invito a inserire del recupero.",
- "recoveryPageTitle": "Recupero",
"recoveryPageBody": "Il recupero riunisce i segnali notturni registrati dal tuo wearable — ricarica, sforzo, carico e i totali di frequenza cardiaca ed energia della giornata — per mostrare quanto bene il tuo corpo si è ripreso. Guarda l'andamento su più giorni piuttosto che un singolo numero; un valore più basso è un invito a rallentare, non un allarme.",
- "pulsePressureTitle": "Pressione differenziale",
"pulsePressureBody": "La pressione differenziale è la differenza tra il valore sistolico e quello diastolico (sistolica meno diastolica). Come orientamento generale, circa 40 mmHg è tipico a riposo; un divario ampio sopra i 60 mmHg circa nella mezza età può riflettere arterie più rigide, mentre un divario stretto sotto i 25 mmHg circa può accompagnare una bassa gittata sistolica (StatPearls/NIH 2023). È uno spunto di discussione, non una diagnosi.",
- "meanArterialPressureTitle": "Pressione arteriosa media",
"meanArterialPressureBody": "La pressione arteriosa media è la pressione media che i tuoi organi vedono in un battito, all'incirca la diastolica più un terzo del divario con la sistolica. Come orientamento generale, un valore a riposo si aggira sui 70–100 mmHg (StatPearls/NIH 2023). La soglia di ~60 mmHg spesso citata è un concetto di terapia intensiva, non un allarme per l'automisurazione.",
"cycleEducationBody": "Il tuo ciclo attraversa quattro fasi guidate dagli ormoni, e la sua durata e regolarità sono una finestra accessibile sulla salute generale. Come orientamento generale, nei adulti i cicli durano tipicamente 21–35 giorni (i riferimenti clinici citano 24–38), con un sanguinamento di circa cinque giorni (ACOG 2015). La fase luteale è l'ancora stabile, quindi l'ovulazione si stima meglio contando a ritroso dalla prossima mestruazione attesa, mai con un «giorno 14» fisso. Una finestra fertile prevista è una stima che richiede più cicli registrati e resta approssimativa, e la temperatura basale conferma l'ovulazione a posteriori anziché prevederla. Non è un metodo contraccettivo.",
- "sleepHygieneBody": "Alcune abitudini basate sull'evidenza favoriscono il sonno: orari costanti per coricarsi e svegliarsi anche nel weekend, luce intensa al mattino e serate soffuse, una stanza fresca, buia e silenziosa, ed evitare la caffeina nel pomeriggio, l'alcol a tarda ora e i pasti abbondanti a fine giornata (CDC 2024). La regolarità predice la salute almeno quanto la durata, e il tempo di sonno totale di un wearable è molto più affidabile della sua suddivisione tra sonno profondo, leggero e REM.",
- "referenceGuidance": "Orientamento generale, non un consiglio medico. Gli intervalli di riferimento sono valori di popolazione che cambiano con la tua età, il sesso e le condizioni di salute; il tuo andamento nel tempo guida la lettura."
+ "sleepHygieneBody": "Alcune abitudini basate sull'evidenza favoriscono il sonno: orari costanti per coricarsi e svegliarsi anche nel weekend, luce intensa al mattino e serate soffuse, una stanza fresca, buia e silenziosa, ed evitare la caffeina nel pomeriggio, l'alcol a tarda ora e i pasti abbondanti a fine giornata (CDC 2024). La regolarità predice la salute almeno quanto la durata, e il tempo di sonno totale di un wearable è molto più affidabile della sua suddivisione tra sonno profondo, leggero e REM."
},
"bpDerived": {
"pulsePressure": "La pressione differenziale (il divario tra i tuoi valori) è di circa {value} mmHg; circa 40 è tipico a riposo.",
@@ -2910,16 +2812,6 @@
"compositionTitle": "Distribuzione delle fasi",
"compositionSubtitle": "Ultime {nights} notti",
"compositionAriaLabel": "Distribuzione delle fasi del sonno su {nights} notti",
- "hypnogram": {
- "title": "Ultima notte",
- "subtitle": "{asleep} di sonno",
- "ariaLabel": "Andamento delle fasi del sonno dell'ultima notte: {asleep} di sonno, {awakenings} risvegli",
- "asleepTotal": "Sonno {value}",
- "inBedTotal": "A letto {value}",
- "awakenings": "{count} risvegli",
- "source": "Fonte: {source}",
- "estimateNote": "I tempi delle fasi sono stimati: questa sorgente riporta i totali per fase, non una cronologia misurata."
- },
"sourceDiscrepancy": {
"tooltipTitle": "Le fonti divergono"
},
@@ -2974,7 +2866,6 @@
"loadError": "Non è stato possibile caricare il tuo ritmo del sonno in questo momento. Riapparirà non appena la connessione sarà stabile."
}
},
- "correlations": "Correlazioni",
"medicationCompliance": "Aderenza ai farmaci",
"dayStreak": "{count} giorni di serie",
"dayStreakOne": "{count} giorno di serie",
@@ -2983,19 +2874,12 @@
"taken": "Assunta",
"skipped": "Saltata",
"missed": "Persa",
- "generalStatusBadge": {
- "good": "Bene",
- "watch": "Osservare",
- "critical": "Critico"
- },
"assessmentTitle": "Valutazione",
"restMode": {
"title": "Modalità riposo",
"body": "È in corso un episodio di malattia o disturbo, quindi questo riflette una fase di recupero. Il tuo punteggio non viene abbassato per questo.",
"bodySince": "È in corso un episodio di malattia o disturbo dal {since}, quindi questo riflette una fase di recupero. Il tuo punteggio non viene abbassato per questo."
},
- "assessmentShowMore": "Mostra altro",
- "assessmentShowLess": "Mostra meno",
"bloodPressureSectionTitle": "Pressione arteriosa",
"weightSectionTitle": "Peso",
"pulseSectionTitle": "Polso",
@@ -3003,8 +2887,6 @@
"complianceNoData": "Nessun dato di aderenza disponibile",
"generate": "Genera nuova analisi",
"moodSectionTitle": "Umore",
- "confidence": "Confidenza",
- "cached": "In cache",
"emptyState": {
"bloodPressure": {
"title": "Nessuna misurazione della pressione",
@@ -3053,11 +2935,6 @@
"title": "Nessun allenamento",
"description": "Apri l'app Salute di Apple sull'iPhone per sincronizzare gli allenamenti. L'import di attività di Withings porta dentro anche camminate e uscite in bici."
},
- "row": {
- "duration": "Durata",
- "distance": "Distanza",
- "energy": "Energia attiva"
- },
"detail": {
"backToList": "Torna agli allenamenti",
"statsTitle": "Statistiche",
@@ -3070,13 +2947,9 @@
"statsStepCount": "Passi",
"statsElevation": "Dislivello",
"statsPace": "Passo",
- "statsSource": "Fonte",
"routeTitle": "Percorso",
- "routeUnavailable": "Nessun tracciato GPS registrato per questo allenamento.",
"hrChartTitle": "Frequenza cardiaca",
- "hrChartUnavailable": "I campioni di frequenza cardiaca al secondo non vengono salvati per questo allenamento.",
"notFound": "Allenamento non trovato.",
- "loading": "Caricamento allenamento…",
"hrFromPulse": "Dai tuoi dati di frequenza cardiaca intorno a questa sessione",
"hrAxisMinutes": "min",
"sportAverageLabel": "La tua media recente:",
@@ -3548,7 +3421,6 @@
"title": "Cosa risalta",
"subheading": "Relazioni scoperte",
"footer": "Estratte da {tested} coppie giorno per giorno testate su tutte le tue misure, dopo la correzione per le false scoperte.",
- "empty": "Nessuna relazione sull'umore ha ancora superato la soglia statistica. Continua a registrare.",
"disclaimer": "Filtrato statisticamente, ma sempre osservazionale: uno schema da tenere d'occhio, non una causa.",
"moodUp": "Un {factor} più alto tende a coincidere con un umore migliore il giorno dopo nei tuoi dati.",
"moodDown": "Un {factor} più alto tende a coincidere con un umore peggiore il giorno dopo nei tuoi dati.",
@@ -3927,8 +3799,6 @@
},
"briefing": {
"memory": {
- "recall": "In precedenza, {text}",
- "forward": "Guardando avanti: {text}",
"forwardWatch": "Vale la pena tenere d'occhio {metric}.",
"forwardHolding": "Per ora è tutto stabile."
}
@@ -3942,9 +3812,7 @@
}
},
"streak": {
- "returnLabel": "Il tuo {metric} è di nuovo nel tuo intervallo abituale dopo {count} giorni — qualunque cosa fosse, è passata.",
- "daysInside": "{count} giorni dentro",
- "daysOutside": "{count} giorni fuori"
+ "returnLabel": "Il tuo {metric} è di nuovo nel tuo intervallo abituale dopo {count} giorni — qualunque cosa fosse, è passata."
},
"updatedTodayAt": "Aggiornato oggi, {time}",
"updatedYesterday": "Aggiornato ieri",
@@ -4827,17 +4695,10 @@
"passwordMismatch": "Le nuove password non coincidono",
"passwordUpdated": "Password aggiornata correttamente",
"passkeys": "Passkeys",
- "registeredPasskeys": "Passkey registrate",
"passkeysDescription": "Le passkey consentono un accesso sicuro senza password.",
"passkeyName": "Nome",
- "passkeyDevice": "Dispositivo",
- "passkeyBackup": "Backup",
- "passkeyCreated": "Creata",
- "passkeyActions": "Azioni",
"noPasskeys": "Nessuna passkey registrata.",
"addPasskey": "Aggiungi passkey",
- "passkeyAdded": "Passkey aggiunta correttamente!",
- "passkeyOptionsError": "Impossibile caricare le opzioni della passkey",
"passkeyRegistrationFailed": "Registrazione della passkey non riuscita",
"passkeyRegistrationCancelled": "Registrazione della passkey annullata",
"passkeyNotSupported": "Il tuo browser o dispositivo non supporta ancora le passkey. Prova un altro browser o dispositivo.",
@@ -4886,7 +4747,6 @@
"webPushSubscribed": "Notifiche push attivate!",
"webPushUnsubscribed": "Notifiche push disattivate.",
"webPushSubscribeFailed": "Attivazione non riuscita",
- "webPushActive": "Attivo",
"kiInsightsDescription": "Facoltativo: salva la tua chiave OpenAI per analisi giornaliere e automatiche in Insights.",
"codexConnected": "ChatGPT connesso correttamente! Insights è ora attivo.",
"codexDisconnected": "Connessione a ChatGPT rimossa.",
@@ -4924,7 +4784,6 @@
"savedShort": "(salvata)",
"saveCta": "Salva",
"saved": "Salvato",
- "saveFailed": "Salvataggio non riuscito",
"errorGeneric": "Errore",
"providerChain": {
"types": {
@@ -5060,11 +4919,9 @@
"hint": "Inviato al Coach e al briefing giornaliero come contesto aggiuntivo. Lascia vuoto per non inviare nulla.",
"profileHint": "Età e genere provengono dal tuo profilo e vengono inclusi automaticamente.",
"freeTextLabel": "Su di me (testo libero)",
- "conditionsLabel": "Malattie croniche",
"conditionsPlaceholder": "Es. ipertensione, diabete di tipo 2…",
"allergiesLabel": "Allergie e intolleranze",
"allergiesPlaceholder": "Es. intolleranza al lattosio, allergia alla penicillina…",
- "focusLabel": "A cosa deve fare attenzione il Coach?",
"focusPlaceholder": "Es. tieni d'occhio la mia pressione, motivami a muovermi di più…",
"completeness": "{answered} su {total} compilati",
"questionsTitle": "Il Coach ha alcune domande",
@@ -5142,8 +4999,6 @@
"token": "Lo scambio di token con Withings non è riuscito. Controlla Client ID e Client Secret e riprova.",
"generic": "Errore sconosciuto durante il collegamento. Avvia di nuovo il collegamento."
},
- "integrationsSourcesHint": "Se più fonti forniscono la stessa metrica, la priorità delle fonti decide quale valore conta.",
- "integrationsSourcesHintLink": "Apri la priorità delle fonti",
"withingsSynchronize": "Sincronizza",
"withingsDisconnect": "Disconnetti",
"withingsDisconnectTitle": "Disconnettere Withings?",
@@ -5334,10 +5189,7 @@
"apiEndpointPath": "Percorso",
"apiEndpointAuth": "Autenticazione",
"apiEndpointExample": "Esempio di body",
- "collapse": "Comprimi",
- "expand": "Espandi",
"dangerZone": "Elimina tutti i dati",
- "dangerZoneTitle": "Zona pericolosa",
"dangerZoneDescription": "Elimina tutti i tuoi dati sanitari e le integrazioni. Il tuo account utente verrà conservato.",
"dangerZoneConfirm": "Eliminare tutti i dati?",
"dangerZoneConfirmDescription": "Questa azione eliminerà definitivamente tutti i tuoi dati sanitari e le integrazioni. Il tuo account utente verrà conservato.",
@@ -5357,7 +5209,6 @@
"about": {
"version": "Versione dell’app",
"gitSha": "Build",
- "builtAt": "Compilato {time}",
"builtAtLabel": "Compilato",
"license": "Licenza",
"repository": "Codice sorgente",
@@ -5475,8 +5326,6 @@
"resourceTypes": "Tipi di risorsa FHIR",
"create": "Crea link",
"tokenCreated": "Link di condivisione creato",
- "tokenOnce": "Copia ora questo link — il token viene mostrato una sola volta e non può essere recuperato.",
- "copy": "Copia link",
"copied": "Copiato negli appunti.",
"activeTitle": "Link attivi",
"noActive": "Nessun link di condivisione attivo.",
@@ -5490,7 +5339,6 @@
"statusExpired": "Scaduto",
"inactiveTitle": "Revocati e scaduti ({count})",
"shownOnce": "Mostrato una sola volta. Salva ora il codice QR e la passphrase — non potranno essere recuperati in seguito.",
- "qrLabel": "Codice QR",
"qrAlt": "Codice QR contenente il link di condivisione e la passphrase",
"linkLabel": "Link",
"passphraseLabel": "Passphrase",
@@ -5549,7 +5397,6 @@
"polar": "Polar",
"polarDescription": "Collega il tuo account Polar per sincronizzare il recupero Nightly Recharge, le fasi del sonno, l'attività giornaliera e le sessioni di allenamento.",
"polarViewData": "Vedi sonno e recupero",
- "polarHelp": "Accedi a Polar per concedere l'accesso. I dati vengono recuperati in background una volta all'ora.",
"polarCredentials": "Credenziali API",
"polarClientId": "Client ID",
"polarClientSecret": "Client Secret",
@@ -5579,7 +5426,6 @@
"oura": "Oura",
"ouraDescription": "Collega il tuo account Oura per sincronizzare prontezza, fasi del sonno, variabilità cardiaca e attività giornaliera.",
"ouraViewData": "Vedi sonno e recupero",
- "ouraHelp": "Accedi a Oura per concedere l'accesso. I dati vengono recuperati in background una volta all'ora.",
"ouraCredentials": "Credenziali API",
"ouraClientId": "Client ID",
"ouraClientSecret": "Client Secret",
@@ -7470,7 +7316,6 @@
"measurementReminders": {
"sectionTitle": "Controlli",
"sectionDescription": "Promemoria su cosa misurare o controllare, quando e dove.",
- "addButton": "Aggiungi promemoria",
"markDone": "Fatto",
"delete": "Elimina",
"deleteConfirmTitle": "Eliminare il promemoria?",
diff --git a/messages/pl.json b/messages/pl.json
index 8c59d3a63..7b852c5ef 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -889,7 +889,6 @@
"emptyDescription": "Utwórz leki, dawki i okna czasowe, aby śledzić przestrzeganie.",
"loadFailed": "Nie udało się załadować leków",
"retryLoad": "Spróbuj ponownie",
- "name": "Nazwa",
"inactive": "Wstrzymany",
"intakeHistory": "Historia przyjęć",
"openDetailPage": "Otwórz stronę szczegółów leku",
@@ -1192,8 +1191,6 @@
"dosesPerUnitLabel": "Dawki na jednostkę",
"dosesPerUnitPlaceholder": "np. 4",
"dosesPerUnitHint": "Dawki na wstrzykiwacz / fiolkę — zostaw puste, aby nie śledzić zapasów.",
- "injectionSiteToggle": "Rotacja miejsc wstrzyknięcia",
- "injectionSiteHint": "Opcjonalny przewodnik rotacji. Miejsce każdej dawki zapisywane jest przy rejestrowaniu przyjęcia.",
"short": "Dawka",
"unitsPerDoseLabel": "Jednostki na dawkę",
"unitsPerDoseHint": "np. 2 tabletki po 2 mg na dawkę 4 mg lub ½ tabletki przy dzielonej dawce"
@@ -1244,7 +1241,6 @@
"remindersDescription": "Push, Telegram lub ntfy — w zależności od ustawień.",
"noChannelHint": "Przypomnienia wymagają kanału powiadomień — obecnie żaden nie jest aktywny.",
"noChannelHintLink": "Skonfiguruj powiadomienia",
- "multiScheduleNote": "Ten lek ma wiele harmonogramów. Szczegółowy edytor wróci w kolejnej wersji.",
"short": "Gotowe"
}
},
@@ -2112,7 +2108,6 @@
"emptyDescription": "Insights pojawią się, gdy zarejestrujesz kilka pomiarów. Zacznij od wagi lub ciśnienia.",
"emptyAddMeasurement": "Zarejestruj pomiar",
"editMode": {
- "open": "Dostosuj",
"done": "Gotowe",
"reset": "Przywróć",
"title": "Dostosuj przegląd",
@@ -2137,16 +2132,6 @@
"bmiEmptyAction": "Otwórz ustawienia konta",
"noProviderConfigured": "Połącz dostawcę AI, aby zobaczyć tutaj ocenę swoich pomiarów.",
"noProviderAction": "Otwórz ustawienia AI",
- "startAnalysis": "Uruchom ocenę",
- "refreshAnalysis": "Odśwież ocenę",
- "keyTakeaway": "Najważniejsze",
- "findingsTitle": "Wnioski",
- "heroFindingPositive": "Najważniejszy wniosek",
- "heroFindingNeutral": "Najważniejszy wniosek",
- "heroFindingAttention": "Do uwagi",
- "heroFindingWarning": "Najważniejszy alert",
- "correlationsTitle": "Korelacje",
- "recommendationsTitle": "Rekomendacje",
"personalRecord": {
"badge": "PR",
"tooltip": "Rekord osobisty: {value} {unit} dnia {date}"
@@ -2154,53 +2139,33 @@
"recommendation": {
"source": "Źródło",
"viewSource": "Otwórz wytyczne w nowej karcie",
- "rationale": "Dlaczego ta rekomendacja",
"rationaleWindow": "Okno",
"rationaleComparedTo": "W porównaniu z",
"rationaleDeviation": "Odchylenie",
"rationaleExpand": "Pokaż uzasadnienie",
"rationaleCollapse": "Ukryj uzasadnienie",
- "legacyPayloadCta": "Insights zaktualizowane — wygeneruj ponownie, aby aktywować nowe wyjaśnienia",
"feedbackHelpful": "Przydatne",
"feedbackNotHelpful": "Mało przydatne",
"feedbackThanks": "Dzięki za opinię",
- "feedbackConfirmed": "Opinia zapisana",
"feedbackAlreadyRated": "Już ocenione",
"feedbackError": "Nie udało się zapisać opinii. Spróbuj ponownie.",
- "confidence": "Pewność",
"confidenceAria": "Pewność: {value} na 100",
- "confidenceHigh": "Wysoka pewność",
- "confidenceMedium": "Średnia pewność",
"confidenceLow": "Niska pewność — mało danych",
"confidenceDraft": "Wersja robocza"
},
- "dataFoundation": "Podstawa danych",
- "dataGaps": "Braki danych:",
- "lastUpdated": "Ostatnia aktualizacja",
- "confidenceHoch": "Wysoka",
- "confidenceMittel": "Średnia",
- "confidenceGering": "Niska",
"loadError": "Nie udało się załadować analiz",
"heroPersonalBaseline": "Na podstawie ostatnich 90 dni",
- "heroRegenerate": "Wygeneruj ponownie",
"heroRegenerating": "Generowanie",
"regenerateAnalysis": "Uruchom ocenę ponownie",
"regenerateSuccess": "Ocena wygenerowana ponownie",
- "warmStarted": "Oceny są przygotowywane w tle",
"narrativeTitle": "Przegląd",
"narrativeWeek": "Ten tydzień",
"narrativeMonth": "Ten miesiąc",
- "narrativePreparing": "Przygotowywanie podsumowania…",
"narrativeUpdating": "Aktualizowanie…",
- "narrativeProvenanceLabel": "Jak to powstało",
- "narrativeProvenanceMethod": "Podsumowanie okresu w prostym języku, oparte wyłącznie na zarejestrowanych wartościach i powiązaniach, które przeszły statystyczną korektę dla porównań wielokrotnych. Opisowe, nigdy nie przyczynowe.",
- "narrativeProvenanceMetrics": "Na podstawie: {metrics}",
- "narrativeProvenanceWindow": "Okno: od {from} do {to}",
"heroFallbackSubtitle": "Codzienne spojrzenie na twoje trendy — prosto z liczb, które rejestrujesz.",
"heroGreetingMorning": "Dzień dobry",
"heroGreetingAfternoon": "Dzień dobry",
"heroGreetingEvening": "Dobry wieczór",
- "heroComingSoonTooltip": "Wkrótce",
"heroActionAskCoach": "Zapytaj coacha",
"healthScore": {
"label": "Wynik zdrowia",
@@ -2213,14 +2178,12 @@
"componentMood": "Nastrój",
"componentCompliance": "Przestrzeganie",
"deltaExplainer": {
- "trigger": "Co oznacza ta zmiana?",
"title": "Jak liczona jest ta zmiana",
"description": "Zmiana względem ubiegłego tygodnia: {delta}",
"body": "Wynik łączy ciśnienie, wagę, nastrój i przyjmowanie leków. Porównujemy dzisiejszy wynik z tym samym momentem tydzień wcześniej. Dodaj świeży pomiar lub wpis o nastroju, żeby pchnąć go w górę."
},
"provenance": {
"toggle": "Składa się z",
- "weightLabel": "udział",
"mixedBanner": "Niektóre komponenty łączą wiele źródeł. Zestawienie pokazuje najświeższą wartość dla każdego komponentu.",
"footnote": "Wagi są przeskalowywane do dostępnych danych.",
"asOfLabel": "stan na {date}",
@@ -2284,10 +2247,6 @@
"viewDetail": "Zobacz szczegóły: {metric}"
},
"vo2Max": {
- "statLatest": "Aktualne",
- "statMin": "Min",
- "statMax": "Maks",
- "empty": "Brak danych VO₂ max — zsynchronizuj Apple Health, aby uzupełnić ten widok.",
"cardioLinkTitle": "Wydolność krążeniowa (VO₂ max)",
"cardioLinkBody": "VO₂ max to główna miara wydolności tlenowej. Pełny wykres i ocenę znajdziesz na osobnej stronie.",
"cardioLinkCta": "Otwórz wydolność krążeniową"
@@ -2752,125 +2711,68 @@
"gewichtDescription": "Przebieg wagi na tle zdrowego pasma wyliczonego z twojego wzrostu, z oceną.",
"pulsDescription": "Resting pulse vs. the personalised Karvonen target band, plus a written assessment.",
"stimmungDescription": "Dzienne wartości nastroju w czasie wraz z oceną tekstową.",
- "stimmungEmptyTitle": "Zarejestruj pierwszy nastrój",
- "stimmungEmptyDescription": "Insights nastroju pojawiają się, gdy zarejestrujesz kilka wpisów. Stuknij poniżej, aby zacząć.",
- "stimmungEmptyAction": "Otwórz dziennik nastroju",
"medikamenteDescription": "Kalendarz przyjęć dla każdego leku plus ocena tekstowa.",
- "medikamenteEmptyTitle": "Dodaj pierwszy lek",
- "medikamenteEmptyDescription": "Insights przestrzegania wymagają przynajmniej jednego zarejestrowanego leku.",
- "medikamenteEmptyAction": "Otwórz leki",
"bmiDescription": "BMI obliczone z twojej wagi i wzrostu, z oceną.",
"target": {
"adjustLink": "Dostosuj zakres docelowy"
},
"explainer": {
- "trigger": "Czym jest {title}?",
- "bloodPressureTitle": "Ciśnienie krwi",
"bloodPressureBody": "Ciśnienie krwi to siła, z jaką krew napiera na ściany tętnic, zapisywana jako skurczowe do rozkurczowego (szczyt podczas uderzenia serca do wartości spoczynkowej między uderzeniami). Stale wysokie wartości z czasem obciążają serce i naczynia.",
- "pulseTitle": "Tętno",
"pulseBody": "Tętno to liczba uderzeń serca na minutę. Pojedynczy pomiar odzwierciedla to, co robiłeś w danej chwili — w spoczynku wynosi zwykle 60–100 uderzeń na minutę i naturalnie rośnie przy ruchu, stresie lub kofeinie.",
- "weightTitle": "Waga",
"weightBody": "Masa ciała to jeden z najprostszych sygnałów długoterminowego bilansu energetycznego i ogólnego zdrowia. Trend na przestrzeni tygodni liczy się znacznie bardziej niż pojedynczy odczyt, który zmienia się wraz z nawodnieniem, jedzeniem i porą dnia.",
- "bmiTitle": "Wskaźnik masy ciała (BMI)",
"bmiBody": "BMI to waga podzielona przez wzrost do kwadratu — jedna liczba przypisująca Cię do szerokiej kategorii wagowej. To badanie przesiewowe dla populacji, a nie diagnoza, i nie odróżnia mięśni od tłuszczu.",
- "sleepTitle": "Sen",
"sleepBody": "Długość snu to czas, który faktycznie przespałeś w nocy, podzielony na fazy lekką, głęboką i REM. Większość dorosłych potrzebuje około siedmiu do dziewięciu godzin; stała ilość i jakość wspierają regenerację, nastrój i metabolizm.",
- "moodTitle": "Nastrój",
"moodBody": "Śledzenie nastroju rejestruje Twoje samopoczucie w czasie na prostej skali. Regularne zapisywanie ujawnia wzorce — jak sen, aktywność czy leki łączą się z samopoczuciem — których pojedynczy dzień nigdy nie pokaże.",
- "medicationsTitle": "Przestrzeganie zaleceń lekowych",
"medicationsBody": "Przestrzeganie zaleceń to rzetelność przyjmowania leków zgodnie z zaleceniami, pokazana tu jako odsetek oczekiwanych dawek, które zarejestrowałeś. Regularne przyjmowanie często decyduje o tym, czy terapia działa zgodnie z zamierzeniem.",
- "restingHrTitle": "Tętno spoczynkowe",
"restingHrBody": "Tętno spoczynkowe to częstość uderzeń serca w pełnym spoczynku, zwykle mierzona w nocy. Niższe tętno zwykle wskazuje na lepszą wydolność krążeniową, a rosnący trend może być wczesnym sygnałem stresu lub choroby.",
- "hrvTitle": "Zmienność rytmu serca (HRV)",
"hrvBody": "HRV to drobna zmienność czasu między kolejnymi uderzeniami serca. Wyższa zmienność zwykle odzwierciedla dobrze zregenerowany, elastyczny układ nerwowy, a utrzymujący się spadek może sygnalizować zmęczenie, stres lub niewystarczającą regenerację.",
- "oxygenSaturationTitle": "Saturacja tlenem (SpO₂)",
"oxygenSaturationBody": "SpO₂ to odsetek czerwonych krwinek przenoszących tlen. Wartości 95–100% są normalne w spoczynku; wartości utrzymujące się poniżej 90% wymagają uwagi, choć krótkie nocne spadki są częste.",
- "bodyTemperatureTitle": "Temperatura ciała",
"bodyTemperatureBody": "Temperatura ciała odzwierciedla stan cieplny rdzenia, zwykle około 37 °C z niewielkimi dobowymi wahaniami. Utrzymujący się wzrost może wskazywać na infekcję, podczas gdy nocne pomiary na nadgarstku śledzą głównie zmiany względne, a nie wartość kliniczną.",
- "activeEnergyTitle": "Energia aktywna",
"activeEnergyBody": "Energia aktywna to kalorie spalane przez ruch, ponad podstawowy wydatek w spoczynku. To bezpośrednia miara tego, jak aktywny fizycznie był Twój dzień.",
- "workoutsTitle": "Treningi",
"workoutsBody": "Treningi to ustrukturyzowane sesje ćwiczeń rejestrowane przez Twoje urządzenia, wraz z typem, czasem trwania i energią. Ich śledzenie pokazuje, jak regularny trening kumuluje się przez tygodnie i jak wiąże się z innymi sygnałami zdrowotnymi.",
- "respiratoryRateTitle": "Częstość oddechów",
"respiratoryRateBody": "Częstość oddechów to liczba wdechów na minutę. W spoczynku większość dorosłych oddycha około dwunastu do dwudziestu razy na minutę; utrzymujący się wzrost może towarzyszyć chorobie, wysiłkowi lub stresowi.",
- "bodyWaterTitle": "Całkowita woda w organizmie",
"bodyWaterBody": "Całkowita woda w organizmie to udział płynów w masie ciała, wewnątrz i wokół komórek. Zmienia się wraz z nawodnieniem, posiłkami i aktywnością, więc trend w ciągu tygodni mówi więcej niż pojedynczy pomiar.",
- "boneMassTitle": "Masa kostna",
"boneMassBody": "Masa kostna to szacowana waga minerałów w szkielecie. Pomiary z wagi są przybliżone, ale stabilny lub rosnący trend sprzyja długotrwałej wytrzymałości kości.",
- "fatFreeMassTitle": "Masa beztłuszczowa",
"fatFreeMassBody": "Masa beztłuszczowa to wszystko w ciele, co nie jest tłuszczem — mięśnie, kości, narządy i woda. Utrzymanie lub jej budowanie przy spadku wagi zwykle wskazuje na zdrowszy skład ciała.",
- "fatMassTitle": "Masa tłuszczowa",
"fatMassBody": "Masa tłuszczowa to łączna waga tłuszczu w ciele. Pewna ilość tłuszczu jest niezbędna dla hormonów i magazynowania energii, ale jego nadmiar — zwłaszcza w okolicy brzucha — zwiększa długoterminowe ryzyko zdrowotne (WHO 2024).",
- "muscleMassTitle": "Masa mięśniowa",
"muscleMassBody": "Masa mięśniowa to szacowana waga mięśni w ciele. Więcej mięśni wspiera siłę, metabolizm i sprawność, a ich utrzymanie z wiekiem zwykle wymaga aktywnego wysiłku.",
- "visceralFatTitle": "Tłuszcz trzewny",
"visceralFatBody": "Tłuszcz trzewny to tłuszcz gromadzony głęboko w jamie brzusznej wokół narządów. W odróżnieniu od tłuszczu podskórnego jego wysoki poziom jest ściśle powiązany z chorobami serca i cukrzycą typu 2 (AHA, Circulation 2021), więc im niżej, tym lepiej. Skala pokazuje wartość bezjednostkową (mniej więcej 1–12 to zakres zdrowy, 13 i więcej to wartość podwyższona); to inna skala niż progi w cm² z badań obrazowych stosowane przez klinicystów i nie należy jej traktować z nimi zamiennie.",
- "leanBodyMassTitle": "Beztłuszczowa masa ciała",
"leanBodyMassBody": "Beztłuszczowa masa ciała to całkowita waga pomniejszona o tłuszcz — mięśnie, kości, narządy i woda razem. Śledzenie jej obok wagi pokazuje, czy zmiany wynikają z mięśni, czy z tłuszczu.",
- "flightsClimbedTitle": "Pokonane piętra",
"flightsClimbedBody": "Pokonane piętra liczą kondygnacje, które pokonujesz w górę każdego dnia, gdzie jedno piętro to około trzech metrów wzniesienia. To prosty wskaźnik codziennej aktywności i wysiłku nóg.",
- "walkingDistanceTitle": "Dystans marszu i biegu",
"walkingDistanceBody": "Dystans marszu i biegu to droga, jaką pokonujesz pieszo każdego dnia. Uzupełnia liczbę kroków, pokazując, jak daleko się poruszałeś, a nie tylko jak często stawiałeś kroki.",
- "walkingSteadinessTitle": "Stabilność chodu",
"walkingSteadinessBody": "Stabilność chodu szacuje, jak stabilny i zrównoważony jest twój sposób chodzenia, w skali od dobrej do niskiej. Niski poziom wskazuje na większe ryzyko upadku i warto omówić go z lekarzem.",
- "walkingHeartRateTitle": "Tętno podczas chodzenia",
"walkingHeartRateBody": "Tętno podczas chodzenia to średni puls w trakcie równego marszu. Niższa wartość w czasie często odzwierciedla poprawę wydolności krążeniowej przy tym samym wysiłku.",
- "walkingAsymmetryTitle": "Asymetria chodu",
"walkingAsymmetryBody": "Asymetria chodu to odsetek czasu, w którym kroki są nierówne między lewą a prawą nogą. Wyższy procent może wskazywać na ból, kontuzję lub problem z chodem wart sprawdzenia.",
- "doubleSupportTimeTitle": "Czas podwójnego podparcia",
"doubleSupportTimeBody": "Czas podwójnego podparcia to udział każdego cyklu chodu, w którym obie stopy dotykają ziemi jednocześnie. Wyższy procent zwykle oznacza ostrożniejszy, mniej stabilny chód.",
- "stepLengthTitle": "Długość kroku",
"stepLengthBody": "Długość kroku to średnia odległość od jednego postawienia stopy do następnego. Krótszy krok w czasie może odzwierciedlać zmęczenie, ostrożność lub zmianę sprawności ruchowej.",
- "walkingSpeedTitle": "Prędkość chodu",
"walkingSpeedBody": "Prędkość chodu to tempo poruszania się po płaskim terenie. To dobrze zbadany wskaźnik ogólnego zdrowia i sprawności, a stałe lub rosnące tempo to dobry znak.",
- "pulseWaveVelocityTitle": "Prędkość fali tętna",
"pulseWaveVelocityBody": "Prędkość fali tętna mierzy, jak szybko fala ciśnienia z każdego uderzenia serca przemieszcza się przez tętnice. Szybsza fala wskazuje na sztywniejsze tętnice, co wiąże się z wyższym ryzykiem sercowo-naczyniowym.",
- "vascularAgeTitle": "Wiek naczyniowy",
"vascularAgeBody": "Wiek naczyniowy szacuje, jak stare zachowują się twoje tętnice w porównaniu z rzeczywistym wiekiem, na podstawie sztywności tętnic. Wiek naczyniowy niższy od rzeczywistego sugeruje zdrowsze naczynia krwionośne.",
- "environmentalAudioTitle": "Poziom dźwięku otoczenia",
"environmentalAudioBody": "Poziom dźwięku otoczenia śledzi głośność otoczenia w decybelach. Długotrwała ekspozycja powyżej około 70 dB może z czasem uszkodzić słuch, więc niższe średnie dzienne są bezpieczniejsze.",
- "headphoneAudioTitle": "Poziom dźwięku w słuchawkach",
"headphoneAudioBody": "Poziom dźwięku w słuchawkach śledzi w decybelach, jak głośno słuchasz przez słuchawki. Ściszanie głośności i robienie przerw chroni słuch w dłuższej perspektywie.",
- "audioEventsTitle": "Zdarzenia głośnego dźwięku",
"audioEventsBody": "Zdarzenia głośnego dźwięku oznaczają momenty, gdy hałas wokół ciebie osiągnął poziom mogący przy wystarczającej ekspozycji uszkodzić słuch. Częste zdarzenia to sygnał, by poszukać cichszego otoczenia lub ochrony słuchu.",
- "daylightTitle": "Czas w świetle dziennym",
"daylightBody": "Czas w świetle dziennym to ilość czasu spędzanego w świetle na zewnątrz każdego dnia. Regularne światło dzienne wspiera zdrowy rytm snu i czuwania oraz nastrój (CDC 2024), a czas na świeżym powietrzu wiąże się ze zdrowiem oczu.",
- "bloodGlucoseTitle": "Poziom glukozy we krwi",
"bloodGlucoseBody": "Poziom glukozy we krwi to ilość cukru we krwi, głównego źródła energii organizmu. Rośnie po jedzeniu i ustala się między posiłkami; wartości utrzymujące się wysoko mogą sygnalizować cukrzycę lub stan przedcukrzycowy.",
- "skinTemperatureTitle": "Temperatura skóry",
"skinTemperatureBody": "Temperatura skóry to tutaj niewielka nocna zmiana temperatury nadgarstka względem twojej własnej wartości bazowej, a nie bezwzględny pomiar gorączki. Zmiany mogą odzwierciedlać sen, otoczenie, chorobę lub fazę cyklu menstruacyjnego.",
- "cardioFitnessTitle": "Wydolność krążeniowa (VO₂ max)",
"cardioFitnessBody": "VO₂ max to ilość tlenu, jaką organizm potrafi wykorzystać przy maksymalnym wysiłku — najlepszy laboratoryjny wskaźnik wydolności tlenowej, który urządzenia noszone szacują pośrednio. Dla orientacji: sprawność fizyczna jest stopniowanym predyktorem przeżycia — każdy dodatkowy MET (około 3,5 ml/kg/min) wiąże się z mniej więcej 10–25 % niższą śmiertelnością (AHA, Circulation 2016). Wyższy lub rosnący trend to mocny sygnał dobrego zdrowia układu krążenia.",
- "cardioRecoveryTitle": "Regeneracja serca",
"cardioRecoveryBody": "Regeneracja serca to liczba uderzeń na minutę, o jaką spada tętno w pierwszej minucie po zakończeniu intensywnego wysiłku. Większy spadek oznacza, że serce szybciej się uspokaja, co świadczy o dobrej kondycji.",
- "wristTemperatureTitle": "Temperatura nadgarstka",
"wristTemperatureBody": "Temperatura nadgarstka jest mierzona podczas snu i podawana jako różnica względem Twojej wartości bazowej, a nie jako wartość bezwzględna. Stabilny odczyt jest normalny, a utrzymujący się wzrost może wskazywać na chorobę, intensywny trening lub zmianę cyklu.",
- "fallsTitle": "Upadki",
"fallsBody": "Tutaj liczone są poważne upadki wykryte przez urządzenie noszone. Celem jest zero; rosnąca liczba to sygnał ryzyka związanego z mobilnością, który warto omówić z lekarzem, zwłaszcza u osób starszych (CDC STEADI 2024).",
- "sixMinuteWalkTitle": "Dystans marszu 6-minutowego",
"sixMinuteWalkBody": "To szacunek, jak daleko mógłbyś przejść w sześć minut — klasyczny test kliniczny mobilności i wydolności krążeniowo-oddechowej. Wartość z urządzenia noszonego to przybliżenie, a nie nadzorowany test kliniczny; liczy się stały lub rosnący trend.",
- "stairAscentSpeedTitle": "Prędkość wchodzenia po schodach",
"stairAscentSpeedBody": "To prędkość, z jaką pokonujesz bieg schodów. To czuły wskaźnik siły nóg i mobilności — stałe lub rosnące tempo to dobry znak, a spadające warto obserwować.",
- "stairDescentSpeedTitle": "Prędkość schodzenia po schodach",
"stairDescentSpeedBody": "To prędkość, z jaką schodzisz bieg schodów. Razem z prędkością wchodzenia odzwierciedla kontrolę nóg i równowagę — stałe tempo to dobry znak, a spadające warto obserwować.",
- "breathingDisturbancesTitle": "Zaburzenia oddychania we śnie",
"breathingDisturbancesBody": "Urządzenie noszone potrafi oszacować, jak często Twój oddech był przerywany podczas snu. Niższy wskaźnik jest lepszy; poziom oznaczony jako podwyższony — zwłaszcza przez wiele nocy — warto omówić z lekarzem. To przybliżenie przesiewowe, a nie diagnoza bezdechu sennego.",
- "stepsTitle": "Kroki",
"stepsBody": "Liczba kroków to łączna liczba kroków wykonanych każdego dnia. To najprostsza miara codziennego ruchu — wyższa dzienna liczba zwykle odzwierciedla bardziej aktywny tryb życia, a stały nawyk liczy się bardziej niż pojedynczy intensywny dzień.",
"readinessBody": "Gotowość to pojedynczy dzienny odczyt tego, jak przygotowane jest Twoje ciało, by zmierzyć się z dniem — łączy tętno spoczynkowe, zmienność rytmu serca, sen i regularność ostatnich nocy. Wyższy wynik oznacza, że Twój organizm wygląda na wypoczęty i pełen zasobów; niższy to delikatna wskazówka, by zwolnić tempo.",
"sleepScoreBody": "Twój wynik snu streszcza całą noc w jednej liczbie — długość, efektywność, regularność pór oraz proporcje snu lekkiego, głębokiego i REM. Obserwuj wzorzec w ujęciu tygodni, zamiast gonić za idealnym wynikiem jednej nocy.",
"recoveryBody": "Regeneracja odzwierciedla, jak dobrze Twoje ciało się odbudowało — opiera się na nocnych sygnałach, takich jak zmienność rytmu serca i tętno spoczynkowe. Wysoki wynik to zielone światło, by przyspieszyć; niższy sugeruje, że organizm wciąż się regeneruje.",
"strainBody": "Obciążenie ujmuje, ile wysiłku sercowo-naczyniowego i fizycznego nałożyłeś na swoje ciało — od treningów po aktywny dzień na nogach. Gdy zbierze się wystarczająco danych, jest oceniane względem Twojego własnego typowego wysiłku, a nie cudzej miary.",
"stressBody": "Stres jest tu szacunkiem fizjologicznego obciążenia Twojego układu nerwowego, odczytywanym głównie ze wzorców zmienności rytmu serca, a nie z tego, jak się świadomie czujesz. Niższy wynik wskazuje na spokojny, dobrze regulowany stan; wyższy to zachęta, by wpleść regenerację.",
- "recoveryPageTitle": "Regeneracja",
"recoveryPageBody": "Regeneracja łączy nocne sygnały rejestrowane przez Twoje urządzenie – recharge, obciążenie, load oraz dzienne sumy tętna i energii – aby pokazać, jak dobrze Twój organizm się zregenerował. Patrz na wzorzec w ciągu kilku dni, a nie na pojedynczą liczbę; niższy wynik to zaproszenie do zwolnienia tempa, a nie alarm.",
- "pulsePressureTitle": "Ciśnienie tętna",
"pulsePressureBody": "Ciśnienie tętna to różnica między wartością skurczową a rozkurczową (skurczowe minus rozkurczowe). Jako ogólna wskazówka, około 40 mmHg jest typowe w spoczynku; szeroka różnica powyżej około 60 mmHg w średnim wieku może odzwierciedlać usztywnienie tętnic, a wąska różnica poniżej około 25 mmHg może towarzyszyć niskiej objętości wyrzutowej (StatPearls/NIH 2023). To powód do rozmowy, nie diagnoza.",
- "meanArterialPressureTitle": "Średnie ciśnienie tętnicze",
"meanArterialPressureBody": "Średnie ciśnienie tętnicze to przeciętne ciśnienie, jakie widzą Twoje narządy w trakcie uderzenia serca, w przybliżeniu rozkurczowe plus jedna trzecia różnicy do skurczowego. Jako ogólna wskazówka, wartość spoczynkowa mieści się w okolicy 70–100 mmHg (StatPearls/NIH 2023). Często przywoływany próg ~60 mmHg to pojęcie z medycyny ostrej, nie alarm do samodzielnego pomiaru.",
"cycleEducationBody": "Twój cykl przebiega przez cztery fazy sterowane hormonami, a jego długość i regularność to dostępne okno na ogólny stan zdrowia. Jako ogólna wskazówka, u osób dorosłych cykle trwają zwykle 21–35 dni (źródła kliniczne podają 24–38), z krwawieniem około pięciu dni (ACOG 2015). Faza lutealna jest stałą kotwicą, dlatego owulację najlepiej szacować, licząc wstecz od następnej spodziewanej miesiączki, nigdy według stałego „14. dnia”. Przewidziane okno płodne to oszacowanie, które wymaga kilku zarejestrowanych cykli i pozostaje przybliżone, a podstawowa temperatura ciała potwierdza owulację po fakcie, a nie ją przewiduje. To nie jest metoda antykoncepcji.",
- "sleepHygieneBody": "Kilka opartych na dowodach nawyków wspiera sen: stałe pory kładzenia się i wstawania także w weekendy, jasne światło rano i przyciemnione wieczory, chłodna, ciemna i cicha sypialnia oraz unikanie kofeiny po południu, alkoholu późnym wieczorem i obfitych posiłków przed snem (CDC 2024). Regularność przewiduje zdrowie co najmniej tak silnie jak długość snu, a całkowity czas snu z urządzenia jest znacznie bardziej wiarygodny niż jego podział na sen głęboki, lekki i REM.",
- "referenceGuidance": "Ogólna wskazówka, nie porada medyczna. Zakresy referencyjne to wartości populacyjne, które zmieniają się wraz z Twoim wiekiem, płcią i stanem zdrowia; to Twój własny trend w czasie prowadzi odczyt."
+ "sleepHygieneBody": "Kilka opartych na dowodach nawyków wspiera sen: stałe pory kładzenia się i wstawania także w weekendy, jasne światło rano i przyciemnione wieczory, chłodna, ciemna i cicha sypialnia oraz unikanie kofeiny po południu, alkoholu późnym wieczorem i obfitych posiłków przed snem (CDC 2024). Regularność przewiduje zdrowie co najmniej tak silnie jak długość snu, a całkowity czas snu z urządzenia jest znacznie bardziej wiarygodny niż jego podział na sen głęboki, lekki i REM."
},
"bpDerived": {
"pulsePressure": "Ciśnienie tętna (różnica między Twoimi wartościami) wynosi około {value} mmHg – około 40 jest typowe w spoczynku.",
@@ -2904,16 +2806,6 @@
"compositionTitle": "Rozkład faz",
"compositionSubtitle": "Ostatnie {nights} nocy",
"compositionAriaLabel": "Rozkład faz snu w {nights} nocach",
- "hypnogram": {
- "title": "Ostatnia noc",
- "subtitle": "{asleep} snu",
- "ariaLabel": "Przebieg faz snu ostatniej nocy: {asleep} snu, {awakenings} przebudzeń",
- "asleepTotal": "Sen {value}",
- "inBedTotal": "W łóżku {value}",
- "awakenings": "{count} przebudzeń",
- "source": "Źródło: {source}",
- "estimateNote": "Czasy faz są szacowane — to źródło podaje sumy dla każdej fazy, a nie zmierzony przebieg."
- },
"sourceDiscrepancy": {
"tooltipTitle": "Źródła się różnią"
},
@@ -2968,7 +2860,6 @@
"loadError": "Nie udało się teraz wczytać Twojego rytmu snu. Pojawi się ponownie, gdy połączenie się ustabilizuje."
}
},
- "correlations": "Korelacje",
"medicationCompliance": "Przestrzeganie leków",
"dayStreak": "{count} dni serii",
"dayStreakOne": "{count} dzień serii",
@@ -2977,19 +2868,12 @@
"taken": "Przyjęte",
"skipped": "Pominięte",
"missed": "Pominięte",
- "generalStatusBadge": {
- "good": "Dobrze",
- "watch": "Obserwuj",
- "critical": "Krytyczny"
- },
"assessmentTitle": "Ocena",
"restMode": {
"title": "Tryb odpoczynku",
"body": "Trwa epizod choroby lub dolegliwości, więc odzwierciedla to okres rekonwalescencji. Twój wynik nie jest za to obniżany.",
"bodySince": "Od {since} trwa epizod choroby lub dolegliwości, więc odzwierciedla to okres rekonwalescencji. Twój wynik nie jest za to obniżany."
},
- "assessmentShowMore": "Pokaż więcej",
- "assessmentShowLess": "Pokaż mniej",
"bloodPressureSectionTitle": "Ciśnienie krwi",
"weightSectionTitle": "Waga",
"pulseSectionTitle": "Tętno",
@@ -2997,8 +2881,6 @@
"complianceNoData": "Brak danych o przestrzeganiu",
"generate": "Wygeneruj nową analizę",
"moodSectionTitle": "Nastrój",
- "confidence": "Pewność",
- "cached": "Z pamięci podręcznej",
"emptyState": {
"bloodPressure": {
"title": "Brak pomiarów ciśnienia krwi",
@@ -3047,11 +2929,6 @@
"title": "Brak treningów",
"description": "Otwórz aplikację Apple Zdrowie na iPhonie, aby zsynchronizować treningi. Import aktywności z Withings także dorzuca marsze i przejażdżki."
},
- "row": {
- "duration": "Czas trwania",
- "distance": "Dystans",
- "energy": "Energia czynna"
- },
"detail": {
"backToList": "Powrót do treningów",
"statsTitle": "Statystyki",
@@ -3064,13 +2941,9 @@
"statsStepCount": "Kroki",
"statsElevation": "Przewyższenie",
"statsPace": "Tempo",
- "statsSource": "Źródło",
"routeTitle": "Trasa",
- "routeUnavailable": "Dla tego treningu nie zapisano trasy GPS.",
"hrChartTitle": "Tętno",
- "hrChartUnavailable": "Próbki tętna co sekundę nie są zapisywane dla tego treningu.",
"notFound": "Nie znaleziono treningu.",
- "loading": "Wczytywanie treningu…",
"hrFromPulse": "Na podstawie Twoich danych tętna z okolic tej sesji",
"hrAxisMinutes": "min",
"sportAverageLabel": "Twoja ostatnia średnia:",
@@ -3542,7 +3415,6 @@
"title": "Co się wyróżnia",
"subheading": "Wykryte relacje",
"footer": "Wyłonione z {tested} par dzień po dniu testowanych na wszystkich Twoich wskaźnikach, po korekcie fałszywych odkryć.",
- "empty": "Żadna relacja nastroju nie przekroczyła jeszcze progu statystycznego. Notuj dalej.",
"disclaimer": "Przefiltrowane statystycznie, ale wciąż obserwacyjne — wzorzec warty uwagi, nie przyczyna.",
"moodUp": "Wyższy {factor} zwykle wiąże się z lepszym nastrojem następnego dnia w Twoich danych.",
"moodDown": "Wyższy {factor} zwykle wiąże się z gorszym nastrojem następnego dnia w Twoich danych.",
@@ -3921,8 +3793,6 @@
},
"briefing": {
"memory": {
- "recall": "Wcześniej: {text}",
- "forward": "Patrząc w przyszłość: {text}",
"forwardWatch": "Warto mieć na oku {metric}.",
"forwardHolding": "Na razie wszystko jest stabilne."
}
@@ -3936,9 +3806,7 @@
}
},
"streak": {
- "returnLabel": "Twój {metric} wrócił do twojego zwykłego zakresu po {count} dniach — cokolwiek to było, minęło.",
- "daysInside": "{count} dni w zakresie",
- "daysOutside": "{count} dni poza zakresem"
+ "returnLabel": "Twój {metric} wrócił do twojego zwykłego zakresu po {count} dniach — cokolwiek to było, minęło."
},
"updatedTodayAt": "Zaktualizowano dzisiaj, {time}",
"updatedYesterday": "Zaktualizowano wczoraj",
@@ -4821,17 +4689,10 @@
"passwordMismatch": "Nowe hasła nie są zgodne",
"passwordUpdated": "Hasło zaktualizowane pomyślnie",
"passkeys": "Passkeys",
- "registeredPasskeys": "Zarejestrowane klucze dostępu",
"passkeysDescription": "Klucze dostępu umożliwiają bezpieczne logowanie bez hasła.",
"passkeyName": "Nazwa",
- "passkeyDevice": "Urządzenie",
- "passkeyBackup": "Backup",
- "passkeyCreated": "Utworzono",
- "passkeyActions": "Akcje",
"noPasskeys": "Brak zarejestrowanych kluczy dostępu.",
"addPasskey": "Dodaj klucz dostępu",
- "passkeyAdded": "Klucz dostępu dodany pomyślnie!",
- "passkeyOptionsError": "Nie udało się wczytać opcji klucza dostępu",
"passkeyRegistrationFailed": "Rejestracja klucza dostępu nie powiodła się",
"passkeyRegistrationCancelled": "Rejestracja klucza dostępu anulowana",
"passkeyNotSupported": "Twoja przeglądarka lub urządzenie nie obsługuje jeszcze kluczy dostępu. Wypróbuj inną przeglądarkę lub urządzenie.",
@@ -4880,7 +4741,6 @@
"webPushSubscribed": "Powiadomienia push włączone!",
"webPushUnsubscribed": "Powiadomienia push wyłączone.",
"webPushSubscribeFailed": "Aktywacja nie powiodła się",
- "webPushActive": "Aktywne",
"kiInsightsDescription": "Opcjonalnie: zapisz swój klucz OpenAI, aby uzyskać codzienne, automatyczne analizy w Insights.",
"codexConnected": "ChatGPT połączony pomyślnie! Insights jest teraz aktywny.",
"codexDisconnected": "Połączenie z ChatGPT usunięte.",
@@ -4918,7 +4778,6 @@
"savedShort": "(zapisano)",
"saveCta": "Zapisz",
"saved": "Zapisano",
- "saveFailed": "Zapis nie powiódł się",
"errorGeneric": "Błąd",
"providerChain": {
"types": {
@@ -5054,11 +4913,9 @@
"hint": "Przekazywane Coachowi i codziennemu briefingowi jako dodatkowy kontekst. Zostaw puste, aby nic nie wysyłać.",
"profileHint": "Wiek i płeć pochodzą z Twojego profilu i są dołączane automatycznie.",
"freeTextLabel": "O mnie (tekst dowolny)",
- "conditionsLabel": "Choroby przewlekłe",
"conditionsPlaceholder": "Np. nadciśnienie, cukrzyca typu 2…",
"allergiesLabel": "Alergie i nietolerancje",
"allergiesPlaceholder": "Np. nietolerancja laktozy, alergia na penicylinę…",
- "focusLabel": "Na co Coach ma zwracać uwagę?",
"focusPlaceholder": "Np. pilnuj mojego ciśnienia, motywuj mnie do ruchu…",
"completeness": "Wypełnione: {answered} z {total}",
"questionsTitle": "Coach ma pytania",
@@ -5136,8 +4993,6 @@
"token": "Wymiana tokenów z Withings nie powiodła się. Sprawdź Client ID i Client Secret i spróbuj ponownie.",
"generic": "Nieznany błąd podczas łączenia. Rozpocznij połączenie ponownie."
},
- "integrationsSourcesHint": "Gdy kilka źródeł dostarcza tę samą metrykę, priorytet źródeł decyduje, która wartość się liczy.",
- "integrationsSourcesHintLink": "Otwórz priorytet źródeł",
"withingsSynchronize": "Synchronizuj",
"withingsDisconnect": "Rozłącz",
"withingsDisconnectTitle": "Rozłączyć Withings?",
@@ -5328,10 +5183,7 @@
"apiEndpointPath": "Ścieżka",
"apiEndpointAuth": "Uwierzytelnianie",
"apiEndpointExample": "Przykład treści",
- "collapse": "Zwiń",
- "expand": "Rozwiń",
"dangerZone": "Usuń wszystkie dane",
- "dangerZoneTitle": "Strefa zagrożenia",
"dangerZoneDescription": "Usuwa wszystkie Twoje dane zdrowotne i integracje. Twoje konto użytkownika zostanie zachowane.",
"dangerZoneConfirm": "Usunąć wszystkie dane?",
"dangerZoneConfirmDescription": "Spowoduje to trwałe usunięcie wszystkich Twoich danych zdrowotnych i integracji. Twoje konto użytkownika zostanie zachowane.",
@@ -5351,7 +5203,6 @@
"about": {
"version": "Wersja aplikacji",
"gitSha": "Build",
- "builtAt": "Skompilowano {time}",
"builtAtLabel": "Skompilowano",
"license": "Licencja",
"repository": "Kod źródłowy",
@@ -5469,8 +5320,6 @@
"resourceTypes": "Typy zasobów FHIR",
"create": "Utwórz link",
"tokenCreated": "Link udostępniania utworzony",
- "tokenOnce": "Skopiuj ten link teraz — token jest pokazywany tylko raz i nie można go odzyskać.",
- "copy": "Kopiuj link",
"copied": "Skopiowano do schowka.",
"activeTitle": "Aktywne linki",
"noActive": "Brak aktywnych linków udostępniania.",
@@ -5484,7 +5333,6 @@
"statusExpired": "Wygasły",
"inactiveTitle": "Unieważnione i wygasłe ({count})",
"shownOnce": "Wyświetlane tylko raz. Zapisz teraz kod QR i hasło — nie można ich później odzyskać.",
- "qrLabel": "Kod QR",
"qrAlt": "Kod QR zawierający link do udostępniania i hasło",
"linkLabel": "Link",
"passphraseLabel": "Hasło",
@@ -5543,7 +5391,6 @@
"polar": "Polar",
"polarDescription": "Połącz konto Polar, aby synchronizować regenerację Nightly Recharge, fazy snu, dzienną aktywność i treningi.",
"polarViewData": "Zobacz sen i regenerację",
- "polarHelp": "Zaloguj się w Polar, aby przyznać dostęp. Dane są pobierane w tle raz na godzinę.",
"polarCredentials": "Dane API",
"polarClientId": "Client ID",
"polarClientSecret": "Client Secret",
@@ -5573,7 +5420,6 @@
"oura": "Oura",
"ouraDescription": "Połącz konto Oura, aby synchronizować gotowość, fazy snu, zmienność rytmu serca i dzienną aktywność.",
"ouraViewData": "Zobacz sen i regenerację",
- "ouraHelp": "Zaloguj się w Oura, aby przyznać dostęp. Dane są pobierane w tle raz na godzinę.",
"ouraCredentials": "Dane API",
"ouraClientId": "Client ID",
"ouraClientSecret": "Client Secret",
@@ -7464,7 +7310,6 @@
"measurementReminders": {
"sectionTitle": "Badania",
"sectionDescription": "Przypomnienia, co, kiedy i gdzie zmierzyć lub sprawdzić.",
- "addButton": "Dodaj przypomnienie",
"markDone": "Gotowe",
"delete": "Usuń",
"deleteConfirmTitle": "Usunąć przypomnienie?",
diff --git a/package.json b/package.json
index 997bd607c..84acc9630 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "healthlog",
- "version": "1.32.35",
+ "version": "1.32.36",
"description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.",
"license": "PolyForm-Noncommercial-1.0.0",
"homepage": "https://healthlog.dev",
diff --git a/public/sw.js b/public/sw.js
index 8978d4ac0..c153bf4ea 100644
--- a/public/sw.js
+++ b/public/sw.js
@@ -36,7 +36,7 @@ try {
// v1.4.38.4 → v1.4.42. Do not hand-edit; bump `package.json` and rebuild.
const CACHE_VERSION =
(typeof self !== "undefined" && self.__APP_VERSION__) ||
- /* @sw-version-fallback */ "v1.32.35";
+ /* @sw-version-fallback */ "v1.32.36";
const STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`;
const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`;
// v1.18.6 — read-only data cache for a curated allowlist of safe GET `/api/*`
diff --git a/scripts/audit-column-readers.ts b/scripts/audit-column-readers.ts
new file mode 100644
index 000000000..fcbfce2ab
--- /dev/null
+++ b/scripts/audit-column-readers.ts
@@ -0,0 +1,254 @@
+/**
+ * Release-battery sweep: which Prisma columns are written but never read?
+ *
+ * Run it as part of the deep-audit step, not in CI:
+ *
+ * pnpm dlx tsx scripts/audit-column-readers.ts
+ * pnpm dlx tsx scripts/audit-column-readers.ts --model Measurement
+ * pnpm dlx tsx scripts/audit-column-readers.ts --all
+ *
+ * ## Why this is a script and not a test
+ *
+ * The question "does this column have a reader anywhere in the tree" is a
+ * whole-program property, and the only cheap way to approximate it is grep.
+ * Grep is unsound in both directions here. Generic field names (`value`,
+ * `type`, `state`, `source`) match unrelated identifiers and read as consumed
+ * when they are not. Consumption through a select-less `findUnique` plus
+ * whole-row serialization reads as unconsumed when it is not. Raw SQL and
+ * dynamic selects fall outside the matcher entirely.
+ *
+ * A gate built on that matcher would drift towards green and stop meaning
+ * anything, which is exactly the failure this whole family of guards exists to
+ * prevent — a pair guard that quietly degrades into a one-end guard is worse
+ * than no guard, because it also removes the suspicion. So this stays a review
+ * tool with a human reading the output. The two questions that CAN be answered
+ * exactly — is every account-payload field consumed, is every Measurement
+ * column backed up — are tests instead:
+ * `src/__tests__/account-payload-consumer-guard.test.ts` and
+ * `src/__tests__/measurement-backup-completeness.test.ts`.
+ *
+ * ## The annotation convention
+ *
+ * A pull request that adds a column, payload field, or preference contains its
+ * consumer. When it genuinely cannot, the schema comment says so and this
+ * sweep treats the marker as an expiring allowlist entry:
+ *
+ * /// @pending-consumer(#123) — iOS reads this from v1.33; server-side unread until then.
+ * /// @internal: cooldown bookkeeping for the reminder cron; never surfaced.
+ *
+ * `@internal` is permanent and silences the field. `@pending-consumer` is
+ * temporary: the field stays out of the findings list but is reported
+ * separately every run, so an issue that closed without the consumer landing
+ * becomes visible at the next release audit instead of never.
+ */
+import { readFileSync, readdirSync, statSync } from "node:fs";
+import { join } from "node:path";
+
+const ROOT = join(import.meta.dirname, "..");
+const SCHEMA = join(ROOT, "prisma", "schema.prisma");
+const SRC = join(ROOT, "src");
+
+/** `generated` is the Prisma client: megabytes of noise, and never a consumer. */
+const SKIP_DIRS = new Set([
+ "generated",
+ "node_modules",
+ ".next",
+ "__tests__",
+ "__mocks__",
+]);
+
+/**
+ * Names too generic for the matcher to say anything useful about. Reported in
+ * their own section rather than dropped, so the sweep never claims a clean
+ * bill it cannot back up.
+ */
+const GENERIC_NAMES = new Set([
+ "id",
+ "name",
+ "type",
+ "value",
+ "unit",
+ "source",
+ "state",
+ "status",
+ "label",
+ "kind",
+ "data",
+ "key",
+ "code",
+ "url",
+ "title",
+ "notes",
+ "userId",
+ "createdAt",
+ "updatedAt",
+ "deletedAt",
+]);
+
+interface Field {
+ model: string;
+ name: string;
+ annotation: "internal" | "pending-consumer" | null;
+ annotationText: string | null;
+}
+
+function walk(dir: string, out: string[] = []): string[] {
+ for (const entry of readdirSync(dir)) {
+ if (SKIP_DIRS.has(entry)) continue;
+ const p = join(dir, entry);
+ if (statSync(p).isDirectory()) walk(p, out);
+ else if (entry.endsWith(".ts") || entry.endsWith(".tsx")) out.push(p);
+ }
+ return out;
+}
+
+function parseSchema(schema: string): Field[] {
+ const models = new Set(
+ [...schema.matchAll(/^model\s+([A-Za-z_]\w*)\s*\{/gm)].map((m) => m[1]),
+ );
+ const fields: Field[] = [];
+
+ for (const model of models) {
+ const start = schema.indexOf(`model ${model} {`);
+ const end = schema.indexOf("\n}", start);
+ const lines = schema.slice(start, end).split("\n").slice(1);
+
+ let pending: string[] = [];
+ for (const rawLine of lines) {
+ const line = rawLine.trim();
+ if (!line) continue;
+ if (line.startsWith("//") || line.startsWith("///")) {
+ pending.push(line);
+ continue;
+ }
+ if (line.startsWith("@@")) {
+ pending = [];
+ continue;
+ }
+ const m = /^([a-zA-Z_]\w*)\s+([A-Za-z_]\w*)(\[\])?(\?)?/.exec(line);
+ if (!m) {
+ pending = [];
+ continue;
+ }
+ const [, name, type, isList] = m;
+ if (isList || models.has(type) || line.includes("@relation")) {
+ pending = [];
+ continue;
+ }
+ const comment = [...pending, line].join(" ");
+ let annotation: Field["annotation"] = null;
+ let annotationText: string | null = null;
+ const internal = /@internal:?\s*(.*)$/.exec(comment);
+ const pendingConsumer = /@pending-consumer\(([^)]*)\)\s*(.*)$/.exec(
+ comment,
+ );
+ if (pendingConsumer) {
+ annotation = "pending-consumer";
+ annotationText = `${pendingConsumer[1]} ${pendingConsumer[2]}`.trim();
+ } else if (internal) {
+ annotation = "internal";
+ annotationText = internal[1].trim();
+ }
+ fields.push({ model, name, annotation, annotationText });
+ pending = [];
+ }
+ }
+
+ return fields;
+}
+
+interface Usage {
+ writes: number;
+ reads: number;
+ selects: number;
+}
+
+function countUsage(text: string, field: string): Usage {
+ const esc = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+ const selects = text.match(
+ new RegExp(`(? !modelFilter || f.model === modelFilter,
+ );
+ const text = walk(SRC)
+ .map((f) => readFileSync(f, "utf8"))
+ .join("\n");
+
+ const findings: Array = [];
+ const generic: Array = [];
+ const pendingConsumers: Field[] = [];
+
+ for (const field of fields) {
+ if (field.annotation === "pending-consumer") pendingConsumers.push(field);
+ if (field.annotation) continue;
+ const usage = countUsage(text, field.name);
+ if (usage.reads > 0 || usage.selects > 0) continue;
+ if (usage.writes === 0) continue;
+ if (GENERIC_NAMES.has(field.name) && !includeGeneric) {
+ generic.push({ ...field, ...usage });
+ continue;
+ }
+ findings.push({ ...field, ...usage });
+ }
+
+ const scope = modelFilter ? `model ${modelFilter}` : "every model";
+ console.log(
+ `Column-reader sweep over ${scope} — ${fields.length} scalar column(s).\n`,
+ );
+
+ if (findings.length === 0) {
+ console.log("No unannotated write-only columns matched.\n");
+ } else {
+ console.log(
+ `${findings.length} column(s) written but never read or selected — verify each by hand:\n`,
+ );
+ for (const f of findings) {
+ console.log(` ${f.model}.${f.name} (writes: ${f.writes})`);
+ }
+ console.log("");
+ }
+
+ if (pendingConsumers.length > 0) {
+ console.log(
+ `${pendingConsumers.length} column(s) marked @pending-consumer — check whether the issue closed without the consumer landing:\n`,
+ );
+ for (const f of pendingConsumers) {
+ console.log(` ${f.model}.${f.name} ${f.annotationText ?? ""}`);
+ }
+ console.log("");
+ }
+
+ if (generic.length > 0) {
+ console.log(
+ `${generic.length} match(es) suppressed as too generic to judge (re-run with --all to see them).\n`,
+ );
+ }
+
+ console.log(
+ "Expect false positives: raw SQL, dynamic selects, and whole-row serialization\n" +
+ "all read columns this matcher cannot see. Confirm a finding before acting on it.",
+ );
+}
+
+main();
diff --git a/src/__tests__/account-payload-consumer-guard.test.ts b/src/__tests__/account-payload-consumer-guard.test.ts
new file mode 100644
index 000000000..86cb77d27
--- /dev/null
+++ b/src/__tests__/account-payload-consumer-guard.test.ts
@@ -0,0 +1,189 @@
+/**
+ * v1.32.36 — pair guard over the account payload (`GET /api/auth/me`).
+ *
+ * The recurring failure this exists to prevent: a two-ended feature whose ends
+ * ship in different releases. The column lands with the schema change, the
+ * payload field lands with the API change, and the client that was supposed to
+ * read it is "the follow-up". Nothing in the gate notices, because every other
+ * guard proves ONE end — typecheck proves the export exists, knip proves it is
+ * imported, the OpenAPI check proves the schema matches. None of them prove the
+ * pair.
+ *
+ * `/api/auth/me` is the preferences envelope: it rides every app boot and is
+ * where user-facing preference fields surface. This test reads the field names
+ * straight out of the route's response literal and asserts each one is read by
+ * at least one client module. `lastReportPracticeName` shipped as column +
+ * doc contract + payload field and sat NULL with no reader for several
+ * releases; this guard would have failed on the commit that introduced it, and
+ * the allowlist entry needed to silence it would have had no honest reason to
+ * write.
+ *
+ * ## What it deliberately does not prove
+ *
+ * - It proves a field is READ somewhere, not that it is rendered correctly, nor
+ * that every surface that should read it does. That is what the
+ * unit-preference display guard does for its own narrower question.
+ * - The matcher is textual (property access or destructuring position), so a
+ * field whose name collides with an unrelated local reads as consumed. Every
+ * such name in this payload (`id`, `email`, `role`, `gender`, `timezone`,
+ * `modules`) is consumed ubiquitously anyway, so the over-acceptance costs
+ * nothing here. It would cost something on a payload full of generic names,
+ * which is why this is a per-envelope guard and not a general sweep.
+ * - It says nothing about iOS. A field consumed only by the native client must
+ * be allowlisted with that reason written down, and the reason is an
+ * assumption about another repository until an audit crosses over.
+ *
+ * Mutation check: append a field to the response literal in
+ * `src/app/api/auth/me/route.ts` and this goes red; delete the practice-name
+ * read in `health-record-export-panel.tsx` and it goes red too.
+ */
+import { readFileSync, readdirSync, statSync } from "node:fs";
+import { join } from "node:path";
+
+import { describe, it, expect } from "vitest";
+
+const SRC = join(process.cwd(), "src");
+const ME_ROUTE = join(SRC, "app", "api", "auth", "me", "route.ts");
+
+/**
+ * Where a client consumer may live. `src/app/api/**` is excluded — the server
+ * writing the field is the other end of the pair, not a consumer of it.
+ */
+const CONSUMER_ROOTS = [
+ join(SRC, "components"),
+ join(SRC, "hooks"),
+ join(SRC, "app"),
+];
+
+const SKIP_DIRS = new Set([
+ "__tests__",
+ "__mocks__",
+ "node_modules",
+ ".next",
+ "generated",
+]);
+
+/**
+ * `src/hooks/use-auth.ts` declares the `AuthUser` transport type. Naming a
+ * field in a type declaration is not consuming it — that is precisely the
+ * shape `lastReportPracticeName` had while it was dead — so the declaration
+ * file is not a consumer.
+ */
+const NOT_A_CONSUMER = new Set([
+ join(SRC, "hooks", "use-auth.ts"),
+ join(SRC, "app", "api", "auth", "me", "route.ts"),
+]);
+
+/**
+ * Fields with no client reader, each with the reason it is nonetheless
+ * correct for the payload to carry it. An entry here is a claim; write one
+ * only when the claim is true and checkable.
+ */
+const NO_CLIENT_CONSUMER: Record = {
+ insurerIkNumber:
+ "No web surface reads or writes it — the account form offers the insurer name and the " +
+ "insurance number, not the institution number. The native client does consume it: it " +
+ "builds the FHIR Coverage payor from insurerName-or-IK, per " +
+ ".planning/ios-coord/v0.11-ios-to-server-kvnr-only-coverage.md. Server-side the stored " +
+ "value feeds the doctor-report cover and the Coverage resource in the health-record " +
+ "export. The missing web field is a real gap, deliberately not closed in a guard-only " +
+ "release; this entry is what keeps it visible.",
+};
+
+function walk(dir: string, out: string[] = []): string[] {
+ for (const name of readdirSync(dir)) {
+ if (SKIP_DIRS.has(name)) continue;
+ const p = join(dir, name);
+ if (statSync(p).isDirectory()) {
+ // The API tree is the producing end, never the consuming one.
+ if (p === join(SRC, "app", "api")) continue;
+ walk(p, out);
+ } else if (
+ (name.endsWith(".ts") || name.endsWith(".tsx")) &&
+ !NOT_A_CONSUMER.has(p) &&
+ !name.endsWith(".test.ts") &&
+ !name.endsWith(".test.tsx")
+ ) {
+ out.push(p);
+ }
+ }
+ return out;
+}
+
+/** Top-level keys of the `apiSuccess({ … })` literal the route returns. */
+function payloadFields(source: string): string[] {
+ const start = source.indexOf("return apiSuccess({");
+ expect(start).toBeGreaterThan(-1);
+ const body = source.slice(start);
+ const fields: string[] = [];
+ for (const m of body.matchAll(/^ {4}([a-zA-Z_$][\w$]*)\s*[:,]/gm)) {
+ fields.push(m[1]);
+ }
+ return [...new Set(fields)];
+}
+
+/**
+ * Comments are stripped before matching. A doc comment naming a field is a
+ * claim about a consumer, not a consumer — `lastReportPracticeName` had one of
+ * those for its whole dead life, and counting it would make this guard agree
+ * with the very prose that was wrong.
+ */
+function stripComments(text: string): string {
+ return text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|\s)\/\/.*$/gm, "$1");
+}
+
+/**
+ * A property access (`user.glucoseUnit`) or a destructuring / object position
+ * (`{ glucoseUnit }`, `{ glucoseUnit: unit }`).
+ */
+function readsField(text: string, field: string): boolean {
+ const re = new RegExp(
+ `\\.\\s*${field}(?![\\w$])|[{,]\\s*${field}\\s*[,}:=]`,
+ "m",
+ );
+ return re.test(text);
+}
+
+describe("account payload consumer guard", () => {
+ const source = readFileSync(ME_ROUTE, "utf8");
+ const fields = payloadFields(source);
+ const consumerFiles = CONSUMER_ROOTS.flatMap((root) => walk(root));
+ const consumerText = consumerFiles.map((f) =>
+ stripComments(readFileSync(f, "utf8")),
+ );
+
+ it("reads a plausible field set out of the route", () => {
+ expect(fields.length).toBeGreaterThan(20);
+ expect(fields).toContain("lastReportPracticeName");
+ expect(fields).toContain("moduleAvailability");
+ expect(consumerFiles.length).toBeGreaterThan(200);
+ });
+
+ it("every field the account endpoint returns has a client consumer", () => {
+ const unconsumed = fields.filter(
+ (field) =>
+ !(field in NO_CLIENT_CONSUMER) &&
+ !consumerText.some((text) => readsField(text, field)),
+ );
+
+ if (unconsumed.length > 0) {
+ const report = unconsumed.map((f) => ` ❌ ${f}`).join("\n");
+ throw new Error(
+ `GET /api/auth/me returns ${unconsumed.length} field(s) no client module reads:\n${report}\n\n` +
+ "Wire the consumer in the same release, or add the field to " +
+ "NO_CLIENT_CONSUMER with the reason it is correct to ship unread. " +
+ "A field with no honest reason belongs in neither the payload nor the schema.",
+ );
+ }
+ });
+
+ it("the allowlist carries no stale entry", () => {
+ for (const [field, reason] of Object.entries(NO_CLIENT_CONSUMER)) {
+ expect(
+ fields,
+ `${field} is allowlisted but no longer returned`,
+ ).toContain(field);
+ expect(reason.length).toBeGreaterThan(20);
+ }
+ });
+});
diff --git a/src/__tests__/helpers/i18n-reference.ts b/src/__tests__/helpers/i18n-reference.ts
new file mode 100644
index 000000000..b376adf92
--- /dev/null
+++ b/src/__tests__/helpers/i18n-reference.ts
@@ -0,0 +1,334 @@
+/**
+ * v1.32.36 — the reachability matcher behind `i18n-reverse-coverage.test.ts`,
+ * extracted so it can be unit-tested in isolation.
+ *
+ * The reverse guard asserts that every leaf in `messages/en.json` is reachable
+ * from at least one call site. It is a pair guard: one end is the bundle, the
+ * other end is the tree. A pair guard is only worth its green state while its
+ * matcher stays honest, and this one had degraded — see the bare-namespace note
+ * below.
+ *
+ * ## Recognised construction patterns
+ *
+ * - a literal t("a.b.c"), and a t("a.b") that returns a subtree;
+ * - pluralKey("a.b", n) / tCount("a.b", n), which resolve to the three
+ * One / Few / Other tiers and nothing else;
+ * - a **pair template** — a t(template) call carrying BOTH a literal prefix and
+ * a literal suffix, as in the per-provider OAuth status keys. Both ends must
+ * match, so the template covers settings.polarOauthConnected and nothing else
+ * under settings.;
+ * - a **prefix template** — a literal prefix and no suffix, as in the cycle
+ * phase labels, covering the cycle.phase.* subtree;
+ * - a **suffix template** — a leading interpolation and a literal tail, as in
+ * the per-metric insight page titles. The tail alone means nothing, so it is
+ * matched only against a base that some other arm has established;
+ * - a declared i18n base — a dotted literal assigned to an `i18nPrefix` prop or
+ * variable. This is the convention the OAuth provider card, the cadence
+ * picker and the HealthKit metric page already follow. A declared base covers
+ * the exact key and serves as the base half of a suffix template; it does NOT
+ * open its subtree;
+ * - a full string-literal node path, covering the subtree it names (map values
+ * handed to t());
+ * - an explicit allowlist for keys resolved through a DB column.
+ *
+ * ## Why a bare namespace is refused rather than accepted
+ *
+ * Before v1.32.36, a prefix ending in a dot was recorded as a subtree base
+ * regardless of depth. The per-provider OAuth template therefore registered the
+ * base `settings` and marked every one of the several thousand keys under
+ * settings.* as reachable, and a second instance did the same for
+ * measurementReminders.*. The guard still passed, so the degradation was
+ * invisible: a pair guard had quietly become a one-end guard.
+ *
+ * Accepting a bare namespace silently is a false pass. Refusing it silently
+ * would only trade that for a false orphan report. So a suffix-less template
+ * rooted at a bare top-level namespace is a **collector violation**: the guard
+ * fails and names the template. The two honest remedies are to build a
+ * fully-qualified key at the call site (as the measurement-reminder due helpers
+ * do) or to give the template a literal suffix so it becomes a pair.
+ */
+
+/** A template whose prefix is a bare top-level namespace and which has no suffix. */
+export interface BareNamespaceViolation {
+ /** The offending literal, as it appears in the source. */
+ literal: string;
+ /** Which ingestion arm saw it first. */
+ arm: "template" | "assignment" | "concat";
+}
+
+/** A template carrying both a literal prefix and a literal suffix. */
+interface KeyPair {
+ /** Raw leading literal, dot-terminated or not. */
+ prefix: string;
+ /** Raw trailing literal, leading dot preserved. */
+ suffix: string;
+}
+
+export interface Reference {
+ /** Literal t("…") keys plus the plural tiers. */
+ literalKeys: Set;
+ /** Dotted bases (deep prefix template / a full string-literal node path): cover the subtree. */
+ dottedBases: Set;
+ /** Dot-less concat prefixes: cover any leaf that string-starts with them. */
+ concatPrefixes: Set;
+ /** Prefix+suffix templates: both ends must match. */
+ pairs: KeyPair[];
+ /** Trailing literals of a leading-interpolation template. */
+ suffixes: Set;
+ /** Bases declared through an `i18nPrefix` literal. Exact match plus suffix base. */
+ declaredPrefixes: Set;
+}
+
+export interface CollectResult {
+ ref: Reference;
+ violations: BareNamespaceViolation[];
+}
+
+/** `settings` is bare; `settings.sections` is not. */
+function isBareNamespace(base: string): boolean {
+ return !base.includes(".");
+}
+
+/**
+ * Split a template body into its leading and trailing literals: the admin
+ * section title template yields the prefix "admin.section." and the suffix
+ * ".title".
+ */
+function splitTemplate(content: string): {
+ prefix: string | null;
+ suffix: string | null;
+} {
+ const open = content.indexOf("${");
+ if (open === -1) return { prefix: null, suffix: null };
+ const rawPrefix = open > 0 ? content.slice(0, open) : "";
+ const close = content.lastIndexOf("}");
+ const rawSuffix =
+ close !== -1 && close < content.length - 1 ? content.slice(close + 1) : "";
+ return {
+ // A prefix with no dot is not an i18n path root; ignore it.
+ prefix: rawPrefix.includes(".") ? rawPrefix : null,
+ suffix: rawSuffix || null,
+ };
+}
+
+/**
+ * Body of the template literal whose opening backtick sits at `start`.
+ *
+ * Scanning forward from a known opening backtick rather than pairing backticks
+ * across the whole tree matters: the concatenated sources hold tens of
+ * thousands of backticks, plenty of them inside comments and strings, so naive
+ * pairing silently shifts and whole files fall out of the scan. That is how a
+ * matcher stops seeing call sites without anyone noticing.
+ */
+function templateBodyAt(text: string, start: number): string | null {
+ let depth = 0;
+ for (let i = start + 1; i < text.length; i++) {
+ const c = text[i];
+ if (c === "\\") {
+ i++;
+ continue;
+ }
+ if (c === "$" && text[i + 1] === "{") {
+ depth++;
+ i++;
+ continue;
+ }
+ if (c === "}" && depth > 0) {
+ depth--;
+ continue;
+ }
+ if (c === "`" && depth === 0) return text.slice(start + 1, i);
+ }
+ return null;
+}
+
+export function collectReferences(
+ allText: string,
+ allNodes: Set,
+ topNamespaces: Set,
+): CollectResult {
+ const literalKeys = new Set();
+ const dottedBases = new Set();
+ const concatPrefixes = new Set();
+ const suffixes = new Set();
+ const declaredPrefixes = new Set();
+ const pairs: KeyPair[] = [];
+ const pairSeen = new Set();
+ const violations: BareNamespaceViolation[] = [];
+ const violationSeen = new Set();
+
+ const addPair = (prefix: string, suffix: string) => {
+ const id = [prefix, suffix].join(" ");
+ if (pairSeen.has(id)) return;
+ pairSeen.add(id);
+ pairs.push({ prefix, suffix });
+ };
+
+ /**
+ * The single door into `dottedBases` for every ingestion arm. A bare
+ * top-level namespace never opens a subtree — it is reported instead.
+ * Violations dedupe on the literal, not on the arm: one template is seen by
+ * both the t() arm and the generic backtick arm, and it is one defect.
+ */
+ const addDottedBase = (
+ base: string,
+ arm: BareNamespaceViolation["arm"],
+ literal: string,
+ ) => {
+ if (isBareNamespace(base)) {
+ if (violationSeen.has(literal)) return;
+ violationSeen.add(literal);
+ violations.push({ literal, arm });
+ return;
+ }
+ dottedBases.add(base);
+ };
+
+ const literalKeyRe =
+ /(?One / Few
+ // / Other at runtime, so the base literal accounts for exactly those
+ // three leaves and no others. Kept as an explicit tier list rather than a
+ // prefix match so a stray sibling key under the same base still reports as
+ // an orphan.
+ const pluralKeyRe =
+ /(?:pluralKey|tCount)\(\s*["']([a-zA-Z][a-zA-Z0-9_.-]+)["']/g;
+ for (const m of allText.matchAll(pluralKeyRe)) {
+ for (const tier of ["One", "Few", "Other"])
+ literalKeys.add(`${m[1]}${tier}`);
+ }
+
+ const ingestTemplate = (
+ content: string,
+ arm: BareNamespaceViolation["arm"],
+ ) => {
+ const { prefix, suffix } = splitTemplate(content);
+ if (prefix && suffix) {
+ addPair(prefix, suffix);
+ return;
+ }
+ if (prefix) {
+ if (prefix.endsWith("."))
+ addDottedBase(prefix.slice(0, -1), arm, content);
+ else concatPrefixes.add(prefix);
+ return;
+ }
+ if (suffix) suffixes.add(suffix);
+ };
+
+ // Templates passed directly to t(…).
+ const tStartRe = /(? prefix.length + suffix.length &&
+ leaf.startsWith(prefix) &&
+ leaf.endsWith(suffix)
+ ) {
+ return true;
+ }
+ }
+ for (const suffix of ref.suffixes) {
+ if (!leaf.endsWith(suffix) || leaf.length === suffix.length) continue;
+ const base = leaf.slice(0, leaf.length - suffix.length).replace(/\.$/, "");
+ if (!base) continue;
+ if (suffixBaseMatches(base, ref)) return true;
+ }
+ return false;
+}
diff --git a/src/__tests__/i18n-reference-matcher.test.ts b/src/__tests__/i18n-reference-matcher.test.ts
new file mode 100644
index 000000000..d711bb9cb
--- /dev/null
+++ b/src/__tests__/i18n-reference-matcher.test.ts
@@ -0,0 +1,147 @@
+/**
+ * v1.32.36 — unit tests for the reverse-i18n reachability matcher.
+ *
+ * The reverse-coverage guard is a pair guard, and a pair guard is worthless the
+ * moment its matcher over-accepts: it stays green while proving nothing. That
+ * is what happened here — one template anchored at a bare `settings.` marked
+ * several thousand keys reachable and nobody could tell from the green run.
+ *
+ * So the matcher gets its own tests over synthetic sources, where the expected
+ * answer is known exactly. The first test below is the regression: it plants
+ * the old hole and asserts the collector now refuses it out loud.
+ */
+import { describe, it, expect } from "vitest";
+
+import { collectReferences, isReferenced } from "./helpers/i18n-reference";
+
+const NAMESPACES = new Set([
+ "settings",
+ "cycle",
+ "measurementReminders",
+ "notifications",
+ "insights",
+]);
+
+function run(source: string, nodes: string[] = []) {
+ return collectReferences(source, new Set(nodes), NAMESPACES);
+}
+
+describe("i18n reference matcher", () => {
+ describe("bare top-level namespaces", () => {
+ it("refuses a suffix-less template anchored at a bare namespace", () => {
+ const { violations } = run("t(`measurementReminders.${due.key}`)");
+
+ expect(violations).toHaveLength(1);
+ expect(violations[0].literal).toContain("measurementReminders.");
+ });
+
+ it("does not silently whitelist the namespace it refused", () => {
+ const { ref } = run("t(`measurementReminders.${due.key}`)");
+
+ expect(isReferenced("measurementReminders.somethingDead", ref)).toBe(
+ false,
+ );
+ });
+
+ it("refuses the same shape arriving through the concat arm", () => {
+ const { violations, ref } = run('const k = "settings." + provider;');
+
+ expect(violations).toHaveLength(1);
+ expect(violations[0].arm).toBe("concat");
+ expect(isReferenced("settings.anythingAtAll", ref)).toBe(false);
+ });
+
+ it("refuses the same shape arriving outside a t() call", () => {
+ const { violations } = run("const key = `cycle.${phase}`;");
+
+ expect(violations[0].arm).toBe("assignment");
+ });
+
+ it("accepts a deep prefix, which confines itself", () => {
+ const { violations, ref } = run("t(`cycle.phase.${phase}`)");
+
+ expect(violations).toHaveLength(0);
+ expect(isReferenced("cycle.phase.follicular", ref)).toBe(true);
+ expect(isReferenced("cycle.somethingElse", ref)).toBe(false);
+ });
+ });
+
+ describe("pair templates", () => {
+ it("covers only leaves matching both ends", () => {
+ const { violations, ref } = run(
+ "t(`settings.${provider}OauthConnected`)",
+ );
+
+ expect(violations).toHaveLength(0);
+ expect(isReferenced("settings.polarOauthConnected", ref)).toBe(true);
+ expect(isReferenced("settings.withingsOauthConnected", ref)).toBe(true);
+ expect(isReferenced("settings.anythingElse", ref)).toBe(false);
+ expect(isReferenced("settings.polarOauthFailed", ref)).toBe(false);
+ });
+
+ it("honours a dot-rooted suffix as a path boundary", () => {
+ const { ref } = run("t(`insights.section.${slug}.title`)");
+
+ expect(isReferenced("insights.section.sleep.title", ref)).toBe(true);
+ // `subtitle` ends in "title" but not in ".title".
+ expect(isReferenced("insights.section.sleep.subtitle", ref)).toBe(false);
+ });
+ });
+
+ describe("suffix templates", () => {
+ it("means nothing without a base another arm established", () => {
+ const { ref } = run("t(`${i18nPrefix}SyncResult`)");
+
+ expect(isReferenced("settings.withingsSyncResult", ref)).toBe(false);
+ });
+
+ it("attaches to a declared i18nPrefix base", () => {
+ const { ref } = run(
+ 'i18nPrefix="settings.withings"\nt(`${i18nPrefix}SyncResult`)',
+ );
+
+ expect(isReferenced("settings.withingsSyncResult", ref)).toBe(true);
+ expect(isReferenced("settings.withings", ref)).toBe(true);
+ // The declaration is a base, not a subtree licence.
+ expect(isReferenced("settings.withingsSomethingDead", ref)).toBe(false);
+ expect(isReferenced("settings.polarSyncResult", ref)).toBe(false);
+ });
+
+ it("attaches to a base established by a literal node path", () => {
+ const { ref } = run(
+ 'const p = "insights.walkingSpeed";\nt(`${p}.title`)',
+ ["insights.walkingSpeed"],
+ );
+
+ expect(isReferenced("insights.walkingSpeed.title", ref)).toBe(true);
+ });
+ });
+
+ describe("literal keys", () => {
+ it("covers the key and its subtree", () => {
+ const { ref } = run('t("settings.export.title")\nt("cycle.phase")');
+
+ expect(isReferenced("settings.export.title", ref)).toBe(true);
+ expect(isReferenced("cycle.phase.luteal", ref)).toBe(true);
+ expect(isReferenced("settings.export.subtitle", ref)).toBe(false);
+ });
+
+ it("covers exactly the three plural tiers of a counted key", () => {
+ const { ref } = run('tCount("insights.staleWeeks", n)');
+
+ expect(isReferenced("insights.staleWeeksOne", ref)).toBe(true);
+ expect(isReferenced("insights.staleWeeksFew", ref)).toBe(true);
+ expect(isReferenced("insights.staleWeeksOther", ref)).toBe(true);
+ expect(isReferenced("insights.staleWeeksMany", ref)).toBe(false);
+ });
+ });
+
+ describe("scope", () => {
+ it("ignores an interpolating template that is not namespace-rooted", () => {
+ const { violations, ref } = run("const cacheKey = `coach.${userId}`;");
+
+ expect(violations).toHaveLength(0);
+ expect(isReferenced("coach.anything", ref)).toBe(false);
+ });
+ });
+});
diff --git a/src/__tests__/i18n-reverse-coverage.test.ts b/src/__tests__/i18n-reverse-coverage.test.ts
index 438d20bca..c55e5dae6 100644
--- a/src/__tests__/i18n-reverse-coverage.test.ts
+++ b/src/__tests__/i18n-reverse-coverage.test.ts
@@ -6,18 +6,11 @@
* REVERSE: every leaf in `messages/en.json` is reachable from at least one
* call site, so orphan keys can't accumulate in the bundle unnoticed.
*
- * "Reachable" recognises the key-construction patterns the codebase actually
- * uses:
- * - a literal `t("a.b.c")` (hyphens allowed — admin section slugs use them);
- * - a `t("a.b")` that returns a subtree, covering every descendant leaf;
- * - a dynamic key built inside a `t(`…`)` template, e.g. the per-metric
- * `${i18nPrefix}.title` insight pages, `cycle.calendar.flow${LEVEL}`, or
- * the `medications.site${Suffix}` label keys — captured by the static
- * leading-literal prefix and/or trailing-literal suffix of the template;
- * - a key string assembled outside the call (`return `medications.site${s}``
- * in `describeInjectionSite`) and passed to `t()`, recognised when the
- * template's leading literal is rooted at a real top-level namespace;
- * - an explicit allowlist for keys resolved through a DB column.
+ * v1.32.36 — the matcher moved to `helpers/i18n-reference.ts` (unit-tested in
+ * `helpers/__tests__/i18n-reference.test.ts`) and stopped accepting a bare
+ * top-level namespace as a subtree base. See that file's header for the
+ * recognised construction patterns and for why the bare-namespace case is a
+ * loud failure rather than a silent accept.
*
* Dynamic detection is scoped to `t(`…`)` and to namespace-rooted key
* templates only — NOT to every backtick string — so a cache key or
@@ -27,11 +20,17 @@
* If this fails after adding a key, either wire a call site, or — for a key
* resolved through a runtime value the static scan can't see — add its prefix
* to `DYNAMIC_ALLOWLIST_PREFIXES` with a one-line note on why.
+ *
+ * Mutation check: change any `t(`ns.sub.${x}`)` call site to `t(`ns.${x}`)`
+ * and the collector-violation assertion goes red; delete a live call site and
+ * its keys report as orphans.
*/
import { describe, it, expect } from "vitest";
import { readFileSync, readdirSync, statSync } from "node:fs";
import { join } from "node:path";
+import { collectReferences, isReferenced } from "./helpers/i18n-reference";
+
const ROOT = join(__dirname, "../..");
const SRC = join(ROOT, "src");
const EN_BUNDLE_PATH = join(ROOT, "messages/en.json");
@@ -45,7 +44,30 @@ const SKIP_DIRS = new Set(["__tests__", "node_modules", ".next", "generated"]);
* is built from a `MoodTag.messageKey` / `MoodTagCategory.messageKey` column,
* so the leaf never appears as a string literal in the tree.
*/
-const DYNAMIC_ALLOWLIST_PREFIXES = ["mood.tag", "mood.tagCategory"] as const;
+const DYNAMIC_ALLOWLIST_PREFIXES = [
+ "mood.tag",
+ "mood.tagCategory",
+ // The whole namespace is a runtime lookup table: the client resolves an
+ // error envelope's `meta.errorCode` through it (`localizedApiError`), so no
+ // leaf ever appears as a literal at a call site. Adding a key here is only
+ // useful together with a server code that emits it.
+ "apiErrors",
+] as const;
+
+/**
+ * Interpolating strings that LOOK like key templates and are not. Each entry
+ * whitelists nothing — the template still contributes no reachable keys — it
+ * only says the collector's bare-namespace complaint has been read and
+ * answered. Keep the reason checkable in one line of the cited file.
+ */
+const EXEMPT_TEMPLATES: Record = {
+ "insights.${scope}-status.${locale}":
+ "auditLog action name for the per-metric status cache (statusCacheAction), not an i18n key",
+ "insights.${scope}.${locale}":
+ "auditLog action name for the previous-insight memory read (getPreviousInsightContext), not an i18n key",
+ "apiErrors.${errorCode}":
+ "the runtime error-code lookup; its namespace is allowlisted above",
+};
function walk(dir: string, out: string[] = []): string[] {
for (const name of readdirSync(dir)) {
@@ -84,153 +106,58 @@ function nodePaths(obj: unknown, prefix: string, out: Set): void {
}
}
-interface Reference {
- /** Literal `t("…")` keys. */
- literalKeys: Set;
- /** Dotted bases (`base.${…}` / a full string-literal node path): cover the subtree. */
- dottedBases: Set;
- /** Dot-less concat prefixes (`medications.site${…}`): cover any leaf that string-starts with them. */
- concatPrefixes: Set;
- /** Trailing static suffixes of a template key (`${…}.title`). */
- dynamicSuffixes: Set;
-}
-
-function collectReferences(
- allNodes: Set,
- topNamespaces: Set,
-): Reference {
- const allText = walk(SRC)
- .map((f) => readFileSync(f, "utf8"))
- .join("\n");
-
- const literalKeys = new Set();
- const literalKeyRe =
- /(?One` /
- // `Few` / `Other` at runtime, so the base literal accounts for
- // exactly those three leaves and no others. Kept as an explicit tier list
- // rather than a prefix match so a stray sibling key under the same base still
- // reports as an orphan.
- const pluralKeyRe =
- /(?:pluralKey|tCount)\(\s*["']([a-zA-Z][a-zA-Z0-9_.-]+)["']/g;
- for (const m of allText.matchAll(pluralKeyRe)) {
- for (const tier of ["One", "Few", "Other"])
- literalKeys.add(`${m[1]}${tier}`);
- }
-
- const dottedBases = new Set();
- const concatPrefixes = new Set();
- const dynamicSuffixes = new Set();
-
- const ingestTemplate = (content: string) => {
- const open = content.indexOf("${");
- if (open > 0) {
- const lit = content.slice(0, open);
- if (lit.includes(".")) {
- if (lit.endsWith(".")) dottedBases.add(lit.slice(0, -1));
- else concatPrefixes.add(lit);
- }
- }
- const close = content.lastIndexOf("}");
- if (close !== -1 && close < content.length - 1) {
- const tail = content.slice(close + 1).replace(/^\./, "");
- if (tail) dynamicSuffixes.add(tail);
- }
- };
-
- // Templates passed directly to t(`…`).
- const tTemplateRe = /(? leaf === p || leaf.startsWith(`${p}.`),
);
}
-describe("i18n reverse coverage", () => {
- it("every key in messages/en.json has a call site or is allowlisted", () => {
- const en = JSON.parse(readFileSync(EN_BUNDLE_PATH, "utf8"));
- const leaves: string[] = [];
- leafPaths(en, "", leaves);
- expect(leaves.length).toBeGreaterThan(1000);
+function scan() {
+ const en = JSON.parse(readFileSync(EN_BUNDLE_PATH, "utf8"));
+ const leaves: string[] = [];
+ leafPaths(en, "", leaves);
- const allNodes = new Set();
- nodePaths(en, "", allNodes);
- const topNamespaces = new Set(Object.keys(en));
- const ref = collectReferences(allNodes, topNamespaces);
+ const allNodes = new Set();
+ nodePaths(en, "", allNodes);
+ const topNamespaces = new Set(Object.keys(en));
+
+ const allText = walk(SRC)
+ .map((f) => readFileSync(f, "utf8"))
+ .join("\n");
+
+ return { leaves, ...collectReferences(allText, allNodes, topNamespaces) };
+}
+
+describe("i18n reverse coverage", () => {
+ it("no key template widens to a bare top-level namespace", () => {
+ const all = scan().violations;
+ const violations = all.filter((v) => !(v.literal in EXEMPT_TEMPLATES));
+
+ for (const literal of Object.keys(EXEMPT_TEMPLATES)) {
+ expect(
+ all.map((v) => v.literal),
+ `${literal} is exempted but no longer present — drop the entry`,
+ ).toContain(literal);
+ }
+
+ if (violations.length > 0) {
+ const report = violations
+ .map((v) => ` ❌ [${v.arm}] ${v.literal}`)
+ .join("\n");
+ throw new Error(
+ `Found ${violations.length} key template(s) whose only static anchor is a bare ` +
+ `top-level namespace:\n${report}\n\n` +
+ "Such a template marks the whole namespace as reachable and turns this guard " +
+ "into a no-op for it. Either build a fully-qualified key at the call site " +
+ "(see the measurement-reminder due helpers), or give the template a literal " +
+ "suffix so both ends confine it.",
+ );
+ }
+ }, 15_000);
+
+ it("every key in messages/en.json has a call site or is allowlisted", () => {
+ const { leaves, ref } = scan();
+ expect(leaves.length).toBeGreaterThan(1000);
// Sanity: the static scan must see the bulk of the bundle.
expect(ref.literalKeys.size).toBeGreaterThan(1000);
diff --git a/src/__tests__/measurement-backup-completeness.test.ts b/src/__tests__/measurement-backup-completeness.test.ts
new file mode 100644
index 000000000..6efa542fa
--- /dev/null
+++ b/src/__tests__/measurement-backup-completeness.test.ts
@@ -0,0 +1,164 @@
+/**
+ * v1.32.36 — pair guard binding `prisma/schema.prisma`'s `Measurement` model to
+ * the disaster-recovery backup select.
+ *
+ * The failure it exists to catch: a column is added to the model in one release
+ * and the backup select is "the follow-up". Nothing fails — the backup keeps
+ * building, the restore keeps parsing (the payload schema is `.passthrough()`),
+ * and the column simply comes back NULL on every restored row. That is how the
+ * aggregation provenance was silently dropped from every disaster-recovery
+ * backup between the release that added it and the one that noticed.
+ *
+ * Both ends of this comparison are literals — a Prisma model block and a named
+ * select object — so the matcher is exact. No grep heuristics, no fuzzy name
+ * matching, nothing that can quietly over-accept. That soundness is the reason
+ * this guard is worth having as a gate while the broader "does any column have
+ * a reader" question is only worth a release-battery script.
+ *
+ * Scoped to `Measurement` on purpose. Extend it model by model as each becomes
+ * backup-relevant; converting every model at once buys exclusion-list churn
+ * rather than safety.
+ *
+ * Mutation check: comment out `sleepStage: true` in `MEASUREMENT_BACKUP_SELECT`
+ * and this goes red naming that column; add a scalar to the model without
+ * adding it to the select and it goes red naming the new one.
+ */
+import { readFileSync } from "node:fs";
+import { join } from "node:path";
+
+import { describe, it, expect } from "vitest";
+
+const ROOT = process.cwd();
+const SCHEMA = join(ROOT, "prisma", "schema.prisma");
+const BACKUP_BUILDER = join(
+ ROOT,
+ "src",
+ "lib",
+ "export",
+ "full-backup-payload.ts",
+);
+
+/**
+ * Columns the backup is correct to omit, each with the reason.
+ * - `userId`: the backup is user-scoped. The owner is implicit in the query
+ * and re-derived from the restoring account; carrying it would let a
+ * payload claim a different owner.
+ */
+const EXCLUDED_COLUMNS: Record = {
+ userId: "user-scoped backup; owner is re-derived from the restoring account",
+};
+
+function modelBlock(schema: string, model: string): string {
+ const start = schema.indexOf(`model ${model} {`);
+ expect(start, `model ${model} not found`).toBeGreaterThan(-1);
+ const end = schema.indexOf("\n}", start);
+ expect(end).toBeGreaterThan(start);
+ return schema.slice(start, end);
+}
+
+/** Names of every `model` / `type` block, so relation fields can be told apart. */
+function declaredModels(schema: string): Set {
+ const out = new Set();
+ for (const m of schema.matchAll(/^model\s+([A-Za-z_]\w*)\s*\{/gm)) {
+ out.add(m[1]);
+ }
+ return out;
+}
+
+/**
+ * Scalar (column-backed) fields of a model block: everything that is not a
+ * relation to another model. Enums and native scalars both map to columns and
+ * therefore both belong in a backup.
+ */
+function scalarFields(block: string, models: Set): string[] {
+ const fields: string[] = [];
+ for (const rawLine of block.split("\n").slice(1)) {
+ const line = rawLine.trim();
+ if (!line || line.startsWith("//") || line.startsWith("@@")) continue;
+ const m = /^([a-zA-Z_]\w*)\s+([A-Za-z_]\w*)(\[\])?(\?)?/.exec(line);
+ if (!m) continue;
+ const [, name, type, isList] = m;
+ if (isList) continue;
+ if (models.has(type)) continue;
+ if (line.includes("@relation")) continue;
+ fields.push(name);
+ }
+ return fields;
+}
+
+/** Keys of the named `MEASUREMENT_BACKUP_SELECT` literal. */
+function selectFields(source: string): string[] {
+ const start = source.indexOf("const MEASUREMENT_BACKUP_SELECT = {");
+ expect(start).toBeGreaterThan(-1);
+ const end = source.indexOf("\n}", start);
+ expect(end).toBeGreaterThan(start);
+ const body = source.slice(start, end);
+ return [...body.matchAll(/^ {2}([a-zA-Z_$][\w$]*): true,$/gm)].map(
+ (m) => m[1],
+ );
+}
+
+/**
+ * Fields either serialization arm actually emits. The two arms share one
+ * restorable wire format, so this is a union and therefore a weaker claim than
+ * the select comparison: it catches a column that is selected and then written
+ * to neither payload, not a column carried by only one arm.
+ */
+function serialisedFields(source: string): Set {
+ return new Set(
+ [...source.matchAll(/([a-zA-Z_$][\w$]*): measurement\./g)].map((m) => m[1]),
+ );
+}
+
+describe("measurement backup completeness", () => {
+ const schema = readFileSync(SCHEMA, "utf8");
+ const source = readFileSync(BACKUP_BUILDER, "utf8");
+ const models = declaredModels(schema);
+ const columns = scalarFields(modelBlock(schema, "Measurement"), models);
+ const selected = selectFields(source);
+
+ it("parses both ends into a plausible shape", () => {
+ expect(models.size).toBeGreaterThan(50);
+ expect(columns.length).toBeGreaterThan(15);
+ expect(columns).toContain("aggregationProvenance");
+ expect(columns).not.toContain("user");
+ expect(columns).not.toContain("ecgRecordings");
+ expect(selected.length).toBeGreaterThan(15);
+ });
+
+ it("the backup select covers every Measurement column", () => {
+ const missing = columns.filter(
+ (c) => !selected.includes(c) && !(c in EXCLUDED_COLUMNS),
+ );
+
+ if (missing.length > 0) {
+ const report = missing.map((c) => ` ❌ ${c}`).join("\n");
+ throw new Error(
+ `MEASUREMENT_BACKUP_SELECT omits ${missing.length} column(s) of the Measurement model:\n${report}\n\n` +
+ "A disaster-recovery restore brings these back NULL. Add each to the " +
+ "select and to both serialization arms, extend the backup validation " +
+ "schema and the restore create — or add it to EXCLUDED_COLUMNS with " +
+ "the reason the backup is correct to drop it.",
+ );
+ }
+ });
+
+ it("every selected column reaches a serialized payload", () => {
+ const emitted = serialisedFields(source);
+ const dropped = selected.filter((c) => !emitted.has(c));
+ expect(
+ dropped,
+ `selected but serialized by neither arm: ${dropped}`,
+ ).toHaveLength(0);
+ });
+
+ it("the exclusion list carries no stale entry", () => {
+ for (const [column, reason] of Object.entries(EXCLUDED_COLUMNS)) {
+ expect(
+ columns,
+ `${column} is excluded but is no longer a Measurement column`,
+ ).toContain(column);
+ expect(reason.length).toBeGreaterThan(20);
+ }
+ });
+});
diff --git a/src/app/insights/__tests__/explainer-metric-keys.test.ts b/src/app/insights/__tests__/explainer-metric-keys.test.ts
index b2782a0f8..b9c867def 100644
--- a/src/app/insights/__tests__/explainer-metric-keys.test.ts
+++ b/src/app/insights/__tests__/explainer-metric-keys.test.ts
@@ -11,10 +11,11 @@ import { join } from "node:path";
* t(`insights.subPage.explainer.${metric}Body`) // inline definition
*
* v1.8.6 — the round `?` popover that also read the `Title` leaf
- * is gone; the shell now renders only the inline `Body` caption. The
- * `Title` leaf is still pinned here so the bundle keeps a stable, fully
- * populated key pair per metric (the locale-integrity guard propagates
- * it across the other five locales).
+ * is gone; the shell now renders only the inline `Body` caption. v1.32.36
+ * finished that removal: the `Title` leaves were kept as a "stable, fully
+ * populated key pair" reserve, which is translated copy no surface renders,
+ * so they were dropped from all six locales and this guard now pins the
+ * `Body` leaf alone.
*
* The static `i18n-call-site-coverage.test.ts` only sees literal
* `t("…")` calls, so a typo in an `explainerMetric=` prop would resolve
@@ -22,19 +23,16 @@ import { join } from "node:path";
* no CI signal. This test closes that gap the same way the
* Health-Score provenance key tests pin their dynamic keys: it walks
* every `explainerMetric="…"` value used under `src/app/insights/**`
- * (plus the shared `healthkit-metric-page` scaffold) and asserts that
- * both the `Title` and `Body` leaves exist in
- * `messages/en.json`. `i18n-locale-integrity.test.ts` then propagates
- * the EN guarantee across the other five locales.
+ * (plus the shared `healthkit-metric-page` scaffold) and asserts that the
+ * `Body` leaf exists in `messages/en.json`.
+ * `i18n-locale-integrity.test.ts` then propagates the EN guarantee across
+ * the other five locales.
*
* v1.15.12 B1 — the score-anatomy detail page (`scores/[metric]/page.tsx`)
* picks its explainer key at runtime from a closed `Record<…, string>`
* literal (`explainerMetric={EXPLAINER_METRIC[metric]}`), one of five score
* suffixes. That single dynamic site is resolved STATICALLY here by parsing
- * the record's string-literal values (the contract stays verifiable). Score
- * explainers render only the inline `Body` caption — there is no `?` popover
- * `Title` for them — so the resolved score values are checked for a `Body`
- * leaf only, while literal sites keep the full Title + Body pairing.
+ * the record's string-literal values (the contract stays verifiable).
*/
const ROOT = join(__dirname, "../../../..");
@@ -62,8 +60,6 @@ interface PropSite {
value: string;
file: string;
line: number;
- /** Score explainers render only the inline Body caption (no `?` Title). */
- bodyOnly?: boolean;
}
// Match `explainerMetric="someValue"` — a string-literal prop. The
@@ -116,12 +112,12 @@ function collectSites(): { sites: PropSite[]; dynamic: PropSite[] } {
}
if (DYNAMIC_PATTERN.test(line)) {
// The score-anatomy record lookup is statically resolvable; every
- // resolved value becomes a Body-only site. Any OTHER dynamic shape
- // is unverifiable and trips the no-dynamic guard.
+ // resolved value becomes a site. Any OTHER dynamic shape is
+ // unverifiable and trips the no-dynamic guard.
const lookup = RECORD_LOOKUP_PATTERN.exec(line);
if (file.endsWith(SCORES_PAGE) && lookup) {
for (const value of resolveRecordValues(source, lookup[1])) {
- sites.push({ value, file: rel, line: i + 1, bodyOnly: true });
+ sites.push({ value, file: rel, line: i + 1 });
}
} else {
dynamic.push({ value: line.trim(), file: rel, line: i + 1 });
@@ -165,15 +161,10 @@ describe("insights explainerMetric key coverage", () => {
expect(dynamic).toEqual([]);
});
- it("every explainerMetric value resolves a Title + Body leaf in messages/en.json", () => {
+ it("every explainerMetric value resolves a Body leaf in messages/en.json", () => {
const missing: string[] = [];
for (const site of sites) {
- const titleKey = `insights.subPage.explainer.${site.value}Title`;
const bodyKey = `insights.subPage.explainer.${site.value}Body`;
- // Score explainers (bodyOnly) carry only the inline Body caption.
- if (!site.bodyOnly && !hasLeaf(en, titleKey)) {
- missing.push(` ❌ ${titleKey} (${site.file}:${site.line})`);
- }
if (!hasLeaf(en, bodyKey)) {
missing.push(` ❌ ${bodyKey} (${site.file}:${site.line})`);
}
diff --git a/src/components/measurement-reminders/vorsorge-dashboard-card.tsx b/src/components/measurement-reminders/vorsorge-dashboard-card.tsx
index 2f8cb057f..cd101028d 100644
--- a/src/components/measurement-reminders/vorsorge-dashboard-card.tsx
+++ b/src/components/measurement-reminders/vorsorge-dashboard-card.tsx
@@ -37,16 +37,29 @@ import {
const DAY_MS = 24 * 60 * 60 * 1000;
const SUMMARY_LIMIT = 3;
+/**
+ * v1.32.36 — returns a FULLY-QUALIFIED i18n key, not a bare tail. A call site
+ * that interpolates the tail onto the namespace root anchors the
+ * reverse-coverage guard at that bare namespace, which marks every key under
+ * it reachable and turns the guard into a no-op for the whole namespace. The
+ * guard now refuses that shape outright, so the key is built whole here.
+ */
function relativeDueKey(
nextDueAt: string | null,
now: number,
): { key: string; days: number } {
- if (!nextDueAt) return { key: "nextDue.none", days: 0 };
+ if (!nextDueAt) return { key: "measurementReminders.nextDue.none", days: 0 };
const deltaDays = Math.round((new Date(nextDueAt).getTime() - now) / DAY_MS);
- if (deltaDays < 0) return { key: "overdueByDays", days: Math.abs(deltaDays) };
- if (deltaDays === 0) return { key: "nextDue.today", days: 0 };
- if (deltaDays === 1) return { key: "nextDue.tomorrow", days: 1 };
- return { key: "nextDue.inDays", days: deltaDays };
+ if (deltaDays < 0)
+ return {
+ key: "measurementReminders.overdueByDays",
+ days: Math.abs(deltaDays),
+ };
+ if (deltaDays === 0)
+ return { key: "measurementReminders.nextDue.today", days: 0 };
+ if (deltaDays === 1)
+ return { key: "measurementReminders.nextDue.tomorrow", days: 1 };
+ return { key: "measurementReminders.nextDue.inDays", days: deltaDays };
}
function resolveLabel(
@@ -141,7 +154,7 @@ export function VorsorgeDashboardCard() {
{resolveLabel(reminder, t)}
- {t(`measurementReminders.${due.key}`, {
+ {t(due.key, {
days: due.days,
})}
diff --git a/src/components/measurement-reminders/vorsorge-section.tsx b/src/components/measurement-reminders/vorsorge-section.tsx
index c66f23a12..5a1d697a7 100644
--- a/src/components/measurement-reminders/vorsorge-section.tsx
+++ b/src/components/measurement-reminders/vorsorge-section.tsx
@@ -143,13 +143,21 @@ const CADENCE_CUSTOM = "custom";
const CADENCE_RRULE = "rrule";
const DAY_MS = 24 * 60 * 60 * 1000;
+/**
+ * v1.32.36 — returns a FULLY-QUALIFIED i18n key, not a bare tail. A call site
+ * that interpolates the tail onto the namespace root anchors the
+ * reverse-coverage guard at that bare namespace, which marks every key under
+ * it reachable and turns the guard into a no-op for the whole namespace. The
+ * guard now refuses that shape outright, so the key is built whole here.
+ */
function relativeDueKey(
nextDueAt: string | null,
now: number,
): { key: string; days: number } {
- if (!nextDueAt) return { key: "nextDue.none", days: 0 };
+ if (!nextDueAt) return { key: "measurementReminders.nextDue.none", days: 0 };
const due = new Date(nextDueAt);
- if (Number.isNaN(due.getTime())) return { key: "nextDue.none", days: 0 };
+ if (Number.isNaN(due.getTime()))
+ return { key: "measurementReminders.nextDue.none", days: 0 };
// v1.18.9 (recon) — compare CALENDAR-day deltas in the user's local zone,
// not a rolling-24h delta. A reminder due at 09:00 viewed the same evening
// at 20:00 must still read "heute", and one whose due calendar-day is before
@@ -159,10 +167,16 @@ function relativeDueKey(
const dueDay = startOfLocalDayInTz(due, undefined).getTime();
const nowDay = startOfLocalDayInTz(new Date(now), undefined).getTime();
const deltaDays = Math.round((dueDay - nowDay) / DAY_MS);
- if (deltaDays < 0) return { key: "overdueByDays", days: Math.abs(deltaDays) };
- if (deltaDays === 0) return { key: "nextDue.today", days: 0 };
- if (deltaDays === 1) return { key: "nextDue.tomorrow", days: 1 };
- return { key: "nextDue.inDays", days: deltaDays };
+ if (deltaDays < 0)
+ return {
+ key: "measurementReminders.overdueByDays",
+ days: Math.abs(deltaDays),
+ };
+ if (deltaDays === 0)
+ return { key: "measurementReminders.nextDue.today", days: 0 };
+ if (deltaDays === 1)
+ return { key: "measurementReminders.nextDue.tomorrow", days: 1 };
+ return { key: "measurementReminders.nextDue.inDays", days: deltaDays };
}
/**
@@ -831,7 +845,9 @@ function VorsorgeCard({
// v1.27.6 — a screening reminder's action is "start the check-in" (it
// routes to the check-in page), never the numeric value-entry wording.
const isScreening = isScreeningReminderType(reminder.measurementType);
- const isDue = due.key === "nextDue.today" || due.key === "overdueByDays";
+ const isDue =
+ due.key === "measurementReminders.nextDue.today" ||
+ due.key === "measurementReminders.overdueByDays";
const progress = intervalProgress(reminder, now);
// Category-style header badge: the measurement label, or "self-planned"
@@ -984,7 +1000,7 @@ function VorsorgeCard({
metric text (no shaded pill). */}
- {t(`measurementReminders.${due.key}`, { days: due.days })}
+ {t(due.key, { days: due.days })}
{categoryLabel}
{cadenceText ?
{cadenceText} : null}
@@ -1037,8 +1053,8 @@ function VorsorgeCard({
// a card-background tint): due today → green, overdue → orange, otherwise the
// normal muted-resolved date. The card surface stays neutral per the house
// rule; only this inline label carries the status hue.
- const dueIsToday = due.key === "nextDue.today";
- const dueIsOverdue = due.key === "overdueByDays";
+ const dueIsToday = due.key === "measurementReminders.nextDue.today";
+ const dueIsOverdue = due.key === "measurementReminders.overdueByDays";
const nextLastSlot = (
@@ -1055,7 +1071,7 @@ function VorsorgeCard({
: "text-foreground",
)}
>
- {t(`measurementReminders.${due.key}`, { days: due.days })}
+ {t(due.key, { days: due.days })}
diff --git a/src/components/mental-health/check-in-wizard.tsx b/src/components/mental-health/check-in-wizard.tsx
index 6a6795462..261da140d 100644
--- a/src/components/mental-health/check-in-wizard.tsx
+++ b/src/components/mental-health/check-in-wizard.tsx
@@ -208,9 +208,7 @@ export function CheckInWizard({
>
{onFunctionalStep
? t(`mentalHealth.functional.${v}`)
- : t(
- `mentalHealth.${optionLabelKey(instrument, questionIndex, v)}`,
- )}
+ : t(optionLabelKey(instrument, questionIndex, v))}
);
})}
diff --git a/src/components/settings/integrations/connections-panel.tsx b/src/components/settings/integrations/connections-panel.tsx
index 0a7d4a6e1..30b238846 100644
--- a/src/components/settings/integrations/connections-panel.tsx
+++ b/src/components/settings/integrations/connections-panel.tsx
@@ -93,6 +93,23 @@ const OAUTH_OUTCOME_KEYS: Record<
strava: queryKeys.strava,
};
+/**
+ * v1.32.36 — the error-copy subtree per provider, spelled out rather than
+ * interpolated from the provider id. Building the key from the bare `settings`
+ * root left the reverse-i18n guard with no anchor deeper than the namespace,
+ * which marked every key under `settings.*` reachable and let genuinely dead
+ * copy sit in the bundle unnoticed. Written out, each base is a full node path
+ * the guard resolves on its own.
+ */
+const OAUTH_ERROR_KEY_BASE: Record = {
+ polar: "settings.polarOauthError",
+ oura: "settings.ouraOauthError",
+ whoop: "settings.whoopOauthError",
+ fitbit: "settings.fitbitOauthError",
+ googleHealth: "settings.googleHealthOauthError",
+ strava: "settings.stravaOauthError",
+};
+
/**
* Reason tags the four OAuth callbacks emit. Known tags resolve to a specific
* message; anything else falls back to the provider's `generic` copy. Union of
@@ -143,9 +160,10 @@ export function oauthReasonKey(
provider: OAuthOutcomeProvider,
reason: string,
): string {
+ const base = OAUTH_ERROR_KEY_BASE[provider];
return OAUTH_OUTCOME_REASONS.has(reason)
- ? `settings.${provider}OauthError.${reason}`
- : `settings.${provider}OauthError.generic`;
+ ? `${base}.${reason}`
+ : `${base}.generic`;
}
/**
diff --git a/src/lib/__tests__/i18n-locale-integrity.test.ts b/src/lib/__tests__/i18n-locale-integrity.test.ts
index fb460441b..9dae78924 100644
--- a/src/lib/__tests__/i18n-locale-integrity.test.ts
+++ b/src/lib/__tests__/i18n-locale-integrity.test.ts
@@ -388,7 +388,6 @@ describe("i18n locale file integrity", () => {
it("Health-Score provenance keys resolve in every locale", () => {
const PROVENANCE_KEYS = [
"insights.healthScore.provenance.toggle",
- "insights.healthScore.provenance.weightLabel",
"insights.healthScore.provenance.mixedBanner",
"insights.healthScore.provenance.footnote",
"insights.healthScore.provenance.asOfLabel",
diff --git a/src/lib/export/full-backup-payload.ts b/src/lib/export/full-backup-payload.ts
index e0c813eb5..835dade03 100644
--- a/src/lib/export/full-backup-payload.ts
+++ b/src/lib/export/full-backup-payload.ts
@@ -11,7 +11,7 @@
* artefact); an admin restore re-encrypts them on re-insert.
*/
import { Buffer } from "node:buffer";
-import type { PrismaClient } from "@/generated/prisma/client";
+import type { Prisma, PrismaClient } from "@/generated/prisma/client";
import { readNote } from "@/lib/crypto/note-cipher";
import { iterateMeasurementPages } from "@/lib/export/paged-measurements";
import { BACKUP_SCHEMA_VERSION } from "@/lib/validations/backup";
@@ -41,6 +41,40 @@ export interface FullBackupOptions {
purpose?: "portable-export" | "disaster-recovery";
exportedAt?: Date;
}
+/**
+ * Every restorable `Measurement` scalar. `userId` is deliberately absent — the
+ * backup is user-scoped, so the owner is implicit in the query and restored
+ * from the target account rather than the payload.
+ *
+ * Named rather than inlined so `measurement-backup-completeness.test.ts` can
+ * compare it field-by-field against the model in `prisma/schema.prisma`. A
+ * column added to the model and forgotten here is how the aggregation
+ * provenance went missing from disaster-recovery backups for several releases.
+ */
+const MEASUREMENT_BACKUP_SELECT = {
+ id: true,
+ type: true,
+ value: true,
+ valueMin: true,
+ valueMax: true,
+ unit: true,
+ measuredAt: true,
+ source: true,
+ notes: true,
+ notesEncrypted: true,
+ externalId: true,
+ externalSourceVersion: true,
+ aggregationProvenance: true,
+ glucoseContext: true,
+ sleepStage: true,
+ rhythmClassification: true,
+ deviceType: true,
+ syncVersion: true,
+ deletedAt: true,
+ createdAt: true,
+ updatedAt: true,
+} as const satisfies Prisma.MeasurementSelect;
+
/**
* Build the canonical full-backup payload for `userId`. Portable exports omit
* tombstones and document ciphertext; disaster-recovery payloads preserve
@@ -70,29 +104,7 @@ export async function buildFullBackupPayload(
const pages = iterateMeasurementPages(
prisma,
disasterRecovery ? { userId } : { userId, deletedAt: null },
- {
- id: true,
- type: true,
- value: true,
- valueMin: true,
- valueMax: true,
- unit: true,
- measuredAt: true,
- source: true,
- notes: true,
- notesEncrypted: true,
- externalId: true,
- externalSourceVersion: true,
- aggregationProvenance: true,
- glucoseContext: true,
- sleepStage: true,
- rhythmClassification: true,
- deviceType: true,
- syncVersion: true,
- deletedAt: true,
- createdAt: true,
- updatedAt: true,
- },
+ MEASUREMENT_BACKUP_SELECT,
);
for await (const page of pages) {
for (const measurement of page) {
diff --git a/src/lib/mental-health/__tests__/instruments.test.ts b/src/lib/mental-health/__tests__/instruments.test.ts
index 55cdf3d9d..4a3be9b4d 100644
--- a/src/lib/mental-health/__tests__/instruments.test.ts
+++ b/src/lib/mental-health/__tests__/instruments.test.ts
@@ -165,14 +165,24 @@ describe("mental-health instruments", () => {
});
it("resolves option-label keys per scheme (shared vs SCI per-item groups)", () => {
- expect(optionLabelKey("PHQ9", 0, 2)).toBe("options.2");
- expect(optionLabelKey("WHO5", 4, 5)).toBe("who5Options.5");
+ expect(optionLabelKey("PHQ9", 0, 2)).toBe("mentalHealth.options.2");
+ expect(optionLabelKey("WHO5", 4, 5)).toBe("mentalHealth.who5Options.5");
// SCI: items 1–2 duration, 3 nights, 4 quality, 5–7 impact, 8 problem duration.
- expect(optionLabelKey("SCI", 0, 4)).toBe("sciOptions.duration.4");
- expect(optionLabelKey("SCI", 2, 0)).toBe("sciOptions.nights.0");
- expect(optionLabelKey("SCI", 3, 2)).toBe("sciOptions.quality.2");
- expect(optionLabelKey("SCI", 6, 1)).toBe("sciOptions.impact.1");
- expect(optionLabelKey("SCI", 7, 3)).toBe("sciOptions.problemDuration.3");
+ expect(optionLabelKey("SCI", 0, 4)).toBe(
+ "mentalHealth.sciOptions.duration.4",
+ );
+ expect(optionLabelKey("SCI", 2, 0)).toBe(
+ "mentalHealth.sciOptions.nights.0",
+ );
+ expect(optionLabelKey("SCI", 3, 2)).toBe(
+ "mentalHealth.sciOptions.quality.2",
+ );
+ expect(optionLabelKey("SCI", 6, 1)).toBe(
+ "mentalHealth.sciOptions.impact.1",
+ );
+ expect(optionLabelKey("SCI", 7, 3)).toBe(
+ "mentalHealth.sciOptions.problemDuration.3",
+ );
});
it("maps recall stems (WHO-5 one stem; SCI three sections; PHQ/GAD none)", () => {
diff --git a/src/lib/mental-health/instruments.ts b/src/lib/mental-health/instruments.ts
index 92103184b..f3e641c4c 100644
--- a/src/lib/mental-health/instruments.ts
+++ b/src/lib/mental-health/instruments.ts
@@ -49,10 +49,12 @@ export interface InstrumentDefinition {
*/
optionOrder: readonly number[];
/**
- * i18n namespace for the response-option labels under `mentalHealth.`:
- * without `optionGroups` one shared anchor set (`.`); with
+ * Fully-qualified i18n namespace for the response-option labels: without
+ * `optionGroups` one shared anchor set (`.`); with
* `optionGroups` (SCI) per-item anchor groups (`..`,
- * group = `optionGroups[itemIndex]`).
+ * group = `optionGroups[itemIndex]`). v1.32.36 — qualified rather than
+ * relative so the reverse-i18n guard resolves the subtree from the literal
+ * instead of being handed the bare `mentalHealth` namespace.
*/
optionNamespace: string;
optionGroups?: readonly string[];
@@ -113,7 +115,7 @@ const PHQ9: InstrumentDefinition = {
itemCount: 9,
itemMax: 3,
optionOrder: [0, 1, 2, 3],
- optionNamespace: "options",
+ optionNamespace: "mentalHealth.options",
scoreMultiplier: 1,
minScore: 0,
maxScore: 27,
@@ -139,7 +141,7 @@ const GAD7: InstrumentDefinition = {
itemCount: 7,
itemMax: 3,
optionOrder: [0, 1, 2, 3],
- optionNamespace: "options",
+ optionNamespace: "mentalHealth.options",
scoreMultiplier: 1,
minScore: 0,
maxScore: 21,
@@ -172,7 +174,7 @@ const WHO5: InstrumentDefinition = {
itemCount: 5,
itemMax: 5,
optionOrder: [5, 4, 3, 2, 1, 0],
- optionNamespace: "who5Options",
+ optionNamespace: "mentalHealth.who5Options",
stemKeys: Array(5).fill("who5.period"),
scoreMultiplier: 4,
minScore: 0,
@@ -207,7 +209,7 @@ const SCI: InstrumentDefinition = {
itemCount: 8,
itemMax: 4,
optionOrder: [4, 3, 2, 1, 0],
- optionNamespace: "sciOptions",
+ optionNamespace: "mentalHealth.sciOptions",
optionGroups: [
"duration",
"duration",