feat(mental-health): one check-in pattern for every questionnaire, worded as published

Every instrument now runs the same wizard: the form's own question stem
above each item (the PHQ-9 and GAD-7 stems were missing), the item's
exact answer options as one radio group in source order, progress, Back,
number-key answers, and a review step that lists every answer, lets any
of them be changed and is the only step that submits. The result leads
with the crisis card when it applies, then shows the score on the
instrument's scale, the band, what the band means, the change since the
last check-in against the meaningful-change threshold, and the next
step. Scoring is untouched.

The wording was read against the published versions. German follows the
PHQ-D and Löwe's GAD-7 verbatim, including the follow-up question in its
published formal address. Spanish, Italian and Polish carry the official
PHQ-9 and GAD-7 translations, French the official GAD-7, and the GAD-7
gets its own answer set because the Italian forms word it differently.
Where a version could not be matched to a published form (the French
PHQ-9, every Korean form) the language now shows the English original.
Each locale list names its sources, and spot checks pin the corrected
wording.
This commit is contained in:
Marc-André Bombeck committed 2026-10-22 19:28:29 +02:00
1 parent 49a999ab94
commit 038f8b10cd
24 files changed
+1398 -435

No files matched your search

+1 -1
View File
@@ -151,7 +151,7 @@ What a reviewer checks on a translation pull request:
- Medical terms are the ones the language's clinical guidelines use, and the register matches the rest of the locale (`du` in German, `você` in Brazilian Portuguese).
- Words the app or the Telegram bot matches literally stay as they are: slash commands, `taken <name>`, and the unit symbols.
- Machine translation is fine as a first draft, but someone who reads the language has gone through every string before the pull request is opened, and the description says so.
- Validated questionnaire items (the PHQ-9, GAD-7, WHO-5 and SCI items, their answer options and their stems under `mentalHealth.items`, `mentalHealth.options`, `mentalHealth.who5Options`, `mentalHealth.sciOptions`, `mentalHealth.stems`, `mentalHealth.functional*`) are never translated freely, not even as a draft. A language gets them only from a published, licensed validated version, copied verbatim and named in `src/lib/mental-health/instruments.ts`; until then the app shows the English original and the bundle carries none of those keys, which the i18n tests enforce.
- Validated questionnaire items (the PHQ-9, GAD-7, WHO-5 and SCI items, their answer options and their stems under `mentalHealth.items`, `mentalHealth.options`, `mentalHealth.gad7Options`, `mentalHealth.who5Options`, `mentalHealth.sciOptions`, `mentalHealth.stems`, `mentalHealth.functional*`) are never translated freely, not even as a draft. A language gets them only from a published, licensed validated version, copied verbatim and named in `src/lib/mental-health/instruments.ts`; until then the app shows the English original and the bundle carries none of those keys, which the i18n tests enforce.
To run the checks Fink shows on a local checkout: `npx @inlang/cli@3.4.4 check --project ./project.inlang`. It reads the files and changes nothing.
+173
View File
@@ -0,0 +1,173 @@
import AxeBuilder from "@axe-core/playwright";
import type { Page } from "@playwright/test";
import { expect, test } from "./setup/test";
import { STORAGE_STATE_PATH } from "./setup/global-setup";
/**
* One full check-in per instrument through the shared wizard: the stem is
* on every step, the answers form one radio group in the source order, the
* number keys answer, the review lists every answer and changes one, and
* the result carries score, band, meaning and the next step. The PHQ-9 run
* answers item 9 above zero, so its result must lead with the crisis card.
*
* The total on the result is the server's; this journey adds the chosen
* option values itself and compares, which is the scoring contract seen
* from the outside (`instruments.test.ts` pins it from the inside). Each
* screen is scanned with axe on the way.
*
* Anchored on data attributes only, so the journey reads the same at every
* width and in every language.
*/
type Case = {
id: "PHQ9" | "GAD7" | "WHO5" | "SCI";
items: number;
options: number;
multiplier: number;
followUp: boolean;
};
const CASES: readonly Case[] = [
{ id: "PHQ9", items: 9, options: 4, multiplier: 1, followUp: true },
{ id: "GAD7", items: 7, options: 4, multiplier: 1, followUp: false },
{ id: "WHO5", items: 5, options: 6, multiplier: 4, followUp: false },
{ id: "SCI", items: 8, options: 5, multiplier: 1, followUp: false },
];
async function expectNoSeriousViolations(page: Page, label: string) {
// The pointer rests where the last tap landed; a hover tint under it is
// not what the screen shows at rest.
await page.mouse.move(0, 0);
await page.waitForTimeout(300);
const results = await new AxeBuilder({ page })
.include(
'[data-slot="mental-health-check-in"], [data-slot="mental-health-result"]',
)
.withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
.analyze();
const blocking = results.violations
.filter((v) => v.impact === "serious" || v.impact === "critical")
.map(
(v) =>
`${v.id}: ${v.nodes.map((n) => `${n.html} ${n.failureSummary ?? ""}`).join(" | ")}`,
);
expect(blocking, `axe on ${label}`).toEqual([]);
}
test.describe("mental-health check-in, every instrument", () => {
test.use({ storageState: STORAGE_STATE_PATH });
for (const c of CASES) {
test(`${c.id}: steps, review, result`, async ({ page }) => {
test.setTimeout(90_000);
await page.goto("/mental-wellbeing");
const card = page.locator(
`[data-slot="instrument-card"][data-instrument="${c.id}"]`,
);
await card.locator('[data-slot="instrument-card-start"]').click();
const wizard = page.locator(
`[data-slot="mental-health-check-in"][data-instrument="${c.id}"]`,
);
await expect(wizard).toBeVisible();
const values: number[] = [];
for (let step = 1; step <= c.items; step++) {
const current = wizard.locator(
`[data-slot="check-in-step"][data-step="${step}"]`,
);
await expect(current).toBeVisible();
await expect(
current.locator('[data-slot="check-in-stem"]'),
).toHaveCount(1);
const group = current.locator('[role="radiogroup"]');
await expect(group.locator('[role="radio"]')).toHaveCount(c.options);
if (step === 1) await expectNoSeriousViolations(page, `${c.id} step 1`);
// PHQ-9 item 9 answered above zero: the option in the last position
// is "nearly every day" (value 3).
const position =
c.id === "PHQ9" && step === 9 ? c.options : (step % c.options) + 1;
const answered = group.locator('[role="radio"]').nth(position - 1);
values.push(Number(await answered.getAttribute("data-value")));
if (step % 2 === 0) {
// Keyboard: the number key picks the answer in that position.
await group.locator('[role="radio"]').first().focus();
await page.keyboard.press(String(position));
} else {
await group
.locator('[role="radio"]')
.nth(position - 1)
.click();
}
}
if (c.followUp) {
const followUp = wizard.locator(
'[data-slot="check-in-step"][data-functional]',
);
await expect(followUp).toBeVisible();
await followUp.locator('[role="radio"]').nth(1).click();
await wizard.locator('[data-slot="check-in-next"]').click();
}
const review = wizard.locator('[data-slot="check-in-review"]');
await expect(review).toBeVisible();
await expect(
review.locator('[data-slot="check-in-review-row"]'),
).toHaveCount(c.items + (c.followUp ? 1 : 0));
await expectNoSeriousViolations(page, `${c.id} review`);
// Change the second answer from the review; the wizard returns there.
await review.locator('[data-review-edit="2"]').click();
const second = wizard.locator(
'[data-slot="check-in-step"][data-step="2"]',
);
await expect(second).toBeVisible();
const other =
values[1] ===
Number(
await second
.locator('[role="radio"]')
.first()
.getAttribute("data-value"),
)
? 1
: 0;
await second.locator('[role="radio"]').nth(other).click();
await expect(review).toBeVisible();
values[1] = Number(
await review
.locator('[data-slot="check-in-review-row"][data-step="2"]')
.getAttribute("data-value"),
);
await wizard.locator('[data-slot="check-in-submit"]').click();
const result = page.locator(
`[data-slot="mental-health-result"][data-instrument="${c.id}"]`,
);
await expect(result).toBeVisible();
const total = values.reduce((sum, v) => sum + v, 0) * c.multiplier;
await expect(result.locator('[data-slot="result-score"]')).toContainText(
String(total),
);
await expect(result.locator('[data-slot="result-band"]')).toBeVisible();
await expect(
result.locator('[data-slot="result-meaning"]'),
).toBeVisible();
await expect(
result.locator(
'[data-slot="result-follow-up-hint"], [data-slot="result-next-step"]',
),
).toHaveCount(1);
if (c.id === "PHQ9") {
await expect(
result.locator('[data-slot="mental-health-crisis-card"]'),
).toBeVisible();
}
await expectNoSeriousViolations(page, `${c.id} result`);
});
}
});
+59 -8
View File
@@ -10953,11 +10953,17 @@
"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?",
"gad7Options": {
"0": "Überhaupt nicht",
"1": "An einzelnen Tagen",
"2": "An mehr als der Hälfte der Tage",
"3": "Beinahe jeden Tag"
},
"functionalTitle": "Wenn eines oder mehrere der bisher in diesem Fragebogen beschriebenen Probleme bei Ihnen vorliegen, geben Sie bitte an, wie sehr diese Probleme es Ihnen erschwert haben, Ihre Arbeit zu tun, Ihren Haushalt zu regeln oder mit anderen Menschen zurecht zu kommen:",
"functional": {
"0": "Überhaupt nicht erschwert",
"1": "Etwas erschwert",
"2": "Sehr erschwert",
"2": "Stark erschwert",
"3": "Extrem erschwert"
},
"functionalOptionalHint": "Optional: Du kannst auch ohne Antwort auf diese Frage absenden.",
@@ -10965,7 +10971,10 @@
"title": "Dein Ergebnis",
"totalLabel": "Gesamtwert",
"bandLabel": "Bereich für dieses Screening",
"takeAnother": "Weiteren Check-in machen"
"takeAnother": "Weiteren Check-in machen",
"scoreOf": "von {max}",
"firstCheckIn": "Das ist dein erstes Ergebnis mit diesem Fragebogen. Ab dem nächsten siehst du hier die Veränderung.",
"nextStep": "Du kannst den Check-in in zwei Wochen oder später wiederholen, um die Veränderung zu sehen. Wenn dich etwas beunruhigt, sprich jederzeit mit einer Fachperson."
},
"band": {
"PHQ9": {
@@ -11014,8 +11023,8 @@
"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?",
"7": "Schwierigkeiten, sich auf etwas zu konzentrieren, z. B. beim Zeitungslesen 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 dadurch einen stärkeren Bewegungsdrang als sonst?",
"9": "Gedanken, dass Sie lieber tot wären oder sich Leid zufügen möchten"
},
"gad7": {
@@ -11023,9 +11032,9 @@
"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",
"5": "Rastlosigkeit, so dass Stillsitzen schwer fällt",
"6": "Schnelle Verärgerung oder Gereiztheit",
"7": "Gefühl der Angst, als würde etwas Schlimmes passieren"
"7": "Gefühl der Angst, so als würde etwas Schlimmes passieren"
},
"who5": {
"1": "… war ich froh und guter Laune",
@@ -11082,7 +11091,6 @@
"who5": "Wohlbefindens-Check mit 5 Fragen. Beziehe dich auf die letzten zwei Wochen; höhere Werte bedeuten besseres Wohlbefinden.",
"sci": "Schlaf-Check mit 8 Fragen. Die meisten Fragen beziehen sich auf den letzten Monat; höhere Werte bedeuten besseren Schlaf."
},
"progress": "{current} / {total}",
"next": "Weiter",
"lastResult": "Letzter Test",
"lastScore": "Letztes Ergebnis",
@@ -11104,6 +11112,12 @@
"5": "Die ganze Zeit"
},
"stems": {
"phq9": {
"period": "Wie oft fühlten Sie sich im Verlauf der letzten 2 Wochen durch die folgenden Beschwerden beeinträchtigt?"
},
"gad7": {
"period": "Wie oft fühlten Sie sich im Verlauf der letzten 2 Wochen durch die folgenden Beschwerden beeinträchtigt?"
},
"who5": {
"period": "In den letzten zwei Wochen …"
}
@@ -11120,6 +11134,43 @@
"worsened": "Das erreicht die Veränderung von {threshold} Punkten, die Studien als bedeutsam ansehen, in die weniger gute Richtung.",
"within_threshold": "Das ist weniger als die Veränderung von {threshold} Punkten, die Studien als bedeutsam ansehen."
}
},
"wizard": {
"progressLabel": "Frage {current} von {total}",
"followUpLabel": "Zusatzfrage (freiwillig)",
"keyboardHint": "Mit den Zahlentasten 1 bis {count} wählst du eine Antwort.",
"allAnswered": "Alle {total} Fragen beantwortet"
},
"review": {
"title": "Antworten prüfen",
"description": "Gespeichert wird erst beim Absenden, bis dahin lässt sich jede Antwort noch ändern.",
"change": "Ändern",
"changeLabel": "Antwort auf Frage {number} ändern",
"notAnswered": "Nicht beantwortet",
"open": "Antworten prüfen"
},
"bandMeaning": {
"PHQ9": {
"minimal": "Deine Antworten weisen für die letzten zwei Wochen auf keine oder kaum depressive Beschwerden hin.",
"mild": "Deine Antworten weisen für die letzten zwei Wochen auf leichte depressive Beschwerden hin.",
"moderate": "Deine Antworten weisen für die letzten zwei Wochen auf mittelgradige depressive Beschwerden hin.",
"modSevere": "Deine Antworten weisen für die letzten zwei Wochen auf mittelschwere depressive Beschwerden hin.",
"severe": "Deine Antworten weisen für die letzten zwei Wochen auf schwere depressive Beschwerden hin."
},
"GAD7": {
"minimal": "Deine Antworten weisen für die letzten zwei Wochen auf keine oder kaum Angstbeschwerden hin.",
"mild": "Deine Antworten weisen für die letzten zwei Wochen auf leichte Angstbeschwerden hin.",
"moderate": "Deine Antworten weisen für die letzten zwei Wochen auf mittelgradige Angstbeschwerden hin.",
"severe": "Deine Antworten weisen für die letzten zwei Wochen auf schwere Angstbeschwerden hin."
},
"WHO5": {
"low": "Dein Wohlbefinden lag in den letzten zwei Wochen unter dem Bereich, den der WHO-5 als gut wertet.",
"good": "Dein Wohlbefinden lag in den letzten zwei Wochen in dem Bereich, den der WHO-5 als gut wertet."
},
"SCI": {
"belowThreshold": "Dein Schlaf lag im letzten Monat in dem Bereich, in dem die Studie hinter diesem Fragebogen eine Insomnie für wahrscheinlich hielt.",
"aboveThreshold": "Dein Schlaf lag im letzten Monat außerhalb des Bereichs, den diese Studie mit Insomnie verbindet."
}
}
},
"records": {
+53 -2
View File
@@ -10953,6 +10953,12 @@
"2": "More than half the days",
"3": "Nearly every day"
},
"gad7Options": {
"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 problems made it for you to do your work, take care of things at home, or get along with other people?",
"functional": {
"0": "Not difficult at all",
@@ -10965,7 +10971,10 @@
"title": "Your result",
"totalLabel": "Total score",
"bandLabel": "Range for this screen",
"takeAnother": "Take another check-in"
"takeAnother": "Take another check-in",
"scoreOf": "of {max}",
"firstCheckIn": "This is your first result for this questionnaire. From the next one on, the change shows here.",
"nextStep": "You can repeat this check-in in two weeks or later to follow the change. If something worries you, talk to a health professional at any time."
},
"band": {
"PHQ9": {
@@ -11092,7 +11101,6 @@
"who5": "5-question well-being check. Answer for the last two weeks; higher numbers mean better well-being.",
"sci": "8-question sleep check. Most questions ask about the past month; higher scores mean better sleep."
},
"progress": "{current} / {total}",
"next": "Next",
"lastResult": "Last test",
"lastScore": "Last result",
@@ -11151,6 +11159,12 @@
}
},
"stems": {
"phq9": {
"period": "Over the last 2 weeks, how often have you been bothered by any of the following problems?"
},
"gad7": {
"period": "Over the last 2 weeks, how often have you been bothered by the following problems?"
},
"who5": {
"period": "Over the last two weeks …"
},
@@ -11172,6 +11186,43 @@
"worsened": "That reaches the {threshold}-point change studies treat as meaningful, in the less good direction.",
"within_threshold": "That is smaller than the {threshold}-point change studies treat as meaningful."
}
},
"wizard": {
"progressLabel": "Question {current} of {total}",
"followUpLabel": "Follow-up question (optional)",
"keyboardHint": "Number keys 1 to {count} choose an answer.",
"allAnswered": "All {total} questions answered"
},
"review": {
"title": "Check your answers",
"description": "Nothing is saved until you submit, and every answer can still be changed.",
"change": "Change",
"changeLabel": "Change the answer to question {number}",
"notAnswered": "Not answered",
"open": "Review answers"
},
"bandMeaning": {
"PHQ9": {
"minimal": "Your answers point to few or no symptoms of depression over the last two weeks.",
"mild": "Your answers point to mild symptoms of depression over the last two weeks.",
"moderate": "Your answers point to moderate symptoms of depression over the last two weeks.",
"modSevere": "Your answers point to moderately severe symptoms of depression over the last two weeks.",
"severe": "Your answers point to severe symptoms of depression over the last two weeks."
},
"GAD7": {
"minimal": "Your answers point to few or no symptoms of anxiety over the last two weeks.",
"mild": "Your answers point to mild symptoms of anxiety over the last two weeks.",
"moderate": "Your answers point to moderate symptoms of anxiety over the last two weeks.",
"severe": "Your answers point to severe symptoms of anxiety over the last two weeks."
},
"WHO5": {
"low": "Your well-being over the last two weeks was below the range the WHO-5 counts as good.",
"good": "Your well-being over the last two weeks was in the range the WHO-5 counts as good."
},
"SCI": {
"belowThreshold": "Your sleep over the last month was in the range where the study behind this questionnaire found insomnia likely.",
"aboveThreshold": "Your sleep over the last month was outside the range that study linked with insomnia."
}
}
},
"records": {
+74 -23
View File
@@ -10951,21 +10951,30 @@
"0": "Nunca",
"1": "Varios días",
"2": "Más de la mitad de los días",
"3": "Casi todos los días"
"3": "Casi cada día"
},
"functionalTitle": "Si marcaste algún problema, ¿cuánta dificultad te han causado para trabajar, ocuparte de la casa o relacionarte con otras personas?",
"gad7Options": {
"0": "Nunca",
"1": "Varios días",
"2": "Más de la mitad de los días",
"3": "Casi cada día"
},
"functionalTitle": "Si ha marcado alguno de los problemas de este cuestionario, ¿hasta qué punto estos problemas le han creado dificultades para hacer su trabajo, ocuparse de la casa o relacionarse con los demás?",
"functional": {
"0": "Ninguna dificultad",
"1": "Algo de dificultad",
"2": "Mucha dificultad",
"3": "Dificultad extrema"
"1": "Algunas dificultades",
"2": "Muchas dificultades",
"3": "Muchísimas dificultades"
},
"functionalOptionalHint": "Opcional: puedes enviar sin responder esta pregunta.",
"result": {
"title": "Tu resultado",
"totalLabel": "Puntuación total",
"bandLabel": "Rango para este cribado",
"takeAnother": "Hacer otra revisión"
"takeAnother": "Hacer otra revisión",
"scoreOf": "de {max}",
"firstCheckIn": "Es tu primer resultado con este cuestionario. A partir del próximo verás aquí el cambio.",
"nextStep": "Puedes repetir este cuestionario dentro de dos semanas o más tarde para seguir el cambio. Si algo te preocupa, habla con un profesional de la salud en cualquier momento."
},
"band": {
"PHQ9": {
@@ -11008,24 +11017,24 @@
},
"items": {
"phq9": {
"1": "Tener poco interés o placer en hacer las cosas",
"2": "Sentirse desanimado/a, deprimido/a o sin esperanza",
"3": "Tener problemas para dormir o mantenerse dormido/a, o dormir demasiado",
"4": "Sentirse cansado/a o tener poca energía",
"5": "Tener poco apetito o comer en exceso",
"6": "Sentirse mal con usted mismo/a, o sentir que es un fracaso o que se ha fallado a sí mismo/a o a su familia",
"7": "Tener dificultad para concentrarse en cosas tales como leer el periódico o ver la televisión",
"8": "¿Moverse o hablar tan despacio que otras personas podrían haberlo notado? O lo contrario: estar tan inquieto/a o agitado/a que se ha estado moviendo mucho más de lo habitual",
"9": "Pensar que estaría mejor muerto/a o en hacerse daño de alguna manera"
"1": "Poco interés o alegría por hacer cosas",
"2": "Sensación de estar decaído/a, deprimido/a o desesperanzado/a",
"3": "Problemas para quedarse dormido/a, para seguir durmiendo o dormir demasiado",
"4": "Sensación de cansancio o de tener poca energía",
"5": "Poco apetito o comer demasiado",
"6": "Sentirse mal consigo mismo/a; sentir que es un/a fracasado/a o que ha decepcionado a su familia o a sí mismo/a",
"7": "Problemas para concentrarse en algo, como leer el periódico o ver la televisión",
"8": "Moverse o hablar tan despacio que los demás pueden haberlo notado. O lo contrario: estar tan inquieto/a o agitado/a que se ha estado moviendo de un lado a otro más de lo habitual",
"9": "Pensamientos de que estaría mejor muerto/a o de querer hacerse daño de algún modo"
},
"gad7": {
"1": "Sentirse nervioso/a, ansioso/a o muy alterado/a",
"2": "No poder dejar de preocuparse o no poder controlar la preocupación",
"3": "Preocuparse demasiado por diferentes cosas",
"4": "Tener dificultad para relajarse",
"5": "Estar tan inquieto/a que es difícil quedarse sentado/a tranquilamente",
"6": "Molestarse o irritarse fácilmente",
"7": "Sentir miedo como si algo terrible fuera a suceder"
"1": "Sentirse nervioso/a, angustiado/a o muy tenso/a",
"2": "Ser incapaz de dejar de preocuparse o de controlar la preocupación",
"3": "Preocuparse demasiado por diferentes cuestiones",
"4": "Tener problemas para relajarse",
"5": "Estar tan inquieto/a que le resulta difícil permanecer sentado/a",
"6": "Enfadarse o irritarse con facilidad",
"7": "Sentir miedo de que algo terrible pueda ocurrir"
},
"who5": {
"1": "Me he sentido alegre y de buen humor",
@@ -11082,7 +11091,6 @@
"who5": "Test de bienestar de 5 preguntas. Responde pensando en las últimas dos semanas; cifras mayores significan mayor bienestar.",
"sci": "Test de sueño de 8 preguntas. La mayoría se refiere al último mes; puntuaciones mayores significan mejor sueño."
},
"progress": "{current} / {total}",
"next": "Siguiente",
"lastResult": "Última prueba",
"lastScore": "Último resultado",
@@ -11104,6 +11112,12 @@
"5": "Todo el tiempo"
},
"stems": {
"phq9": {
"period": "Durante las últimas 2 semanas ¿con qué frecuencia le ha molestado alguno de los siguientes problemas?"
},
"gad7": {
"period": "En los últimos 14 días, ¿con qué frecuencia le han supuesto una molestia los siguientes problemas?"
},
"who5": {
"period": "Durante las últimas dos semanas:"
}
@@ -11120,6 +11134,43 @@
"worsened": "Alcanza el cambio de {threshold} puntos que los estudios consideran relevante, en la dirección menos favorable.",
"within_threshold": "Es menor que el cambio de {threshold} puntos que los estudios consideran relevante."
}
},
"wizard": {
"progressLabel": "Pregunta {current} de {total}",
"followUpLabel": "Pregunta adicional (opcional)",
"keyboardHint": "Las teclas 1 a {count} eligen una respuesta.",
"allAnswered": "Las {total} preguntas tienen respuesta"
},
"review": {
"title": "Revisar respuestas",
"description": "No se guarda nada hasta que lo envíes, y cada respuesta todavía se puede cambiar.",
"change": "Cambiar",
"changeLabel": "Cambiar la respuesta a la pregunta {number}",
"notAnswered": "Sin respuesta",
"open": "Revisar respuestas"
},
"bandMeaning": {
"PHQ9": {
"minimal": "Tus respuestas apuntan a pocos o ningún síntoma depresivo en las últimas dos semanas.",
"mild": "Tus respuestas apuntan a síntomas depresivos leves en las últimas dos semanas.",
"moderate": "Tus respuestas apuntan a síntomas depresivos moderados en las últimas dos semanas.",
"modSevere": "Tus respuestas apuntan a síntomas depresivos moderadamente graves en las últimas dos semanas.",
"severe": "Tus respuestas apuntan a síntomas depresivos graves en las últimas dos semanas."
},
"GAD7": {
"minimal": "Tus respuestas apuntan a pocos o ningún síntoma de ansiedad en las últimas dos semanas.",
"mild": "Tus respuestas apuntan a síntomas de ansiedad leves en las últimas dos semanas.",
"moderate": "Tus respuestas apuntan a síntomas de ansiedad moderados en las últimas dos semanas.",
"severe": "Tus respuestas apuntan a síntomas de ansiedad graves en las últimas dos semanas."
},
"WHO5": {
"low": "Tu bienestar en las últimas dos semanas estuvo por debajo del rango que el WHO-5 considera bueno.",
"good": "Tu bienestar en las últimas dos semanas estuvo dentro del rango que el WHO-5 considera bueno."
},
"SCI": {
"belowThreshold": "Tu sueño del último mes estuvo en el rango en el que el estudio detrás de este cuestionario consideró probable un insomnio.",
"aboveThreshold": "Tu sueño del último mes estuvo fuera del rango que ese estudio asocia con el insomnio."
}
}
},
"records": {
+53 -29
View File
@@ -10947,25 +10947,21 @@
"who5": "Test de bien-être, 5 questions",
"sci": "Test de sommeil, 8 questions"
},
"options": {
"gad7Options": {
"0": "Jamais",
"1": "Plusieurs jours",
"2": "Plus de la moitié des jours",
"2": "Plus de la moitié du temps",
"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"
},
"functionalOptionalHint": "Facultatif : tu peux envoyer sans répondre à cette question.",
"result": {
"title": "Ton résultat",
"totalLabel": "Score total",
"bandLabel": "Plage pour ce dépistage",
"takeAnother": "Faire un autre bilan"
"takeAnother": "Faire un autre bilan",
"scoreOf": "sur {max}",
"firstCheckIn": "C’est votre premier résultat pour ce questionnaire. À partir du prochain, l’évolution s’affichera ici.",
"nextStep": "Vous pouvez refaire ce questionnaire dans deux semaines ou plus tard pour suivre l’évolution. Si quelque chose vous inquiète, parlez-en à tout moment à un professionnel de santé."
},
"band": {
"PHQ9": {
@@ -11007,25 +11003,14 @@
"resourcesTitle": "Des personnes qui peuvent aider"
},
"items": {
"phq9": {
"1": "Peu d'intérêt ou de plaisir à faire les choses",
"2": "Se sentir triste, déprimé(e) ou désespéré(e)",
"3": "Difficultés à s'endormir ou à rester endormi(e), ou dormir trop",
"4": "Se sentir fatigué(e) ou avoir peu d'énergie",
"5": "Avoir peu d'appétit ou manger trop",
"6": "Avoir une mauvaise opinion de soi-même — ou avoir le sentiment d'être nul(le) ou d'avoir déçu sa famille ou soi-même",
"7": "Avoir du mal à se concentrer, par exemple pour lire le journal ou regarder la télévision",
"8": "Bouger ou parler si lentement que les autres ont pu le remarquer. Ou au contraire, être si agité(e) que vous avez bougé beaucoup plus que d'habitude",
"9": "Penser qu'il vaudrait mieux mourir ou envisager de vous faire du mal d'une manière ou d'une autre"
},
"gad7": {
"1": "Se sentir nerveux/nerveuse, anxieux/anxieuse ou tendu(e)",
"2": "Être incapable d'arrêter de vous inquiéter ou de contrôler vos inquiétudes",
"3": "Vous inquiéter trop à propos de choses diverses",
"4": "Avoir du mal à vous détendre",
"5": "Être si agité(e) qu'il est difficile de rester tranquille",
"6": "Devenir facilement contrarié(e) ou irritable",
"7": "Avoir peur que quelque chose d'épouvantable puisse arriver"
"1": "Un sentiment de nervosité, d’anxiété ou de tension",
"2": "Une incapacité à arrêter de s’inquiéter ou à contrôler ses inquiétudes",
"3": "Une inquiétude excessive à propos de différentes choses",
"4": "Des difficultés à se détendre",
"5": "Une agitation telle qu’il est difficile à tenir en place",
"6": "Une tendance à être facilement contrarié(e) ou irritable",
"7": "Un sentiment de peur comme si quelque chose de terrible risquait de se produire"
},
"who5": {
"1": "Je me suis senti(e) bien et de bonne humeur",
@@ -11082,7 +11067,6 @@
"who5": "Test de bien-être en 5 questions. Répondez en pensant aux deux dernières semaines ; des chiffres plus élevés signifient un meilleur bien-être.",
"sci": "Test de sommeil en 8 questions. La plupart des questions portent sur le dernier mois ; des scores plus élevés signifient un meilleur sommeil."
},
"progress": "{current} / {total}",
"next": "Suivant",
"lastResult": "Dernier test",
"lastScore": "Dernier résultat",
@@ -11104,6 +11088,9 @@
"5": "Tout le temps"
},
"stems": {
"gad7": {
"period": "Au cours des 2 dernières semaines, selon quelle fréquence avez-vous été gêné(e) par les problèmes suivants ?"
},
"who5": {
"period": "Au cours des deux dernières semaines"
}
@@ -11120,6 +11107,43 @@
"worsened": "Cela atteint la variation de {threshold} points que les études jugent significative, dans le sens moins favorable.",
"within_threshold": "C'est moins que la variation de {threshold} points que les études jugent significative."
}
},
"wizard": {
"progressLabel": "Question {current} sur {total}",
"followUpLabel": "Question complémentaire (facultative)",
"keyboardHint": "Les touches 1 à {count} choisissent une réponse.",
"allAnswered": "Les {total} questions ont une réponse"
},
"review": {
"title": "Vérifier les réponses",
"description": "Rien n’est enregistré avant l’envoi, et chaque réponse peut encore être modifiée.",
"change": "Modifier",
"changeLabel": "Modifier la réponse à la question {number}",
"notAnswered": "Sans réponse",
"open": "Vérifier les réponses"
},
"bandMeaning": {
"PHQ9": {
"minimal": "Vos réponses indiquent peu ou pas de symptômes dépressifs au cours des deux dernières semaines.",
"mild": "Vos réponses indiquent des symptômes dépressifs légers au cours des deux dernières semaines.",
"moderate": "Vos réponses indiquent des symptômes dépressifs modérés au cours des deux dernières semaines.",
"modSevere": "Vos réponses indiquent des symptômes dépressifs modérément sévères au cours des deux dernières semaines.",
"severe": "Vos réponses indiquent des symptômes dépressifs sévères au cours des deux dernières semaines."
},
"GAD7": {
"minimal": "Vos réponses indiquent peu ou pas de symptômes d’anxiété au cours des deux dernières semaines.",
"mild": "Vos réponses indiquent des symptômes d’anxiété légers au cours des deux dernières semaines.",
"moderate": "Vos réponses indiquent des symptômes d’anxiété modérés au cours des deux dernières semaines.",
"severe": "Vos réponses indiquent des symptômes d’anxiété sévères au cours des deux dernières semaines."
},
"WHO5": {
"low": "Votre bien-être des deux dernières semaines était en dessous de la zone que le WHO-5 considère comme bonne.",
"good": "Votre bien-être des deux dernières semaines était dans la zone que le WHO-5 considère comme bonne."
},
"SCI": {
"belowThreshold": "Votre sommeil du dernier mois se situait dans la zone où l’étude à l’origine de ce questionnaire jugeait une insomnie probable.",
"aboveThreshold": "Votre sommeil du dernier mois se situait en dehors de la zone que cette étude associe à l’insomnie."
}
}
},
"records": {
+67 -16
View File
@@ -10948,15 +10948,21 @@
"sci": "Test sul sonno, 8 domande"
},
"options": {
"0": "Per niente",
"0": "Mai",
"1": "Alcuni giorni",
"2": "Più della metà dei giorni",
"2": "Per 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?",
"gad7Options": {
"0": "Mai",
"1": "Alcuni giorni",
"2": "Per oltre la metà dei giorni",
"3": "Quasi ogni giorno"
},
"functionalTitle": "Se ha fatto una crocetta su uno qualsiasi di questi problemi, quanto questi problemi le hanno reso difficile fare il suo lavoro, occuparsi delle sue cose a casa o avere buoni rapporti con gli altri?",
"functional": {
"0": "Per niente difficile",
"1": "Un po' difficile",
"1": "Abbastanza difficile",
"2": "Molto difficile",
"3": "Estremamente difficile"
},
@@ -10965,7 +10971,10 @@
"title": "Il tuo risultato",
"totalLabel": "Punteggio totale",
"bandLabel": "Intervallo per questo screening",
"takeAnother": "Fai un altro check-in"
"takeAnother": "Fai un altro check-in",
"scoreOf": "su {max}",
"firstCheckIn": "È il tuo primo risultato con questo questionario. Dal prossimo vedrai qui il cambiamento.",
"nextStep": "Puoi ripetere questo questionario tra due settimane o più tardi per seguire il cambiamento. Se qualcosa ti preoccupa, parlane in qualsiasi momento con un professionista sanitario."
},
"band": {
"PHQ9": {
@@ -11009,23 +11018,23 @@
"items": {
"phq9": {
"1": "Scarso interesse o piacere nel fare le cose",
"2": "Sentirsi giù di morale, depresso/a o disperato/a",
"3": "Difficoltà ad addormentarsi o a mantenere il sonno, oppure dormire troppo",
"2": "Sentirsi giù, triste o disperato/a",
"3": "Problemi ad addormentarsi o a dormire tutta la notte senza svegliarsi, o a dormire troppo",
"4": "Sentirsi stanco/a o avere poca energia",
"5": "Scarso appetito o mangiare troppo",
"6": "Avere una cattiva opinione di sé, o sentirsi un/una fallito/a o aver deluso se stesso/a o i propri familiari",
"7": "Difficoltà a concentrarsi, ad esempio nel leggere il giornale o nel guardare la televisione",
"8": "Muoversi o parlare così lentamente che altre persone potrebbero averlo notato? Oppure, al contrario, essere così agitato/a o irrequieto/a da muoversi molto più del solito",
"9": "Pensare che sarebbe meglio morire o di farsi del male in qualche modo"
"6": "Avere una scarsa opinione di sé, o sentirsi un/una fallito/a o aver deluso se stesso/a o i propri familiari",
"7": "Difficoltà a concentrarsi su qualcosa, per esempio leggere il giornale o guardare la televisione",
"8": "Muoversi o parlare così lentamente da poter essere notato/a da altre persone. O, al contrario, essere così irrequieto/a da muoversi molto più del solito",
"9": "Pensare che sarebbe meglio morire o farsi del male in un modo o nell’altro"
},
"gad7": {
"1": "Sentirsi nervoso/a, ansioso/a o teso/a",
"2": "Non riuscire a smettere di preoccuparsi o a controllare le preoccupazioni",
"3": "Preoccuparsi troppo per cose diverse",
"2": "Non riuscire a smettere di preoccuparsi o a tenere sotto controllo le preoccupazioni",
"3": "Preoccuparsi troppo per varie cose",
"4": "Avere difficoltà a rilassarsi",
"5": "Essere così irrequieto/a da far fatica a stare seduto/a fermo/a",
"5": "Essere talmente irrequieto/a da far fatica a stare seduto/a fermo/a",
"6": "Infastidirsi o irritarsi facilmente",
"7": "Avere paura come se potesse accadere qualcosa di terribile"
"7": "Avere paura che possa succedere qualcosa di terribile"
},
"who5": {
"1": "Mi sono sentito/a allegro/a e di buon umore",
@@ -11082,7 +11091,6 @@
"who5": "Test sul benessere in 5 domande. Rispondi pensando alle ultime due settimane; numeri più alti indicano un maggior benessere.",
"sci": "Test sul sonno in 8 domande. La maggior parte riguarda l'ultimo mese; punteggi più alti indicano un sonno migliore."
},
"progress": "{current} / {total}",
"next": "Avanti",
"lastResult": "Ultimo test",
"lastScore": "Ultimo risultato",
@@ -11104,6 +11112,12 @@
"5": "Sempre"
},
"stems": {
"phq9": {
"period": "Nelle ultime 2 settimane, con quale frequenza le ha dato fastidio ciascuno dei seguenti problemi?"
},
"gad7": {
"period": "Nelle ultime 2 settimane, con quale frequenza le ha dato fastidio ciascuno dei seguenti problemi?"
},
"who5": {
"period": "Nelle ultime due settimane"
}
@@ -11120,6 +11134,43 @@
"worsened": "Raggiunge la variazione di {threshold} punti che gli studi considerano rilevante, nella direzione meno buona.",
"within_threshold": "È meno della variazione di {threshold} punti che gli studi considerano rilevante."
}
},
"wizard": {
"progressLabel": "Domanda {current} di {total}",
"followUpLabel": "Domanda aggiuntiva (facoltativa)",
"keyboardHint": "I tasti da 1 a {count} scelgono una risposta.",
"allAnswered": "Tutte le {total} domande hanno una risposta"
},
"review": {
"title": "Controlla le risposte",
"description": "Nulla viene salvato prima dell’invio, e ogni risposta si può ancora cambiare.",
"change": "Modifica",
"changeLabel": "Modifica la risposta alla domanda {number}",
"notAnswered": "Senza risposta",
"open": "Controlla le risposte"
},
"bandMeaning": {
"PHQ9": {
"minimal": "Le tue risposte indicano sintomi depressivi assenti o minimi nelle ultime due settimane.",
"mild": "Le tue risposte indicano sintomi depressivi lievi nelle ultime due settimane.",
"moderate": "Le tue risposte indicano sintomi depressivi moderati nelle ultime due settimane.",
"modSevere": "Le tue risposte indicano sintomi depressivi moderatamente gravi nelle ultime due settimane.",
"severe": "Le tue risposte indicano sintomi depressivi gravi nelle ultime due settimane."
},
"GAD7": {
"minimal": "Le tue risposte indicano sintomi d’ansia assenti o minimi nelle ultime due settimane.",
"mild": "Le tue risposte indicano sintomi d’ansia lievi nelle ultime due settimane.",
"moderate": "Le tue risposte indicano sintomi d’ansia moderati nelle ultime due settimane.",
"severe": "Le tue risposte indicano sintomi d’ansia gravi nelle ultime due settimane."
},
"WHO5": {
"low": "Il tuo benessere nelle ultime due settimane era sotto la fascia che il WHO-5 considera buona.",
"good": "Il tuo benessere nelle ultime due settimane era nella fascia che il WHO-5 considera buona."
},
"SCI": {
"belowThreshold": "Il tuo sonno nell’ultimo mese era nella fascia in cui lo studio alla base di questo questionario riteneva probabile un’insonnia.",
"aboveThreshold": "Il tuo sonno nell’ultimo mese era fuori dalla fascia che quello studio collega all’insonnia."
}
}
},
"records": {
+41 -57
View File
@@ -10947,25 +10947,15 @@
"who5": "웰빙 점검, 5문항",
"sci": "수면 점검, 8문항"
},
"options": {
"0": "전혀 방해 받지 않았다",
"1": "며칠 동안 방해 받았다",
"2": "7 일 이상 방해 받았다",
"3": "거의 매일 방해 받았다"
},
"functionalTitle": "만일 당신이 위의 문제 중 하나 이상 \"예\" 라고 응답하셨으면, 이러한 문제들로 인해서 당신은 일을 하거나 가정일을 돌보거나 다른 사람과 어울리는 것이 얼마나 어려웠습니까?",
"functional": {
"0": "전혀 어렵지 않았다",
"1": "약간 어려웠다",
"2": "많이 어려웠다",
"3": "매우 많이 어려웠다"
},
"functionalOptionalHint": "선택 사항이에요. 이 문항에 답하지 않아도 제출할 수 있어요.",
"result": {
"title": "내 결과",
"totalLabel": "총점",
"bandLabel": "이 검사의 구간",
"takeAnother": "다른 점검 하기"
"takeAnother": "다른 점검 하기",
"scoreOf": "/ {max}",
"firstCheckIn": "이 설문의 첫 결과예요. 다음 결과부터 여기에 변화가 표시돼요.",
"nextStep": "변화를 보려면 2주 뒤나 그 이후에 다시 해 볼 수 있어요. 걱정되는 점이 있으면 언제든 전문가와 상담하세요."
},
"band": {
"PHQ9": {
@@ -11006,35 +10996,6 @@
"ifDanger": "당장 위험한 상황이라면 지금 {emergency}로 전화하세요.",
"resourcesTitle": "도움을 줄 수 있는 곳"
},
"items": {
"phq9": {
"1": "일 또는 여가 활동을 하는 데 흥미나 즐거움을 느끼지 못함",
"2": "기분이 가라앉거나, 우울하거나, 희망이 없음",
"3": "잠이 들거나 계속 잠을 자는 것이 어려움, 또는 잠을 너무 많이 잠",
"4": "피곤하다고 느끼거나 기운이 거의 없음",
"5": "입맛이 없거나 과식을 함",
"6": "자신을 부정적으로 봄 — 혹은 자신이 실패자라고 느끼거나 자신 또는 가족을 실망시킴",
"7": "신문을 읽거나 텔레비전 보는 것과 같은 일에 집중하는 것이 어려움",
"8": "다른 사람들이 주목할 정도로 너무 느리게 움직이거나 말을 함. 또는 반대로 평상시보다 많이 움직여서, 너무 안절부절 못하거나 들떠 있음",
"9": "자신이 죽는 것이 더 낫다고 생각하거나 어떤 식으로든 자신을 해칠 것이라고 생각함"
},
"gad7": {
"1": "초조하거나 불안하거나 조마조마하게 느낀다",
"2": "걱정하는 것을 멈추거나 조절할 수가 없다",
"3": "여러 가지 것들에 대해 걱정을 너무 많이 한다",
"4": "편하게 있기가 어렵다",
"5": "너무 안절부절못해서 가만히 있기가 힘들다",
"6": "쉽게 짜증이 나거나 쉽게 성을 내게 된다",
"7": "마치 끔찍한 일이 생길 것처럼 두렵게 느껴진다"
},
"who5": {
"1": "나는 즐겁고 기분이 좋다.",
"2": "나는 평온하고 느긋하다.",
"3": "나는 의욕적이고 활기차다.",
"4": "나는 자고 일어나면 상쾌하고 푹 쉬었다는 느낌이 든다.",
"5": "나의 일상생활은 흥미로운 일들로 가득하다."
}
},
"crisisResource": {
"telefonSeelsorge": {
"name": "TelefonSeelsorge (24시간 연중무휴, 무료, 비밀 보장)"
@@ -11082,7 +11043,6 @@
"who5": "5문항 웰빙 점검이에요. 지난 2주를 기준으로 답해 주세요. 숫자가 클수록 웰빙이 좋아요.",
"sci": "8문항 수면 점검이에요. 대부분 지난 한 달을 묻고, 점수가 높을수록 수면이 좋아요."
},
"progress": "{current} / {total}",
"next": "다음",
"lastResult": "최근 검사",
"lastScore": "최근 결과",
@@ -11095,19 +11055,6 @@
"sci": "이 설문의 바탕이 된 연구에서는 16점 이하가 불면증 가능성과 관련이 있었어요. 수면 문제가 계속 힘들게 한다면 전문가와 이야기해 보는 게 도움이 될 수 있어요. 진단은 아니에요."
},
"validatedInEnglishNote": "이 설문은 영어로 검증된 도구예요. 문항은 원문 영어 표현 그대로 보여줘요.",
"who5Options": {
"0": "전혀 그렇지 않다.",
"1": "가끔 그렇다.",
"2": "절반 미만이 그렇다.",
"3": "절반 이상이 그렇다.",
"4": "거의 그렇다.",
"5": "항상 그렇다."
},
"stems": {
"who5": {
"period": "지난 2주 동안"
}
},
"openDetail": "자세히 보기",
"historyLoadError": "검사 기록을 불러오지 못했어요",
"historyLoadErrorHint": "결과가 없다는 뜻은 아니에요. 목록을 가져오지 못했을 뿐이에요. 다시 시도해 주세요.",
@@ -11120,6 +11067,43 @@
"worsened": "연구에서 의미 있다고 보는 {threshold}점 변화에 도달했고, 덜 좋은 방향입니다.",
"within_threshold": "연구에서 의미 있다고 보는 {threshold}점 변화보다 작습니다."
}
},
"wizard": {
"progressLabel": "{total}개 중 {current}번째 질문",
"followUpLabel": "추가 질문(선택)",
"keyboardHint": "숫자 키 1부터 {count}까지로 답을 고를 수 있어요.",
"allAnswered": "{total}개 질문에 모두 답했어요"
},
"review": {
"title": "답변 확인",
"description": "제출하기 전에는 아무것도 저장되지 않으며, 모든 답변을 아직 바꿀 수 있어요.",
"change": "변경",
"changeLabel": "{number}번 질문의 답변 변경",
"notAnswered": "답하지 않음",
"open": "답변 확인"
},
"bandMeaning": {
"PHQ9": {
"minimal": "지난 2주 동안 우울 증상이 거의 또는 전혀 없었던 것으로 보여요.",
"mild": "지난 2주 동안 가벼운 우울 증상이 있었던 것으로 보여요.",
"moderate": "지난 2주 동안 중간 정도의 우울 증상이 있었던 것으로 보여요.",
"modSevere": "지난 2주 동안 다소 심한 우울 증상이 있었던 것으로 보여요.",
"severe": "지난 2주 동안 심한 우울 증상이 있었던 것으로 보여요."
},
"GAD7": {
"minimal": "지난 2주 동안 불안 증상이 거의 또는 전혀 없었던 것으로 보여요.",
"mild": "지난 2주 동안 가벼운 불안 증상이 있었던 것으로 보여요.",
"moderate": "지난 2주 동안 중간 정도의 불안 증상이 있었던 것으로 보여요.",
"severe": "지난 2주 동안 심한 불안 증상이 있었던 것으로 보여요."
},
"WHO5": {
"low": "지난 2주 동안의 웰빙이 WHO-5가 좋다고 보는 범위보다 낮았어요.",
"good": "지난 2주 동안의 웰빙이 WHO-5가 좋다고 보는 범위에 있었어요."
},
"SCI": {
"belowThreshold": "지난 한 달 동안의 수면이 이 설문의 바탕이 된 연구에서 불면증 가능성이 높다고 본 범위에 있었어요.",
"aboveThreshold": "지난 한 달 동안의 수면이 그 연구가 불면증과 연결한 범위 밖에 있었어요."
}
}
},
"records": {
+41 -2
View File
@@ -10952,7 +10952,10 @@
"title": "Je resultaat",
"totalLabel": "Totaalscore",
"bandLabel": "Bereik voor deze screening",
"takeAnother": "Nog een check-in doen"
"takeAnother": "Nog een check-in doen",
"scoreOf": "van {max}",
"firstCheckIn": "Dit is je eerste uitslag voor deze vragenlijst. Vanaf de volgende zie je hier de verandering.",
"nextStep": "Je kunt deze check-in over twee weken of later herhalen om de verandering te volgen. Maak je je ergens zorgen over, praat dan op elk moment met een zorgverlener."
},
"band": {
"PHQ9": {
@@ -11040,7 +11043,6 @@
"who5": "Welzijnscheck met 5 vragen. Beantwoord ze voor de afgelopen twee weken; hogere getallen betekenen een beter welzijn.",
"sci": "Slaapcheck met 8 vragen. De meeste vragen gaan over de afgelopen maand; hogere scores betekenen een betere slaap."
},
"progress": "{current} / {total}",
"next": "Volgende",
"lastResult": "Laatste test",
"lastScore": "Laatste resultaat",
@@ -11065,6 +11067,43 @@
"worsened": "Dat bereikt de verandering van {threshold} punten die studies als betekenisvol zien, in de minder goede richting.",
"within_threshold": "Dat is minder dan de verandering van {threshold} punten die studies als betekenisvol zien."
}
},
"wizard": {
"progressLabel": "Vraag {current} van {total}",
"followUpLabel": "Aanvullende vraag (niet verplicht)",
"keyboardHint": "Met de cijfertoetsen 1 tot en met {count} kies je een antwoord.",
"allAnswered": "Alle {total} vragen beantwoord"
},
"review": {
"title": "Antwoorden controleren",
"description": "Er wordt niets opgeslagen voordat je verstuurt, en elk antwoord kun je nog wijzigen.",
"change": "Wijzigen",
"changeLabel": "Antwoord op vraag {number} wijzigen",
"notAnswered": "Niet beantwoord",
"open": "Antwoorden controleren"
},
"bandMeaning": {
"PHQ9": {
"minimal": "Je antwoorden wijzen op weinig of geen depressieve klachten in de afgelopen twee weken.",
"mild": "Je antwoorden wijzen op lichte depressieve klachten in de afgelopen twee weken.",
"moderate": "Je antwoorden wijzen op matige depressieve klachten in de afgelopen twee weken.",
"modSevere": "Je antwoorden wijzen op matig ernstige depressieve klachten in de afgelopen twee weken.",
"severe": "Je antwoorden wijzen op ernstige depressieve klachten in de afgelopen twee weken."
},
"GAD7": {
"minimal": "Je antwoorden wijzen op weinig of geen angstklachten in de afgelopen twee weken.",
"mild": "Je antwoorden wijzen op lichte angstklachten in de afgelopen twee weken.",
"moderate": "Je antwoorden wijzen op matige angstklachten in de afgelopen twee weken.",
"severe": "Je antwoorden wijzen op ernstige angstklachten in de afgelopen twee weken."
},
"WHO5": {
"low": "Je welzijn in de afgelopen twee weken lag onder het bereik dat de WHO-5 als goed beschouwt.",
"good": "Je welzijn in de afgelopen twee weken lag in het bereik dat de WHO-5 als goed beschouwt."
},
"SCI": {
"belowThreshold": "Je slaap in de afgelopen maand lag in het bereik waarin het onderzoek achter deze vragenlijst slapeloosheid waarschijnlijk vond.",
"aboveThreshold": "Je slaap in de afgelopen maand lag buiten het bereik dat dat onderzoek met slapeloosheid verbindt."
}
}
},
"records": {
+70 -19
View File
@@ -10948,24 +10948,33 @@
"sci": "Test snu, 8 pytań"
},
"options": {
"0": "Wcale",
"0": "Wcale nie dokuczały",
"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?",
"gad7Options": {
"0": "Wcale nie dokuczały",
"1": "Kilka dni",
"2": "Więcej niż połowę dni",
"3": "Niemal codziennie"
},
"functionalTitle": "Jeżeli zaznaczył/-a Pan/Pani którekolwiek z problemów, jak bardzo utrudniły one Panu/Pani wykonywanie pracy, zajmowanie się domem lub relacje z innymi ludźmi?",
"functional": {
"0": "Wcale nie utrudniły",
"0": "W ogóle nie utrudniły",
"1": "Trochę utrudniły",
"2": "Bardzo utrudniły",
"3": "Skrajnie utrudniły"
"3": "Niezmiernie utrudniły"
},
"functionalOptionalHint": "Opcjonalne: możesz wysłać bez odpowiedzi na to pytanie.",
"result": {
"title": "Twój wynik",
"totalLabel": "Wynik łączny",
"bandLabel": "Zakres dla tego przesiewu",
"takeAnother": "Wykonaj kolejne sprawdzenie"
"takeAnother": "Wykonaj kolejne sprawdzenie",
"scoreOf": "z {max}",
"firstCheckIn": "To Twój pierwszy wynik w tym kwestionariuszu. Od następnego zobaczysz tu zmianę.",
"nextStep": "Możesz powtórzyć ten kwestionariusz za dwa tygodnie lub później, aby śledzić zmianę. Jeśli coś Cię niepokoi, porozmawiaj w każdej chwili z profesjonalistą."
},
"band": {
"PHQ9": {
@@ -11009,23 +11018,23 @@
"items": {
"phq9": {
"1": "Niewielkie zainteresowanie lub odczuwanie przyjemności z wykonywania czynności",
"2": "Uczucie przygnębienia, depresji lub beznadziejności",
"3": "Kłopoty z zaśnięciem lub utrzymaniem snu albo zbyt długi sen",
"2": "Uczucie smutku, przygnębienia lub beznadziejności",
"3": "Kłopoty z zaśnięciem lub przerywany sen, albo zbyt długi sen",
"4": "Uczucie zmęczenia lub brak energii",
"5": "Brak apetytu lub przejadanie się",
"6": "Złe myślenie o sobie — poczucie, że jest się nieudacznikiem lub że zawiodło się siebie albo rodzinę",
"7": "Trudności ze skupieniem się, na przykład podczas czytania gazety lub oglądania telewizji",
"8": "Poruszanie się lub mówienie tak wolno, że inni mogli to zauważyć? Albo przeciwnie — niepokój lub pobudzenie powodujące, że poruszasz się znacznie więcej niż zwykle",
"9": "Myśli, że lepiej byłoby umrzeć albo chęć zrobienia sobie w jakiś sposób krzywdy"
"6": "Poczucie niezadowolenia z siebie — lub uczucie, że jest się do niczego, albo że zawiódł/zawiodła Pan/Pani siebie lub rodzinę",
"7": "Problemy ze skupieniem się na przykład przy czytaniu gazety lub oglądaniu telewizji",
"8": "Poruszanie się lub mówienie tak wolno, że inni mogliby to zauważyć? Albo wręcz przeciwnie — niemożność usiedzenia w miejscu lub podenerwowanie powodujące ruchliwość znacznie większą niż zwykle",
"9": "Myśli, że lepiej byłoby umrzeć, albo chęć zrobienia sobie jakiejś krzywdy"
},
"gad7": {
"1": "Odczuwanie zdenerwowania, lęku lub napięcia",
"2": "Niemożność przerwania lub kontrolowania zamartwiania się",
"3": "Zbytnie zamartwianie się różnymi sprawami",
"4": "Trudności z odprężeniem się",
"5": "Niepokój tak duży, że trudno usiedzieć w miejscu",
"6": "Łatwe wpadanie w rozdrażnienie lub irytację",
"7": "Odczuwanie lęku, jakby miało się zdarzyć coś strasznego"
"1": "Czuł(a) się Pan(i) podenerwowany(a), niespokojny(a), mocno spięty(a)",
"2": "Nie mógł(a) Pan(i) przestać się martwić albo zapanować nad tym",
"3": "Za bardzo się Pan(i) martwił(a) różnymi rzeczami",
"4": "Miał(a) Pan(i) trudności z relaksowaniem się",
"5": "Był(a) Pan(i) tak niespokojny(a), że nie mógł(a) usiedzieć na miejscu",
"6": "Łatwo stawał(a) się Pan(i) rozdrażniony(a) lub poirytowany(a)",
"7": "Obawiał(a) się Pan(i), tak jakby miało się stać coś strasznego"
},
"who5": {
"1": "Czułam się wesoła/Czułem się wesoły i w dobrym nastroju",
@@ -11082,7 +11091,6 @@
"who5": "Test samopoczucia, 5 pytań. Odpowiadaj, myśląc o ostatnich dwóch tygodniach; wyższe cyfry oznaczają lepsze samopoczucie.",
"sci": "Test snu, 8 pytań. Większość pytań dotyczy ostatniego miesiąca; wyższe wyniki oznaczają lepszy sen."
},
"progress": "{current} / {total}",
"next": "Dalej",
"lastResult": "Ostatni test",
"lastScore": "Ostatni wynik",
@@ -11104,6 +11112,12 @@
"5": "Cały czas"
},
"stems": {
"phq9": {
"period": "Jak często w ciągu ostatnich 2 tygodni dokuczały Panu/Pani następujące problemy?"
},
"gad7": {
"period": "Jak często w ciągu ostatnich 2 tygodni dokuczały Panu(i) następujące problemy?"
},
"who5": {
"period": "W ciągu ostatnich dwóch tygodni"
}
@@ -11120,6 +11134,43 @@
"worsened": "To osiąga zmianę o {threshold} pkt, którą badania uznają za istotną, w mniej korzystnym kierunku.",
"within_threshold": "To mniej niż zmiana o {threshold} pkt, którą badania uznają za istotną."
}
},
"wizard": {
"progressLabel": "Pytanie {current} z {total}",
"followUpLabel": "Pytanie dodatkowe (nieobowiązkowe)",
"keyboardHint": "Klawisze od 1 do {count} wybierają odpowiedź.",
"allAnswered": "Odpowiedziano na wszystkie pytania ({total})"
},
"review": {
"title": "Sprawdź odpowiedzi",
"description": "Nic nie zostanie zapisane przed wysłaniem, a każdą odpowiedź można jeszcze zmienić.",
"change": "Zmień",
"changeLabel": "Zmień odpowiedź na pytanie {number}",
"notAnswered": "Bez odpowiedzi",
"open": "Sprawdź odpowiedzi"
},
"bandMeaning": {
"PHQ9": {
"minimal": "Twoje odpowiedzi wskazują na brak lub niewiele objawów depresji w ostatnich dwóch tygodniach.",
"mild": "Twoje odpowiedzi wskazują na łagodne objawy depresji w ostatnich dwóch tygodniach.",
"moderate": "Twoje odpowiedzi wskazują na umiarkowane objawy depresji w ostatnich dwóch tygodniach.",
"modSevere": "Twoje odpowiedzi wskazują na umiarkowanie ciężkie objawy depresji w ostatnich dwóch tygodniach.",
"severe": "Twoje odpowiedzi wskazują na ciężkie objawy depresji w ostatnich dwóch tygodniach."
},
"GAD7": {
"minimal": "Twoje odpowiedzi wskazują na brak lub niewiele objawów lęku w ostatnich dwóch tygodniach.",
"mild": "Twoje odpowiedzi wskazują na łagodne objawy lęku w ostatnich dwóch tygodniach.",
"moderate": "Twoje odpowiedzi wskazują na umiarkowane objawy lęku w ostatnich dwóch tygodniach.",
"severe": "Twoje odpowiedzi wskazują na ciężkie objawy lęku w ostatnich dwóch tygodniach."
},
"WHO5": {
"low": "Twoje samopoczucie w ostatnich dwóch tygodniach było poniżej zakresu, który WHO-5 uznaje za dobry.",
"good": "Twoje samopoczucie w ostatnich dwóch tygodniach mieściło się w zakresie, który WHO-5 uznaje za dobry."
},
"SCI": {
"belowThreshold": "Twój sen w ostatnim miesiącu mieścił się w zakresie, w którym badanie stojące za tym kwestionariuszem uznało bezsenność za prawdopodobną.",
"aboveThreshold": "Twój sen w ostatnim miesiącu był poza zakresem, który to badanie wiąże z bezsennością."
}
}
},
"records": {
+41 -2
View File
@@ -10952,7 +10952,10 @@
"title": "Seu resultado",
"totalLabel": "Pontuação total",
"bandLabel": "Faixa para esta triagem",
"takeAnother": "Fazer outro check-in"
"takeAnother": "Fazer outro check-in",
"scoreOf": "de {max}",
"firstCheckIn": "Este é o seu primeiro resultado com este questionário. A partir do próximo, a mudança aparece aqui.",
"nextStep": "Você pode repetir este questionário daqui a duas semanas ou depois para acompanhar a mudança. Se algo preocupar você, fale com um profissional de saúde a qualquer momento."
},
"band": {
"PHQ9": {
@@ -11040,7 +11043,6 @@
"who5": "Check de bem-estar de 5 perguntas. Responda sobre as últimas duas semanas; números mais altos significam melhor bem-estar.",
"sci": "Check de sono de 8 perguntas. A maioria das perguntas trata do último mês; pontuações mais altas significam melhor sono."
},
"progress": "{current} / {total}",
"next": "Próximo",
"lastResult": "Último teste",
"lastScore": "Último resultado",
@@ -11065,6 +11067,43 @@
"worsened": "Isso alcança a mudança de {threshold} pontos que os estudos consideram significativa, na direção menos favorável.",
"within_threshold": "Isso é menor do que a mudança de {threshold} pontos que os estudos consideram significativa."
}
},
"wizard": {
"progressLabel": "Pergunta {current} de {total}",
"followUpLabel": "Pergunta adicional (opcional)",
"keyboardHint": "As teclas de 1 a {count} escolhem uma resposta.",
"allAnswered": "Todas as {total} perguntas respondidas"
},
"review": {
"title": "Conferir respostas",
"description": "Nada é salvo antes do envio, e cada resposta ainda pode ser alterada.",
"change": "Alterar",
"changeLabel": "Alterar a resposta da pergunta {number}",
"notAnswered": "Sem resposta",
"open": "Conferir respostas"
},
"bandMeaning": {
"PHQ9": {
"minimal": "Suas respostas indicam poucos ou nenhum sintoma de depressão nas últimas duas semanas.",
"mild": "Suas respostas indicam sintomas leves de depressão nas últimas duas semanas.",
"moderate": "Suas respostas indicam sintomas moderados de depressão nas últimas duas semanas.",
"modSevere": "Suas respostas indicam sintomas moderadamente graves de depressão nas últimas duas semanas.",
"severe": "Suas respostas indicam sintomas graves de depressão nas últimas duas semanas."
},
"GAD7": {
"minimal": "Suas respostas indicam poucos ou nenhum sintoma de ansiedade nas últimas duas semanas.",
"mild": "Suas respostas indicam sintomas leves de ansiedade nas últimas duas semanas.",
"moderate": "Suas respostas indicam sintomas moderados de ansiedade nas últimas duas semanas.",
"severe": "Suas respostas indicam sintomas graves de ansiedade nas últimas duas semanas."
},
"WHO5": {
"low": "Seu bem-estar nas últimas duas semanas ficou abaixo da faixa que o WHO-5 considera boa.",
"good": "Seu bem-estar nas últimas duas semanas ficou na faixa que o WHO-5 considera boa."
},
"SCI": {
"belowThreshold": "Seu sono no último mês ficou na faixa em que o estudo por trás deste questionário considerou provável uma insônia.",
"aboveThreshold": "Seu sono no último mês ficou fora da faixa que esse estudo associa à insônia."
}
}
},
"records": {
@@ -1,7 +1,7 @@
{
"$comment": "Production functions over 200 lines, keyed \"file :: name\", and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.",
"count": 411,
"total": 148369,
"total": 148357,
"entries": {
"app/achievements/page.tsx :: AchievementsPage": 206,
"app/api/admin/backups/upload/route.ts :: apiHandler(arg)": 314,
@@ -210,8 +210,8 @@
"components/medications/wizard/medication-wizard-dialog.tsx :: WizardDialogShell": 516,
"components/medications/wizard/steps/step3-dose.tsx :: Step3Dose": 222,
"components/medications/wizard/steps/step8-summary.tsx :: Step8Summary": 230,
"components/mental-health/check-in-wizard.tsx :: CheckInWizard": 214,
"components/mental-health/mental-wellbeing.tsx :: MentalWellbeing": 210,
"components/mental-health/check-in-wizard.tsx :: CheckInWizard": 205,
"components/mental-health/mental-wellbeing.tsx :: MentalWellbeing": 209,
"components/mood/manage/tag-groups-card.tsx :: TagGroupsCard": 256,
"components/mood/manage/tag-manager-card.tsx :: TagManagerCard": 270,
"components/mood/mood-form.tsx :: MoodForm": 513,
@@ -1,7 +1,7 @@
{
"$comment": "Comment lines that open with a release version, per file under src/. Maintained by src/__tests__/version-stamp-ratchet.test.ts.",
"count": 2957,
"total": 10516,
"count": 2954,
"total": 10508,
"entries": {
"__tests__/account-payload-consumer-guard.test.ts": 2,
"__tests__/acting-account-boundary-guard.test.ts": 7,
@@ -1245,10 +1245,7 @@
"components/mental-health/__tests__/mental-wellbeing.test.tsx": 3,
"components/mental-health/assessment-history-chart.tsx": 4,
"components/mental-health/assessment-history.tsx": 2,
"components/mental-health/assessment-result.tsx": 2,
"components/mental-health/assessment-trend-note.tsx": 1,
"components/mental-health/check-in-nav.ts": 2,
"components/mental-health/check-in-wizard.tsx": 4,
"components/mental-health/crisis-card.tsx": 1,
"components/mental-health/instrument-card.tsx": 5,
"components/mental-health/instrument-detail.tsx": 3,
@@ -2,19 +2,19 @@ import { describe, it, expect } from "vitest";
import {
UNANSWERED,
checkInSteps,
firstUnanswered,
isComplete,
isLastStep,
nextStep,
prevStep,
} from "../check-in-nav";
/**
* The one-question-at-a-time check-in is LINEAR: the steps are exactly the
* questions `1..itemCount`, backward navigation is always allowed, and the
* last question arms the submit directly (v1.27.6 — no review/summary step).
* These are the contracts the wizard's advance / back behaviour rests on
* (the SSR-only test convention can't drive the clicks, so the pure grammar
* is pinned here — the medication wizard's `wizard-payload` precedent).
* The check-in is LINEAR and the same for every instrument: the items, the
* PHQ-9's optional follow-up, then a review step that is the only place that
* submits. These are the contracts the wizard's advance / back behaviour
* rests on (the SSR-only component tests cannot drive clicks; the e2e
* journey drives them end to end).
*/
const PHQ9 = 9;
@@ -24,30 +24,37 @@ function blank(count: number): number[] {
return Array(count).fill(UNANSWERED);
}
describe("check-in step grammar (v1.27.6 — questions only, no review step)", () => {
it("advances forward and stays put on the last question (submit, not advance)", () => {
expect(nextStep(1, PHQ9)).toBe(2);
expect(nextStep(8, PHQ9)).toBe(9);
// The last question is the end of the line — no review step behind it.
expect(nextStep(9, PHQ9)).toBe(9);
expect(nextStep(7, GAD7)).toBe(7);
describe("check-in step layout", () => {
it("puts the review step after the last question of every instrument", () => {
expect(checkInSteps(PHQ9, true)).toEqual({
questions: 10,
functionalStep: 10,
review: 11,
});
expect(checkInSteps(GAD7, false)).toEqual({
questions: 7,
functionalStep: null,
review: 8,
});
expect(checkInSteps(5, false).review).toBe(6);
expect(checkInSteps(8, false).review).toBe(9);
});
it("advances forward and stops at the review step", () => {
const { review } = checkInSteps(PHQ9, true);
expect(nextStep(1, review)).toBe(2);
expect(nextStep(10, review)).toBe(11);
expect(nextStep(11, review)).toBe(11);
});
it("steps back, clamped at the first question", () => {
expect(prevStep(9)).toBe(8);
expect(prevStep(11)).toBe(10);
expect(prevStep(2)).toBe(1);
expect(prevStep(1)).toBe(1);
});
it("identifies the last question per instrument", () => {
expect(isLastStep(8, PHQ9)).toBe(false);
expect(isLastStep(9, PHQ9)).toBe(true);
expect(isLastStep(6, GAD7)).toBe(false);
expect(isLastStep(7, GAD7)).toBe(true);
});
});
describe("completeness (gates the submit on the last question)", () => {
describe("completeness (gates the review and the submit)", () => {
it("isComplete requires every item answered", () => {
expect(isComplete(blank(PHQ9))).toBe(false);
const partial = blank(PHQ9).map((_, i) => (i < 5 ? 1 : UNANSWERED));
@@ -55,4 +62,10 @@ describe("completeness (gates the submit on the last question)", () => {
expect(isComplete(blank(PHQ9).map(() => 0))).toBe(true);
expect(isComplete(blank(GAD7).map(() => 3))).toBe(true);
});
it("names the first unanswered item's step", () => {
expect(firstUnanswered(blank(GAD7))).toBe(1);
expect(firstUnanswered([0, 1, UNANSWERED, 2])).toBe(3);
expect(firstUnanswered([0, 1, 2, 3])).toBeNull();
});
});
@@ -257,7 +257,7 @@ describe("WHO-5 / SCI on the same infrastructure (v1.27.9)", () => {
// English items are the validated wording for en — no translation note.
expect(html).not.toContain('data-slot="check-in-validated-note"');
// No functional follow-up outside the PHQ-9: 5 steps total.
expect(html).toContain("1 / 5");
expect(html).toContain("Question 1 of 5");
});
it("SCI wizard opens on item 1 with the item-specific duration anchors and section stem", () => {
@@ -276,7 +276,7 @@ describe("WHO-5 / SCI on the same infrastructure (v1.27.9)", () => {
for (const v of ["4", "3", "2", "1", "0"] as const) {
expect(html).toContain(mh.sciOptions.duration[v]);
}
expect(html).toContain("1 / 8");
expect(html).toContain("Question 1 of 8");
});
it("shows the gentle PHQ-9 pointer on a WHO-5 total of 50 or below", () => {
@@ -1,50 +1,58 @@
"use client";
/**
* v1.25.3 — the contained result block: instrument header → result card →
* crisis card (on a positive PHQ-9 item 9) → required attribution → "take
* another". Content is unchanged from the v1.25 monolith; it is re-housed as
* one stack inside the page spine instead of loose top-level sections.
* The result of one check-in, the same layout for every instrument.
*
* v1.27.9 — direction-aware follow-up hints from the registry: PHQ-9/GAD-7
* keep the ≥10 professional-help suggestion, a WHO-5 total of 50 or below
* gets a gentle pointer to the PHQ-9 check-in (per the WHO scoring comment),
* and an SCI total of 16 or below gets the paper's neutral band wording.
* The attribution footer now comes from the instrument definition.
* Order: the crisis card first when PHQ-9 item 9 was answered above zero
* (support before numbers), then one result card with the score on the
* instrument's own scale, its band, what the band means in plain words, the
* change since the last check-in measured against the meaningful-change
* threshold, and the next step: the direction-aware follow-up hint when the
* score crosses the instrument's threshold, a routine note otherwise. The
* attribution and "take another" close the view.
*
* SAFETY: the crisis set rides the POST response (`crisis`) on any non-zero
* item 9 and renders immediately; this surface never invites an AI Coach
* conversation about item content.
* Every number and verdict is the server's (the create response and the
* history read's `trends`); nothing is re-scored here. SAFETY: this surface
* never invites an AI Coach conversation about item content.
*/
import { useEffect, useRef } from "react";
import { ArrowLeft } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { useTranslations } from "@/lib/i18n/context";
import { PageHeader } from "@/components/ui/page-header";
import { useFormatters, useTranslations } from "@/lib/i18n/context";
import { INSTRUMENTS, needsFollowUp } from "@/lib/mental-health/instruments";
import { AssessmentTrendNote } from "./assessment-trend-note";
import { CrisisCard } from "./crisis-card";
import type { CreateResponse } from "./types";
import { PageHeader } from "@/components/ui/page-header";
import type { AssessmentTrendDto, CreateResponse } from "./types";
export function AssessmentResult({
result,
trend,
onTakeAnother,
onBack,
}: {
result: CreateResponse;
/** This instrument's trend, once the history read includes this result. */
trend?: AssessmentTrendDto;
onTakeAnother: () => void;
onBack: () => void;
}) {
const { t } = useTranslations();
const instrument = result.assessment.instrument;
const { integer } = useFormatters();
const { assessment } = result;
const instrument = assessment.instrument;
const def = INSTRUMENTS[instrument];
const key = def.i18nKey;
const band = assessment.severityBand;
// The trend counts only once it ends on this very result.
const ownTrend =
trend && trend.latest.takenAt === assessment.takenAt ? trend : undefined;
// a11y: on submit the wizard unmounts and this view mounts; move focus to the
// result heading so keyboard / screen-reader users land on the new content
// rather than being dropped to <body>.
// result heading so keyboard / screen-reader users land on the new content.
const headingRef = useRef<HTMLHeadingElement | null>(null);
useEffect(() => {
headingRef.current?.focus();
@@ -54,6 +62,8 @@ export function AssessmentResult({
<section
className="flex flex-col gap-4"
data-slot="mental-health-result"
data-instrument={instrument}
data-band={band}
aria-live="polite"
>
<Button
@@ -79,46 +89,61 @@ export function AssessmentResult({
{t("mentalHealth.saved")}
</p>
{result.crisis && <CrisisCard crisis={result.crisis} />}
<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">
<div className="flex items-baseline justify-between gap-3">
<span className="text-muted-foreground">
{t("mentalHealth.result.totalLabel")}
</span>
<span className="text-2xl font-semibold">
{result.assessment.totalScore}
<span data-slot="result-score">
<span className="text-2xl font-semibold tabular-nums">
{integer(assessment.totalScore)}
</span>{" "}
<span className="text-muted-foreground text-xs">
{t("mentalHealth.result.scoreOf", {
max: integer(def.maxScore),
})}
</span>
</span>
</div>
<div className="flex items-baseline justify-between">
<div className="flex items-baseline justify-between gap-3">
<span className="text-muted-foreground">
{t("mentalHealth.result.bandLabel")}
</span>
<span className="font-medium">
{t(
`mentalHealth.band.${result.assessment.instrument}.${result.assessment.severityBand}`,
)}
<span className="font-medium" data-slot="result-band">
{t(`mentalHealth.band.${instrument}.${band}`)}
</span>
</div>
{/* Direction-aware follow-up hint: PHQ-9/GAD-7 point up (≥ 10),
WHO-5 (≤ 50 → gentle PHQ-9 pointer) and SCI (≤ 16 → the paper's
neutral band wording) point down. Soft suggestions, never a
diagnosis. */}
{needsFollowUp(instrument, result.assessment.totalScore) && (
<p data-slot="result-meaning">
{t(`mentalHealth.bandMeaning.${instrument}.${band}`)}
</p>
{ownTrend && ownTrend.previous ? (
<AssessmentTrendNote trend={ownTrend} />
) : ownTrend ? (
<p className="text-muted-foreground" data-slot="result-first">
{t("mentalHealth.result.firstCheckIn")}
</p>
) : null}
{needsFollowUp(instrument, assessment.totalScore) ? (
<p
className="text-muted-foreground bg-muted/40 rounded-md p-3 text-xs"
className="bg-muted/40 rounded-md p-3"
data-slot="result-follow-up-hint"
>
{t(`mentalHealth.followUpHint.${key}`)}
</p>
) : (
<p className="text-muted-foreground" data-slot="result-next-step">
{t("mentalHealth.result.nextStep")}
</p>
)}
</CardContent>
</Card>
{result.crisis && <CrisisCard crisis={result.crisis} />}
<p className="text-muted-foreground text-xs leading-snug">
<span className="font-medium">
{t("mentalHealth.attributionLabel")}:
+36 -22
View File
@@ -1,33 +1,27 @@
/**
* v1.25.3 — pure step grammar for the one-question-at-a-time check-in.
* Pure step grammar for the one-question-at-a-time check-in.
*
* The PHQ-9 / GAD-7 screeners are LINEAR (no mode/cadence branching like the
* medication wizard), so the steps are exactly the questions `1..itemCount`:
* you can always step backward to change an answer, and never skip ahead past
* an unanswered question. Keeping the navigation here (mirroring the
* medication wizard's `wizard-payload.ts`) makes the advance/back contract
* unit-testable without driving a DOM.
* Every instrument runs the same sequence: its items `1..itemCount`, the
* PHQ-9's optional follow-up question as one more step, then a review step
* that lists every answer before anything is sent. The review step came
* back at a maintainer's request: a screener that submits on the last tap
* gives no chance to see what is about to be recorded. Backward navigation
* is always allowed and nothing skips past an unanswered item. Keeping the
* grammar here makes it unit-testable without driving a DOM.
*
* v1.27.6 — the review/summary step is gone: answering the last question
* arms the submit directly (no "9 of 9 answered" interstitial demanding one
* more decision). Step numbers are 1-based and run `1..itemCount`.
*
* An unanswered item carries the sentinel `-1`.
* Step numbers are 1-based. An unanswered item carries the sentinel `-1`.
*/
export const UNANSWERED = -1;
/** Every item answered (0–3) — gates the submit on the last question. */
/** Every item answered — gates the review step and the submit. */
export function isComplete(items: readonly number[]): boolean {
return items.length > 0 && items.every((v) => v >= 0);
}
/**
* The next step after `step`: a question advances to the next question; the
* last question stays put (submit, not advance).
*/
export function nextStep(step: number, itemCount: number): number {
if (step >= itemCount) return itemCount;
/** The step after `step`, clamped at the last one. */
export function nextStep(step: number, totalSteps: number): number {
if (step >= totalSteps) return totalSteps;
return step + 1;
}
@@ -36,7 +30,27 @@ export function prevStep(step: number): number {
return step > 1 ? step - 1 : 1;
}
/** Whether `step` is the last question for this instrument. */
export function isLastStep(step: number, itemCount: number): boolean {
return step >= itemCount;
/**
* The step layout of one instrument: how many questions it asks (its items
* plus the optional follow-up) and the review step after them.
*/
export function checkInSteps(
itemCount: number,
hasFunctional: boolean,
): { questions: number; functionalStep: number | null; review: number } {
const questions = itemCount + (hasFunctional ? 1 : 0);
return {
questions,
functionalStep: hasFunctional ? itemCount + 1 : null,
review: questions + 1,
};
}
/**
* The first unanswered item's step, or null when every item is answered.
* The review step is only reachable once this is null.
*/
export function firstUnanswered(items: readonly number[]): number | null {
const index = items.findIndex((v) => v < 0);
return index === -1 ? null : index + 1;
}
+415 -136
View File
@@ -1,34 +1,39 @@
"use client";
/**
* v1.25.3 — one-question-at-a-time check-in, rendered INLINE on the page
* (not a modal), because here the page *is* the surface.
* The one-question-at-a-time check-in, the same pattern for every instrument.
*
* v1.27.6 — slimmed to questions only. The question-overview strip (the
* dot/jump row borrowed from the medication wizard) and the "N of N
* answered" review interstitial are gone: a screening is a quiet, linear
* moment, not a form to audit. The Back button stays; a small "3 / 9"
* caption keeps orientation without turning progress into a scoreboard.
* Answering the last question arms the submit directly — the next thing
* the user sees is the dignified result view.
* Each step shows the instrument's own question stem ("Over the last 2
* weeks, how often have you been bothered by…", the WHO-5 and SCI section
* stems) above one item, the item's exact answer options as a radio group,
* the progress through the scored items, and Back. The PHQ-9's follow-up
* question is one more, optional step. A review step then lists every
* answer with a way back to each one, and only the review submits.
*
* v1.27.9 — the wizard drives from the instrument registry instead of a
* hard-wired 0–3 scale: option values/order, option-label scheme (shared
* anchors vs the SCI's per-item anchor groups), and the recall-stem caption
* (the WHO-5 / SCI items are fragments under a shared stem on the source
* forms) all come from `INSTRUMENTS`. Locales without a validated item
* translation (SCI outside English) get an honest chrome note; the items
* themselves stay in the validated source language.
* Items, answers and stems come from `instrumentText`: the validated
* translation where the language has one, the English original otherwise,
* under an honest note. The header carries the app's own title and one
* explanation line; the disclaimer stays on the landing.
*
* Header: instrument title + ONE standardized explanation line, NO
* disclaimer (§2 — the "voluntary self-test, not a diagnosis" disclaimer
* renders only on the landing, never while taking the test).
* Keyboard: Tab reaches the selected answer (or the first), the arrow keys
* move between answers, Space or Enter chooses, and the number keys choose
* the answer in that position. Choosing advances; on the review step every
* answer can be changed and the change returns to the review.
*/
import { useEffect, useRef, useState } from "react";
import {
useEffect,
useRef,
useState,
type KeyboardEvent,
type RefObject,
} from "react";
import { ArrowLeft } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { InlineAction } from "@/components/ui/inline-action";
import { PageHeader } from "@/components/ui/page-header";
import { Progress } from "@/components/ui/progress";
import { useTranslations } from "@/lib/i18n/context";
import {
INSTRUMENTS,
@@ -45,17 +50,19 @@ import {
} from "@/lib/mental-health/instrument-wording";
import {
UNANSWERED,
checkInSteps,
firstUnanswered,
isComplete,
isLastStep,
nextStep,
prevStep,
UNANSWERED,
} from "./check-in-nav";
import type { InstrumentId } from "./types";
import { PageHeader } from "@/components/ui/page-header";
const FUNCTIONAL_SCALE = [0, 1, 2, 3] as const;
type Wording = (key: string) => string;
export function CheckInWizard({
instrument,
onSubmit,
@@ -73,66 +80,65 @@ export function CheckInWizard({
}) {
const { t, locale } = useTranslations();
const def = INSTRUMENTS[instrument];
const wording = (k: string) => instrumentText(instrument, k, locale, t);
const itemCount = def.itemCount;
// v1.27.8 — the PHQ-9 asks its validated functional-impairment
// follow-up as a regular LAST question (10 of 10) instead of a
// separate review-step widget. It is OPTIONAL per the instrument
// (answering is not required to submit) and never scores into the
// total — the value rides the existing `functionalDifficulty` API
// field. The other instruments have no such item.
const hasFunctionalStep = hasFunctionalItem(instrument);
const totalSteps = itemCount + (hasFunctionalStep ? 1 : 0);
const wording: Wording = (k) => instrumentText(instrument, k, locale, t);
const steps = checkInSteps(def.itemCount, hasFunctionalItem(instrument));
const [step, setStep] = useState(1);
const [items, setItems] = useState<number[]>(() =>
Array(itemCount).fill(UNANSWERED),
Array(def.itemCount).fill(UNANSWERED),
);
const [functional, setFunctional] = useState<number>(UNANSWERED);
// Set when a step was opened from the review: choosing returns there.
const [fromReview, setFromReview] = useState(false);
const onFunctionalStep = hasFunctionalStep && step === itemCount + 1;
const onLast = isLastStep(step, totalSteps);
const questionIndex = step - 1; // 0-based item index for the current question
const complete = isComplete(items);
const questionLabel = onFunctionalStep
? wording(FUNCTIONAL_TITLE_KEY)
: wording(itemKey(instrument, questionIndex));
// Recall-stem caption above the question (WHO-5 / SCI fragments).
const stem = onFunctionalStep ? null : stemKey(instrument, questionIndex);
const scale = onFunctionalStep ? FUNCTIONAL_SCALE : def.optionOrder;
const onReview = step === steps.review;
const onFunctional = step === steps.functionalStep;
const questionIndex = step - 1;
// a11y: announce each step by moving focus to its heading (the question
// text). Mirrors the medication dialog's per-step title focus so a
// keyboard / screen-reader user hears the new question after an
// auto-advance, not silence.
const headingRef = useRef<HTMLHeadingElement | null>(null);
useEffect(() => {
headingRef.current?.focus({ preventScroll: false });
}, [step]);
function selectAnswer(value: number) {
if (onFunctionalStep) {
// Optional follow-up: selecting never advances (it IS the last
// step) and tapping the selected option again clears it — the
// answer stays skippable right up to submit.
function advance() {
if (fromReview && isComplete(items)) {
setFromReview(false);
setStep(steps.review);
return;
}
setStep((s) => nextStep(s, steps.review));
}
function choose(value: number) {
if (onFunctional) {
// Optional and unscored: choosing the chosen answer again clears it.
setFunctional((prev) => (prev === value ? UNANSWERED : value));
return;
}
setItems((prev) => {
const next = [...prev];
next[questionIndex] = value;
return next;
});
// Forward-on-select: advance to the next question. On the last question
// the selection stays visible and the footer submit arms — submitting is
// a deliberate second tap, never a side effect of answering.
if (!onLast) setStep((s) => nextStep(s, totalSteps));
const next = [...items];
next[questionIndex] = value;
setItems(next);
if (fromReview && isComplete(next)) {
setFromReview(false);
setStep(steps.review);
return;
}
setStep((s) => nextStep(s, steps.review));
}
function edit(target: number) {
setFromReview(true);
setStep(target);
}
const canReview = firstUnanswered(items) === null;
const answered = items.filter((v) => v >= 0).length;
return (
<section
className="flex flex-col gap-4"
data-slot="mental-health-check-in"
data-instrument={instrument}
aria-busy={isPending || undefined}
>
<Button
@@ -147,15 +153,11 @@ export function CheckInWizard({
{t("mentalHealth.backToList")}
</Button>
{/* In-test header — instrument title + ONE standardized explanation line.
No disclaimer here (§2); the landing carries it. */}
<header className="flex flex-col gap-1">
<PageHeader
title={t(`mentalHealth.instrument.${def.i18nKey}`)}
description={t(`mentalHealth.instrumentDescription.${def.i18nKey}`)}
/>
{/* Honest translation note: the instrument is validated in its source
language; the surrounding chrome stays localized. */}
{!hasValidatedItems(instrument, locale) && (
<p
className="text-muted-foreground text-xs"
@@ -168,64 +170,51 @@ export function CheckInWizard({
<Card className="gap-0 p-0">
<CardContent className="flex flex-col gap-4 p-4 md:p-6">
<div className="flex flex-col gap-4">
<p className="text-muted-foreground text-xs">
{t("mentalHealth.progress", {
current: step,
total: totalSteps,
})}
<CheckInProgress
answered={onReview ? def.itemCount : answered}
current={Math.min(step, def.itemCount)}
total={def.itemCount}
label={
onReview
? t("mentalHealth.wizard.allAnswered", {
total: def.itemCount,
})
: onFunctional
? t("mentalHealth.wizard.followUpLabel")
: t("mentalHealth.wizard.progressLabel", {
current: step,
total: def.itemCount,
})
}
/>
{onReview ? (
<ReviewStep
instrument={instrument}
items={items}
functional={functional}
functionalStep={steps.functionalStep}
wording={wording}
headingRef={headingRef}
onEdit={edit}
/>
) : (
<QuestionStep
key={step}
instrument={instrument}
step={step}
onFunctional={onFunctional}
selected={onFunctional ? functional : items[questionIndex]}
wording={wording}
headingRef={headingRef}
disabled={isPending}
onChoose={choose}
/>
)}
{isError && (
<p className="text-destructive text-sm" role="alert">
{t("mentalHealth.error")}
</p>
{stem && (
<p className="text-sm" data-slot="check-in-stem">
{wording(`mentalHealth.stems.${stem}`)}
</p>
)}
<h2
ref={headingRef}
tabIndex={-1}
className="scroll-mt-4 text-base font-medium outline-none"
>
{questionLabel}
</h2>
{onFunctionalStep && (
<p className="text-muted-foreground text-xs">
{t("mentalHealth.functionalOptionalHint")}
</p>
)}
<div
className="flex flex-col gap-2"
role="group"
aria-label={questionLabel}
>
{scale.map((v) => {
const selected = onFunctionalStep
? functional === v
: items[questionIndex] === v;
return (
<Button
key={v}
type="button"
// 44px touch-target floor (WCAG 2.5.5).
className="min-h-11 justify-start"
variant={selected ? "default" : "outline"}
aria-pressed={selected}
onClick={() => selectAnswer(v)}
disabled={isPending}
>
{onFunctionalStep
? wording(functionalOptionKey(v))
: wording(optionLabelKey(instrument, questionIndex, v))}
</Button>
);
})}
</div>
{isError && (
<p className="text-destructive text-sm" role="alert">
{t("mentalHealth.error")}
</p>
)}
</div>
)}
</CardContent>
<div className="border-border/70 flex items-center justify-between gap-2 border-t p-4 md:p-6">
@@ -233,30 +222,23 @@ export function CheckInWizard({
type="button"
variant="ghost"
className="h-11"
onClick={() => setStep((s) => prevStep(s))}
onClick={() => {
setFromReview(false);
setStep((s) => prevStep(s));
}}
disabled={step === 1 || isPending}
data-slot="check-in-back"
>
{t("mentalHealth.back")}
</Button>
{!onLast ? (
<Button
type="button"
className="h-11"
onClick={() => setStep((s) => nextStep(s, totalSteps))}
disabled={items[questionIndex] < 0}
data-slot="check-in-next"
>
{t("mentalHealth.next")}
</Button>
) : (
{onReview ? (
<Button
type="button"
className="h-11"
onClick={() =>
onSubmit(items, functional >= 0 ? functional : undefined)
}
disabled={!complete || isPending}
disabled={!isComplete(items) || isPending}
aria-busy={isPending || undefined}
data-slot="check-in-submit"
>
@@ -264,9 +246,306 @@ export function CheckInWizard({
? t("mentalHealth.submitting")
: t("mentalHealth.submit")}
</Button>
) : (
<Button
type="button"
className="h-11"
onClick={advance}
disabled={
onFunctional
? !canReview
: items[questionIndex] < 0 || isPending
}
data-slot="check-in-next"
>
{step >= steps.questions
? t("mentalHealth.review.open")
: t("mentalHealth.next")}
</Button>
)}
</div>
</Card>
</section>
);
}
function CheckInProgress({
answered,
current,
total,
label,
}: {
answered: number;
current: number;
total: number;
label: string;
}) {
return (
<div className="flex flex-col gap-2" data-slot="check-in-progress">
<p
className="text-muted-foreground text-xs"
data-current={current}
data-total={total}
>
{label}
</p>
<Progress
value={Math.round((answered / total) * 100)}
aria-label={label}
className="h-1.5"
/>
</div>
);
}
function QuestionStep({
instrument,
step,
onFunctional,
selected,
wording,
headingRef,
disabled,
onChoose,
}: {
instrument: InstrumentId;
step: number;
onFunctional: boolean;
selected: number;
wording: Wording;
headingRef: RefObject<HTMLHeadingElement | null>;
disabled: boolean;
onChoose: (value: number) => void;
}) {
const { t } = useTranslations();
const def = INSTRUMENTS[instrument];
const index = step - 1;
const stem = onFunctional ? null : stemKey(instrument, index);
const question = onFunctional
? wording(FUNCTIONAL_TITLE_KEY)
: wording(itemKey(instrument, index));
const scale = onFunctional ? FUNCTIONAL_SCALE : def.optionOrder;
const options = scale.map((value) => ({
value,
label: onFunctional
? wording(functionalOptionKey(value))
: wording(optionLabelKey(instrument, index, value)),
}));
const questionId = `check-in-question-${instrument}-${step}`;
return (
<div
className="flex flex-col gap-4"
data-slot="check-in-step"
data-step={step}
data-functional={onFunctional || undefined}
>
{stem && (
<p className="text-sm" data-slot="check-in-stem">
{wording(`mentalHealth.stems.${stem}`)}
</p>
)}
<h2
ref={headingRef}
id={questionId}
tabIndex={-1}
className="scroll-mt-4 text-base font-medium outline-none"
data-slot="check-in-question"
>
{question}
</h2>
{onFunctional && (
<p className="text-muted-foreground text-xs">
{t("mentalHealth.functionalOptionalHint")}
</p>
)}
<AnswerGroup
labelledBy={questionId}
options={options}
selected={selected}
disabled={disabled}
onChoose={onChoose}
/>
<p className="text-muted-foreground hidden text-xs md:block">
{t("mentalHealth.wizard.keyboardHint", { count: options.length })}
</p>
</div>
);
}
/**
* The answer control every instrument shares: a radio group of full-width
* options in the order the source form prints them.
*/
function AnswerGroup({
labelledBy,
options,
selected,
disabled,
onChoose,
}: {
labelledBy: string;
options: ReadonlyArray<{ value: number; label: string }>;
selected: number;
disabled: boolean;
onChoose: (value: number) => void;
}) {
const refs = useRef<Array<HTMLButtonElement | null>>([]);
const selectedIndex = options.findIndex((o) => o.value === selected);
const tabIndexAt = selectedIndex >= 0 ? selectedIndex : 0;
function onKeyDown(event: KeyboardEvent<HTMLDivElement>) {
const focused = refs.current.findIndex(
(el) => el === document.activeElement,
);
const from = focused >= 0 ? focused : tabIndexAt;
const last = options.length - 1;
const move: Record<string, number> = {
ArrowDown: from === last ? 0 : from + 1,
ArrowRight: from === last ? 0 : from + 1,
ArrowUp: from === 0 ? last : from - 1,
ArrowLeft: from === 0 ? last : from - 1,
Home: 0,
End: last,
};
if (event.key in move) {
event.preventDefault();
refs.current[move[event.key]]?.focus();
return;
}
const position = Number.parseInt(event.key, 10);
if (position >= 1 && position <= options.length && !disabled) {
event.preventDefault();
onChoose(options[position - 1].value);
}
}
return (
<div
role="radiogroup"
aria-labelledby={labelledBy}
className="flex flex-col gap-2"
data-slot="check-in-options"
onKeyDown={onKeyDown}
>
{options.map((option, i) => {
const checked = option.value === selected;
return (
<Button
key={option.value}
ref={(el) => {
refs.current[i] = el;
}}
type="button"
role="radio"
aria-checked={checked}
tabIndex={i === tabIndexAt ? 0 : -1}
// 44px touch-target floor (WCAG 2.5.5).
className="h-auto min-h-11 justify-start py-2 text-left whitespace-normal"
variant={checked ? "default" : "outline"}
onClick={() => onChoose(option.value)}
disabled={disabled}
data-slot="check-in-option"
data-value={option.value}
data-selected={checked || undefined}
>
{option.label}
</Button>
);
})}
</div>
);
}
function ReviewStep({
instrument,
items,
functional,
functionalStep,
wording,
headingRef,
onEdit,
}: {
instrument: InstrumentId;
items: readonly number[];
functional: number;
functionalStep: number | null;
wording: Wording;
headingRef: RefObject<HTMLHeadingElement | null>;
onEdit: (step: number) => void;
}) {
const { t } = useTranslations();
const rows = items.map((value, index) => ({
step: index + 1,
question: wording(itemKey(instrument, index)),
answer: wording(optionLabelKey(instrument, index, value)),
value: value as number | null,
stem: stemKey(instrument, index),
}));
if (functionalStep !== null) {
rows.push({
step: functionalStep,
question: wording(FUNCTIONAL_TITLE_KEY),
answer:
functional >= 0
? wording(functionalOptionKey(functional))
: t("mentalHealth.review.notAnswered"),
value: null,
stem: null,
});
}
return (
<div className="flex flex-col gap-3" data-slot="check-in-review">
<h2
ref={headingRef}
tabIndex={-1}
className="text-base font-medium outline-none"
>
{t("mentalHealth.review.title")}
</h2>
<p className="text-sm">{t("mentalHealth.review.description")}</p>
<ol className="flex flex-col gap-3">
{rows.map((row, i) => (
<li
key={row.step}
className="flex flex-col gap-1 text-sm"
data-slot="check-in-review-row"
data-step={row.step}
data-value={row.value ?? undefined}
>
{row.stem && row.stem !== rows[i - 1]?.stem && (
<p className="text-muted-foreground text-xs">
{wording(`mentalHealth.stems.${row.stem}`)}
</p>
)}
<div className="flex items-start justify-between gap-3">
<span className="min-w-0 flex-1">{row.question}</span>
<InlineAction
tone="primary"
size="sm"
className="shrink-0"
onClick={() => onEdit(row.step)}
aria-label={t("mentalHealth.review.changeLabel", {
number: row.step,
})}
data-review-edit={row.step}
>
{t("mentalHealth.review.change")}
</InlineAction>
</div>
<span className="text-foreground font-medium">
{row.answer}
{row.value !== null && (
<span className="text-muted-foreground font-normal tabular-nums">
{" "}
({row.value})
</span>
)}
</span>
</li>
))}
</ol>
</div>
);
}
@@ -89,6 +89,7 @@ export function InstrumentCard({
<Card
className="relative h-full gap-3 md:gap-3"
data-slot="instrument-card"
data-instrument={instrument}
>
{/* Source / licence — a discreet info control at the top-right of the
header, at the heading's height. A sibling of the detail-open
@@ -187,7 +188,12 @@ export function InstrumentCard({
{canManageMind && (
<CardContent className="flex h-full flex-col">
<div className="mt-auto pt-0">
<Button type="button" className="min-h-11 w-full" onClick={onStart}>
<Button
type="button"
className="min-h-11 w-full"
onClick={onStart}
data-slot="instrument-card-start"
>
{t("mentalHealth.start")}
</Button>
</div>
@@ -203,11 +203,7 @@ export function MentalWellbeing() {
)}
</section>
{/* The pointer to mood history that used to sit here is gone. The
cross-link on `/mood` still points at this page, which is the
direction that earns its keep — someone logging a mood may not
know the screeners exist, while someone who came here for a
screener already knows where their mood diary is. */}
{/* No mood pointer here: `/mood` links to this page instead. */}
{/* Per-instrument detail, opened from a card body: last result,
trend chart, dated history, Start — the Verlauf lives here,
@@ -255,6 +251,9 @@ export function MentalWellbeing() {
{phase === "result" && result && (
<AssessmentResult
result={result}
trend={history?.trends?.find(
(trend) => trend.instrument === result.assessment.instrument,
)}
onTakeAnother={backToLanding}
onBack={backToLanding}
/>
@@ -122,6 +122,62 @@ describe("instrument wording", () => {
});
});
describe("validated languages carry the published wording", () => {
// Spot checks against the published forms named at the locale lists in
// instruments.ts: the places where the bundles had drifted (an informal
// follow-up question, a reworded anchor, a modernised spelling, the
// Italian GAD-7 anchor that differs from the Italian PHQ-9 one).
const PUBLISHED: Record<string, Record<string, string>> = {
de: {
"mentalHealth.stems.phq9.period":
"Wie oft fühlten Sie sich im Verlauf der letzten 2 Wochen durch die folgenden Beschwerden beeinträchtigt?",
"mentalHealth.items.phq9.7":
"Schwierigkeiten, sich auf etwas zu konzentrieren, z. B. beim Zeitungslesen oder Fernsehen",
"mentalHealth.items.gad7.5":
"Rastlosigkeit, so dass Stillsitzen schwer fällt",
"mentalHealth.items.gad7.7":
"Gefühl der Angst, so als würde etwas Schlimmes passieren",
"mentalHealth.functional.2": "Stark erschwert",
"mentalHealth.who5Options.5": "Die ganze Zeit",
},
es: {
"mentalHealth.options.3": "Casi cada día",
"mentalHealth.items.phq9.9":
"Pensamientos de que estaría mejor muerto/a o de querer hacerse daño de algún modo",
"mentalHealth.items.gad7.1":
"Sentirse nervioso/a, angustiado/a o muy tenso/a",
},
it: {
"mentalHealth.options.2": "Per più della metà dei giorni",
"mentalHealth.gad7Options.2": "Per oltre la metà dei giorni",
"mentalHealth.items.phq9.2": "Sentirsi giù, triste o disperato/a",
},
pl: {
"mentalHealth.options.0": "Wcale nie dokuczały",
"mentalHealth.items.gad7.1":
"Czuł(a) się Pan(i) podenerwowany(a), niespokojny(a), mocno spięty(a)",
},
fr: {
"mentalHealth.gad7Options.2": "Plus de la moitié du temps",
"mentalHealth.items.gad7.1":
"Un sentiment de nervosité, d’anxiété ou de tension",
},
};
it.each(Object.keys(PUBLISHED))("%s", (locale) => {
const flat = bundle(locale);
for (const [key, text] of Object.entries(PUBLISHED[locale])) {
expect(flat.get(key), `${locale} ${key}`).toBe(text);
}
});
it("keeps the German follow-up question in the published formal address", () => {
const title = bundle("de").get("mentalHealth.functionalTitle") ?? "";
expect(title).toMatch(/^Wenn eines oder mehrere der bisher/);
expect(title).not.toMatch(/\b(?:du|dich|dir|dein\w*)\b/i);
});
});
describe("no surface renders instrument wording through a bare t()", () => {
// Item text, answer labels and stems must go through `instrumentText`. A
// bare `t()` on one of these keys reads the bundle whatever the language.
@@ -166,6 +166,7 @@ describe("mental-health instruments", () => {
it("resolves option-label keys per scheme (shared vs SCI per-item groups)", () => {
expect(optionLabelKey("PHQ9", 0, 2)).toBe("mentalHealth.options.2");
expect(optionLabelKey("GAD7", 0, 2)).toBe("mentalHealth.gad7Options.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(
@@ -185,29 +186,37 @@ describe("mental-health instruments", () => {
);
});
it("maps recall stems (WHO-5 one stem; SCI three sections; PHQ/GAD none)", () => {
it("maps every item to its form's stem (PHQ/GAD/WHO-5 one each; SCI three sections)", () => {
expect(stemKey("WHO5", 0)).toBe("who5.period");
expect(stemKey("SCI", 0)).toBe("sci.night");
expect(stemKey("SCI", 3)).toBe("sci.night");
expect(stemKey("SCI", 4)).toBe("sci.impact");
expect(stemKey("SCI", 7)).toBe("sci.finally");
expect(stemKey("PHQ9", 0)).toBeNull();
expect(stemKey("GAD7", 0)).toBeNull();
expect(stemKey("PHQ9", 0)).toBe("phq9.period");
expect(stemKey("PHQ9", 8)).toBe("phq9.period");
expect(stemKey("GAD7", 6)).toBe("gad7.period");
for (const def of Object.values(INSTRUMENTS)) {
expect(def.stemKeys).toHaveLength(def.itemCount);
}
});
it("declares validated item locales honestly (SCI = English only)", () => {
for (const locale of ["de", "en", "es", "fr", "it", "pl", "ko"]) {
expect(hasValidatedItems("WHO5", locale)).toBe(true);
it("declares validated item locales honestly, per instrument", () => {
// Each listed language reproduces a published version word for word
// (sources at the lists in instruments.ts).
for (const locale of ["de", "en", "es", "it", "pl"]) {
expect(hasValidatedItems("PHQ9", locale)).toBe(true);
}
// The Dutch wording has not been read against the published forms yet.
expect(hasValidatedItems("PHQ9", "nl")).toBe(false);
expect(hasValidatedItems("GAD7", "nl")).toBe(false);
expect(hasValidatedItems("WHO5", "nl")).toBe(false);
// Nor has the Brazilian Portuguese one.
expect(hasValidatedItems("PHQ9", "pt-BR")).toBe(false);
expect(hasValidatedItems("GAD7", "pt-BR")).toBe(false);
expect(hasValidatedItems("WHO5", "pt-BR")).toBe(false);
for (const locale of ["de", "en", "es", "fr", "it", "pl"]) {
expect(hasValidatedItems("GAD7", locale)).toBe(true);
expect(hasValidatedItems("WHO5", locale)).toBe(true);
}
// No obtainable French-for-France PHQ-9; no checkable Korean forms.
expect(hasValidatedItems("PHQ9", "fr")).toBe(false);
for (const id of ["PHQ9", "GAD7", "WHO5"] as const) {
expect(hasValidatedItems(id, "ko")).toBe(false);
expect(hasValidatedItems(id, "nl")).toBe(false);
expect(hasValidatedItems(id, "pt-BR")).toBe(false);
}
expect(hasValidatedItems("SCI", "en")).toBe(true);
expect(hasValidatedItems("SCI", "en-GB")).toBe(true);
expect(hasValidatedItems("SCI", "de")).toBe(false);
@@ -72,6 +72,10 @@ export const SOURCE_WORDING: Readonly<Record<string, string>> = {
"mentalHealth.options.1": "Several days",
"mentalHealth.options.2": "More than half the days",
"mentalHealth.options.3": "Nearly every day",
"mentalHealth.gad7Options.0": "Not at all",
"mentalHealth.gad7Options.1": "Several days",
"mentalHealth.gad7Options.2": "More than half the days",
"mentalHealth.gad7Options.3": "Nearly every day",
"mentalHealth.functionalTitle":
"If you checked off any problems, how difficult have these problems made it for you to do your work, take care of things at home, or get along with other people?",
"mentalHealth.functional.0": "Not difficult at all",
@@ -110,6 +114,10 @@ export const SOURCE_WORDING: Readonly<Record<string, string>> = {
"mentalHealth.sciOptions.problemDuration.3": "1–2 months",
"mentalHealth.sciOptions.problemDuration.4":
"I don't have a problem / < 1 month",
"mentalHealth.stems.phq9.period":
"Over the last 2 weeks, how often have you been bothered by any of the following problems?",
"mentalHealth.stems.gad7.period":
"Over the last 2 weeks, how often have you been bothered by the following problems?",
"mentalHealth.stems.who5.period": "Over the last two weeks …",
"mentalHealth.stems.sci.night":
"Thinking about a typical night in the last month …",
+74 -31
View File
@@ -11,9 +11,8 @@
* - PHQ-9: Kroenke 2001; GAD-7: Spitzer 2006 (public-domain Pfizer grant).
* - WHO-5: WHO/UCN/MSD/MHE/2024.1 (© WHO 2024, CC BY-NC-SA 3.0 IGO) — raw
* 0–25, × 4 → 0–100; "a percentage score below 50 … an indication for
* further assessment". Six app locales ship the WHO/PRU translations
* verbatim; Korean, which neither distribution carries, ships the
* published Kim 2010 translation instead.
* further assessment". Five other app languages ship the PRU
* translations WHO republishes, verbatim (sources at `WHO5_LOCALES`).
* - SCI: Espie et al., BMJ Open 2014;4:e004183 (© Sleepio Ltd, licensed
* free for non-commercial use, CC BY-NC) — 8 items, 0–4 each, total 0–32,
* HIGHER = better sleep; "a score of ≤16 … seems reasonable to detect
@@ -62,12 +61,12 @@ export interface InstrumentDefinition {
optionNamespace: string;
optionGroups?: readonly string[];
/**
* Recall-stem caption key per item under `mentalHealth.stems.` — the WHO-5
* and SCI items are fragments under a shared stem on the source forms, so
* the wizard paints the applicable stem above the question. PHQ/GAD keep
* their frame in the instrument description line (shipped contract).
* Stem key per item under `mentalHealth.stems.`: the question the source
* form prints above its items (the PHQ-9 / GAD-7 "Over the last 2 weeks…",
* the WHO-5 "Over the last two weeks …", the SCI section stems). The
* wizard keeps it visible above every item.
*/
stemKeys?: readonly string[];
stemKeys: readonly string[];
/** Multiplier from item-sum to the reported total (WHO-5: × 4). */
scoreMultiplier: number;
/** Inclusive REPORTED total range (WHO-5: 0–100 percentage). */
@@ -101,15 +100,58 @@ export interface InstrumentDefinition {
attribution: string;
}
/**
* The app languages whose PHQ-9 / GAD-7 / WHO-5 item wording is bundled from
* a published validated version. `nl` and `pt-BR` are absent on purpose: no
* published Dutch or Brazilian Portuguese form has been read and bundled yet,
* so those readers get the English items, answers and stems plus the honest
* note, and their bundles carry none of that wording. Adding a language here
* means bundling its validated form verbatim in the same change.
/*
* Which languages carry an instrument's own wording, and where it comes from.
*
* A language is listed only when its bundle reproduces a published version
* word for word: the question stem, every item, every answer option and, for
* the PHQ-9, the follow-up question. Each list below names its sources. A
* language that is not listed shows the English original with the honest
* note, and its bundle carries none of the instrument's keys
* (`instrument-wording.ts`). Adding a language means bundling its published
* form verbatim in the same change, with the source named here.
*
* Paper-form instructions ("mark your answer with a cross") and the
* row-end full stops of the printed tables are left out; nothing else is
* changed. The app's own copy may address the reader informally; the
* instruments keep the address form they were published in.
*/
const ALL_APP_LOCALES = ["de", "en", "es", "fr", "it", "pl", "ko"] as const;
/**
* PHQ-9. English: the Pfizer form (phqscreeners.com). The others are the
* official Pfizer translations, read from the PDFs as distributed:
* - de: "Gesundheitsfragebogen für Patienten (PHQ-9)", German for Germany,
* the PHQ-D wording of Löwe, Spitzer, Zipfel & Herzog (Pfizer 2002).
* - es: Spanish for Spain. it: Italian for Italy. pl: Polish for Poland.
* (ECFS Mental Health working group mirror of the Pfizer files:
* ecfs.eu/sites/default/files/general-content-files/working-groups/
* Mental Health/PHQ9_<language> for <country>.pdf)
* Not listed: fr (the France version could not be obtained; the bundle's
* wording matched neither it nor the Belgian one), ko (no published Korean
* form could be checked), nl and pt-BR (never bundled).
*/
const PHQ9_LOCALES = ["en", "de", "es", "it", "pl"] as const;
/**
* GAD-7. English: the Pfizer form. de: Löwe B et al., "Gesundheitsfragebogen
* für Patienten (GAD-7)", German version © Bernd Löwe 2015, Universitätsklinikum
* Hamburg-Eppendorf (validation: Löwe et al., Med Care 2008;46:266-274). es,
* fr, it, pl: the Pfizer translations for Spain, France, Italy and Poland
* (same ECFS mirror, GAD7_<language> for <country>.pdf). Not listed: ko, nl,
* pt-BR, as for the PHQ-9.
*/
const GAD7_LOCALES = ["en", "de", "es", "fr", "it", "pl"] as const;
/**
* WHO-5. English: WHO/UCN/MSD/MHE/2024.1. de, es, fr, it, pl: the Psychiatric
* Research Unit translations (1998/1999 versions) that WHO republishes beside
* the 2024 English text (cdn.who.int …/five-well-being-index-(who-5)/
* who-5_<language>.pdf; the Spanish one via infocop.es). The Spanish sheet
* misprints item 4 as "descandado"; the bundle keeps "descansado". Not
* listed: ko (the Kim 2010 translation could not be checked against the
* paper), nl and pt-BR.
*/
const WHO5_LOCALES = ["en", "de", "es", "fr", "it", "pl"] as const;
/**
* The PHQ family is public-domain (Pfizer 2010); this is the canonical footer
@@ -139,10 +181,13 @@ const PHQ9: InstrumentDefinition = {
{ key: "modSevere", min: 15, max: 19 },
{ key: "severe", min: 20, max: 27 },
],
// The form's own question, "Over the last 2 weeks, how often have you been
// bothered by any of the following problems?", stands above every item.
stemKeys: Array(9).fill("phq9.period"),
actionThreshold: 10,
// Item 9 ("Thoughts that you would be better off dead…") — the safety field.
safetyItemIndex: 8,
validatedItemLocales: ALL_APP_LOCALES,
validatedItemLocales: PHQ9_LOCALES,
attribution: PHQ_GAD_ATTRIBUTION,
};
@@ -152,7 +197,10 @@ const GAD7: InstrumentDefinition = {
itemCount: 7,
itemMax: 3,
optionOrder: [0, 1, 2, 3],
optionNamespace: "mentalHealth.options",
// Its own anchors: the Italian GAD-7 words option 2 differently from the
// Italian PHQ-9, so the two cannot share one set.
optionNamespace: "mentalHealth.gad7Options",
stemKeys: Array(7).fill("gad7.period"),
scoreMultiplier: 1,
minScore: 0,
maxScore: 21,
@@ -166,7 +214,7 @@ const GAD7: InstrumentDefinition = {
],
actionThreshold: 10,
safetyItemIndex: null,
validatedItemLocales: ALL_APP_LOCALES,
validatedItemLocales: GAD7_LOCALES,
attribution: PHQ_GAD_ATTRIBUTION,
};
@@ -198,18 +246,11 @@ const WHO5: InstrumentDefinition = {
],
actionThreshold: 50,
safetyItemIndex: null,
// WHO publishes official de/en/es/fr/it/pl versions — all bundled verbatim.
// There is no Korean one, so `ko` carries the published forward/back-
// translation from Kim 2010 instead, which is CC BY-NC and so
// redistributable. Its validation sample was community-dwelling elderly,
// not general adults — narrower than the other six arms.
validatedItemLocales: ALL_APP_LOCALES,
validatedItemLocales: WHO5_LOCALES,
attribution:
"World Health Organization. The World Health Organization-Five Well-Being " +
"Index (WHO-5). Geneva: World Health Organization; 2024. Licence: " +
"CC BY-NC-SA 3.0 IGO. WHO does not endorse this application. Korean " +
"translation: Kim HJ, Moon YS, Son BK, Lee SK, Rho HJ, Kim DH. J Korean " +
"Geriatr Psychiatry 2010;14(2):90-96 (CC BY-NC 3.0).",
"CC BY-NC-SA 3.0 IGO. WHO does not endorse this application.",
};
/**
@@ -260,8 +301,10 @@ const SCI: InstrumentDefinition = {
safetyItemIndex: null,
// Validated in English (the CC BY-NC BMJ Open publication). French/Italian
// validations exist in the literature but their item text is not openly
// redistributable, so every non-EN locale presents the English items with
// an honest note — never a self-made translation.
// redistributable, and no validated German version is published (a search
// in October 2026 found the German ISI, Gerber et al. 2016, not the SCI),
// so every non-EN locale presents the English items with an honest note —
// never a self-made translation.
validatedItemLocales: ["en"],
attribution:
"Espie CA, Kyle SD, Hames P, et al. The Sleep Condition Indicator: a " +
@@ -341,7 +384,7 @@ export function optionLabelKey(
/** Stem caption key (under `mentalHealth.stems.`) for an item, or null. */
export function stemKey(id: InstrumentId, itemIndex: number): string | null {
return INSTRUMENTS[id].stemKeys?.[itemIndex] ?? null;
return INSTRUMENTS[id].stemKeys[itemIndex] ?? null;
}
/**