feat(mental-health): screener UI, locale-aware copy, and OpenAPI surface

Add the opt-in PHQ-9 / GAD-7 questionnaire surface at /insights/mental-wellbeing:
instrument picker, 0–3 item scale, server-scored result with the validated
severity band, the verbatim Pfizer/PHQ attribution, the screening-not-diagnosis
disclaimer, and a calm crisis-resource card on a positive item-9. The surface
deliberately does NOT mount the shared Coach-launch card — mental-health item
content stays out of the AI.

- i18n: full mentalHealth block plus the six new measurement-type labels and the
  longevity-marker catalogue names/descriptions across all six locales
  (de/en/es/fr/it/pl). Instrument item wording uses the validated German text and
  the canonical English elsewhere (re-wording invalidates the score).
- OpenAPI: regenerate the contract for /api/mental-health/assessments (response
  excludes raw item answers by construction).
- Exclude the new manual / instrument types from the HealthKit-counterpart guard.
This commit is contained in:
Marc-André Bombeck committed 2026-06-28 16:05:51 +02:00
1 parent 7575275d7e
commit c322a68d19
13 files changed
+1453 -22

No files matched your search

+268
View File
@@ -2087,6 +2087,12 @@ paths:
- SLEEP_SCORE
- BODY_TEMPERATURE_DEVIATION
- RESILIENCE
- PHQ9_SCORE
- GAD7_SCORE
- GRIP_STRENGTH
- PAIN_NRS
- WAIST_CIRCUMFERENCE
- WAIST_TO_HEIGHT
- in: query
name: from
schema:
@@ -2276,6 +2282,12 @@ paths:
- SLEEP_SCORE
- BODY_TEMPERATURE_DEVIATION
- RESILIENCE
- PHQ9_SCORE
- GAD7_SCORE
- GRIP_STRENGTH
- PAIN_NRS
- WAIST_CIRCUMFERENCE
- WAIST_TO_HEIGHT
value:
type: number
measuredAt:
@@ -5770,6 +5782,12 @@ paths:
- SLEEP_SCORE
- BODY_TEMPERATURE_DEVIATION
- RESILIENCE
- PHQ9_SCORE
- GAD7_SCORE
- GRIP_STRENGTH
- PAIN_NRS
- WAIST_CIRCUMFERENCE
- WAIST_TO_HEIGHT
description: "The measurement type to read (single metric — no fan-out). Closed enum: an unknown type 422s."
required: true
description: "The measurement type to read (single metric — no fan-out). Closed enum: an unknown type 422s."
@@ -7992,6 +8010,95 @@ paths:
"401": *a1
"422": *a3
"429": *a4
/api/mental-health/assessments:
get:
tags:
- Mental health
summary: List the caller's screener history
description: Returns PHQ-9 / GAD-7 administrations (newest first). Totals + bands + flags only; raw item answers are
never returned.
parameters:
- in: query
name: instrument
schema:
type: string
enum:
- PHQ9
- GAD7
- in: query
name: limit
schema:
type: integer
minimum: 1
maximum: 200
- in: query
name: offset
schema:
type: integer
minimum: 0
maximum: 9007199254740991
responses:
"200":
description: Screener history.
content:
application/json:
schema:
$ref: "#/components/schemas/ListMentalHealthAssessmentsEnvelope"
"401": *a1
"422": *a3
"429": *a4
post:
tags:
- Mental health
summary: Record a completed PHQ-9 / GAD-7 screener
description: Stores one administration (item answers encrypted) and writes the derived total as a PHQ9_SCORE /
GAD7_SCORE measurement. On a positive PHQ-9 item-9 the response carries the locale-aware crisis-resource set.
requestBody:
content:
application/json:
schema:
type: object
properties:
instrument:
type: string
enum:
- PHQ9
- GAD7
items:
minItems: 1
maxItems: 12
type: array
items:
type: integer
minimum: 0
maximum: 3
functionalDifficulty:
type: integer
minimum: 0
maximum: 3
takenAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$
tz:
type: string
maxLength: 64
locale:
type: string
maxLength: 16
required:
- instrument
- items
responses:
"201":
description: Assessment recorded.
content:
application/json:
schema:
$ref: "#/components/schemas/CreateMentalHealthAssessmentEnvelope"
"401": *a1
"422": *a3
"429": *a4
/api/onboarding/disclaimer:
post:
tags:
@@ -12353,6 +12460,12 @@ components:
- SLEEP_SCORE
- BODY_TEMPERATURE_DEVIATION
- RESILIENCE
- PHQ9_SCORE
- GAD7_SCORE
- GRIP_STRENGTH
- PAIN_NRS
- WAIST_CIRCUMFERENCE
- WAIST_TO_HEIGHT
value:
type: number
unit:
@@ -12669,6 +12782,12 @@ components:
- SLEEP_SCORE
- BODY_TEMPERATURE_DEVIATION
- RESILIENCE
- PHQ9_SCORE
- GAD7_SCORE
- GRIP_STRENGTH
- PAIN_NRS
- WAIST_CIRCUMFERENCE
- WAIST_TO_HEIGHT
additionalProperties:
type: array
items:
@@ -12746,6 +12865,12 @@ components:
- SLEEP_SCORE
- BODY_TEMPERATURE_DEVIATION
- RESILIENCE
- PHQ9_SCORE
- GAD7_SCORE
- GRIP_STRENGTH
- PAIN_NRS
- WAIST_CIRCUMFERENCE
- WAIST_TO_HEIGHT
value:
type: number
description: Daily mean (spot metrics) or SUM (cumulative).
@@ -12844,6 +12969,12 @@ components:
- SLEEP_SCORE
- BODY_TEMPERATURE_DEVIATION
- RESILIENCE
- PHQ9_SCORE
- GAD7_SCORE
- GRIP_STRENGTH
- PAIN_NRS
- WAIST_CIRCUMFERENCE
- WAIST_TO_HEIGHT
description: Per-type daily series, keyed by MeasurementType.
required:
- series
@@ -13077,6 +13208,12 @@ components:
- SLEEP_SCORE
- BODY_TEMPERATURE_DEVIATION
- RESILIENCE
- PHQ9_SCORE
- GAD7_SCORE
- GRIP_STRENGTH
- PAIN_NRS
- WAIST_CIRCUMFERENCE
- WAIST_TO_HEIGHT
value:
type: number
unit:
@@ -23363,6 +23500,137 @@ components:
(median) recovery gap (null below the min-sample floor — withholds a thin claim), a recurrence-by-month tally, a
per-type breakdown, and the dominant driving vital (`gapDriverType`, null when no gap is surfaced).
Retrospective only."
ListMentalHealthAssessmentsEnvelope:
type: object
properties:
data:
$ref: "#/components/schemas/ListMentalHealthAssessmentsResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
ListMentalHealthAssessmentsResponse:
type: object
properties:
assessments:
type: array
items:
$ref: "#/components/schemas/MentalHealthAssessment"
required:
- assessments
additionalProperties: false
MentalHealthAssessment:
type: object
properties:
id:
type: string
instrument:
type: string
enum:
- PHQ9
- GAD7
locale:
type: string
version:
type: string
totalScore:
type: number
severityBand:
type: string
item9Flagged:
type: boolean
crisisShownAt:
anyOf:
- type: string
- type: "null"
takenAt:
type: string
createdAt:
type: string
required:
- id
- instrument
- locale
- version
- totalScore
- severityBand
- item9Flagged
- crisisShownAt
- takenAt
- createdAt
additionalProperties: false
description: One completed screener administration. `severityBand` is a descriptive label for the screen (never a
diagnosis). `item9Flagged` is true when the PHQ-9 self-harm item was answered > 0 — the client MUST show crisis
resources when it is. Raw per-item answers are intentionally absent.
CreateMentalHealthAssessmentEnvelope:
type: object
properties:
data:
$ref: "#/components/schemas/CreateMentalHealthAssessmentResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
CreateMentalHealthAssessmentResponse:
type: object
properties:
assessment:
$ref: "#/components/schemas/MentalHealthAssessment"
actionThreshold:
type: number
crisis:
anyOf:
- $ref: "#/components/schemas/CrisisResourceSet"
- type: "null"
required:
- assessment
- actionThreshold
- crisis
additionalProperties: false
CrisisResourceSet:
type: object
properties:
emergencyNumber:
type: string
resources:
type: array
items:
$ref: "#/components/schemas/CrisisResource"
required:
- emergencyNumber
- resources
additionalProperties: false
description: Locale-aware crisis-resource signposting, present only when item-9 is flagged. Names resolve via i18n on
the client; contacts are literal.
CrisisResource:
type: object
properties:
id:
type: string
contacts:
type: array
items:
type: string
required:
- id
- contacts
additionalProperties: false
DisclaimerAckEnvelope:
type: object
properties:
+130 -3
View File
@@ -575,7 +575,13 @@
"bulkDeleteSuccess": "{count} Messungen gelöscht",
"bulkDeleteError": "Fehler beim Löschen der ausgewählten Messungen",
"bulkDeleteConfirmTitle": "{count} Messungen löschen?",
"bulkDeleteConfirmBody": "Die {count} ausgewählten Messungen werden endgültig gelöscht."
"bulkDeleteConfirmBody": "Die {count} ausgewählten Messungen werden endgültig gelöscht.",
"typePhq9Score": "PHQ-9-Wert",
"typeGad7Score": "GAD-7-Wert",
"typeGripStrength": "Griffstärke",
"typePainNrs": "Schmerz (0–10)",
"typeWaistCircumference": "Taillenumfang",
"typeWaistToHeight": "Taille-Größe-Verhältnis"
},
"mood": {
"title": "Stimmung",
@@ -7090,8 +7096,16 @@
"hemoglobin": "Das sauerstofftransportierende Protein in den roten Blutkörperchen.",
"hematocrit": "Der Anteil der roten Blutkörperchen am Blutvolumen.",
"wbc": "Gesamtzahl der weißen Blutkörperchen, der zirkulierenden Abwehr des Immunsystems.",
"platelets": "Zellfragmente, die sich zusammenlagern und die Blutgerinnung bewirken."
}
"platelets": "Zellfragmente, die sich zusammenlagern und die Blutgerinnung bewirken.",
"apob": "Zählt atherogene Partikel direkt; oft ein besserer Risikomarker als LDL-C, wenn beide auseinandergehen.",
"lp-a": "Ein weitgehend vererbter, lebenslanger Risikofaktor — meist einmal gemessen. In molaren Einheiten (nmol/L) angeben.",
"fasting-insulin": "Ergänzt den Nüchtern-Blutzucker zur Abschätzung der Insulinresistenz (HOMA-IR).",
"omega-3-index": "Anteil von EPA+DHA in den roten Blutkörperchen; 8–12 % gelten als kardioprotektiver Bereich."
},
"apob": "Apolipoprotein B (ApoB)",
"lp-a": "Lipoprotein(a)",
"fasting-insulin": "Nüchtern-Insulin",
"omega-3-index": "Omega-3-Index"
},
"noteLoadFailed": "Notiz konnte nicht geladen werden; sie bleibt beim Speichern unverändert.",
"showingLatestOf": "Die neuesten {shown} von {total} Messwerten werden angezeigt",
@@ -7426,5 +7440,118 @@
"reload": "Neu laden",
"dismiss": "Schließen"
}
},
"mentalHealth": {
"pageTitle": "Check-in zum seelischen Wohlbefinden",
"pageDescription": "Zwei validierte, freiwillige Selbsttests — PHQ-9 (Stimmung) und GAD-7 (Sorgen) — ergänzend zu deinem Stimmungstagebuch. Aussagekräftig ist der Verlauf über die Zeit, nicht ein einzelner Wert.",
"disclaimer": "Dies ist ein Screening-Selbsttest, keine Diagnose. Ein hoher oder niedriger Wert bedeutet nicht, dass eine Erkrankung vorliegt oder nicht. Nur eine qualifizierte Fachperson kann eine Diagnose stellen. Bei Sorgen sprich bitte mit deiner Ärztin oder deinem Arzt.",
"optInNote": "Freiwillig und privat: Deine einzelnen Antworten sind verschlüsselt, bleiben aus dem KI-Coach und aus Exporten heraus und werden nie geteilt.",
"attributionLabel": "Quelle",
"choosePrompt": "Check-in auswählen",
"start": "Starten",
"back": "Zurück",
"stem": "Wie oft fühltest du dich im Verlauf der letzten 2 Wochen durch die folgenden Beschwerden beeinträchtigt?",
"submit": "Ergebnis ansehen",
"submitting": "Wird gespeichert…",
"saved": "Gespeichert",
"error": "Etwas ist schiefgelaufen. Bitte versuche es erneut.",
"considerProfessional": "Dein Wert liegt in einem Bereich, in dem ein Gespräch mit einer Fachperson helfen kann. Das ist ein behutsamer Hinweis, keine Diagnose.",
"instrument": {
"phq9": "PHQ-9 — Stimmung",
"gad7": "GAD-7 — Sorgen"
},
"instrumentSub": {
"phq9": "Depressions-Screening, 9 Fragen",
"gad7": "Angst-Screening, 7 Fragen"
},
"options": {
"0": "Überhaupt nicht",
"1": "An einzelnen Tagen",
"2": "An mehr als der Hälfte der Tage",
"3": "Beinahe jeden Tag"
},
"functionalTitle": "Wenn du eine oder mehrere Beschwerden angegeben hast: Wie sehr haben diese deine Arbeit, deinen Haushalt oder den Umgang mit anderen Menschen erschwert?",
"functional": {
"0": "Überhaupt nicht erschwert",
"1": "Etwas erschwert",
"2": "Sehr erschwert",
"3": "Extrem erschwert"
},
"result": {
"title": "Dein Ergebnis",
"totalLabel": "Gesamtwert",
"bandLabel": "Bereich für dieses Screening",
"takeAnother": "Weiteren Check-in machen"
},
"band": {
"PHQ9": {
"minimal": "Keine / minimal",
"mild": "Leicht",
"moderate": "Mittelgradig",
"modSevere": "Mittelschwer",
"severe": "Schwer"
},
"GAD7": {
"minimal": "Keine / minimal",
"mild": "Leicht",
"moderate": "Mittelgradig",
"severe": "Schwer"
}
},
"history": {
"title": "Dein Verlauf",
"empty": "Noch keine Check-ins. Dein Verlauf erscheint hier, sobald du einen abgeschlossen hast."
},
"crisis": {
"title": "Du musst das nicht allein durchstehen",
"intro": "Du hast Gedanken angegeben, lieber tot zu sein oder dir etwas anzutun. Es gibt jetzt sofort Unterstützung — sich zu melden ist ein Zeichen von Stärke.",
"ifDanger": "Wenn du in unmittelbarer Gefahr bist, ruf jetzt {emergency} an.",
"resourcesTitle": "Menschen, die helfen können"
},
"items": {
"phq9": {
"1": "Wenig Interesse oder Freude an Ihren Tätigkeiten",
"2": "Niedergeschlagenheit, Schwermut oder Hoffnungslosigkeit",
"3": "Schwierigkeiten, ein- oder durchzuschlafen, oder vermehrter Schlaf",
"4": "Müdigkeit oder Gefühl, keine Energie zu haben",
"5": "Verminderter Appetit oder übermäßiges Bedürfnis zu essen",
"6": "Schlechte Meinung von sich selbst; Gefühl, ein Versager zu sein oder die Familie enttäuscht zu haben",
"7": "Schwierigkeiten, sich auf etwas zu konzentrieren, z. B. beim Zeitunglesen oder Fernsehen",
"8": "Waren Ihre Bewegungen oder Ihre Sprache so verlangsamt, dass es auch anderen auffallen würde? Oder waren Sie im Gegenteil „zappelig“ oder ruhelos und hatten einen stärkeren Bewegungsdrang als sonst?",
"9": "Gedanken, dass Sie lieber tot wären oder sich Leid zufügen möchten"
},
"gad7": {
"1": "Nervosität, Ängstlichkeit oder Anspannung",
"2": "Nicht in der Lage sein, Sorgen zu stoppen oder zu kontrollieren",
"3": "Übermäßige Sorgen bezüglich verschiedener Angelegenheiten",
"4": "Schwierigkeiten zu entspannen",
"5": "Rastlosigkeit, sodass Stillsitzen schwerfällt",
"6": "Schnelle Verärgerung oder Gereiztheit",
"7": "Gefühl der Angst, als würde etwas Schlimmes passieren"
}
},
"crisisResource": {
"telefonSeelsorge": {
"name": "TelefonSeelsorge (rund um die Uhr, kostenlos, vertraulich)"
},
"nummerGegenKummer": {
"name": "Nummer gegen Kummer (für junge Menschen)"
},
"krisenchat": {
"name": "krisenchat (24/7-Chat, junge Menschen)"
},
"findahelpline": {
"name": "Find a Helpline (weltweites Verzeichnis)"
},
"euEmotionalSupport": {
"name": "Telefonseelsorge-Hotline"
},
"lifeline988": {
"name": "988 Suicide & Crisis Lifeline"
},
"crisisTextLine": {
"name": "Crisis Text Line"
}
}
}
}
+130 -3
View File
@@ -575,7 +575,13 @@
"bulkDeleteSuccess": "{count} measurements deleted",
"bulkDeleteError": "Error deleting the selected measurements",
"bulkDeleteConfirmTitle": "Delete {count} measurements?",
"bulkDeleteConfirmBody": "The {count} selected measurements will be permanently deleted."
"bulkDeleteConfirmBody": "The {count} selected measurements will be permanently deleted.",
"typePhq9Score": "PHQ-9 score",
"typeGad7Score": "GAD-7 score",
"typeGripStrength": "Grip strength",
"typePainNrs": "Pain (0–10)",
"typeWaistCircumference": "Waist circumference",
"typeWaistToHeight": "Waist-to-height ratio"
},
"mood": {
"title": "Mood",
@@ -7090,8 +7096,16 @@
"hemoglobin": "The oxygen-carrying protein in red blood cells.",
"hematocrit": "The fraction of blood volume made up of red blood cells.",
"wbc": "Total count of white blood cells, the immune system's circulating defence.",
"platelets": "Cell fragments that clump together to form blood clots."
}
"platelets": "Cell fragments that clump together to form blood clots.",
"apob": "Counts atherogenic particles directly; often a better risk marker than LDL-C when the two disagree.",
"lp-a": "A largely inherited, lifelong risk factor — usually measured once. Report molar units (nmol/L).",
"fasting-insulin": "Pairs with fasting glucose to estimate insulin resistance (HOMA-IR).",
"omega-3-index": "Red-blood-cell EPA+DHA share; 8–12% is the commonly cited cardioprotective range."
},
"apob": "Apolipoprotein B (ApoB)",
"lp-a": "Lipoprotein(a)",
"fasting-insulin": "Fasting insulin",
"omega-3-index": "Omega-3 index"
},
"noteLoadFailed": "Note could not be loaded; it will be kept unchanged when you save.",
"showingLatestOf": "Showing the latest {shown} of {total} readings",
@@ -7426,5 +7440,118 @@
"reload": "Reload",
"dismiss": "Dismiss"
}
},
"mentalHealth": {
"pageTitle": "Mental wellbeing check-in",
"pageDescription": "Two validated, opt-in self-assessments — PHQ-9 (mood) and GAD-7 (worry) — that sit beside your mood tracking. The value is the trend over time, not any single number.",
"disclaimer": "This is a screening self-assessment, not a diagnosis. A high or low score does not mean you do or do not have a condition. Only a qualified health professional can diagnose. If you have concerns, please talk to your doctor.",
"optInNote": "Opt-in and private: your individual answers are encrypted, kept out of the AI Coach and exports, and never shared.",
"attributionLabel": "Attribution",
"choosePrompt": "Choose a check-in",
"start": "Start",
"back": "Back",
"stem": "Over the last 2 weeks, how often have you been bothered by the following problems?",
"submit": "See my result",
"submitting": "Saving…",
"saved": "Saved",
"error": "Something went wrong. Please try again.",
"considerProfessional": "Your score is in a range where talking to a health professional may help. This is a gentle suggestion, not a diagnosis.",
"instrument": {
"phq9": "PHQ-9 — mood",
"gad7": "GAD-7 — worry"
},
"instrumentSub": {
"phq9": "Depression screen, 9 questions",
"gad7": "Anxiety screen, 7 questions"
},
"options": {
"0": "Not at all",
"1": "Several days",
"2": "More than half the days",
"3": "Nearly every day"
},
"functionalTitle": "If you checked off any problems, how difficult have these made it to do your work, take care of things at home, or get along with other people?",
"functional": {
"0": "Not difficult at all",
"1": "Somewhat difficult",
"2": "Very difficult",
"3": "Extremely difficult"
},
"result": {
"title": "Your result",
"totalLabel": "Total score",
"bandLabel": "Range for this screen",
"takeAnother": "Take another check-in"
},
"band": {
"PHQ9": {
"minimal": "None / minimal",
"mild": "Mild",
"moderate": "Moderate",
"modSevere": "Moderately severe",
"severe": "Severe"
},
"GAD7": {
"minimal": "None / minimal",
"mild": "Mild",
"moderate": "Moderate",
"severe": "Severe"
}
},
"history": {
"title": "Your history",
"empty": "No check-ins yet. Your trend will appear here once you complete one."
},
"crisis": {
"title": "You don't have to face this alone",
"intro": "You indicated thoughts of being better off dead or of hurting yourself. Support is available right now, and reaching out is a sign of strength.",
"ifDanger": "If you are in immediate danger, call {emergency} now.",
"resourcesTitle": "People who can help"
},
"items": {
"phq9": {
"1": "Little interest or pleasure in doing things",
"2": "Feeling down, depressed, or hopeless",
"3": "Trouble falling or staying asleep, or sleeping too much",
"4": "Feeling tired or having little energy",
"5": "Poor appetite or overeating",
"6": "Feeling bad about yourself — or that you are a failure or have let yourself or your family down",
"7": "Trouble concentrating on things, such as reading the newspaper or watching television",
"8": "Moving or speaking so slowly that other people could have noticed. Or the opposite — being so fidgety or restless that you have been moving around a lot more than usual",
"9": "Thoughts that you would be better off dead, or of hurting yourself in some way"
},
"gad7": {
"1": "Feeling nervous, anxious, or on edge",
"2": "Not being able to stop or control worrying",
"3": "Worrying too much about different things",
"4": "Trouble relaxing",
"5": "Being so restless that it is hard to sit still",
"6": "Becoming easily annoyed or irritable",
"7": "Feeling afraid as if something awful might happen"
}
},
"crisisResource": {
"telefonSeelsorge": {
"name": "TelefonSeelsorge (24/7, free, confidential)"
},
"nummerGegenKummer": {
"name": "Nummer gegen Kummer (for young people)"
},
"krisenchat": {
"name": "krisenchat (24/7 chat, young people)"
},
"findahelpline": {
"name": "Find a helpline (worldwide directory)"
},
"euEmotionalSupport": {
"name": "Emotional-support helpline"
},
"lifeline988": {
"name": "988 Suicide & Crisis Lifeline"
},
"crisisTextLine": {
"name": "Crisis Text Line"
}
}
}
}
+130 -3
View File
@@ -575,7 +575,13 @@
"bulkDeleteSuccess": "{count} mediciones eliminadas",
"bulkDeleteError": "Error al eliminar las mediciones seleccionadas",
"bulkDeleteConfirmTitle": "¿Eliminar {count} mediciones?",
"bulkDeleteConfirmBody": "Las {count} mediciones seleccionadas se eliminarán de forma permanente."
"bulkDeleteConfirmBody": "Las {count} mediciones seleccionadas se eliminarán de forma permanente.",
"typePhq9Score": "Puntuación PHQ-9",
"typeGad7Score": "Puntuación GAD-7",
"typeGripStrength": "Fuerza de agarre",
"typePainNrs": "Dolor (0–10)",
"typeWaistCircumference": "Perímetro de cintura",
"typeWaistToHeight": "Índice cintura-altura"
},
"mood": {
"title": "Estado de ánimo",
@@ -7090,8 +7096,16 @@
"hemoglobin": "La proteína de los glóbulos rojos que transporta el oxígeno.",
"hematocrit": "La fracción del volumen sanguíneo formada por glóbulos rojos.",
"wbc": "Recuento total de glóbulos blancos, la defensa circulante del sistema inmunitario.",
"platelets": "Fragmentos celulares que se agregan para formar coágulos sanguíneos."
}
"platelets": "Fragmentos celulares que se agregan para formar coágulos sanguíneos.",
"apob": "Cuenta directamente las partículas aterogénicas; a menudo mejor marcador de riesgo que el LDL-C cuando discrepan.",
"lp-a": "Factor de riesgo en gran parte hereditario y de por vida — suele medirse una vez. Informa en unidades molares (nmol/L).",
"fasting-insulin": "Se combina con la glucosa en ayunas para estimar la resistencia a la insulina (HOMA-IR).",
"omega-3-index": "Proporción de EPA+DHA en glóbulos rojos; 8–12 % es el rango cardioprotector citado habitualmente."
},
"apob": "Apolipoproteína B (ApoB)",
"lp-a": "Lipoproteína(a)",
"fasting-insulin": "Insulina en ayunas",
"omega-3-index": "Índice omega-3"
},
"noteLoadFailed": "No se pudo cargar la nota; se mantendrá sin cambios al guardar.",
"showingLatestOf": "Mostrando los {shown} resultados más recientes de {total}",
@@ -7426,5 +7440,118 @@
"reload": "Recargar",
"dismiss": "Descartar"
}
},
"mentalHealth": {
"pageTitle": "Revisión de bienestar mental",
"pageDescription": "Dos autoevaluaciones validadas y opcionales — PHQ-9 (estado de ánimo) y GAD-7 (preocupación) — junto a tu registro de ánimo. Lo valioso es la tendencia en el tiempo, no un único número.",
"disclaimer": "Esto es una autoevaluación de cribado, no un diagnóstico. Una puntuación alta o baja no significa que tengas o no una afección. Solo un profesional de salud cualificado puede diagnosticar. Si tienes dudas, habla con tu médico.",
"optInNote": "Opcional y privado: tus respuestas individuales están cifradas, se mantienen fuera del Coach de IA y de las exportaciones, y nunca se comparten.",
"attributionLabel": "Fuente",
"choosePrompt": "Elige una revisión",
"start": "Empezar",
"back": "Atrás",
"stem": "Durante las últimas 2 semanas, ¿con qué frecuencia te han molestado los siguientes problemas?",
"submit": "Ver mi resultado",
"submitting": "Guardando…",
"saved": "Guardado",
"error": "Algo salió mal. Inténtalo de nuevo.",
"considerProfessional": "Tu puntuación está en un rango en el que hablar con un profesional de salud puede ayudar. Es una sugerencia amable, no un diagnóstico.",
"instrument": {
"phq9": "PHQ-9 — ánimo",
"gad7": "GAD-7 — preocupación"
},
"instrumentSub": {
"phq9": "Cribado de depresión, 9 preguntas",
"gad7": "Cribado de ansiedad, 7 preguntas"
},
"options": {
"0": "Nunca",
"1": "Varios días",
"2": "Más de la mitad de los días",
"3": "Casi todos los días"
},
"functionalTitle": "Si marcaste algún problema, ¿cuánta dificultad te han causado para trabajar, ocuparte de la casa o relacionarte con otras personas?",
"functional": {
"0": "Ninguna dificultad",
"1": "Algo de dificultad",
"2": "Mucha dificultad",
"3": "Dificultad extrema"
},
"result": {
"title": "Tu resultado",
"totalLabel": "Puntuación total",
"bandLabel": "Rango para este cribado",
"takeAnother": "Hacer otra revisión"
},
"band": {
"PHQ9": {
"minimal": "Ninguna / mínima",
"mild": "Leve",
"moderate": "Moderada",
"modSevere": "Moderadamente grave",
"severe": "Grave"
},
"GAD7": {
"minimal": "Ninguna / mínima",
"mild": "Leve",
"moderate": "Moderada",
"severe": "Grave"
}
},
"history": {
"title": "Tu historial",
"empty": "Aún no hay revisiones. Tu tendencia aparecerá aquí cuando completes una."
},
"crisis": {
"title": "No tienes que afrontar esto a solas",
"intro": "Indicaste pensamientos de estar mejor muerto o de hacerte daño. Hay apoyo disponible ahora mismo, y pedir ayuda es una señal de fortaleza.",
"ifDanger": "Si estás en peligro inmediato, llama ahora al {emergency}.",
"resourcesTitle": "Personas que pueden ayudar"
},
"items": {
"phq9": {
"1": "Little interest or pleasure in doing things",
"2": "Feeling down, depressed, or hopeless",
"3": "Trouble falling or staying asleep, or sleeping too much",
"4": "Feeling tired or having little energy",
"5": "Poor appetite or overeating",
"6": "Feeling bad about yourself — or that you are a failure or have let yourself or your family down",
"7": "Trouble concentrating on things, such as reading the newspaper or watching television",
"8": "Moving or speaking so slowly that other people could have noticed. Or the opposite — being so fidgety or restless that you have been moving around a lot more than usual",
"9": "Thoughts that you would be better off dead, or of hurting yourself in some way"
},
"gad7": {
"1": "Feeling nervous, anxious, or on edge",
"2": "Not being able to stop or control worrying",
"3": "Worrying too much about different things",
"4": "Trouble relaxing",
"5": "Being so restless that it is hard to sit still",
"6": "Becoming easily annoyed or irritable",
"7": "Feeling afraid as if something awful might happen"
}
},
"crisisResource": {
"telefonSeelsorge": {
"name": "TelefonSeelsorge (24/7, free, confidential)"
},
"nummerGegenKummer": {
"name": "Nummer gegen Kummer (for young people)"
},
"krisenchat": {
"name": "krisenchat (24/7 chat, young people)"
},
"findahelpline": {
"name": "Find a helpline (worldwide directory)"
},
"euEmotionalSupport": {
"name": "Emotional-support helpline"
},
"lifeline988": {
"name": "988 Suicide & Crisis Lifeline"
},
"crisisTextLine": {
"name": "Crisis Text Line"
}
}
}
}
+130 -3
View File
@@ -575,7 +575,13 @@
"bulkDeleteSuccess": "{count} mesures supprimées",
"bulkDeleteError": "Erreur lors de la suppression des mesures sélectionnées",
"bulkDeleteConfirmTitle": "Supprimer {count} mesures ?",
"bulkDeleteConfirmBody": "Les {count} mesures sélectionnées seront définitivement supprimées."
"bulkDeleteConfirmBody": "Les {count} mesures sélectionnées seront définitivement supprimées.",
"typePhq9Score": "Score PHQ-9",
"typeGad7Score": "Score GAD-7",
"typeGripStrength": "Force de préhension",
"typePainNrs": "Douleur (0–10)",
"typeWaistCircumference": "Tour de taille",
"typeWaistToHeight": "Rapport taille-taille"
},
"mood": {
"title": "Humeur",
@@ -7090,8 +7096,16 @@
"hemoglobin": "La protéine des globules rouges qui transporte l'oxygène.",
"hematocrit": "La fraction du volume sanguin constituée de globules rouges.",
"wbc": "Nombre total de globules blancs, la défense circulante du système immunitaire.",
"platelets": "Fragments cellulaires qui s'agrègent pour former des caillots sanguins."
}
"platelets": "Fragments cellulaires qui s'agrègent pour former des caillots sanguins.",
"apob": "Compte directement les particules athérogènes ; souvent un meilleur marqueur de risque que le LDL-C en cas de discordance.",
"lp-a": "Facteur de risque en grande partie héréditaire et permanent — mesuré une seule fois en général. À exprimer en unités molaires (nmol/L).",
"fasting-insulin": "Se combine à la glycémie à jeun pour estimer la résistance à l'insuline (HOMA-IR).",
"omega-3-index": "Part d'EPA+DHA dans les globules rouges ; 8–12 % est la plage cardioprotectrice habituellement citée."
},
"apob": "Apolipoprotéine B (ApoB)",
"lp-a": "Lipoprotéine(a)",
"fasting-insulin": "Insuline à jeun",
"omega-3-index": "Indice oméga-3"
},
"noteLoadFailed": "La note n’a pas pu être chargée ; elle restera inchangée à l’enregistrement.",
"showingLatestOf": "Affichage des {shown} derniers résultats sur {total}",
@@ -7426,5 +7440,118 @@
"reload": "Recharger",
"dismiss": "Ignorer"
}
},
"mentalHealth": {
"pageTitle": "Point sur le bien-être mental",
"pageDescription": "Deux auto-évaluations validées et facultatives — PHQ-9 (humeur) et GAD-7 (inquiétude) — en complément de ton suivi de l'humeur. Ce qui compte, c'est la tendance dans le temps, pas un chiffre isolé.",
"disclaimer": "Il s'agit d'une auto-évaluation de dépistage, pas d'un diagnostic. Un score élevé ou bas ne signifie pas que tu as ou n'as pas un trouble. Seul un professionnel de santé qualifié peut poser un diagnostic. En cas de doute, parles-en à ton médecin.",
"optInNote": "Facultatif et privé : tes réponses individuelles sont chiffrées, tenues à l'écart du Coach IA et des exports, et ne sont jamais partagées.",
"attributionLabel": "Source",
"choosePrompt": "Choisis un bilan",
"start": "Commencer",
"back": "Retour",
"stem": "Au cours des 2 dernières semaines, à quelle fréquence as-tu été gêné(e) par les problèmes suivants ?",
"submit": "Voir mon résultat",
"submitting": "Enregistrement…",
"saved": "Enregistré",
"error": "Une erreur est survenue. Réessaie.",
"considerProfessional": "Ton score se situe dans une plage où parler à un professionnel de santé peut aider. C'est une suggestion bienveillante, pas un diagnostic.",
"instrument": {
"phq9": "PHQ-9 — humeur",
"gad7": "GAD-7 — inquiétude"
},
"instrumentSub": {
"phq9": "Dépistage de la dépression, 9 questions",
"gad7": "Dépistage de l'anxiété, 7 questions"
},
"options": {
"0": "Jamais",
"1": "Plusieurs jours",
"2": "Plus de la moitié des jours",
"3": "Presque tous les jours"
},
"functionalTitle": "Si tu as coché des problèmes, à quel point t'ont-ils gêné(e) dans ton travail, à la maison ou dans tes relations avec les autres ?",
"functional": {
"0": "Pas du tout gênant",
"1": "Un peu gênant",
"2": "Très gênant",
"3": "Extrêmement gênant"
},
"result": {
"title": "Ton résultat",
"totalLabel": "Score total",
"bandLabel": "Plage pour ce dépistage",
"takeAnother": "Faire un autre bilan"
},
"band": {
"PHQ9": {
"minimal": "Aucune / minimale",
"mild": "Légère",
"moderate": "Modérée",
"modSevere": "Modérément sévère",
"severe": "Sévère"
},
"GAD7": {
"minimal": "Aucune / minimale",
"mild": "Légère",
"moderate": "Modérée",
"severe": "Sévère"
}
},
"history": {
"title": "Ton historique",
"empty": "Aucun bilan pour le moment. Ta tendance apparaîtra ici dès que tu en auras terminé un."
},
"crisis": {
"title": "Tu n'as pas à affronter cela seul(e)",
"intro": "Tu as indiqué des pensées qu'il vaudrait mieux être mort(e) ou de te faire du mal. De l'aide est disponible dès maintenant, et demander de l'aide est un signe de force.",
"ifDanger": "Si tu es en danger immédiat, appelle le {emergency} maintenant.",
"resourcesTitle": "Des personnes qui peuvent aider"
},
"items": {
"phq9": {
"1": "Little interest or pleasure in doing things",
"2": "Feeling down, depressed, or hopeless",
"3": "Trouble falling or staying asleep, or sleeping too much",
"4": "Feeling tired or having little energy",
"5": "Poor appetite or overeating",
"6": "Feeling bad about yourself — or that you are a failure or have let yourself or your family down",
"7": "Trouble concentrating on things, such as reading the newspaper or watching television",
"8": "Moving or speaking so slowly that other people could have noticed. Or the opposite — being so fidgety or restless that you have been moving around a lot more than usual",
"9": "Thoughts that you would be better off dead, or of hurting yourself in some way"
},
"gad7": {
"1": "Feeling nervous, anxious, or on edge",
"2": "Not being able to stop or control worrying",
"3": "Worrying too much about different things",
"4": "Trouble relaxing",
"5": "Being so restless that it is hard to sit still",
"6": "Becoming easily annoyed or irritable",
"7": "Feeling afraid as if something awful might happen"
}
},
"crisisResource": {
"telefonSeelsorge": {
"name": "TelefonSeelsorge (24/7, free, confidential)"
},
"nummerGegenKummer": {
"name": "Nummer gegen Kummer (for young people)"
},
"krisenchat": {
"name": "krisenchat (24/7 chat, young people)"
},
"findahelpline": {
"name": "Find a helpline (worldwide directory)"
},
"euEmotionalSupport": {
"name": "Emotional-support helpline"
},
"lifeline988": {
"name": "988 Suicide & Crisis Lifeline"
},
"crisisTextLine": {
"name": "Crisis Text Line"
}
}
}
}
+130 -3
View File
@@ -575,7 +575,13 @@
"bulkDeleteSuccess": "{count} misurazioni eliminate",
"bulkDeleteError": "Errore durante l'eliminazione delle misurazioni selezionate",
"bulkDeleteConfirmTitle": "Eliminare {count} misurazioni?",
"bulkDeleteConfirmBody": "Le {count} misurazioni selezionate verranno eliminate definitivamente."
"bulkDeleteConfirmBody": "Le {count} misurazioni selezionate verranno eliminate definitivamente.",
"typePhq9Score": "Punteggio PHQ-9",
"typeGad7Score": "Punteggio GAD-7",
"typeGripStrength": "Forza di presa",
"typePainNrs": "Dolore (0–10)",
"typeWaistCircumference": "Circonferenza vita",
"typeWaistToHeight": "Rapporto vita-altezza"
},
"mood": {
"title": "Umore",
@@ -7090,8 +7096,16 @@
"hemoglobin": "La proteina dei globuli rossi che trasporta l'ossigeno.",
"hematocrit": "La frazione del volume sanguigno costituita da globuli rossi.",
"wbc": "Conteggio totale dei globuli bianchi, la difesa circolante del sistema immunitario.",
"platelets": "Frammenti cellulari che si aggregano per formare coaguli di sangue."
}
"platelets": "Frammenti cellulari che si aggregano per formare coaguli di sangue.",
"apob": "Conta direttamente le particelle aterogene; spesso un marcatore di rischio migliore del LDL-C quando i due discordano.",
"lp-a": "Fattore di rischio in gran parte ereditario e permanente — di solito misurato una volta. Riportare in unità molari (nmol/L).",
"fasting-insulin": "Si abbina alla glicemia a digiuno per stimare la resistenza insulinica (HOMA-IR).",
"omega-3-index": "Quota di EPA+DHA nei globuli rossi; 8–12% è l'intervallo cardioprotettivo comunemente citato."
},
"apob": "Apolipoproteina B (ApoB)",
"lp-a": "Lipoproteina(a)",
"fasting-insulin": "Insulina a digiuno",
"omega-3-index": "Indice omega-3"
},
"noteLoadFailed": "Impossibile caricare la nota; resterà invariata al salvataggio.",
"showingLatestOf": "Visualizzazione degli ultimi {shown} risultati su {total}",
@@ -7426,5 +7440,118 @@
"reload": "Ricarica",
"dismiss": "Ignora"
}
},
"mentalHealth": {
"pageTitle": "Check-in sul benessere mentale",
"pageDescription": "Due autovalutazioni validate e facoltative — PHQ-9 (umore) e GAD-7 (preoccupazione) — accanto al tuo diario dell'umore. Ciò che conta è l'andamento nel tempo, non un singolo numero.",
"disclaimer": "Questa è un'autovalutazione di screening, non una diagnosi. Un punteggio alto o basso non significa che tu abbia o non abbia un disturbo. Solo un professionista sanitario qualificato può fare una diagnosi. In caso di dubbi, parlane con il tuo medico.",
"optInNote": "Facoltativo e privato: le tue risposte individuali sono cifrate, tenute fuori dal Coach IA e dalle esportazioni, e non vengono mai condivise.",
"attributionLabel": "Fonte",
"choosePrompt": "Scegli un check-in",
"start": "Inizia",
"back": "Indietro",
"stem": "Nelle ultime 2 settimane, con quale frequenza ti hanno disturbato i seguenti problemi?",
"submit": "Vedi il mio risultato",
"submitting": "Salvataggio…",
"saved": "Salvato",
"error": "Qualcosa è andato storto. Riprova.",
"considerProfessional": "Il tuo punteggio è in un intervallo in cui parlare con un professionista sanitario può aiutare. È un suggerimento gentile, non una diagnosi.",
"instrument": {
"phq9": "PHQ-9 — umore",
"gad7": "GAD-7 — preoccupazione"
},
"instrumentSub": {
"phq9": "Screening della depressione, 9 domande",
"gad7": "Screening dell'ansia, 7 domande"
},
"options": {
"0": "Per niente",
"1": "Alcuni giorni",
"2": "Più della metà dei giorni",
"3": "Quasi ogni giorno"
},
"functionalTitle": "Se hai indicato dei problemi, quanto ti hanno reso difficile lavorare, occuparti della casa o andare d'accordo con gli altri?",
"functional": {
"0": "Per niente difficile",
"1": "Un po' difficile",
"2": "Molto difficile",
"3": "Estremamente difficile"
},
"result": {
"title": "Il tuo risultato",
"totalLabel": "Punteggio totale",
"bandLabel": "Intervallo per questo screening",
"takeAnother": "Fai un altro check-in"
},
"band": {
"PHQ9": {
"minimal": "Nessuna / minima",
"mild": "Lieve",
"moderate": "Moderata",
"modSevere": "Moderatamente grave",
"severe": "Grave"
},
"GAD7": {
"minimal": "Nessuna / minima",
"mild": "Lieve",
"moderate": "Moderata",
"severe": "Grave"
}
},
"history": {
"title": "La tua cronologia",
"empty": "Ancora nessun check-in. Il tuo andamento apparirà qui dopo che ne avrai completato uno."
},
"crisis": {
"title": "Non devi affrontare tutto questo da solo/a",
"intro": "Hai indicato pensieri che sarebbe meglio essere morto/a o di farti del male. Un supporto è disponibile proprio ora, e chiedere aiuto è un segno di forza.",
"ifDanger": "Se sei in pericolo immediato, chiama subito il {emergency}.",
"resourcesTitle": "Persone che possono aiutare"
},
"items": {
"phq9": {
"1": "Little interest or pleasure in doing things",
"2": "Feeling down, depressed, or hopeless",
"3": "Trouble falling or staying asleep, or sleeping too much",
"4": "Feeling tired or having little energy",
"5": "Poor appetite or overeating",
"6": "Feeling bad about yourself — or that you are a failure or have let yourself or your family down",
"7": "Trouble concentrating on things, such as reading the newspaper or watching television",
"8": "Moving or speaking so slowly that other people could have noticed. Or the opposite — being so fidgety or restless that you have been moving around a lot more than usual",
"9": "Thoughts that you would be better off dead, or of hurting yourself in some way"
},
"gad7": {
"1": "Feeling nervous, anxious, or on edge",
"2": "Not being able to stop or control worrying",
"3": "Worrying too much about different things",
"4": "Trouble relaxing",
"5": "Being so restless that it is hard to sit still",
"6": "Becoming easily annoyed or irritable",
"7": "Feeling afraid as if something awful might happen"
}
},
"crisisResource": {
"telefonSeelsorge": {
"name": "TelefonSeelsorge (24/7, free, confidential)"
},
"nummerGegenKummer": {
"name": "Nummer gegen Kummer (for young people)"
},
"krisenchat": {
"name": "krisenchat (24/7 chat, young people)"
},
"findahelpline": {
"name": "Find a helpline (worldwide directory)"
},
"euEmotionalSupport": {
"name": "Emotional-support helpline"
},
"lifeline988": {
"name": "988 Suicide & Crisis Lifeline"
},
"crisisTextLine": {
"name": "Crisis Text Line"
}
}
}
}
+130 -3
View File
@@ -575,7 +575,13 @@
"bulkDeleteSuccess": "Usunięto pomiary: {count}",
"bulkDeleteError": "Błąd podczas usuwania wybranych pomiarów",
"bulkDeleteConfirmTitle": "Usunąć pomiary ({count})?",
"bulkDeleteConfirmBody": "Wybrane pomiary ({count}) zostaną trwale usunięte."
"bulkDeleteConfirmBody": "Wybrane pomiary ({count}) zostaną trwale usunięte.",
"typePhq9Score": "Wynik PHQ-9",
"typeGad7Score": "Wynik GAD-7",
"typeGripStrength": "Siła chwytu",
"typePainNrs": "Ból (0–10)",
"typeWaistCircumference": "Obwód talii",
"typeWaistToHeight": "Stosunek talii do wzrostu"
},
"mood": {
"title": "Nastrój",
@@ -7090,8 +7096,16 @@
"hemoglobin": "Białko czerwonych krwinek transportujące tlen.",
"hematocrit": "Udział czerwonych krwinek w objętości krwi.",
"wbc": "Całkowita liczba białych krwinek, krążącej obrony układu odpornościowego.",
"platelets": "Fragmenty komórkowe, które zlepiają się, tworząc skrzepy krwi."
}
"platelets": "Fragmenty komórkowe, które zlepiają się, tworząc skrzepy krwi.",
"apob": "Bezpośrednio liczy cząstki aterogenne; często lepszy marker ryzyka niż LDL-C, gdy się różnią.",
"lp-a": "W dużej mierze dziedziczny, dożywotni czynnik ryzyka — zwykle mierzony raz. Podawaj w jednostkach molowych (nmol/L).",
"fasting-insulin": "Łączy się z glukozą na czczo do oszacowania insulinooporności (HOMA-IR).",
"omega-3-index": "Udział EPA+DHA w czerwonych krwinkach; 8–12% to powszechnie podawany zakres kardioprotekcyjny."
},
"apob": "Apolipoproteina B (ApoB)",
"lp-a": "Lipoproteina(a)",
"fasting-insulin": "Insulina na czczo",
"omega-3-index": "Indeks omega-3"
},
"noteLoadFailed": "Nie udało się wczytać notatki; pozostanie niezmieniona po zapisaniu.",
"showingLatestOf": "Wyświetlanie najnowszych {shown} z {total} wyników",
@@ -7426,5 +7440,118 @@
"reload": "Załaduj ponownie",
"dismiss": "Odrzuć"
}
},
"mentalHealth": {
"pageTitle": "Sprawdzenie dobrostanu psychicznego",
"pageDescription": "Dwa zwalidowane, dobrowolne testy — PHQ-9 (nastrój) i GAD-7 (lęk) — obok Twojego dziennika nastroju. Liczy się trend w czasie, a nie pojedynczy wynik.",
"disclaimer": "To przesiewowa samoocena, a nie diagnoza. Wysoki lub niski wynik nie oznacza, że masz lub nie masz danego schorzenia. Tylko wykwalifikowany pracownik ochrony zdrowia może postawić diagnozę. W razie wątpliwości porozmawiaj z lekarzem.",
"optInNote": "Dobrowolne i prywatne: Twoje poszczególne odpowiedzi są szyfrowane, pozostają poza Coachem AI i eksportami i nigdy nie są udostępniane.",
"attributionLabel": "Źródło",
"choosePrompt": "Wybierz sprawdzenie",
"start": "Rozpocznij",
"back": "Wstecz",
"stem": "Jak często w ciągu ostatnich 2 tygodni dokuczały Ci następujące problemy?",
"submit": "Zobacz mój wynik",
"submitting": "Zapisywanie…",
"saved": "Zapisano",
"error": "Coś poszło nie tak. Spróbuj ponownie.",
"considerProfessional": "Twój wynik mieści się w zakresie, w którym rozmowa ze specjalistą może pomóc. To życzliwa sugestia, a nie diagnoza.",
"instrument": {
"phq9": "PHQ-9 — nastrój",
"gad7": "GAD-7 — lęk"
},
"instrumentSub": {
"phq9": "Przesiew depresji, 9 pytań",
"gad7": "Przesiew lęku, 7 pytań"
},
"options": {
"0": "Wcale",
"1": "Kilka dni",
"2": "Więcej niż połowę dni",
"3": "Niemal codziennie"
},
"functionalTitle": "Jeśli zaznaczyłeś(-aś) jakiekolwiek problemy, na ile utrudniły Ci one pracę, prowadzenie domu lub kontakty z innymi ludźmi?",
"functional": {
"0": "Wcale nie utrudniły",
"1": "Trochę utrudniły",
"2": "Bardzo utrudniły",
"3": "Skrajnie utrudniły"
},
"result": {
"title": "Twój wynik",
"totalLabel": "Wynik łączny",
"bandLabel": "Zakres dla tego przesiewu",
"takeAnother": "Wykonaj kolejne sprawdzenie"
},
"band": {
"PHQ9": {
"minimal": "Brak / minimalny",
"mild": "Łagodny",
"moderate": "Umiarkowany",
"modSevere": "Umiarkowanie ciężki",
"severe": "Ciężki"
},
"GAD7": {
"minimal": "Brak / minimalny",
"mild": "Łagodny",
"moderate": "Umiarkowany",
"severe": "Ciężki"
}
},
"history": {
"title": "Twoja historia",
"empty": "Brak sprawdzeń. Twój trend pojawi się tutaj po ukończeniu pierwszego."
},
"crisis": {
"title": "Nie musisz mierzyć się z tym sam(a)",
"intro": "Wskazałeś(-aś) myśli, że lepiej byłoby nie żyć lub zrobić sobie krzywdę. Wsparcie jest dostępne już teraz, a sięgnięcie po pomoc to oznaka siły.",
"ifDanger": "Jeśli grozi Ci bezpośrednie niebezpieczeństwo, zadzwoń teraz pod {emergency}.",
"resourcesTitle": "Osoby, które mogą pomóc"
},
"items": {
"phq9": {
"1": "Little interest or pleasure in doing things",
"2": "Feeling down, depressed, or hopeless",
"3": "Trouble falling or staying asleep, or sleeping too much",
"4": "Feeling tired or having little energy",
"5": "Poor appetite or overeating",
"6": "Feeling bad about yourself — or that you are a failure or have let yourself or your family down",
"7": "Trouble concentrating on things, such as reading the newspaper or watching television",
"8": "Moving or speaking so slowly that other people could have noticed. Or the opposite — being so fidgety or restless that you have been moving around a lot more than usual",
"9": "Thoughts that you would be better off dead, or of hurting yourself in some way"
},
"gad7": {
"1": "Feeling nervous, anxious, or on edge",
"2": "Not being able to stop or control worrying",
"3": "Worrying too much about different things",
"4": "Trouble relaxing",
"5": "Being so restless that it is hard to sit still",
"6": "Becoming easily annoyed or irritable",
"7": "Feeling afraid as if something awful might happen"
}
},
"crisisResource": {
"telefonSeelsorge": {
"name": "TelefonSeelsorge (24/7, free, confidential)"
},
"nummerGegenKummer": {
"name": "Nummer gegen Kummer (for young people)"
},
"krisenchat": {
"name": "krisenchat (24/7 chat, young people)"
},
"findahelpline": {
"name": "Find a helpline (worldwide directory)"
},
"euEmotionalSupport": {
"name": "Emotional-support helpline"
},
"lifeline988": {
"name": "988 Suicide & Crisis Lifeline"
},
"crisisTextLine": {
"name": "Crisis Text Line"
}
}
}
}
@@ -40,7 +40,10 @@ import {
type InstrumentId,
} from "@/lib/mental-health/instruments";
import { crisisResourcesForLocale } from "@/lib/mental-health/crisis-resources";
import type { MeasurementType, MeasurementSource } from "@/generated/prisma/client";
import type {
MeasurementType,
MeasurementSource,
} from "@/generated/prisma/client";
function shapeRow(row: {
id: string;
@@ -81,7 +84,11 @@ export const GET = apiHandler(async (request: NextRequest) => {
}
const { instrument, limit, offset } = parsed.data;
const rows = await prisma.mentalHealthAssessment.findMany({
where: { userId: user.id, deletedAt: null, ...(instrument ? { instrument } : {}) },
where: {
userId: user.id,
deletedAt: null,
...(instrument ? { instrument } : {}),
},
orderBy: { takenAt: "desc" },
take: limit ?? 100,
skip: offset ?? 0,
@@ -0,0 +1,19 @@
"use client";
import { MentalWellbeing } from "@/components/mental-health/mental-wellbeing";
/**
* v1.25 — `/insights/mental-wellbeing`.
*
* Opt-in PHQ-9 / GAD-7 screener surface, beside mood tracking. The screen owns
* its own questionnaire + result + crisis-resource flow; it deliberately does
* NOT mount the shared Coach-launch assessment card (mental-health item content
* is kept out of the AI Coach by construction).
*/
export default function MentalWellbeingPage() {
return (
<div className="p-4 sm:p-6">
<MentalWellbeing />
</div>
);
}
@@ -0,0 +1,360 @@
"use client";
/**
* v1.25 — opt-in mental-health screener surface (PHQ-9 / GAD-7).
*
* Beside mood tracking, never replacing it; framed as a screening self-check,
* not a diagnosis. The per-item answers stay on the device until submit, the
* server encrypts them, and only the total trend is persisted as a measurement.
*
* SAFETY: this surface is deliberately kept OFF the AI Coach — the shared
* Coach-launch assessment card is intentionally NOT used here, so a depression /
* anxiety screen never invites an AI conversation about item content. On a
* positive PHQ-9 item-9 the server returns a calm, locale-aware crisis-resource
* set which is rendered immediately below the result.
*/
import { useMemo, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useTranslations, useFormatters } from "@/lib/i18n/context";
import { apiGet, apiPost } from "@/lib/api/api-fetch";
import { queryKeys } from "@/lib/query-keys";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
INSTRUMENTS,
PHQ_GAD_ATTRIBUTION,
type InstrumentId,
} from "@/lib/mental-health/instruments";
type Phase = "choose" | "form" | "result";
interface AssessmentRow {
id: string;
instrument: InstrumentId;
totalScore: number;
severityBand: string;
item9Flagged: boolean;
takenAt: string;
}
interface CrisisSet {
emergencyNumber: string;
resources: { id: string; contacts: string[] }[];
}
interface CreateResponse {
assessment: AssessmentRow;
actionThreshold: number;
crisis: CrisisSet | null;
}
const SCALE = [0, 1, 2, 3] as const;
const FUNCTIONAL = [0, 1, 2, 3] as const;
export function MentalWellbeing() {
const { t, locale } = useTranslations();
const { date: formatDate } = useFormatters();
const queryClient = useQueryClient();
const [phase, setPhase] = useState<Phase>("choose");
const [instrument, setInstrument] = useState<InstrumentId>("PHQ9");
const [items, setItems] = useState<number[]>([]);
const [functional, setFunctional] = useState<number | null>(null);
const [result, setResult] = useState<CreateResponse | null>(null);
const lower = (id: InstrumentId) => (id === "PHQ9" ? "phq9" : "gad7");
const { data: history } = useQuery({
queryKey: queryKeys.mentalHealthAssessments(),
queryFn: () =>
apiGet<{ assessments: AssessmentRow[] }>(
"/api/mental-health/assessments",
),
});
const mutation = useMutation({
mutationFn: (body: unknown) =>
apiPost<CreateResponse>("/api/mental-health/assessments", body),
onSuccess: (data) => {
setResult(data);
setPhase("result");
void queryClient.invalidateQueries({
queryKey: queryKeys.mentalHealthAssessments(),
});
},
});
function begin(id: InstrumentId) {
setInstrument(id);
setItems(Array(INSTRUMENTS[id].itemCount).fill(-1));
setFunctional(null);
setResult(null);
setPhase("form");
}
const complete = items.length > 0 && items.every((v) => v >= 0);
function submit() {
if (!complete) return;
mutation.mutate({
instrument,
items,
...(functional !== null ? { functionalDifficulty: functional } : {}),
locale,
});
}
const itemCount = INSTRUMENTS[instrument].itemCount;
const itemIndexes = useMemo(
() => Array.from({ length: itemCount }, (_, i) => i),
[itemCount],
);
return (
<div className="mx-auto flex max-w-2xl flex-col gap-6">
<header className="flex flex-col gap-2">
<h1 className="text-2xl font-semibold">
{t("mentalHealth.pageTitle")}
</h1>
<p className="text-muted-foreground text-sm">
{t("mentalHealth.pageDescription")}
</p>
<p className="text-muted-foreground border-border/60 bg-muted/40 rounded-md border p-3 text-xs">
{t("mentalHealth.disclaimer")}
</p>
<p className="text-muted-foreground text-xs">
{t("mentalHealth.optInNote")}
</p>
</header>
{phase === "choose" && (
<section
className="flex flex-col gap-3"
aria-label={t("mentalHealth.choosePrompt")}
>
<h2 className="text-sm font-medium">
{t("mentalHealth.choosePrompt")}
</h2>
{(["PHQ9", "GAD7"] as InstrumentId[]).map((id) => (
<Card key={id}>
<CardContent className="flex items-center justify-between gap-4 p-4">
<div className="flex flex-col">
<span className="font-medium">
{t(`mentalHealth.instrument.${lower(id)}`)}
</span>
<span className="text-muted-foreground text-xs">
{t(`mentalHealth.instrumentSub.${lower(id)}`)}
</span>
</div>
<Button onClick={() => begin(id)}>
{t("mentalHealth.start")}
</Button>
</CardContent>
</Card>
))}
</section>
)}
{phase === "form" && (
<section className="flex flex-col gap-5">
<div className="flex items-center justify-between">
<Button
variant="ghost"
size="sm"
onClick={() => setPhase("choose")}
>
{t("mentalHealth.back")}
</Button>
<span className="text-sm font-medium">
{t(`mentalHealth.instrument.${lower(instrument)}`)}
</span>
</div>
<p className="text-sm">{t("mentalHealth.stem")}</p>
<ol className="flex flex-col gap-5">
{itemIndexes.map((i) => (
<li key={i} className="flex flex-col gap-2">
<span className="text-sm">
{i + 1}.{" "}
{t(`mentalHealth.items.${lower(instrument)}.${i + 1}`)}
</span>
<div
className="flex flex-wrap gap-2"
role="group"
aria-label={t(
`mentalHealth.items.${lower(instrument)}.${i + 1}`,
)}
>
{SCALE.map((v) => (
<Button
key={v}
type="button"
size="sm"
variant={items[i] === v ? "default" : "outline"}
aria-pressed={items[i] === v}
onClick={() =>
setItems((prev) => {
const next = [...prev];
next[i] = v;
return next;
})
}
>
{t(`mentalHealth.options.${v}`)}
</Button>
))}
</div>
</li>
))}
</ol>
<div className="border-border/60 flex flex-col gap-2 border-t pt-4">
<span className="text-sm">{t("mentalHealth.functionalTitle")}</span>
<div
className="flex flex-wrap gap-2"
role="group"
aria-label={t("mentalHealth.functionalTitle")}
>
{FUNCTIONAL.map((v) => (
<Button
key={v}
type="button"
size="sm"
variant={functional === v ? "default" : "outline"}
aria-pressed={functional === v}
onClick={() => setFunctional(v)}
>
{t(`mentalHealth.functional.${v}`)}
</Button>
))}
</div>
</div>
{mutation.isError && (
<p className="text-destructive text-sm">
{t("mentalHealth.error")}
</p>
)}
<Button onClick={submit} disabled={!complete || mutation.isPending}>
{mutation.isPending
? t("mentalHealth.submitting")
: t("mentalHealth.submit")}
</Button>
</section>
)}
{phase === "result" && result && (
<section className="flex flex-col gap-4">
<p className="text-muted-foreground text-xs" role="status">
{t("mentalHealth.saved")}
</p>
<Card>
<CardHeader>
<CardTitle>{t("mentalHealth.result.title")}</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-3 text-sm">
<div className="flex items-baseline justify-between">
<span className="text-muted-foreground">
{t("mentalHealth.result.totalLabel")}
</span>
<span className="text-2xl font-semibold">
{result.assessment.totalScore}
</span>
</div>
<div className="flex items-baseline justify-between">
<span className="text-muted-foreground">
{t("mentalHealth.result.bandLabel")}
</span>
<span className="font-medium">
{t(
`mentalHealth.band.${result.assessment.instrument}.${result.assessment.severityBand}`,
)}
</span>
</div>
{result.assessment.totalScore >= result.actionThreshold && (
<p className="text-muted-foreground bg-muted/40 rounded-md p-3 text-xs">
{t("mentalHealth.considerProfessional")}
</p>
)}
</CardContent>
</Card>
{result.crisis && (
<Card className="border-amber-400/50">
<CardHeader>
<CardTitle className="text-base">
{t("mentalHealth.crisis.title")}
</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-3 text-sm">
<p>{t("mentalHealth.crisis.intro")}</p>
<p className="font-medium">
{t("mentalHealth.crisis.ifDanger", {
emergency: result.crisis.emergencyNumber,
})}
</p>
<div className="flex flex-col gap-2">
<span className="text-muted-foreground text-xs font-medium">
{t("mentalHealth.crisis.resourcesTitle")}
</span>
<ul className="flex flex-col gap-2">
{result.crisis.resources.map((r) => (
<li key={r.id} className="flex flex-col">
<span className="font-medium">
{t(`mentalHealth.crisisResource.${r.id}.name`)}
</span>
<span className="text-muted-foreground">
{r.contacts.join(" · ")}
</span>
</li>
))}
</ul>
</div>
</CardContent>
</Card>
)}
<p className="text-muted-foreground text-[11px] leading-snug">
<span className="font-medium">
{t("mentalHealth.attributionLabel")}:
</span>{" "}
{PHQ_GAD_ATTRIBUTION}
</p>
<Button variant="outline" onClick={() => setPhase("choose")}>
{t("mentalHealth.result.takeAnother")}
</Button>
</section>
)}
<section className="border-border/60 flex flex-col gap-2 border-t pt-4">
<h2 className="text-sm font-medium">
{t("mentalHealth.history.title")}
</h2>
{!history || history.assessments.length === 0 ? (
<p className="text-muted-foreground text-sm">
{t("mentalHealth.history.empty")}
</p>
) : (
<ul className="flex flex-col gap-1">
{history.assessments.map((row) => (
<li
key={row.id}
className="flex items-center justify-between text-sm"
>
<span>
{t(`mentalHealth.instrument.${lower(row.instrument)}`)}
</span>
<span className="text-muted-foreground">
{t(`mentalHealth.band.${row.instrument}.${row.severityBand}`)}{" "}
· {formatDate(row.takenAt)}
</span>
</li>
))}
</ul>
)}
</section>
</div>
);
}
@@ -77,6 +77,17 @@ const MEASUREMENT_TYPES_WITHOUT_HK_COUNTERPART = new Set<MeasurementType>([
// the Oura API (source = OURA), never from HealthKit: Apple ships no
// resilience metric. No HK mapping by design.
"RESILIENCE",
// v1.25 — clinical-signals wave. The mental-health screener totals are
// derived from an in-app questionnaire, and grip strength / pain NRS / waist
// circumference + waist-to-height are manual / instrument readings — none
// ships a first-class HealthKit quantity we ingest, so they have no HK
// mapping by design.
"PHQ9_SCORE",
"GAD7_SCORE",
"GRIP_STRENGTH",
"PAIN_NRS",
"WAIST_CIRCUMFERENCE",
"WAIST_TO_HEIGHT",
]);
describe("APPLE_HEALTH_TYPE_MAP", () => {
+2 -1
View File
@@ -1222,7 +1222,8 @@ export const SIGNALS: Record<string, SignalDefinition> = {
},
fhir: {
loinc: "44261-6",
display: "Patient Health Questionnaire 9 item (PHQ-9) total score [Reported]",
display:
"Patient Health Questionnaire 9 item (PHQ-9) total score [Reported]",
unit: "{score}",
category: "survey",
},
+4 -1
View File
@@ -5,7 +5,10 @@
* it is narrowed from the session in the handler (CLAUDE.md convention).
*/
import { z } from "zod/v4";
import { INSTRUMENTS, type InstrumentId } from "@/lib/mental-health/instruments";
import {
INSTRUMENTS,
type InstrumentId,
} from "@/lib/mental-health/instruments";
export const assessmentInstrumentEnum = z.enum(["PHQ9", "GAD7"]);