mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
fix(mental-health): show unvalidated languages the English questionnaire wording
The PHQ-9, GAD-7 and WHO-5 are not validated in Dutch or Brazilian Portuguese, and the SCI is validated in English only, yet the check-in wizard and the answer breakdown read the item text, answer options and stems through t() in the reader's language. Dutch and Brazilian Portuguese readers saw unvalidated translations of every item, including PHQ-9 item 9, under a note saying the questions were in English. Every surface now resolves instrument wording through instrumentText, which returns the English source for a language outside the instrument's validated set. The unvalidated wording is removed from the bundles; the locale-parity check accepts exactly those keys as absent and fails when a bundle carries one, and a structural test keeps item text out of bare t() calls. The PHQ-9 follow-up question regains its original wording. CONTRIBUTING states the rule for translators.
This commit is contained in:
1 parent
3decab47a0
commit
5448a43651
19 files changed
+517
-608
No files matched your search
+2
-1
@@ -151,6 +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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -159,7 +160,7 @@ To run the checks Fink shows on a local checkout: `npx @inlang/cli@3.4.4 check -
|
||||
If you want to introduce a locale that isn't shipped (e.g. `sv`, `cs`):
|
||||
|
||||
1. Open an issue first using the **Translation request** template. We'll confirm the locale is in scope and decide whether it joins the AI-initial-community-maintained tier or the maintained tier.
|
||||
2. Copy `messages/en.json` to `messages/<locale>.json`.
|
||||
2. Copy `messages/en.json` to `messages/<locale>.json`, then delete the questionnaire wording the new language has no validated version of (the test output lists the keys).
|
||||
3. Translate. AI assistance is welcome as a starting point but the PR description should note whether it's an AI-initial pass or a fully human-reviewed one — that information drives the banner state.
|
||||
4. Add the locale to `locales` in `src/lib/i18n/config.ts` and to `locales` in `project.inlang/settings.json`, then run `pnpm typecheck`. Every per-locale table that still lacks the new locale fails to compile, and that list is the work: date and number formatting, the reply-language footers of the Coach and insights prompts, the outbound safety banks, the prose-grounding patterns and the no-key fallback copy. Include those in the same PR.
|
||||
5. Run `pnpm test` before submitting. The i18n guards hold the new bundle to the English keys and placeholders.
|
||||
|
||||
@@ -11033,16 +11033,6 @@
|
||||
"3": "… habe ich mich energisch und aktiv gefühlt",
|
||||
"4": "… habe ich mich beim Aufwachen frisch und ausgeruht gefühlt",
|
||||
"5": "… war mein Alltag voller Dinge, die mich interessieren"
|
||||
},
|
||||
"sci": {
|
||||
"1": "… how long does it take you to fall asleep?",
|
||||
"2": "… if you then wake up during the night … how long are you awake for in total? (add all the wakenings up)",
|
||||
"3": "… how many nights a week do you have a problem with your sleep?",
|
||||
"4": "… how would you rate your sleep quality?",
|
||||
"5": "… affected your mood, energy, or relationships?",
|
||||
"6": "… affected your concentration, productivity, or ability to stay awake?",
|
||||
"7": "… troubled you in general?",
|
||||
"8": "… how long have you had a problem with your sleep?"
|
||||
}
|
||||
},
|
||||
"crisisResource": {
|
||||
@@ -11104,51 +11094,9 @@
|
||||
"4": "Meistens",
|
||||
"5": "Die ganze Zeit"
|
||||
},
|
||||
"sciOptions": {
|
||||
"duration": {
|
||||
"0": "≥ 61 min",
|
||||
"1": "46–60 min",
|
||||
"2": "31–45 min",
|
||||
"3": "16–30 min",
|
||||
"4": "0–15 min"
|
||||
},
|
||||
"nights": {
|
||||
"0": "5–7",
|
||||
"1": "4",
|
||||
"2": "3",
|
||||
"3": "2",
|
||||
"4": "0–1"
|
||||
},
|
||||
"quality": {
|
||||
"0": "Very poor",
|
||||
"1": "Poor",
|
||||
"2": "Average",
|
||||
"3": "Good",
|
||||
"4": "Very good"
|
||||
},
|
||||
"impact": {
|
||||
"0": "Very much",
|
||||
"1": "Much",
|
||||
"2": "Somewhat",
|
||||
"3": "A little",
|
||||
"4": "Not at all"
|
||||
},
|
||||
"problemDuration": {
|
||||
"0": "> 1 year",
|
||||
"1": "7–12 months",
|
||||
"2": "3–6 months",
|
||||
"3": "1–2 months",
|
||||
"4": "I don't have a problem / < 1 month"
|
||||
}
|
||||
},
|
||||
"stems": {
|
||||
"who5": {
|
||||
"period": "In den letzten zwei Wochen …"
|
||||
},
|
||||
"sci": {
|
||||
"night": "Thinking about a typical night in the last month …",
|
||||
"impact": "Thinking about the past month, to what extent has poor sleep …",
|
||||
"finally": "Finally …"
|
||||
}
|
||||
},
|
||||
"openDetail": "Details anzeigen",
|
||||
|
||||
+1
-1
@@ -10953,7 +10953,7 @@
|
||||
"2": "More than half the days",
|
||||
"3": "Nearly every day"
|
||||
},
|
||||
"functionalTitle": "If you checked off any problems, how difficult have these made it to do your work, take care of things at home, or get along with other people?",
|
||||
"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",
|
||||
"1": "Somewhat difficult",
|
||||
|
||||
@@ -11033,16 +11033,6 @@
|
||||
"3": "Me he sentido activo y enérgico",
|
||||
"4": "Me he despertado fresco y descansado",
|
||||
"5": "Mi vida cotidiana ha estado llena de cosas que me interesan"
|
||||
},
|
||||
"sci": {
|
||||
"1": "… how long does it take you to fall asleep?",
|
||||
"2": "… if you then wake up during the night … how long are you awake for in total? (add all the wakenings up)",
|
||||
"3": "… how many nights a week do you have a problem with your sleep?",
|
||||
"4": "… how would you rate your sleep quality?",
|
||||
"5": "… affected your mood, energy, or relationships?",
|
||||
"6": "… affected your concentration, productivity, or ability to stay awake?",
|
||||
"7": "… troubled you in general?",
|
||||
"8": "… how long have you had a problem with your sleep?"
|
||||
}
|
||||
},
|
||||
"crisisResource": {
|
||||
@@ -11104,51 +11094,9 @@
|
||||
"4": "La mayor parte del tiempo",
|
||||
"5": "Todo el tiempo"
|
||||
},
|
||||
"sciOptions": {
|
||||
"duration": {
|
||||
"0": "≥ 61 min",
|
||||
"1": "46–60 min",
|
||||
"2": "31–45 min",
|
||||
"3": "16–30 min",
|
||||
"4": "0–15 min"
|
||||
},
|
||||
"nights": {
|
||||
"0": "5–7",
|
||||
"1": "4",
|
||||
"2": "3",
|
||||
"3": "2",
|
||||
"4": "0–1"
|
||||
},
|
||||
"quality": {
|
||||
"0": "Very poor",
|
||||
"1": "Poor",
|
||||
"2": "Average",
|
||||
"3": "Good",
|
||||
"4": "Very good"
|
||||
},
|
||||
"impact": {
|
||||
"0": "Very much",
|
||||
"1": "Much",
|
||||
"2": "Somewhat",
|
||||
"3": "A little",
|
||||
"4": "Not at all"
|
||||
},
|
||||
"problemDuration": {
|
||||
"0": "> 1 year",
|
||||
"1": "7–12 months",
|
||||
"2": "3–6 months",
|
||||
"3": "1–2 months",
|
||||
"4": "I don't have a problem / < 1 month"
|
||||
}
|
||||
},
|
||||
"stems": {
|
||||
"who5": {
|
||||
"period": "Durante las últimas dos semanas:"
|
||||
},
|
||||
"sci": {
|
||||
"night": "Thinking about a typical night in the last month …",
|
||||
"impact": "Thinking about the past month, to what extent has poor sleep …",
|
||||
"finally": "Finally …"
|
||||
}
|
||||
},
|
||||
"openDetail": "Ver detalles",
|
||||
|
||||
@@ -11033,16 +11033,6 @@
|
||||
"3": "Je me suis senti(e) plein(e) d'énergie et vigoureux(se)",
|
||||
"4": "Je me suis réveillé(e) en me sentant frais(che) et dispos(e)",
|
||||
"5": "Ma vie quotidienne a été remplie de choses intéressantes"
|
||||
},
|
||||
"sci": {
|
||||
"1": "… how long does it take you to fall asleep?",
|
||||
"2": "… if you then wake up during the night … how long are you awake for in total? (add all the wakenings up)",
|
||||
"3": "… how many nights a week do you have a problem with your sleep?",
|
||||
"4": "… how would you rate your sleep quality?",
|
||||
"5": "… affected your mood, energy, or relationships?",
|
||||
"6": "… affected your concentration, productivity, or ability to stay awake?",
|
||||
"7": "… troubled you in general?",
|
||||
"8": "… how long have you had a problem with your sleep?"
|
||||
}
|
||||
},
|
||||
"crisisResource": {
|
||||
@@ -11104,51 +11094,9 @@
|
||||
"4": "La plupart du temps",
|
||||
"5": "Tout le temps"
|
||||
},
|
||||
"sciOptions": {
|
||||
"duration": {
|
||||
"0": "≥ 61 min",
|
||||
"1": "46–60 min",
|
||||
"2": "31–45 min",
|
||||
"3": "16–30 min",
|
||||
"4": "0–15 min"
|
||||
},
|
||||
"nights": {
|
||||
"0": "5–7",
|
||||
"1": "4",
|
||||
"2": "3",
|
||||
"3": "2",
|
||||
"4": "0–1"
|
||||
},
|
||||
"quality": {
|
||||
"0": "Very poor",
|
||||
"1": "Poor",
|
||||
"2": "Average",
|
||||
"3": "Good",
|
||||
"4": "Very good"
|
||||
},
|
||||
"impact": {
|
||||
"0": "Very much",
|
||||
"1": "Much",
|
||||
"2": "Somewhat",
|
||||
"3": "A little",
|
||||
"4": "Not at all"
|
||||
},
|
||||
"problemDuration": {
|
||||
"0": "> 1 year",
|
||||
"1": "7–12 months",
|
||||
"2": "3–6 months",
|
||||
"3": "1–2 months",
|
||||
"4": "I don't have a problem / < 1 month"
|
||||
}
|
||||
},
|
||||
"stems": {
|
||||
"who5": {
|
||||
"period": "Au cours des deux dernières semaines"
|
||||
},
|
||||
"sci": {
|
||||
"night": "Thinking about a typical night in the last month …",
|
||||
"impact": "Thinking about the past month, to what extent has poor sleep …",
|
||||
"finally": "Finally …"
|
||||
}
|
||||
},
|
||||
"openDetail": "Voir les détails",
|
||||
|
||||
@@ -11033,16 +11033,6 @@
|
||||
"3": "Mi sono sentito/a attivo/a ed energico/a",
|
||||
"4": "Mi sono svegliato/a sentendomi fresco/a e riposato/a",
|
||||
"5": "La mia vita di tutti i giorni è stata piena di cose che mi interessano"
|
||||
},
|
||||
"sci": {
|
||||
"1": "… how long does it take you to fall asleep?",
|
||||
"2": "… if you then wake up during the night … how long are you awake for in total? (add all the wakenings up)",
|
||||
"3": "… how many nights a week do you have a problem with your sleep?",
|
||||
"4": "… how would you rate your sleep quality?",
|
||||
"5": "… affected your mood, energy, or relationships?",
|
||||
"6": "… affected your concentration, productivity, or ability to stay awake?",
|
||||
"7": "… troubled you in general?",
|
||||
"8": "… how long have you had a problem with your sleep?"
|
||||
}
|
||||
},
|
||||
"crisisResource": {
|
||||
@@ -11104,51 +11094,9 @@
|
||||
"4": "La maggior parte del tempo",
|
||||
"5": "Sempre"
|
||||
},
|
||||
"sciOptions": {
|
||||
"duration": {
|
||||
"0": "≥ 61 min",
|
||||
"1": "46–60 min",
|
||||
"2": "31–45 min",
|
||||
"3": "16–30 min",
|
||||
"4": "0–15 min"
|
||||
},
|
||||
"nights": {
|
||||
"0": "5–7",
|
||||
"1": "4",
|
||||
"2": "3",
|
||||
"3": "2",
|
||||
"4": "0–1"
|
||||
},
|
||||
"quality": {
|
||||
"0": "Very poor",
|
||||
"1": "Poor",
|
||||
"2": "Average",
|
||||
"3": "Good",
|
||||
"4": "Very good"
|
||||
},
|
||||
"impact": {
|
||||
"0": "Very much",
|
||||
"1": "Much",
|
||||
"2": "Somewhat",
|
||||
"3": "A little",
|
||||
"4": "Not at all"
|
||||
},
|
||||
"problemDuration": {
|
||||
"0": "> 1 year",
|
||||
"1": "7–12 months",
|
||||
"2": "3–6 months",
|
||||
"3": "1–2 months",
|
||||
"4": "I don't have a problem / < 1 month"
|
||||
}
|
||||
},
|
||||
"stems": {
|
||||
"who5": {
|
||||
"period": "Nelle ultime due settimane"
|
||||
},
|
||||
"sci": {
|
||||
"night": "Thinking about a typical night in the last month …",
|
||||
"impact": "Thinking about the past month, to what extent has poor sleep …",
|
||||
"finally": "Finally …"
|
||||
}
|
||||
},
|
||||
"openDetail": "Mostra dettagli",
|
||||
|
||||
@@ -11033,16 +11033,6 @@
|
||||
"3": "나는 의욕적이고 활기차다.",
|
||||
"4": "나는 자고 일어나면 상쾌하고 푹 쉬었다는 느낌이 든다.",
|
||||
"5": "나의 일상생활은 흥미로운 일들로 가득하다."
|
||||
},
|
||||
"sci": {
|
||||
"1": "… how long does it take you to fall asleep?",
|
||||
"2": "… if you then wake up during the night … how long are you awake for in total? (add all the wakenings up)",
|
||||
"3": "… how many nights a week do you have a problem with your sleep?",
|
||||
"4": "… how would you rate your sleep quality?",
|
||||
"5": "… affected your mood, energy, or relationships?",
|
||||
"6": "… affected your concentration, productivity, or ability to stay awake?",
|
||||
"7": "… troubled you in general?",
|
||||
"8": "… how long have you had a problem with your sleep?"
|
||||
}
|
||||
},
|
||||
"crisisResource": {
|
||||
@@ -11104,51 +11094,9 @@
|
||||
"4": "거의 그렇다.",
|
||||
"5": "항상 그렇다."
|
||||
},
|
||||
"sciOptions": {
|
||||
"duration": {
|
||||
"0": "≥ 61 min",
|
||||
"1": "46–60 min",
|
||||
"2": "31–45 min",
|
||||
"3": "16–30 min",
|
||||
"4": "0–15 min"
|
||||
},
|
||||
"nights": {
|
||||
"0": "5–7",
|
||||
"1": "4",
|
||||
"2": "3",
|
||||
"3": "2",
|
||||
"4": "0–1"
|
||||
},
|
||||
"quality": {
|
||||
"0": "Very poor",
|
||||
"1": "Poor",
|
||||
"2": "Average",
|
||||
"3": "Good",
|
||||
"4": "Very good"
|
||||
},
|
||||
"impact": {
|
||||
"0": "Very much",
|
||||
"1": "Much",
|
||||
"2": "Somewhat",
|
||||
"3": "A little",
|
||||
"4": "Not at all"
|
||||
},
|
||||
"problemDuration": {
|
||||
"0": "> 1 year",
|
||||
"1": "7–12 months",
|
||||
"2": "3–6 months",
|
||||
"3": "1–2 months",
|
||||
"4": "I don't have a problem / < 1 month"
|
||||
}
|
||||
},
|
||||
"stems": {
|
||||
"who5": {
|
||||
"period": "지난 2주 동안"
|
||||
},
|
||||
"sci": {
|
||||
"night": "Thinking about a typical night in the last month …",
|
||||
"impact": "Thinking about the past month, to what extent has poor sleep …",
|
||||
"finally": "Finally …"
|
||||
}
|
||||
},
|
||||
"openDetail": "자세히 보기",
|
||||
|
||||
@@ -10947,19 +10947,6 @@
|
||||
"who5": "Welzijnscheck, 5 vragen",
|
||||
"sci": "Slaapcheck, 8 vragen"
|
||||
},
|
||||
"options": {
|
||||
"0": "Helemaal niet",
|
||||
"1": "Meerdere dagen",
|
||||
"2": "Meer dan de helft van de dagen",
|
||||
"3": "Bijna elke dag"
|
||||
},
|
||||
"functionalTitle": "Als je een of meer problemen hebt aangevinkt: hoe moeilijk was het door deze problemen om je werk te doen, voor je huishouden te zorgen of met andere mensen om te gaan?",
|
||||
"functional": {
|
||||
"0": "Helemaal niet moeilijk",
|
||||
"1": "Enigszins moeilijk",
|
||||
"2": "Erg moeilijk",
|
||||
"3": "Extreem moeilijk"
|
||||
},
|
||||
"functionalOptionalHint": "Optioneel: je kunt ook versturen zonder deze vraag te beantwoorden.",
|
||||
"result": {
|
||||
"title": "Je resultaat",
|
||||
@@ -11006,45 +10993,6 @@
|
||||
"ifDanger": "Ben je in direct gevaar? Bel dan nu {emergency}.",
|
||||
"resourcesTitle": "Mensen die je kunnen helpen"
|
||||
},
|
||||
"items": {
|
||||
"phq9": {
|
||||
"1": "Weinig interesse in of plezier aan activiteiten",
|
||||
"2": "Zich neerslachtig, depressief of hopeloos voelen",
|
||||
"3": "Moeilijk in slaap vallen of doorslapen, of te veel slapen",
|
||||
"4": "Zich moe voelen of weinig energie hebben",
|
||||
"5": "Weinig eetlust of overmatig eten",
|
||||
"6": "Een negatief beeld van uzelf hebben, of het gevoel hebben dat u een mislukkeling bent of dat u uzelf of uw familie teleurgesteld hebt",
|
||||
"7": "Moeite met concentreren, bijvoorbeeld bij het lezen van de krant of het televisiekijken",
|
||||
"8": "Zo traag bewegen of praten dat andere mensen het zouden kunnen opmerken. Of juist het tegenovergestelde: zo gejaagd of rusteloos zijn dat u veel meer beweegt dan gewoonlijk",
|
||||
"9": "Gedachten dat u beter dood zou kunnen zijn of gedachten om uzelf op een of andere manier pijn te doen"
|
||||
},
|
||||
"gad7": {
|
||||
"1": "Zich nerveus, angstig of gespannen voelen",
|
||||
"2": "Niet in staat zijn om te stoppen met piekeren of om het piekeren onder controle te houden",
|
||||
"3": "Zich te veel zorgen maken over verschillende dingen",
|
||||
"4": "Moeite met ontspannen",
|
||||
"5": "Zo rusteloos zijn dat het moeilijk is om stil te zitten",
|
||||
"6": "Snel geërgerd of geïrriteerd zijn",
|
||||
"7": "Bang zijn dat er iets vreselijks zou kunnen gebeuren"
|
||||
},
|
||||
"who5": {
|
||||
"1": "… voelde ik me opgewekt en goedgehumeurd",
|
||||
"2": "… voelde ik me rustig en ontspannen",
|
||||
"3": "… voelde ik me actief en energiek",
|
||||
"4": "… voelde ik me fris en uitgerust bij het wakker worden",
|
||||
"5": "… was mijn dagelijks leven gevuld met dingen die mij interesseren"
|
||||
},
|
||||
"sci": {
|
||||
"1": "… how long does it take you to fall asleep?",
|
||||
"2": "… if you then wake up during the night … how long are you awake for in total? (add all the wakenings up)",
|
||||
"3": "… how many nights a week do you have a problem with your sleep?",
|
||||
"4": "… how would you rate your sleep quality?",
|
||||
"5": "… affected your mood, energy, or relationships?",
|
||||
"6": "… affected your concentration, productivity, or ability to stay awake?",
|
||||
"7": "… troubled you in general?",
|
||||
"8": "… how long have you had a problem with your sleep?"
|
||||
}
|
||||
},
|
||||
"crisisResource": {
|
||||
"telefonSeelsorge": {
|
||||
"name": "TelefonSeelsorge (dag en nacht, gratis, vertrouwelijk)"
|
||||
@@ -11096,61 +11044,6 @@
|
||||
"sci": "In het onderzoek achter deze vragenlijst hingen scores van 16 of lager samen met waarschijnlijke slapeloosheid. Blijf je last houden van slecht slapen, dan kan een gesprek met een zorgverlener helpen. Dit is geen diagnose."
|
||||
},
|
||||
"validatedInEnglishNote": "Deze vragenlijst is in het Engels gevalideerd; de vragen worden in de oorspronkelijke Engelse formulering getoond.",
|
||||
"who5Options": {
|
||||
"0": "Nooit",
|
||||
"1": "Af en toe",
|
||||
"2": "Minder dan de helft van de tijd",
|
||||
"3": "Meer dan de helft van de tijd",
|
||||
"4": "Meestal",
|
||||
"5": "Altijd"
|
||||
},
|
||||
"sciOptions": {
|
||||
"duration": {
|
||||
"0": "≥ 61 min",
|
||||
"1": "46–60 min",
|
||||
"2": "31–45 min",
|
||||
"3": "16–30 min",
|
||||
"4": "0–15 min"
|
||||
},
|
||||
"nights": {
|
||||
"0": "5–7",
|
||||
"1": "4",
|
||||
"2": "3",
|
||||
"3": "2",
|
||||
"4": "0–1"
|
||||
},
|
||||
"quality": {
|
||||
"0": "Very poor",
|
||||
"1": "Poor",
|
||||
"2": "Average",
|
||||
"3": "Good",
|
||||
"4": "Very good"
|
||||
},
|
||||
"impact": {
|
||||
"0": "Very much",
|
||||
"1": "Much",
|
||||
"2": "Somewhat",
|
||||
"3": "A little",
|
||||
"4": "Not at all"
|
||||
},
|
||||
"problemDuration": {
|
||||
"0": "> 1 year",
|
||||
"1": "7–12 months",
|
||||
"2": "3–6 months",
|
||||
"3": "1–2 months",
|
||||
"4": "I don't have a problem / < 1 month"
|
||||
}
|
||||
},
|
||||
"stems": {
|
||||
"who5": {
|
||||
"period": "In de afgelopen twee weken …"
|
||||
},
|
||||
"sci": {
|
||||
"night": "Thinking about a typical night in the last month …",
|
||||
"impact": "Thinking about the past month, to what extent has poor sleep …",
|
||||
"finally": "Finally …"
|
||||
}
|
||||
},
|
||||
"openDetail": "Details tonen",
|
||||
"historyLoadError": "Je testgeschiedenis kon niet worden geladen",
|
||||
"historyLoadErrorHint": "Dit betekent niet dat je geen resultaten hebt; de lijst kon niet worden opgehaald. Probeer het opnieuw.",
|
||||
|
||||
@@ -11033,16 +11033,6 @@
|
||||
"3": "Czułam się aktywna i energiczna/Czułem się aktywny i energiczny",
|
||||
"4": "Budziłam się z uczuciem świeżości i wypoczęta/Budziłem się z uczuciem świeżości i wypoczęty",
|
||||
"5": "Moje życie codzienne było wypełnione interesującymi mnie sprawami"
|
||||
},
|
||||
"sci": {
|
||||
"1": "… how long does it take you to fall asleep?",
|
||||
"2": "… if you then wake up during the night … how long are you awake for in total? (add all the wakenings up)",
|
||||
"3": "… how many nights a week do you have a problem with your sleep?",
|
||||
"4": "… how would you rate your sleep quality?",
|
||||
"5": "… affected your mood, energy, or relationships?",
|
||||
"6": "… affected your concentration, productivity, or ability to stay awake?",
|
||||
"7": "… troubled you in general?",
|
||||
"8": "… how long have you had a problem with your sleep?"
|
||||
}
|
||||
},
|
||||
"crisisResource": {
|
||||
@@ -11104,51 +11094,9 @@
|
||||
"4": "Prawie cały czas",
|
||||
"5": "Cały czas"
|
||||
},
|
||||
"sciOptions": {
|
||||
"duration": {
|
||||
"0": "≥ 61 min",
|
||||
"1": "46–60 min",
|
||||
"2": "31–45 min",
|
||||
"3": "16–30 min",
|
||||
"4": "0–15 min"
|
||||
},
|
||||
"nights": {
|
||||
"0": "5–7",
|
||||
"1": "4",
|
||||
"2": "3",
|
||||
"3": "2",
|
||||
"4": "0–1"
|
||||
},
|
||||
"quality": {
|
||||
"0": "Very poor",
|
||||
"1": "Poor",
|
||||
"2": "Average",
|
||||
"3": "Good",
|
||||
"4": "Very good"
|
||||
},
|
||||
"impact": {
|
||||
"0": "Very much",
|
||||
"1": "Much",
|
||||
"2": "Somewhat",
|
||||
"3": "A little",
|
||||
"4": "Not at all"
|
||||
},
|
||||
"problemDuration": {
|
||||
"0": "> 1 year",
|
||||
"1": "7–12 months",
|
||||
"2": "3–6 months",
|
||||
"3": "1–2 months",
|
||||
"4": "I don't have a problem / < 1 month"
|
||||
}
|
||||
},
|
||||
"stems": {
|
||||
"who5": {
|
||||
"period": "W ciągu ostatnich dwóch tygodni"
|
||||
},
|
||||
"sci": {
|
||||
"night": "Thinking about a typical night in the last month …",
|
||||
"impact": "Thinking about the past month, to what extent has poor sleep …",
|
||||
"finally": "Finally …"
|
||||
}
|
||||
},
|
||||
"openDetail": "Pokaż szczegóły",
|
||||
|
||||
@@ -10947,19 +10947,6 @@
|
||||
"who5": "Check de bem-estar, 5 perguntas",
|
||||
"sci": "Check de sono, 8 perguntas"
|
||||
},
|
||||
"options": {
|
||||
"0": "De forma alguma",
|
||||
"1": "Vários dias",
|
||||
"2": "Mais da metade dos dias",
|
||||
"3": "Quase todos os dias"
|
||||
},
|
||||
"functionalTitle": "Se você marcou algum problema, quão difícil isso tornou fazer seu trabalho, cuidar das coisas em casa ou se dar bem com outras pessoas?",
|
||||
"functional": {
|
||||
"0": "Não difícil de forma alguma",
|
||||
"1": "Um pouco difícil",
|
||||
"2": "Muito difícil",
|
||||
"3": "Extremamente difícil"
|
||||
},
|
||||
"functionalOptionalHint": "Opcional: você pode enviar sem responder esta pergunta.",
|
||||
"result": {
|
||||
"title": "Seu resultado",
|
||||
@@ -11006,45 +10993,6 @@
|
||||
"ifDanger": "Se você está em perigo imediato, ligue para {emergency} agora.",
|
||||
"resourcesTitle": "Pessoas que podem ajudar"
|
||||
},
|
||||
"items": {
|
||||
"phq9": {
|
||||
"1": "Pouco interesse ou prazer em fazer coisas",
|
||||
"2": "Sentir-se desanimado, deprimido ou sem esperança",
|
||||
"3": "Dificuldade para adormecer ou permanecer dormindo, ou dormir demais",
|
||||
"4": "Sentir-se cansado ou ter pouca energia",
|
||||
"5": "Pouco apetite ou comer demais",
|
||||
"6": "Sentir-se mal consigo mesmo — ou que é um fracasso ou decepcionou a si mesmo ou sua família",
|
||||
"7": "Dificuldade para se concentrar em coisas, como ler o jornal ou assistir televisão",
|
||||
"8": "Mover-se ou falar tão devagar que outras pessoas poderiam ter notado. Ou o oposto — estar tão agitado ou inquieto que se moveu muito mais que o usual",
|
||||
"9": "Pensamentos de que estaria melhor morto, ou de se machucar de alguma forma"
|
||||
},
|
||||
"gad7": {
|
||||
"1": "Sentir-se nervoso, ansioso ou tenso",
|
||||
"2": "Não conseguir parar ou controlar a preocupação",
|
||||
"3": "Preocupar-se demais com várias coisas",
|
||||
"4": "Dificuldade para relaxar",
|
||||
"5": "Estar tão inquieto que é difícil ficar parado",
|
||||
"6": "Ficar facilmente irritado ou aborrecido",
|
||||
"7": "Sentir medo como se algo terrível pudesse acontecer"
|
||||
},
|
||||
"who5": {
|
||||
"1": "Senti-me alegre e de bom humor",
|
||||
"2": "Senti-me calmo e relaxado",
|
||||
"3": "Senti-me ativo e vigoroso",
|
||||
"4": "Acordei sentindo-me fresco e descansado",
|
||||
"5": "Minha vida diária esteve cheia de coisas que me interessam"
|
||||
},
|
||||
"sci": {
|
||||
"1": "… quanto tempo você leva para adormecer?",
|
||||
"2": "… se você então acorda durante a noite … quanto tempo fica acordado no total? (some todos os despertares)",
|
||||
"3": "… quantas noites por semana você tem um problema com seu sono?",
|
||||
"4": "… como você avaliaria a qualidade do seu sono?",
|
||||
"5": "… afetou seu humor, energia ou relacionamentos?",
|
||||
"6": "… afetou sua concentração, produtividade ou capacidade de permanecer acordado?",
|
||||
"7": "… incomodou você em geral?",
|
||||
"8": "… há quanto tempo você tem um problema com seu sono?"
|
||||
}
|
||||
},
|
||||
"crisisResource": {
|
||||
"telefonSeelsorge": {
|
||||
"name": "TelefonSeelsorge (24/7, gratuito, confidencial)"
|
||||
@@ -11096,61 +11044,6 @@
|
||||
"sci": "No estudo por trás deste questionário, pontuações de 16 ou abaixo foram associadas a provável insônia. Se o sono ruim continuar incomodando, conversar com um profissional de saúde pode ajudar. Isto não é um diagnóstico."
|
||||
},
|
||||
"validatedInEnglishNote": "Este questionário é validado em inglês; as perguntas são exibidas na redação original em inglês.",
|
||||
"who5Options": {
|
||||
"0": "Em nenhum momento",
|
||||
"1": "Algum do tempo",
|
||||
"2": "Menos da metade do tempo",
|
||||
"3": "Mais da metade do tempo",
|
||||
"4": "A maior parte do tempo",
|
||||
"5": "Todo o tempo"
|
||||
},
|
||||
"sciOptions": {
|
||||
"duration": {
|
||||
"0": "≥ 61 min",
|
||||
"1": "46–60 min",
|
||||
"2": "31–45 min",
|
||||
"3": "16–30 min",
|
||||
"4": "0–15 min"
|
||||
},
|
||||
"nights": {
|
||||
"0": "5–7",
|
||||
"1": "4",
|
||||
"2": "3",
|
||||
"3": "2",
|
||||
"4": "0–1"
|
||||
},
|
||||
"quality": {
|
||||
"0": "Muito ruim",
|
||||
"1": "Ruim",
|
||||
"2": "Média",
|
||||
"3": "Boa",
|
||||
"4": "Muito boa"
|
||||
},
|
||||
"impact": {
|
||||
"0": "Muito",
|
||||
"1": "Muito",
|
||||
"2": "Um pouco",
|
||||
"3": "Um pouco",
|
||||
"4": "De forma alguma"
|
||||
},
|
||||
"problemDuration": {
|
||||
"0": "> 1 ano",
|
||||
"1": "7–12 meses",
|
||||
"2": "3–6 meses",
|
||||
"3": "1–2 meses",
|
||||
"4": "Não tenho um problema / < 1 mês"
|
||||
}
|
||||
},
|
||||
"stems": {
|
||||
"who5": {
|
||||
"period": "Nas últimas duas semanas …"
|
||||
},
|
||||
"sci": {
|
||||
"night": "Pensando em uma noite típica do último mês …",
|
||||
"impact": "Pensando no último mês, em que medida o sono ruim …",
|
||||
"finally": "Finalmente …"
|
||||
}
|
||||
},
|
||||
"openDetail": "Mostrar detalhes",
|
||||
"historyLoadError": "Não foi possível carregar seu histórico de avaliações",
|
||||
"historyLoadErrorHint": "Isso não significa que você não tem resultados; a lista não pôde ser buscada. Tente novamente.",
|
||||
|
||||
@@ -144,10 +144,8 @@ const LEGIT_IDENTICAL = new Set<string>([
|
||||
"settings.whoopClientId",
|
||||
"settings.whoopClientSecret",
|
||||
"settings.sections.api.title",
|
||||
// Mental-health screener + crisis-resource copy is owned and translated by
|
||||
// the mental-health workstream (PHQ-9/GAD-7 wording is locale-validated
|
||||
// separately); the US brand names (988 / Crisis Text Line) stay verbatim.
|
||||
// Listed here so this guard does not double-own those strings.
|
||||
// Crisis-resource names: services and US brand names (988 / Crisis Text
|
||||
// Line) that stay verbatim.
|
||||
"mentalHealth.crisisResource.crisisTextLine.name",
|
||||
"mentalHealth.crisisResource.euEmotionalSupport.name",
|
||||
"mentalHealth.crisisResource.findahelpline.name",
|
||||
@@ -155,55 +153,10 @@ const LEGIT_IDENTICAL = new Set<string>([
|
||||
"mentalHealth.crisisResource.lifeline988.name",
|
||||
"mentalHealth.crisisResource.nummerGegenKummer.name",
|
||||
"mentalHealth.crisisResource.telefonSeelsorge.name",
|
||||
"mentalHealth.items.gad7.1",
|
||||
"mentalHealth.items.gad7.2",
|
||||
"mentalHealth.items.gad7.3",
|
||||
"mentalHealth.items.gad7.4",
|
||||
"mentalHealth.items.gad7.5",
|
||||
"mentalHealth.items.gad7.6",
|
||||
"mentalHealth.items.gad7.7",
|
||||
"mentalHealth.items.phq9.1",
|
||||
"mentalHealth.items.phq9.2",
|
||||
"mentalHealth.items.phq9.3",
|
||||
"mentalHealth.items.phq9.4",
|
||||
"mentalHealth.items.phq9.5",
|
||||
"mentalHealth.items.phq9.6",
|
||||
"mentalHealth.items.phq9.7",
|
||||
"mentalHealth.items.phq9.8",
|
||||
"mentalHealth.items.phq9.9",
|
||||
// v1.27.9 — the Sleep Condition Indicator is validated in English only
|
||||
// (Espie 2014, CC BY-NC). Item text, section stems and response anchors
|
||||
// ship VERBATIM in the source language in every locale — a self-made
|
||||
// translation would invalidate the instrument — and the UI shows a
|
||||
// localized "validated in English" note instead. Identical-to-EN is the
|
||||
// intended state here, not a leak.
|
||||
"mentalHealth.items.sci.1",
|
||||
"mentalHealth.items.sci.2",
|
||||
"mentalHealth.items.sci.3",
|
||||
"mentalHealth.items.sci.4",
|
||||
"mentalHealth.items.sci.5",
|
||||
"mentalHealth.items.sci.6",
|
||||
"mentalHealth.items.sci.7",
|
||||
"mentalHealth.items.sci.8",
|
||||
"mentalHealth.stems.sci.night",
|
||||
"mentalHealth.stems.sci.impact",
|
||||
"mentalHealth.stems.sci.finally",
|
||||
"mentalHealth.sciOptions.duration.0",
|
||||
"mentalHealth.sciOptions.quality.4",
|
||||
"mentalHealth.sciOptions.quality.3",
|
||||
"mentalHealth.sciOptions.quality.2",
|
||||
"mentalHealth.sciOptions.quality.1",
|
||||
"mentalHealth.sciOptions.quality.0",
|
||||
"mentalHealth.sciOptions.impact.4",
|
||||
"mentalHealth.sciOptions.impact.3",
|
||||
"mentalHealth.sciOptions.impact.2",
|
||||
"mentalHealth.sciOptions.impact.1",
|
||||
"mentalHealth.sciOptions.impact.0",
|
||||
"mentalHealth.sciOptions.problemDuration.4",
|
||||
"mentalHealth.sciOptions.problemDuration.3",
|
||||
"mentalHealth.sciOptions.problemDuration.2",
|
||||
"mentalHealth.sciOptions.problemDuration.1",
|
||||
"mentalHealth.sciOptions.problemDuration.0",
|
||||
// Questionnaire items, answers and stems are absent from every bundle whose
|
||||
// language an instrument is not validated in (`instrument-wording.ts`), so
|
||||
// no entry is needed for them here: a locale either carries a validated
|
||||
// translation or nothing.
|
||||
// Proper nouns, a URL label and an acronym — the accepted written form in
|
||||
// Korean is the Latin original, so these stay identical to en.json.
|
||||
"auth.emailPlaceholder",
|
||||
|
||||
@@ -5,6 +5,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import en from "../../../../messages/en.json";
|
||||
import ptBR from "../../../../messages/pt-BR.json";
|
||||
|
||||
/**
|
||||
* The expandable per-item answer breakdown (the read side of
|
||||
@@ -45,7 +46,11 @@ function detailRow(overrides: Partial<AssessmentDetail>): AssessmentDetail {
|
||||
};
|
||||
}
|
||||
|
||||
function render(detail: AssessmentDetail): string {
|
||||
function render(
|
||||
detail: AssessmentDetail,
|
||||
locale: "en" | "pt-BR" = "en",
|
||||
messages?: Record<string, unknown>,
|
||||
): string {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
@@ -54,7 +59,7 @@ function render(detail: AssessmentDetail): string {
|
||||
});
|
||||
return renderToStaticMarkup(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<I18nProvider initialLocale="en">
|
||||
<I18nProvider initialLocale={locale} initialMessages={messages}>
|
||||
<AssessmentItemBreakdown
|
||||
assessmentId={detail.id}
|
||||
instrument={detail.instrument}
|
||||
@@ -115,4 +120,30 @@ describe("AssessmentItemBreakdown", () => {
|
||||
expect(html).toContain(mh.history.answersUnavailable);
|
||||
expect(html).not.toContain(mh.items.phq9["1"]);
|
||||
});
|
||||
|
||||
it("shows the English PHQ-9 items to a pt-BR reader, even from a bundle that carries a translation", () => {
|
||||
// A drifted pt-BR bundle with the self-made wording the release once
|
||||
// shipped. The PHQ-9 is not validated in Brazilian Portuguese, so none
|
||||
// of it may reach the page.
|
||||
const drifted = structuredClone(ptBR) as Record<string, unknown>;
|
||||
const mhDrifted = drifted.mentalHealth as Record<string, unknown>;
|
||||
mhDrifted.items = {
|
||||
phq9: {
|
||||
"9": "Pensamentos de que estaria melhor morto, ou de se machucar de alguma forma",
|
||||
},
|
||||
};
|
||||
mhDrifted.options = { "2": "Mais da metade dos dias" };
|
||||
mhDrifted.functionalTitle = "Se você assinalou algum problema";
|
||||
const html = render(
|
||||
detailRow({ functionalDifficulty: 1 }),
|
||||
"pt-BR",
|
||||
drifted,
|
||||
);
|
||||
expect(html).toContain(mh.items.phq9["9"]);
|
||||
expect(html).toContain(mh.options["2"]);
|
||||
expect(html).toContain(mh.functionalTitle);
|
||||
expect(html).not.toContain("Pensamentos de que estaria melhor morto");
|
||||
expect(html).not.toContain("Mais da metade dos dias");
|
||||
expect(html).not.toContain("Se você assinalou");
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import en from "../../../../messages/en.json";
|
||||
import nl from "../../../../messages/nl.json";
|
||||
|
||||
/**
|
||||
* Mental-wellbeing surface redesign (v1.25.3). The SSR-only convention
|
||||
@@ -498,3 +499,66 @@ describe("instrument detail (v1.27.9 — opened from a card)", () => {
|
||||
expect(html).toContain("Sleepio Limited");
|
||||
});
|
||||
});
|
||||
|
||||
describe("unvalidated languages see the English instrument wording", () => {
|
||||
function renderIn(
|
||||
locale: "nl",
|
||||
messages: Record<string, unknown>,
|
||||
node: React.ReactNode,
|
||||
): string {
|
||||
return renderToStaticMarkup(
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<I18nProvider initialLocale={locale} initialMessages={messages}>
|
||||
{node}
|
||||
</I18nProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
it("the PHQ-9 wizard in Dutch shows the English item and answers under the honest note", () => {
|
||||
// A bundle that still carries the self-made Dutch wording: the surface
|
||||
// must not read it.
|
||||
const drifted = structuredClone(nl) as Record<string, unknown>;
|
||||
const mhDrifted = drifted.mentalHealth as Record<string, unknown>;
|
||||
mhDrifted.items = {
|
||||
phq9: { "1": "Weinig interesse in of plezier aan activiteiten" },
|
||||
};
|
||||
mhDrifted.options = { "0": "Helemaal niet", "3": "Bijna elke dag" };
|
||||
const html = renderIn(
|
||||
"nl",
|
||||
drifted,
|
||||
<CheckInWizard
|
||||
instrument="PHQ9"
|
||||
onSubmit={() => {}}
|
||||
onBack={() => {}}
|
||||
isPending={false}
|
||||
isError={false}
|
||||
/>,
|
||||
);
|
||||
expect(html).toContain(mh.items.phq9["1"]);
|
||||
for (const v of ["0", "1", "2", "3"] as const) {
|
||||
expect(html).toContain(mh.options[v]);
|
||||
}
|
||||
expect(html).not.toContain("Weinig interesse");
|
||||
expect(html).not.toContain("Helemaal niet");
|
||||
expect(html).toContain('data-slot="check-in-validated-note"');
|
||||
expect(html).toContain(nl.mentalHealth.validatedInEnglishNote);
|
||||
});
|
||||
|
||||
it("the WHO-5 wizard in Dutch shows the English stem and anchors", () => {
|
||||
const html = renderIn(
|
||||
"nl",
|
||||
nl as Record<string, unknown>,
|
||||
<CheckInWizard
|
||||
instrument="WHO5"
|
||||
onSubmit={() => {}}
|
||||
onBack={() => {}}
|
||||
isPending={false}
|
||||
isError={false}
|
||||
/>,
|
||||
);
|
||||
expect(html).toContain(mh.stems.who5.period);
|
||||
expect(html).toContain(mh.items.who5["1"]);
|
||||
expect(html).toContain(mh.who5Options["5"]);
|
||||
});
|
||||
});
|
||||
@@ -7,8 +7,9 @@
|
||||
* list payload.
|
||||
*
|
||||
* Grammar: the detail surface's label/value rows — official item text muted
|
||||
* (validated instrument copy via the SAME `mentalHealth.items.*` keys the
|
||||
* check-in wizard renders; never re-worded here), the chosen answer label +
|
||||
* (validated instrument copy resolved through `instrumentText`, exactly as
|
||||
* the check-in wizard does: the validated translation, or the English source
|
||||
* for a language the instrument is not validated in; never re-worded here), the chosen answer label +
|
||||
* its value in foreground. The WHO-5 / SCI recall stems paint once above each
|
||||
* run of items, as on the source forms. PHQ-9 item 9 renders like every other
|
||||
* item — no alarm colour; the row's existing "support shown" marker is the
|
||||
@@ -25,6 +26,12 @@ import {
|
||||
optionLabelKey,
|
||||
stemKey,
|
||||
} from "@/lib/mental-health/instruments";
|
||||
import {
|
||||
FUNCTIONAL_TITLE_KEY,
|
||||
functionalOptionKey,
|
||||
instrumentText,
|
||||
itemKey,
|
||||
} from "@/lib/mental-health/instrument-wording";
|
||||
|
||||
import type { AssessmentDetail, InstrumentId } from "./types";
|
||||
import { QueryErrorRow } from "@/components/ui/query-error-row";
|
||||
@@ -36,8 +43,9 @@ export function AssessmentItemBreakdown({
|
||||
assessmentId: string;
|
||||
instrument: InstrumentId;
|
||||
}) {
|
||||
const { t } = useTranslations();
|
||||
const { t, locale } = useTranslations();
|
||||
const def = INSTRUMENTS[instrument];
|
||||
const wording = (k: string) => instrumentText(instrument, k, locale, t);
|
||||
|
||||
const { data, isPending, isError, error, refetch } = useQuery({
|
||||
queryKey: queryKeys.mentalHealthAssessmentDetail(assessmentId),
|
||||
@@ -104,15 +112,15 @@ export function AssessmentItemBreakdown({
|
||||
<div key={index} className="flex flex-col gap-1">
|
||||
{caption && (
|
||||
<p className="text-muted-foreground pt-1 text-xs">
|
||||
{t(`mentalHealth.stems.${caption}`)}
|
||||
{wording(`mentalHealth.stems.${caption}`)}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-baseline justify-between gap-3">
|
||||
<span className="text-muted-foreground min-w-0 flex-1">
|
||||
{t(`mentalHealth.items.${def.i18nKey}.${index + 1}`)}
|
||||
{wording(itemKey(instrument, index))}
|
||||
</span>
|
||||
<span className="text-foreground shrink-0 text-right">
|
||||
{t(optionLabelKey(instrument, index, value))}
|
||||
{wording(optionLabelKey(instrument, index, value))}
|
||||
{" · "}
|
||||
<span className="tabular-nums">{value}</span>
|
||||
</span>
|
||||
@@ -123,12 +131,12 @@ export function AssessmentItemBreakdown({
|
||||
{detail.functionalDifficulty !== null && (
|
||||
<div className="flex items-baseline justify-between gap-3">
|
||||
<span className="text-muted-foreground min-w-0 flex-1">
|
||||
{t("mentalHealth.functionalTitle")}
|
||||
{wording(FUNCTIONAL_TITLE_KEY)}
|
||||
</span>
|
||||
{/* Unscored follow-up: the label alone, no value — it never counts
|
||||
into the total and a number would imply it does. */}
|
||||
<span className="text-foreground shrink-0 text-right">
|
||||
{t(`mentalHealth.functional.${detail.functionalDifficulty}`)}
|
||||
{wording(functionalOptionKey(detail.functionalDifficulty))}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -36,6 +36,13 @@ import {
|
||||
optionLabelKey,
|
||||
stemKey,
|
||||
} from "@/lib/mental-health/instruments";
|
||||
import {
|
||||
FUNCTIONAL_TITLE_KEY,
|
||||
functionalOptionKey,
|
||||
hasFunctionalItem,
|
||||
instrumentText,
|
||||
itemKey,
|
||||
} from "@/lib/mental-health/instrument-wording";
|
||||
|
||||
import {
|
||||
isComplete,
|
||||
@@ -67,6 +74,9 @@ export function CheckInWizard({
|
||||
const { t, locale } = useTranslations();
|
||||
const def = INSTRUMENTS[instrument];
|
||||
const key = def.i18nKey;
|
||||
// Items, answers and stems: the validated translation, or the English
|
||||
// source for a language the instrument has not been validated in.
|
||||
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
|
||||
@@ -74,7 +84,7 @@ export function CheckInWizard({
|
||||
// (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 = instrument === "PHQ9";
|
||||
const hasFunctionalStep = hasFunctionalItem(instrument);
|
||||
const totalSteps = itemCount + (hasFunctionalStep ? 1 : 0);
|
||||
|
||||
const [step, setStep] = useState(1);
|
||||
@@ -88,8 +98,8 @@ export function CheckInWizard({
|
||||
const questionIndex = step - 1; // 0-based item index for the current question
|
||||
const complete = isComplete(items);
|
||||
const questionLabel = onFunctionalStep
|
||||
? t("mentalHealth.functionalTitle")
|
||||
: t(`mentalHealth.items.${key}.${step}`);
|
||||
? 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;
|
||||
@@ -170,7 +180,7 @@ export function CheckInWizard({
|
||||
</p>
|
||||
{stem && (
|
||||
<p className="text-sm" data-slot="check-in-stem">
|
||||
{t(`mentalHealth.stems.${stem}`)}
|
||||
{wording(`mentalHealth.stems.${stem}`)}
|
||||
</p>
|
||||
)}
|
||||
<h2
|
||||
@@ -206,8 +216,8 @@ export function CheckInWizard({
|
||||
disabled={isPending}
|
||||
>
|
||||
{onFunctionalStep
|
||||
? t(`mentalHealth.functional.${v}`)
|
||||
: t(optionLabelKey(instrument, questionIndex, v))}
|
||||
? wording(functionalOptionKey(v))
|
||||
: wording(optionLabelKey(instrument, questionIndex, v))}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { readFileSync, readdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { requireFloor } from "@/__tests__/helpers/source-files";
|
||||
import { englishOnlyWordingKeys } from "@/lib/mental-health/instrument-wording";
|
||||
|
||||
const ROOT = join(__dirname, "../../..");
|
||||
const MESSAGES_DIR = join(ROOT, "messages");
|
||||
@@ -216,13 +217,25 @@ describe("i18n locale file integrity", () => {
|
||||
enArr.forEach((k) => enKeys.add(k));
|
||||
otherArr.forEach((k) => otherKeys.add(k));
|
||||
|
||||
const onlyInEn = [...enKeys].filter((k) => !otherKeys.has(k)).sort();
|
||||
// The one sanctioned gap: questionnaire wording of an instrument whose
|
||||
// validated set excludes this language. Those keys must be ABSENT —
|
||||
// the surface shows the English source (`instrument-wording.ts`), so a
|
||||
// translation here would be a self-made questionnaire waiting for a
|
||||
// renderer to pick it up. The list is exact in both directions: a
|
||||
// missing key outside it is drift, a carried key inside it fails too.
|
||||
const englishOnly = englishOnlyWordingKeys(locale);
|
||||
const englishOnlySet = new Set(englishOnly);
|
||||
const onlyInEn = [...enKeys]
|
||||
.filter((k) => !otherKeys.has(k) && !englishOnlySet.has(k))
|
||||
.sort();
|
||||
const onlyInOther = [...otherKeys].filter((k) => !enKeys.has(k)).sort();
|
||||
const carriedEnglishOnly = englishOnly.filter((k) => otherKeys.has(k));
|
||||
|
||||
expect(
|
||||
{ onlyInEn, onlyInOther },
|
||||
`Locale files drifted apart — every key in en.json must exist in ${locale}.json and vice versa.`,
|
||||
).toEqual({ onlyInEn: [], onlyInOther: [] });
|
||||
{ onlyInEn, onlyInOther, carriedEnglishOnly },
|
||||
`Locale files drifted apart — every key in en.json must exist in ${locale}.json and vice versa, ` +
|
||||
`except validated questionnaire wording, which ${locale}.json must not carry for an instrument not validated in it.`,
|
||||
).toEqual({ onlyInEn: [], onlyInOther: [], carriedEnglishOnly: [] });
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { INSTRUMENTS, INSTRUMENT_ORDER } from "../instruments";
|
||||
import {
|
||||
SOURCE_WORDING,
|
||||
englishOnlyWordingKeys,
|
||||
instrumentText,
|
||||
instrumentWordingKeys,
|
||||
} from "../instrument-wording";
|
||||
|
||||
/**
|
||||
* Validated questionnaire wording is never freely translated. A language
|
||||
* outside an instrument's validated set sees the English source, and its
|
||||
* bundle carries none of that instrument's wording, so no renderer can pick a
|
||||
* self-made translation up by accident.
|
||||
*/
|
||||
|
||||
const ROOT = join(__dirname, "../../../..");
|
||||
const MESSAGES = join(ROOT, "messages");
|
||||
const LOCALES = readdirSync(MESSAGES)
|
||||
.filter((f) => f.endsWith(".json"))
|
||||
.map((f) => f.replace(/\.json$/, ""));
|
||||
|
||||
function flatten(obj: unknown, prefix = "", out = new Map<string, string>()) {
|
||||
if (obj && typeof obj === "object") {
|
||||
for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
|
||||
const key = prefix ? `${prefix}.${k}` : k;
|
||||
if (typeof v === "string") out.set(key, v);
|
||||
else flatten(v, key, out);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function bundle(locale: string): Map<string, string> {
|
||||
return flatten(
|
||||
JSON.parse(readFileSync(join(MESSAGES, `${locale}.json`), "utf8")),
|
||||
);
|
||||
}
|
||||
|
||||
describe("instrument wording", () => {
|
||||
it("holds the English source of every instrument's wording, word for word as en.json", () => {
|
||||
const en = bundle("en");
|
||||
const expected = new Set(INSTRUMENT_ORDER.flatMap(instrumentWordingKeys));
|
||||
// PHQ-9 9 + GAD-7 7 + WHO-5 5 + SCI 8 items alone are 29 keys.
|
||||
expect(expected.size).toBeGreaterThan(29);
|
||||
expect(Object.keys(SOURCE_WORDING).sort()).toEqual([...expected].sort());
|
||||
for (const [key, text] of Object.entries(SOURCE_WORDING)) {
|
||||
expect(en.get(key), key).toBe(text);
|
||||
}
|
||||
});
|
||||
|
||||
it("names PHQ-9 item 9 English-only for the languages its wording is not validated in", () => {
|
||||
for (const locale of ["nl", "pt-BR"]) {
|
||||
const keys = englishOnlyWordingKeys(locale);
|
||||
expect(keys).toContain("mentalHealth.items.phq9.9");
|
||||
expect(keys).toContain("mentalHealth.options.3");
|
||||
expect(keys).toContain("mentalHealth.functionalTitle");
|
||||
expect(keys).toContain("mentalHealth.who5Options.5");
|
||||
expect(keys).toContain("mentalHealth.stems.who5.period");
|
||||
expect(keys).toContain("mentalHealth.items.sci.1");
|
||||
}
|
||||
// German is validated for the PHQ family and the WHO-5, not the SCI.
|
||||
const de = englishOnlyWordingKeys("de");
|
||||
expect(de).not.toContain("mentalHealth.items.phq9.9");
|
||||
expect(de).toContain("mentalHealth.items.sci.1");
|
||||
expect(de).toContain("mentalHealth.sciOptions.duration.4");
|
||||
expect(englishOnlyWordingKeys("en")).toEqual([]);
|
||||
});
|
||||
|
||||
it.each(LOCALES.filter((l) => l !== "en"))(
|
||||
"messages/%s.json carries no wording of an instrument it is not validated for",
|
||||
(locale) => {
|
||||
const flat = bundle(locale);
|
||||
expect(flat.size).toBeGreaterThan(3000);
|
||||
const englishOnly = englishOnlyWordingKeys(locale);
|
||||
expect(englishOnly.length).toBeGreaterThan(0);
|
||||
expect(englishOnly.filter((key) => flat.has(key))).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
it("resolves the English source, not the bundle, outside the validated set", () => {
|
||||
const drifted = (key: string) => `TRANSLATED(${key})`;
|
||||
for (const locale of ["nl", "pt-BR"]) {
|
||||
expect(
|
||||
instrumentText("PHQ9", "mentalHealth.items.phq9.9", locale, drifted),
|
||||
).toBe(
|
||||
"Thoughts that you would be better off dead, or of hurting yourself in some way",
|
||||
);
|
||||
expect(
|
||||
instrumentText("GAD7", "mentalHealth.options.0", locale, drifted),
|
||||
).toBe("Not at all");
|
||||
}
|
||||
expect(
|
||||
instrumentText("SCI", "mentalHealth.items.sci.1", "fr", drifted),
|
||||
).toBe(SOURCE_WORDING["mentalHealth.items.sci.1"]);
|
||||
});
|
||||
|
||||
it("resolves the bundle's validated translation inside the validated set", () => {
|
||||
const t = (key: string) => `VALIDATED(${key})`;
|
||||
expect(instrumentText("PHQ9", "mentalHealth.items.phq9.9", "de", t)).toBe(
|
||||
"VALIDATED(mentalHealth.items.phq9.9)",
|
||||
);
|
||||
expect(instrumentText("SCI", "mentalHealth.items.sci.1", "en", t)).toBe(
|
||||
"VALIDATED(mentalHealth.items.sci.1)",
|
||||
);
|
||||
});
|
||||
|
||||
it("every validated locale's bundle carries a complete set for its instruments", () => {
|
||||
for (const locale of LOCALES) {
|
||||
const flat = bundle(locale);
|
||||
for (const id of INSTRUMENT_ORDER) {
|
||||
if (!INSTRUMENTS[id].validatedItemLocales.includes(locale)) continue;
|
||||
const missing = instrumentWordingKeys(id).filter((k) => !flat.has(k));
|
||||
expect(missing, `${locale} ${id}`).toEqual([]);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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.
|
||||
const SRC = join(ROOT, "src");
|
||||
const BARE = [
|
||||
/\bt\(\s*`mentalHealth\.items\./,
|
||||
/\bt\(\s*`mentalHealth\.stems\./,
|
||||
/\bt\(\s*`mentalHealth\.functional\./,
|
||||
/\bt\(\s*"mentalHealth\.functionalTitle"/,
|
||||
/\bt\(\s*optionLabelKey\(/,
|
||||
/\bt\(\s*itemKey\(/,
|
||||
];
|
||||
|
||||
function walk(dir: string, out: string[] = []): string[] {
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
if (entry.name === "generated" || entry.name === "__tests__") continue;
|
||||
const path = join(dir, entry.name);
|
||||
if (entry.isDirectory()) walk(path, out);
|
||||
else if (/\.tsx?$/.test(entry.name) && !/\.test\.tsx?$/.test(entry.name))
|
||||
out.push(path);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
it("finds the two item surfaces and no bare lookup anywhere", () => {
|
||||
const files = walk(SRC);
|
||||
expect(files.length).toBeGreaterThan(500);
|
||||
const users = files.filter((f) =>
|
||||
readFileSync(f, "utf8").includes("instrumentText("),
|
||||
);
|
||||
// The wizard and the history breakdown, plus the module itself.
|
||||
expect(users.length).toBeGreaterThanOrEqual(3);
|
||||
const offenders = files.filter((f) => {
|
||||
const src = readFileSync(f, "utf8");
|
||||
return BARE.some((re) => re.test(src));
|
||||
});
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* The English source wording of every validated questionnaire, and the one
|
||||
* function every surface resolves item text through.
|
||||
*
|
||||
* An app language outside an instrument's `validatedItemLocales` must show the
|
||||
* instrument as published in English: the items, the answer options, the
|
||||
* recall stems, and for the PHQ-9 its unscored follow-up question. Changing a
|
||||
* word in a validated questionnaire changes what it measures, so a freely made
|
||||
* translation is never an acceptable stand-in, even with a note beside it.
|
||||
*
|
||||
* The text is held here rather than looked up in `messages/en.json` because
|
||||
* the client does not hold the English bundle when it renders another
|
||||
* language (`load-locale.ts` fetches it only on a key miss), and the items
|
||||
* must not flash as raw keys or wait on a network round trip. A guard pins
|
||||
* this map to `messages/en.json` key for key, and pins every bundle of a
|
||||
* language outside an instrument's validated set to carry none of these keys:
|
||||
* `instrument-wording.test.ts` and the locale-parity check in
|
||||
* `i18n-locale-integrity.test.ts`.
|
||||
*/
|
||||
import {
|
||||
INSTRUMENTS,
|
||||
INSTRUMENT_ORDER,
|
||||
hasValidatedItems,
|
||||
optionLabelKey,
|
||||
stemKey,
|
||||
type InstrumentId,
|
||||
} from "./instruments";
|
||||
|
||||
/** English source text, keyed by the i18n key a validated bundle uses. */
|
||||
export const SOURCE_WORDING: Readonly<Record<string, string>> = {
|
||||
"mentalHealth.items.phq9.1": "Little interest or pleasure in doing things",
|
||||
"mentalHealth.items.phq9.2": "Feeling down, depressed, or hopeless",
|
||||
"mentalHealth.items.phq9.3":
|
||||
"Trouble falling or staying asleep, or sleeping too much",
|
||||
"mentalHealth.items.phq9.4": "Feeling tired or having little energy",
|
||||
"mentalHealth.items.phq9.5": "Poor appetite or overeating",
|
||||
"mentalHealth.items.phq9.6":
|
||||
"Feeling bad about yourself — or that you are a failure or have let yourself or your family down",
|
||||
"mentalHealth.items.phq9.7":
|
||||
"Trouble concentrating on things, such as reading the newspaper or watching television",
|
||||
"mentalHealth.items.phq9.8":
|
||||
"Moving or speaking so slowly that other people could have noticed. Or the opposite — being so fidgety or restless that you have been moving around a lot more than usual",
|
||||
"mentalHealth.items.phq9.9":
|
||||
"Thoughts that you would be better off dead, or of hurting yourself in some way",
|
||||
"mentalHealth.items.gad7.1": "Feeling nervous, anxious, or on edge",
|
||||
"mentalHealth.items.gad7.2": "Not being able to stop or control worrying",
|
||||
"mentalHealth.items.gad7.3": "Worrying too much about different things",
|
||||
"mentalHealth.items.gad7.4": "Trouble relaxing",
|
||||
"mentalHealth.items.gad7.5": "Being so restless that it is hard to sit still",
|
||||
"mentalHealth.items.gad7.6": "Becoming easily annoyed or irritable",
|
||||
"mentalHealth.items.gad7.7":
|
||||
"Feeling afraid as if something awful might happen",
|
||||
"mentalHealth.items.who5.1": "I have felt cheerful and in good spirits",
|
||||
"mentalHealth.items.who5.2": "I have felt calm and relaxed",
|
||||
"mentalHealth.items.who5.3": "I have felt active and vigorous",
|
||||
"mentalHealth.items.who5.4": "I woke up feeling fresh and rested",
|
||||
"mentalHealth.items.who5.5":
|
||||
"My daily life has been filled with things that interest me",
|
||||
"mentalHealth.items.sci.1": "… how long does it take you to fall asleep?",
|
||||
"mentalHealth.items.sci.2":
|
||||
"… if you then wake up during the night … how long are you awake for in total? (add all the wakenings up)",
|
||||
"mentalHealth.items.sci.3":
|
||||
"… how many nights a week do you have a problem with your sleep?",
|
||||
"mentalHealth.items.sci.4": "… how would you rate your sleep quality?",
|
||||
"mentalHealth.items.sci.5": "… affected your mood, energy, or relationships?",
|
||||
"mentalHealth.items.sci.6":
|
||||
"… affected your concentration, productivity, or ability to stay awake?",
|
||||
"mentalHealth.items.sci.7": "… troubled you in general?",
|
||||
"mentalHealth.items.sci.8":
|
||||
"… how long have you had a problem with your sleep?",
|
||||
"mentalHealth.options.0": "Not at all",
|
||||
"mentalHealth.options.1": "Several days",
|
||||
"mentalHealth.options.2": "More than half the days",
|
||||
"mentalHealth.options.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",
|
||||
"mentalHealth.functional.1": "Somewhat difficult",
|
||||
"mentalHealth.functional.2": "Very difficult",
|
||||
"mentalHealth.functional.3": "Extremely difficult",
|
||||
"mentalHealth.who5Options.0": "At no time",
|
||||
"mentalHealth.who5Options.1": "Some of the time",
|
||||
"mentalHealth.who5Options.2": "Less than half of the time",
|
||||
"mentalHealth.who5Options.3": "More than half of the time",
|
||||
"mentalHealth.who5Options.4": "Most of the time",
|
||||
"mentalHealth.who5Options.5": "All of the time",
|
||||
"mentalHealth.sciOptions.duration.0": "≥ 61 min",
|
||||
"mentalHealth.sciOptions.duration.1": "46–60 min",
|
||||
"mentalHealth.sciOptions.duration.2": "31–45 min",
|
||||
"mentalHealth.sciOptions.duration.3": "16–30 min",
|
||||
"mentalHealth.sciOptions.duration.4": "0–15 min",
|
||||
"mentalHealth.sciOptions.nights.0": "5–7",
|
||||
"mentalHealth.sciOptions.nights.1": "4",
|
||||
"mentalHealth.sciOptions.nights.2": "3",
|
||||
"mentalHealth.sciOptions.nights.3": "2",
|
||||
"mentalHealth.sciOptions.nights.4": "0–1",
|
||||
"mentalHealth.sciOptions.quality.0": "Very poor",
|
||||
"mentalHealth.sciOptions.quality.1": "Poor",
|
||||
"mentalHealth.sciOptions.quality.2": "Average",
|
||||
"mentalHealth.sciOptions.quality.3": "Good",
|
||||
"mentalHealth.sciOptions.quality.4": "Very good",
|
||||
"mentalHealth.sciOptions.impact.0": "Very much",
|
||||
"mentalHealth.sciOptions.impact.1": "Much",
|
||||
"mentalHealth.sciOptions.impact.2": "Somewhat",
|
||||
"mentalHealth.sciOptions.impact.3": "A little",
|
||||
"mentalHealth.sciOptions.impact.4": "Not at all",
|
||||
"mentalHealth.sciOptions.problemDuration.0": "> 1 year",
|
||||
"mentalHealth.sciOptions.problemDuration.1": "7–12 months",
|
||||
"mentalHealth.sciOptions.problemDuration.2": "3–6 months",
|
||||
"mentalHealth.sciOptions.problemDuration.3": "1–2 months",
|
||||
"mentalHealth.sciOptions.problemDuration.4":
|
||||
"I don't have a problem / < 1 month",
|
||||
"mentalHealth.stems.who5.period": "Over the last two weeks …",
|
||||
"mentalHealth.stems.sci.night":
|
||||
"Thinking about a typical night in the last month …",
|
||||
"mentalHealth.stems.sci.impact":
|
||||
"Thinking about the past month, to what extent has poor sleep …",
|
||||
"mentalHealth.stems.sci.finally": "Finally …",
|
||||
};
|
||||
|
||||
/** The PHQ-9's unscored follow-up question and its four answers. */
|
||||
export const FUNCTIONAL_TITLE_KEY = "mentalHealth.functionalTitle";
|
||||
export function functionalOptionKey(value: number): string {
|
||||
return `mentalHealth.functional.${value}`;
|
||||
}
|
||||
const FUNCTIONAL_VALUES = [0, 1, 2, 3] as const;
|
||||
|
||||
/** Instruments whose form carries the unscored functional follow-up. */
|
||||
export function hasFunctionalItem(id: InstrumentId): boolean {
|
||||
return id === "PHQ9";
|
||||
}
|
||||
|
||||
export function itemKey(id: InstrumentId, itemIndex: number): string {
|
||||
return `mentalHealth.items.${INSTRUMENTS[id].i18nKey}.${itemIndex + 1}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every i18n key that carries this instrument's own wording: items, answer
|
||||
* options, recall stems and the functional follow-up.
|
||||
*/
|
||||
export function instrumentWordingKeys(id: InstrumentId): string[] {
|
||||
const def = INSTRUMENTS[id];
|
||||
const keys = new Set<string>();
|
||||
for (let i = 0; i < def.itemCount; i++) {
|
||||
keys.add(itemKey(id, i));
|
||||
for (const value of def.optionOrder) {
|
||||
keys.add(optionLabelKey(id, i, value));
|
||||
}
|
||||
const stem = stemKey(id, i);
|
||||
if (stem) keys.add(`mentalHealth.stems.${stem}`);
|
||||
}
|
||||
if (hasFunctionalItem(id)) {
|
||||
keys.add(FUNCTIONAL_TITLE_KEY);
|
||||
for (const value of FUNCTIONAL_VALUES) keys.add(functionalOptionKey(value));
|
||||
}
|
||||
return [...keys];
|
||||
}
|
||||
|
||||
/**
|
||||
* The keys a bundle for `locale` must NOT carry: wording of an instrument
|
||||
* whose validated set excludes the language. A key shared by several
|
||||
* instruments (the PHQ-9 / GAD-7 answer options) is English-only only when
|
||||
* every instrument using it excludes the language.
|
||||
*/
|
||||
export function englishOnlyWordingKeys(locale: string): string[] {
|
||||
const allowed = new Set<string>();
|
||||
const all = new Set<string>();
|
||||
for (const id of INSTRUMENT_ORDER) {
|
||||
for (const key of instrumentWordingKeys(id)) {
|
||||
all.add(key);
|
||||
if (hasValidatedItems(id, locale)) allowed.add(key);
|
||||
}
|
||||
}
|
||||
return [...all].filter((key) => !allowed.has(key)).sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve one piece of instrument wording for the reader's language: the
|
||||
* bundle's validated translation where one exists, the English source
|
||||
* otherwise. Every surface that shows an item, an answer label or a stem goes
|
||||
* through here, never through a bare `t()`.
|
||||
*/
|
||||
export function instrumentText(
|
||||
id: InstrumentId,
|
||||
key: string,
|
||||
locale: string,
|
||||
t: (key: string) => string,
|
||||
): string {
|
||||
if (hasValidatedItems(id, locale)) return t(key);
|
||||
return SOURCE_WORDING[key] ?? key;
|
||||
}
|
||||
@@ -19,9 +19,11 @@
|
||||
* HIGHER = better sleep; "a score of ≤16 … seems reasonable to detect
|
||||
* possible insomnia disorder". Item text is validated in English; app
|
||||
* locales without a validated translation present the English items.
|
||||
* This module owns the canonical NUMERIC contract; the human wording lives in
|
||||
* `messages/*.json` under `mentalHealth.items.*` (validated translations per
|
||||
* locale where they exist — re-wording invalidates the score). See
|
||||
* This module owns the canonical NUMERIC contract. The human wording lives in
|
||||
* `messages/*.json` under `mentalHealth.items.*` for the languages in an
|
||||
* instrument's validated set, and in `instrument-wording.ts` as the English
|
||||
* source every other language is shown (re-wording invalidates the score).
|
||||
* Surfaces resolve it through `instrumentText`, never a bare `t()`. See
|
||||
* `.planning/research/screening-instruments-2026-07.md`.
|
||||
*
|
||||
* PHQ-9 item 9 (index 8) is the passive self-harm ideation screen and is the
|
||||
@@ -101,10 +103,11 @@ export interface InstrumentDefinition {
|
||||
|
||||
/**
|
||||
* 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: their
|
||||
* bundles carry a wording that has not yet been read against the published
|
||||
* Dutch and Brazilian Portuguese PHQ-9, GAD-7 and WHO-5 forms, so those
|
||||
* readers get the English items plus the honest note until it has.
|
||||
* 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.
|
||||
*/
|
||||
const ALL_APP_LOCALES = ["de", "en", "es", "fr", "it", "pl", "ko"] as const;
|
||||
|
||||
|
||||
Reference in new issue
Block a user