diff --git a/messages/de.json b/messages/de.json index d749c64b4..d60aaf06c 100644 --- a/messages/de.json +++ b/messages/de.json @@ -8497,23 +8497,23 @@ }, "mcp": { "label": "Externer Assistentenzugriff (MCP)", - "description": "Einem MCP-fähigen Assistenten Lesezugriff auf deine Gesundheitsdaten über ein begrenztes Token erlauben. Standardmäßig aus." + "description": "Lesezugriff für einen MCP-fähigen Assistenten, über ein begrenztes Token." }, "environment": { "label": "Umwelt", - "description": "Tägliches Wetter und Tageslicht für deinen Ort einbeziehen, um Zusammenhänge mit Stimmung, Schlaf und Vitalwerten zu erkennen. Standardmäßig aus." + "description": "Tägliches Wetter und Tageslicht für deinen Ort, neben Stimmung, Schlaf und Vitalwerten." }, "inboundDocuments": { "label": "Dokumente", - "description": "Bewahre jedes medizinische Schreiben, jeden Befund und Scan an einem verschlüsselten, durchsuchbaren Ort auf — verknüpft mit deinen Erkrankungen. Standardmäßig aus." + "description": "Arztbriefe, Befunde und Scans verschlüsselt und durchsuchbar an einem Ort." }, "mentalHealth": { "label": "Seelisches Wohlbefinden", - "description": "Freiwillige PHQ-9- und GAD-7-Selbsttests ergänzend zum Stimmungstagebuch. Standardmäßig aus; Antworten sind verschlüsselt und bleiben aus dem KI-Coach heraus." + "description": "Vier freiwillige Selbsttests neben dem Stimmungstagebuch." }, "nutrients": { "label": "Nährstoffzufuhr", - "description": "Speichert tägliche Mikronährstoff-Summen (Vitamine, Mineralstoffe, Wasser, Koffein) aus Apple Health. Standardmäßig aus." + "description": "Tägliche Mikronährstoff-Summen aus Apple Health." } }, "illness": { @@ -9010,7 +9010,8 @@ }, "openDetail": "Details anzeigen", "historyLoadError": "Dein Testverlauf konnte nicht geladen werden", - "historyLoadErrorHint": "Das heißt nicht, dass keine Ergebnisse vorliegen — die Liste konnte nicht abgerufen werden. Versuche es erneut." + "historyLoadErrorHint": "Das heißt nicht, dass keine Ergebnisse vorliegen — die Liste konnte nicht abgerufen werden. Versuche es erneut.", + "privacyNote": "Deine Antworten sind verschlüsselt gespeichert und erreichen den KI-Coach nicht." }, "records": { "aiInclusion": { diff --git a/messages/en.json b/messages/en.json index 3ab28641f..bcf9dbf47 100644 --- a/messages/en.json +++ b/messages/en.json @@ -8497,23 +8497,23 @@ }, "mcp": { "label": "External assistant access (MCP)", - "description": "Let an MCP-capable assistant read your health data over a scoped token. Off by default." + "description": "Read access for an MCP-capable assistant, over a scoped token." }, "environment": { "label": "Environment", - "description": "Pull in daily weather and daylight for your location to correlate with mood, sleep, and vitals. Off by default." + "description": "Daily weather and daylight for your location, beside mood, sleep, and vitals." }, "inboundDocuments": { "label": "Documents", - "description": "Keep every medical letter, report, and scan in one encrypted, searchable place — linked to your conditions. Off by default." + "description": "Medical letters, reports, and scans in one encrypted, searchable place." }, "mentalHealth": { "label": "Mental wellbeing", - "description": "Opt-in PHQ-9 and GAD-7 screeners beside mood tracking. Off by default; answers are encrypted and kept out of the AI Coach." + "description": "Four opt-in screeners beside mood tracking." }, "nutrients": { "label": "Nutrient intake", - "description": "Store daily micronutrient totals (vitamins, minerals, water, caffeine) synced from Apple Health. Off by default." + "description": "Daily micronutrient totals synced from Apple Health." } }, "illness": { @@ -9010,7 +9010,8 @@ }, "openDetail": "Show details", "historyLoadError": "Your assessment history could not be loaded", - "historyLoadErrorHint": "This does not mean you have no results — the list could not be fetched. Try again." + "historyLoadErrorHint": "This does not mean you have no results — the list could not be fetched. Try again.", + "privacyNote": "Your answers are stored encrypted and never reach the AI Coach." }, "records": { "aiInclusion": { diff --git a/messages/es.json b/messages/es.json index ff86aae20..54791c8d1 100644 --- a/messages/es.json +++ b/messages/es.json @@ -8497,23 +8497,23 @@ }, "mcp": { "label": "Acceso de asistente externo (MCP)", - "description": "Permite que un asistente compatible con MCP lea tus datos de salud mediante un token con alcance limitado. Desactivado de forma predeterminada." + "description": "Acceso de lectura para un asistente compatible con MCP, mediante un token limitado." }, "environment": { "label": "Entorno", - "description": "Incorpora el tiempo y las horas de luz diarias de tu ubicación para correlacionarlos con el ánimo, el sueño y las constantes. Desactivado por defecto." + "description": "El tiempo y las horas de luz diarias de tu ubicación, junto al ánimo, el sueño y las constantes." }, "inboundDocuments": { "label": "Documentos", - "description": "Guarda cada carta médica, informe y escaneo en un solo lugar cifrado y con búsqueda — vinculado a tus afecciones. Desactivado por defecto." + "description": "Cartas médicas, informes y escaneos en un lugar cifrado y con búsqueda." }, "mentalHealth": { "label": "Bienestar mental", - "description": "Cuestionarios PHQ-9 y GAD-7 opcionales, junto al registro de ánimo. Desactivado por defecto; las respuestas se cifran y se mantienen fuera del Coach de IA." + "description": "Cuatro cuestionarios opcionales, junto al registro de ánimo." }, "nutrients": { "label": "Ingesta de nutrientes", - "description": "Guarda los totales diarios de micronutrientes (vitaminas, minerales, agua, cafeína) sincronizados desde Apple Health. Desactivado por defecto." + "description": "Totales diarios de micronutrientes sincronizados desde Apple Health." } }, "illness": { @@ -9010,7 +9010,8 @@ }, "openDetail": "Ver detalles", "historyLoadError": "No se pudo cargar tu historial de evaluaciones", - "historyLoadErrorHint": "Esto no significa que no tengas resultados: no se pudo obtener la lista. Vuelve a intentarlo." + "historyLoadErrorHint": "Esto no significa que no tengas resultados: no se pudo obtener la lista. Vuelve a intentarlo.", + "privacyNote": "Tus respuestas se guardan cifradas y nunca llegan al Coach de IA." }, "records": { "aiInclusion": { diff --git a/messages/fr.json b/messages/fr.json index 0731663e4..5bebe5ce5 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -8497,23 +8497,23 @@ }, "mcp": { "label": "Accès assistant externe (MCP)", - "description": "Autorise un assistant compatible MCP à lire vos données de santé via un jeton à portée limitée. Désactivé par défaut." + "description": "Accès en lecture pour un assistant compatible MCP, via un jeton limité." }, "environment": { "label": "Environnement", - "description": "Intégrez la météo et la durée du jour quotidiennes de votre lieu pour les corréler à l'humeur, au sommeil et aux constantes. Désactivé par défaut." + "description": "Météo et durée du jour quotidiennes de votre lieu, à côté de l'humeur, du sommeil et des constantes." }, "inboundDocuments": { "label": "Documents", - "description": "Conservez chaque courrier médical, compte rendu et scan dans un espace chiffré et consultable — lié à vos affections. Désactivé par défaut." + "description": "Courriers médicaux, comptes rendus et scans dans un espace chiffré et consultable." }, "mentalHealth": { "label": "Bien-être mental", - "description": "Questionnaires PHQ-9 et GAD-7 facultatifs, en complément du suivi de l'humeur. Désactivé par défaut ; les réponses sont chiffrées et tenues à l'écart du Coach IA." + "description": "Quatre questionnaires facultatifs, à côté du suivi de l'humeur." }, "nutrients": { "label": "Apports en nutriments", - "description": "Enregistre les totaux quotidiens de micronutriments (vitamines, minéraux, eau, caféine) synchronisés depuis Apple Health. Désactivé par défaut." + "description": "Totaux quotidiens de micronutriments synchronisés depuis Apple Health." } }, "illness": { @@ -9010,7 +9010,8 @@ }, "openDetail": "Voir les détails", "historyLoadError": "Ton historique d'évaluations n'a pas pu être chargé", - "historyLoadErrorHint": "Cela ne veut pas dire que tu n'as aucun résultat : la liste n'a pas pu être récupérée. Réessaie." + "historyLoadErrorHint": "Cela ne veut pas dire que tu n'as aucun résultat : la liste n'a pas pu être récupérée. Réessaie.", + "privacyNote": "Vos réponses sont stockées chiffrées et n'atteignent jamais le Coach IA." }, "records": { "aiInclusion": { diff --git a/messages/it.json b/messages/it.json index e4528699f..a453e7c92 100644 --- a/messages/it.json +++ b/messages/it.json @@ -8497,23 +8497,23 @@ }, "mcp": { "label": "Accesso assistente esterno (MCP)", - "description": "Consente a un assistente compatibile con MCP di leggere i tuoi dati sanitari tramite un token con ambito limitato. Disattivato per impostazione predefinita." + "description": "Accesso in lettura per un assistente compatibile con MCP, tramite un token limitato." }, "environment": { "label": "Ambiente", - "description": "Integra il meteo e le ore di luce giornaliere della tua posizione per correlarli con umore, sonno e parametri vitali. Disattivato di default." + "description": "Meteo e ore di luce giornaliere della tua posizione, accanto a umore, sonno e parametri vitali." }, "inboundDocuments": { "label": "Documenti", - "description": "Conserva ogni lettera medica, referto e scansione in un unico archivio cifrato e ricercabile — collegato alle tue patologie. Disattivato di default." + "description": "Lettere mediche, referti e scansioni in un archivio cifrato e ricercabile." }, "mentalHealth": { "label": "Benessere mentale", - "description": "Questionari PHQ-9 e GAD-7 facoltativi, accanto al diario dell'umore. Disattivato per impostazione predefinita; le risposte sono cifrate e tenute fuori dal Coach IA." + "description": "Quattro questionari facoltativi, accanto al diario dell'umore." }, "nutrients": { "label": "Assunzione di nutrienti", - "description": "Salva i totali giornalieri di micronutrienti (vitamine, minerali, acqua, caffeina) sincronizzati da Apple Health. Disattivato per impostazione predefinita." + "description": "Totali giornalieri di micronutrienti sincronizzati da Apple Health." } }, "illness": { @@ -9010,7 +9010,8 @@ }, "openDetail": "Mostra dettagli", "historyLoadError": "Non è stato possibile caricare la cronologia dei test", - "historyLoadErrorHint": "Questo non significa che non ci siano risultati: non è stato possibile recuperare l'elenco. Riprova." + "historyLoadErrorHint": "Questo non significa che non ci siano risultati: non è stato possibile recuperare l'elenco. Riprova.", + "privacyNote": "Le tue risposte sono salvate cifrate e non raggiungono mai il Coach IA." }, "records": { "aiInclusion": { diff --git a/messages/pl.json b/messages/pl.json index 1e4bc806c..5027504c1 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -8497,23 +8497,23 @@ }, "mcp": { "label": "Dostęp asystenta zewnętrznego (MCP)", - "description": "Pozwól asystentowi obsługującemu MCP czytać Twoje dane zdrowotne za pomocą tokena o ograniczonym zakresie. Domyślnie wyłączone." + "description": "Dostęp do odczytu dla asystenta obsługującego MCP, przez token o ograniczonym zakresie." }, "environment": { "label": "Środowisko", - "description": "Pobieraj dzienną pogodę i długość dnia dla swojej lokalizacji, aby powiązać je z nastrojem, snem i parametrami życiowymi. Domyślnie wyłączone." + "description": "Dzienna pogoda i długość dnia dla Twojej lokalizacji, obok nastroju, snu i parametrów życiowych." }, "inboundDocuments": { "label": "Dokumenty", - "description": "Przechowuj każde pismo lekarskie, wynik i skan w jednym zaszyfrowanym, przeszukiwalnym miejscu — powiązane z Twoimi schorzeniami. Domyślnie wyłączone." + "description": "Pisma lekarskie, wyniki i skany w jednym zaszyfrowanym, przeszukiwalnym miejscu." }, "mentalHealth": { "label": "Dobrostan psychiczny", - "description": "Dobrowolne testy PHQ-9 i GAD-7 obok dziennika nastroju. Domyślnie wyłączone; odpowiedzi są szyfrowane i pozostają poza Coachem AI." + "description": "Cztery dobrowolne testy obok dziennika nastroju." }, "nutrients": { "label": "Spożycie składników odżywczych", - "description": "Zapisuje dzienne sumy mikroskładników (witaminy, minerały, woda, kofeina) synchronizowane z Apple Health. Domyślnie wyłączone." + "description": "Dzienne sumy mikroskładników synchronizowane z Apple Health." } }, "illness": { @@ -9010,7 +9010,8 @@ }, "openDetail": "Pokaż szczegóły", "historyLoadError": "Nie udało się wczytać historii testów", - "historyLoadErrorHint": "To nie znaczy, że nie masz wyników — nie udało się pobrać listy. Spróbuj ponownie." + "historyLoadErrorHint": "To nie znaczy, że nie masz wyników — nie udało się pobrać listy. Spróbuj ponownie.", + "privacyNote": "Twoje odpowiedzi są przechowywane w postaci zaszyfrowanej i nigdy nie trafiają do Coacha AI." }, "records": { "aiInclusion": { diff --git a/src/components/mental-health/mental-wellbeing.tsx b/src/components/mental-health/mental-wellbeing.tsx index b33f3902f..abbe508d0 100644 --- a/src/components/mental-health/mental-wellbeing.tsx +++ b/src/components/mental-health/mental-wellbeing.tsx @@ -142,6 +142,17 @@ export function MentalWellbeing() { description={t("mentalHealth.pageDescription")} /> + {/* What happens to the answers. The Modules hub used to carry this + as the second sentence of the module's one-line description, + which was the only place in the app that said it — so it moved + to the screen where somebody is about to answer. */} +

+ {t("mentalHealth.privacyNote")} +

+

{t("mentalHealth.choosePrompt")}

{historyError ? ( diff --git a/src/components/settings/__tests__/module-description-length.test.ts b/src/components/settings/__tests__/module-description-length.test.ts new file mode 100644 index 000000000..3634f2b3b --- /dev/null +++ b/src/components/settings/__tests__/module-description-length.test.ts @@ -0,0 +1,77 @@ +import { describe, it, expect } from "vitest"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +import { MODULE_KEYS } from "@/lib/modules/registry"; +import { locales } from "@/lib/i18n/config"; + +/** + * A module row in the "What you track" hub gets ONE sentence, same as a card + * header's description slot (design standards §3). + * + * Five of the eighteen rows had grown a second sentence — always the same + * one, "Off by default", and in one case a third clause after it. The + * neighbours sat at 30-75 characters and those five at 120-165, so the hub + * read as two different lists stacked on each other. The switch beside the + * row already says whether the module is on, so the sentence was restating + * what the control shows. + * + * The single-sentence half of the rule is checked in every locale: it is a + * structural claim, not a length one, and a translation that re-adds the + * clause is the same defect. The character cap is checked in EN and DE, the + * two the maintainer reads, matching `card-description-length.test.ts`. + */ + +const ROOT = process.cwd(); +const MAX_CHARS = 120; + +function bundle(locale: string): Record { + const raw = readFileSync(join(ROOT, "messages", `${locale}.json`), "utf8"); + return JSON.parse(raw).modules; +} + +describe("module hub descriptions", () => { + it("names a description for every module in every locale", () => { + // Positive control: without it, a renamed `modules` namespace would make + // the two checks below iterate an empty set and report green. + for (const locale of locales) { + const modules = bundle(locale); + const missing = MODULE_KEYS.filter( + (key) => !modules[key]?.description?.trim(), + ); + expect(missing, `${locale} is missing descriptions`).toEqual([]); + } + }); + + it("holds each one to a single sentence, in every locale", () => { + const offenders: string[] = []; + for (const locale of locales) { + const modules = bundle(locale); + for (const key of MODULE_KEYS) { + const text = modules[key].description!; + // Drop the closing terminator, then any sentence break left inside + // is a second sentence. A semicolon splice counts — it is how the + // longest of the five carried its third clause. + const body = text.replace(/[.!?…]\s*$/, ""); + if (/[.!?;]\s/.test(body)) { + offenders.push(`${locale}/${key}: ${text}`); + } + } + } + expect(offenders).toEqual([]); + }); + + it("holds each one under the character guideline in EN and DE", () => { + const offenders: string[] = []; + for (const locale of ["en", "de"] as const) { + const modules = bundle(locale); + for (const key of MODULE_KEYS) { + const text = modules[key].description!; + if (text.length > MAX_CHARS) { + offenders.push(`${locale}/${key}: ${text.length} chars`); + } + } + } + expect(offenders).toEqual([]); + }); +});