refactor(i18n): shorten the five descriptions the gate could not see

The gate the diet shipped with matched `<SettingsCardHeader` in three
component folders. That is not where every card description lives, so five
real ones were outside its reach — including the full backup's, which the
sweep's own shortlist had named and which stayed at 351 German characters
across three sentences while the guard reported zero violations.

Two ways in were missing. A card header can sit in a folder the walk never
entered (`labs`), and a description can be FORWARDED to the header by a shell
— `ExportCardShell`, `ImportCardShell` — where the prop is on the shell's tag,
not the header's. The gate now matches both, over seven roots, and the
"finds what it is meant to check" floor moved with it.

`SubPageShell` is deliberately not included, with the reason written at the
list: its description renders as `text-foreground text-sm`, the body tier,
where the standards allow prose. Gating it would enforce a rule §3 does not
make for that tier.

The five, all six locales:

- full backup — the twelve-item enumeration was the description. It is now
  the card's opening body paragraph, where a list of what is in the file
  belongs; the description is one sentence.
- local OCR — the how-it-works sentence stays in the slot; "less accurate,
  review every value" is safety copy and reads foreground in the body.
- CSV import, dose-history import — second sentence to the body.
- the ECG page — the "does not interpret the waveform" claim gets its own
  paragraph rather than riding the page explainer.

Proven by reverting the full backup's text: the widened gate reports it as
both two sentences and over length, at the call site, in both languages. The
old gate could not see the key at all.
This commit is contained in:
Marc-André Bombeck committed 2026-08-07 23:20:12 +02:00
1 parent ff61aec51f
commit 23eae2823e
12 files changed
+172 -73

No files matched your search

+16 -10
View File
@@ -2951,7 +2951,8 @@
"valuesBack": "Zurück zu Insights",
"valuesBackToMetric": "Zurück zu {metric}",
"scoresBack": "Zurück zu Insights",
"ecgDescription": "Einkanal-EKG-Aufzeichnungen deines Geräts, jeweils mit dem Ergebnis des Geräts. HealthLog zeigt die Kurve und die Aufzeichnung – die Kurve wird nicht interpretiert."
"ecgDescription": "Einkanal-EKG-Aufzeichnungen deines Geräts, jeweils mit dem Ergebnis des Geräts.",
"ecgDisclaimer": "HealthLog zeigt die Kurve und die Aufzeichnung – die Kurve wird nicht interpretiert."
},
"units": {
"steps": "Schritte",
@@ -4738,13 +4739,15 @@
},
"fullBackup": {
"title": "Voll-Backup",
"description": "Deine Messwerte, Medikamente, Stimmung, Zyklus, Laborwerte, Krankheitsepisoden, Allergien, Familienanamnese, Nährstoffe, Workouts, dein Gesundheitsprofil und deine eigenen Messgrößen in einer JSON-Datei. Gleiche Form wie die wöchentliche automatische Sicherung. Einige Satzarten sind noch nicht enthalten — das Manifest der Datei nennt, was drinsteht.",
"description": "Deine gesamte Akte in einer JSON-Datei.",
"scopeNote": "Dokumentmetadaten und die KI-Zusammenfassung, falls vorhanden, sind als Klartext enthalten; die Originaldateien der Dokumente sowie GPS-Routen und Einzelmesswerte von Workouts sind es nicht — lade diese separat aus dem Dokumententresor bzw. dem Workout-Export herunter.",
"encryptToggle": "Mit einer Passphrase verschlüsseln",
"passphrasePlaceholder": "Passphrase (mind. 12 Zeichen)",
"passphraseHelp": "Leitet den Archivschlüssel per Argon2id ab und versiegelt ihn mit AES-256-GCM. Speichere die Datei mit der Endung .hlx.",
"noRecoveryWarning": "Es gibt keine Wiederherstellung — geht diese Passphrase verloren, lässt sich das Archiv nicht mehr öffnen. Bewahre sie sicher auf.",
"stepUpNeeded": "Bestätige deinen zweiten Faktor und versuche es erneut."
"stepUpNeeded": "Bestätige deinen zweiten Faktor und versuche es erneut.",
"detail": "Gleiche Form wie die wöchentliche automatische Sicherung. Einige Satzarten sind noch nicht enthalten — das Manifest der Datei nennt, was drinsteht.",
"contents": "Deine Messwerte, Medikamente, Stimmung, Zyklus, Laborwerte, Krankheitsepisoden, Allergien, Familienanamnese, Nährstoffe, Workouts, dein Gesundheitsprofil und deine eigenen Messgrößen in einer JSON-Datei."
}
},
"import": {
@@ -4798,7 +4801,7 @@
},
"csv": {
"title": "CSV-Datei",
"description": "Messwerte aus einer Tabelle oder einem Messgerät-Export importieren. Eine Zeile pro Wert.",
"description": "Messwerte aus einer Tabelle oder einem Messgerät-Export importieren.",
"pasteLabel": "CSV einfügen",
"fileInputLabel": "CSV-Datei zum Importieren auswählen",
"schemaHint": "Spalten: type, value, unit, measuredAt (mit Zeitzonen-Offset) sowie optional glucoseContext, notes, externalId.",
@@ -4838,11 +4841,12 @@
"reasonExternalIdTooLong": "Externe ID ist zu lang",
"reasonUnstableExternalId": "Externe ID ist über Exporte hinweg nicht stabil",
"reasonDuplicate": "bereits importiert",
"reasonUnknown": "übersprungen"
"reasonUnknown": "übersprungen",
"detail": "Eine Zeile pro Wert."
},
"doseHistory": {
"title": "Einnahmeverlauf",
"description": "Ein Einnahmeverlauf, wie ihn ein anderer Medikamenten-Tracker exportiert, direkt gelesen. Eine Datei deckt alle Medikamente darin ab.",
"description": "Ein Einnahmeverlauf, wie ihn ein anderer Medikamenten-Tracker exportiert, direkt gelesen.",
"pasteLabel": "Export einfügen",
"fileInputLabel": "Datei mit Einnahmeverlauf wählen",
"uploadFile": "Datei wählen",
@@ -4892,7 +4896,8 @@
"jsonNotAnArray": "Dieses JSON enthält keine Liste von Dosen.",
"jsonNoIntakeTime": "Der JSON-Export hält fest, wann eine Dosis fällig war, nicht wann sie genommen wurde, und eine geplante Zeit wird nicht als Einnahmezeit geschrieben. Stattdessen die CSV exportieren, die beides trägt.",
"tooManyRows": "Diese Datei enthält mehr Zeilen, als ein Import aufnehmen kann. Teilen und in Teilen importieren."
}
},
"detail": "Eine Datei deckt alle Medikamente darin ab."
}
},
"subtitle": "Exportiere deine Daten oder importiere aus einer Sicherung."
@@ -5796,7 +5801,7 @@
"errorModuleDisabled": "Der Gesundheitsakten-Export ist für dieses Konto ausgeschaltet."
},
"globalExcludedInjectionSitesLabel": "Global ausgeschlossene Injektionsstellen",
"globalExcludedInjectionSitesHint": "Diese Stellen werden nie angeboten und beim Erfassen einer Dosis abgelehnt.",
"globalExcludedInjectionSitesHint": "Nie angeboten und beim Erfassen abgelehnt, auch wenn eine Medikation sie bevorzugt.",
"sharing": {
"createTitle": "Neuer Freigabe-Link",
"createDescription": "Der Link ist schreibgeschützt und zeitlich begrenzt.",
@@ -6089,7 +6094,7 @@
},
"export": {
"title": "Meine Daten herunterladen",
"description": "Deine Datensätze als CSV oder JSON, wahlweise mit Passphrase verschlüsselt.",
"description": "Deine Datensätze als CSV oder JSON — oder eine vollständige Akte für die Praxis.",
"openExport": "Export & Backup",
"openHealthRecord": "Gesundheitsakte"
},
@@ -7988,7 +7993,8 @@
"customize": "Laborwerte anpassen",
"localOcr": {
"heading": "Scans auf diesem Gerät lesen",
"description": "Ermöglicht das Scannen eines Laborbefunds, auch wenn dein KI-Anbieter keine Bilder lesen kann: Das Foto wird auf deinem Gerät gelesen und nur der erkannte Text an deinen Anbieter gesendet. Weniger genau – prüfe jeden Wert vor dem Speichern."
"description": "Liest das Foto auf deinem Gerät und sendet nur den erkannten Text an deinen Anbieter.",
"detail": "Nützlich, wenn dein KI-Anbieter keine Bilder lesen kann. Weniger genau — prüfe jeden Wert vor dem Speichern."
},
"sort": {
"heading": "Sortierung",
+16 -10
View File
@@ -2951,7 +2951,8 @@
"valuesBack": "Back to insights",
"valuesBackToMetric": "Back to {metric}",
"scoresBack": "Back to insights",
"ecgDescription": "Single-lead ECG recordings from your device, each with the device's own result. HealthLog shows the trace and the recording — it does not interpret the waveform."
"ecgDescription": "Single-lead ECG recordings from your device, each with the device's own result.",
"ecgDisclaimer": "HealthLog shows the trace and the recording — it does not interpret the waveform."
},
"units": {
"steps": "steps",
@@ -4738,13 +4739,15 @@
},
"fullBackup": {
"title": "Full backup",
"description": "Your measurements, medications, mood, cycle, labs, illness episodes, allergies, family history, nutrients, workouts, health profile and custom metrics in one JSON file. Same shape as the weekly auto-backup. Some record types are not carried yet — the file's manifest names what is in it.",
"description": "Your whole record in one JSON file.",
"scopeNote": "Document metadata and the AI-generated summary, if one exists, are included as plain text; original document files and workout GPS/sample data are not — download those separately from the vault or the workout export.",
"encryptToggle": "Encrypt with a passphrase",
"passphrasePlaceholder": "Passphrase (min. 12 characters)",
"passphraseHelp": "Derives the archive key with Argon2id and seals it with AES-256-GCM. Save the file with a .hlx extension.",
"noRecoveryWarning": "There is no recovery — if you lose this passphrase, the archive cannot be opened. Keep it somewhere safe.",
"stepUpNeeded": "Confirm your second factor, then try again."
"stepUpNeeded": "Confirm your second factor, then try again.",
"detail": "Same shape as the weekly auto-backup. Some record types are not carried yet — the file's manifest names what is in it.",
"contents": "Your measurements, medications, mood, cycle, labs, illness episodes, allergies, family history, nutrients, workouts, health profile and custom metrics in one JSON file."
}
},
"import": {
@@ -4798,7 +4801,7 @@
},
"csv": {
"title": "CSV file",
"description": "Import measurements from a spreadsheet or meter export. One row per reading.",
"description": "Import measurements from a spreadsheet or meter export.",
"pasteLabel": "Paste CSV",
"fileInputLabel": "Choose a CSV file to import",
"schemaHint": "Columns: type, value, unit, measuredAt (with timezone offset), and optional glucoseContext, notes, externalId.",
@@ -4838,11 +4841,12 @@
"reasonExternalIdTooLong": "external ID is too long",
"reasonUnstableExternalId": "external ID is not stable across exports",
"reasonDuplicate": "already imported",
"reasonUnknown": "skipped"
"reasonUnknown": "skipped",
"detail": "One row per reading."
},
"doseHistory": {
"title": "Medication dose history",
"description": "A dose history exported from another medication tracker, read as it comes. One file covers every medication in it.",
"description": "A dose history exported from another medication tracker, read as it comes.",
"pasteLabel": "Paste the export",
"fileInputLabel": "Choose a dose-history export file",
"uploadFile": "Choose file",
@@ -4892,7 +4896,8 @@
"jsonNotAnArray": "That JSON holds no list of doses.",
"jsonNoIntakeTime": "The JSON export records when a dose was due but not when it was taken, and a scheduled time will not be written as if it were the take. Export the CSV instead, which carries both.",
"tooManyRows": "That file holds more rows than one import can take. Split it and import the parts."
}
},
"detail": "One file covers every medication in it."
}
},
"subtitle": "Export your data or import from a backup."
@@ -5796,7 +5801,7 @@
"errorModuleDisabled": "The health record export is switched off for this account."
},
"globalExcludedInjectionSitesLabel": "Globally excluded injection sites",
"globalExcludedInjectionSitesHint": "These sites are never offered and are rejected when a dose is logged.",
"globalExcludedInjectionSitesHint": "Never offered and rejected when logging a dose, even if a medication prefers them.",
"sharing": {
"createTitle": "New share link",
"createDescription": "The link is read-only and time-boxed.",
@@ -6089,7 +6094,7 @@
},
"export": {
"title": "Download my data",
"description": "Your records as CSV or JSON, optionally passphrase-encrypted.",
"description": "Your records as CSV or JSON, or a complete health record for your doctor.",
"openExport": "Export & backup",
"openHealthRecord": "Health record"
},
@@ -7988,7 +7993,8 @@
"customize": "Customize labs",
"localOcr": {
"heading": "Read scans on this device",
"description": "Lets you scan a lab report even when your AI provider can't read images: the photo is read on your device and only the extracted text is sent to your provider. Less accurate — review every value before saving."
"description": "Reads the photo on your device and sends only the extracted text to your provider.",
"detail": "Useful when your AI provider cannot read images. Less accurate — review every value before saving."
},
"sort": {
"heading": "Sort order",
+16 -10
View File
@@ -2951,7 +2951,8 @@
"valuesBack": "Volver a Insights",
"valuesBackToMetric": "Volver a {metric}",
"scoresBack": "Volver a Insights",
"ecgDescription": "Registros de ECG de una derivación de tu dispositivo, cada uno con el resultado del propio dispositivo. HealthLog muestra el trazado y el registro, pero no interpreta la onda."
"ecgDescription": "Registros de ECG de una derivación de tu dispositivo, cada uno con el resultado del propio dispositivo.",
"ecgDisclaimer": "HealthLog muestra el trazado y el registro, pero no interpreta la onda."
},
"units": {
"steps": "pasos",
@@ -4738,13 +4739,15 @@
},
"fullBackup": {
"title": "Copia completa",
"description": "Tus mediciones, medicamentos, estado de ánimo, ciclo, analíticas, episodios de enfermedad, alergias, antecedentes familiares, nutrientes, entrenamientos, tu perfil de salud y tus métricas personalizadas en un archivo JSON. El mismo formato que la copia automática semanal. Algunos tipos de registro aún no se incluyen: el manifiesto del archivo indica qué contiene.",
"description": "Todo tu historial en un archivo JSON.",
"scopeNote": "Los metadatos de los documentos y el resumen de IA, si existe, se incluyen en texto plano; los archivos originales de los documentos y los datos de rutas GPS/muestras de los entrenamientos no están incluidos — descárgalos por separado desde el archivo de documentos o la exportación de entrenamientos.",
"encryptToggle": "Cifrar con una contraseña",
"passphrasePlaceholder": "Contraseña (mín. 12 caracteres)",
"passphraseHelp": "Deriva la clave del archivo con Argon2id y lo sella con AES-256-GCM. Guarda el archivo con la extensión .hlx.",
"noRecoveryWarning": "No hay recuperación: si pierdes esta contraseña, el archivo no se podrá abrir. Guárdala en un lugar seguro.",
"stepUpNeeded": "Confirma tu segundo factor y vuelve a intentarlo."
"stepUpNeeded": "Confirma tu segundo factor y vuelve a intentarlo.",
"detail": "El mismo formato que la copia automática semanal. Algunos tipos de registro aún no se incluyen: el manifiesto del archivo indica qué contiene.",
"contents": "Tus mediciones, medicamentos, estado de ánimo, ciclo, analíticas, episodios de enfermedad, alergias, antecedentes familiares, nutrientes, entrenamientos, tu perfil de salud y tus métricas personalizadas en un archivo JSON."
}
},
"import": {
@@ -4798,7 +4801,7 @@
},
"csv": {
"title": "Archivo CSV",
"description": "Importa mediciones desde una hoja de cálculo o la exportación de un medidor. Una fila por lectura.",
"description": "Importa mediciones desde una hoja de cálculo o la exportación de un medidor.",
"pasteLabel": "Pegar CSV",
"fileInputLabel": "Elegir un archivo CSV para importar",
"schemaHint": "Columnas: type, value, unit, measuredAt (con desfase de zona horaria) y opcionales glucoseContext, notes, externalId.",
@@ -4838,11 +4841,12 @@
"reasonExternalIdTooLong": "el ID externo es demasiado largo",
"reasonUnstableExternalId": "el ID externo no es estable entre exportaciones",
"reasonDuplicate": "ya importada",
"reasonUnknown": "omitida"
"reasonUnknown": "omitida",
"detail": "Una fila por lectura."
},
"doseHistory": {
"title": "Historial de dosis",
"description": "Un historial de dosis exportado desde otra aplicación de seguimiento, leído tal cual. Un archivo abarca todos los medicamentos que contiene.",
"description": "Un historial de dosis exportado desde otra aplicación de seguimiento, leído tal cual.",
"pasteLabel": "Pega la exportación",
"fileInputLabel": "Elegir un archivo de historial de dosis",
"uploadFile": "Elegir archivo",
@@ -4892,7 +4896,8 @@
"jsonNotAnArray": "Ese JSON no contiene ninguna lista de dosis.",
"jsonNoIntakeTime": "La exportación JSON registra cuándo tocaba una dosis pero no cuándo se tomó, y una hora prevista no se escribirá como si fuera la toma. Exporta el CSV, que lleva ambas.",
"tooManyRows": "Ese archivo tiene más filas de las que admite una importación. Divídelo e importa las partes."
}
},
"detail": "Un archivo abarca todos los medicamentos que contiene."
}
},
"subtitle": "Exporta tus datos o importa desde una copia de seguridad."
@@ -5796,7 +5801,7 @@
"errorModuleDisabled": "La exportación del expediente está desactivada para esta cuenta."
},
"globalExcludedInjectionSitesLabel": "Sitios de inyección excluidos globalmente",
"globalExcludedInjectionSitesHint": "Estos sitios nunca se ofrecen y se rechazan al registrar una dosis.",
"globalExcludedInjectionSitesHint": "Nunca se ofrecen y se rechazan al registrar una dosis, aunque un medicamento los prefiera.",
"sharing": {
"createTitle": "Nuevo enlace para compartir",
"createDescription": "El enlace es de solo lectura y limitado en el tiempo.",
@@ -6089,7 +6094,7 @@
},
"export": {
"title": "Descargar mis datos",
"description": "Tus registros en CSV o JSON, con cifrado por frase de contraseña opcional.",
"description": "Tus registros en CSV o JSON, o un historial completo para tu médico.",
"openExport": "Exportar y copia de seguridad",
"openHealthRecord": "Historial médico"
},
@@ -7988,7 +7993,8 @@
"customize": "Personalizar laboratorio",
"localOcr": {
"heading": "Leer escaneos en este dispositivo",
"description": "Permite escanear un informe de laboratorio aunque tu proveedor de IA no pueda leer imágenes: la foto se lee en tu dispositivo y solo se envía el texto extraído a tu proveedor. Menos preciso: revisa cada valor antes de guardar."
"description": "Lee la foto en tu dispositivo y envía solo el texto extraído a tu proveedor.",
"detail": "Útil cuando tu proveedor de IA no puede leer imágenes. Menos preciso: revisa cada valor antes de guardar."
},
"sort": {
"heading": "Orden",
+16 -10
View File
@@ -2951,7 +2951,8 @@
"valuesBack": "Retour aux Insights",
"valuesBackToMetric": "Retour à {metric}",
"scoresBack": "Retour aux Insights",
"ecgDescription": "Enregistrements ECG à dérivation unique de votre appareil, chacun accompagné du résultat de l'appareil. HealthLog affiche le tracé et l'enregistrement, mais n'interprète pas la courbe."
"ecgDescription": "Enregistrements ECG à dérivation unique de votre appareil, chacun accompagné du résultat de l'appareil.",
"ecgDisclaimer": "HealthLog affiche le tracé et l'enregistrement, mais n'interprète pas la courbe."
},
"units": {
"steps": "pas",
@@ -4738,13 +4739,15 @@
},
"fullBackup": {
"title": "Sauvegarde complète",
"description": "Vos mesures, médicaments, humeur, cycle, analyses, épisodes de maladie, allergies, antécédents familiaux, nutriments, séances, votre profil de santé et vos mesures personnalisées dans un seul fichier JSON. Même format que la sauvegarde automatique hebdomadaire. Certains types d'enregistrement ne sont pas encore inclus : le manifeste du fichier indique ce qu'il contient.",
"description": "L'ensemble de votre dossier dans un fichier JSON.",
"scopeNote": "Les métadonnées des documents et le résumé IA, s'il existe, sont inclus en texte clair ; les fichiers originaux des documents ainsi que les tracés GPS/données d'échantillons des entraînements ne le sont pas — téléchargez-les séparément depuis le coffre de documents ou l'export des entraînements.",
"encryptToggle": "Chiffrer avec une phrase secrète",
"passphrasePlaceholder": "Phrase secrète (12 caractères min.)",
"passphraseHelp": "Dérive la clé de l'archive avec Argon2id et la scelle avec AES-256-GCM. Enregistrez le fichier avec l'extension .hlx.",
"noRecoveryWarning": "Aucune récupération possible : si vous perdez cette phrase secrète, l'archive ne pourra pas être ouverte. Conservez-la en lieu sûr.",
"stepUpNeeded": "Confirmez votre second facteur, puis réessayez."
"stepUpNeeded": "Confirmez votre second facteur, puis réessayez.",
"detail": "Même format que la sauvegarde automatique hebdomadaire. Certains types d'enregistrement ne sont pas encore inclus : le manifeste du fichier indique ce qu'il contient.",
"contents": "Vos mesures, médicaments, humeur, cycle, analyses, épisodes de maladie, allergies, antécédents familiaux, nutriments, séances, votre profil de santé et vos mesures personnalisées dans un seul fichier JSON."
}
},
"import": {
@@ -4798,7 +4801,7 @@
},
"csv": {
"title": "Fichier CSV",
"description": "Importez des mesures depuis un tableur ou l'export d'un appareil. Une ligne par relevé.",
"description": "Importez des mesures depuis un tableur ou l'export d'un appareil.",
"pasteLabel": "Coller le CSV",
"fileInputLabel": "Choisir un fichier CSV à importer",
"schemaHint": "Colonnes : type, value, unit, measuredAt (avec décalage horaire) et, en option, glucoseContext, notes, externalId.",
@@ -4838,11 +4841,12 @@
"reasonExternalIdTooLong": "l'identifiant externe est trop long",
"reasonUnstableExternalId": "l'identifiant externe n'est pas stable d'un export à l'autre",
"reasonDuplicate": "déjà importée",
"reasonUnknown": "ignorée"
"reasonUnknown": "ignorée",
"detail": "Une ligne par relevé."
},
"doseHistory": {
"title": "Historique des prises",
"description": "Un historique de prises exporté depuis une autre application de suivi, lu tel quel. Un fichier couvre tous les médicaments qu'il contient.",
"description": "Un historique de prises exporté depuis une autre application de suivi, lu tel quel.",
"pasteLabel": "Coller l'export",
"fileInputLabel": "Choisir un fichier d'historique des prises",
"uploadFile": "Choisir un fichier",
@@ -4892,7 +4896,8 @@
"jsonNotAnArray": "Ce JSON ne contient aucune liste de prises.",
"jsonNoIntakeTime": "L'export JSON consigne le moment où une dose était due, pas celui où elle a été prise, et une heure prévue ne sera pas écrite comme si c'était la prise. Exportez le CSV, qui porte les deux.",
"tooManyRows": "Ce fichier contient plus de lignes qu'un import peut en prendre. Découpez-le et importez les parties."
}
},
"detail": "Un fichier couvre tous les médicaments qu'il contient."
}
},
"subtitle": "Exportez vos données ou importez depuis une sauvegarde."
@@ -5796,7 +5801,7 @@
"errorModuleDisabled": "L'export du dossier est désactivé pour ce compte."
},
"globalExcludedInjectionSitesLabel": "Sites d'injection exclus globalement",
"globalExcludedInjectionSitesHint": "Ces sites ne sont jamais proposés et sont refusés à l'enregistrement d'une dose.",
"globalExcludedInjectionSitesHint": "Jamais proposés et refusés à l'enregistrement, même si un médicament les privilégie.",
"sharing": {
"createTitle": "Nouveau lien de partage",
"createDescription": "Le lien est en lecture seule et limité dans le temps.",
@@ -6089,7 +6094,7 @@
},
"export": {
"title": "Télécharger mes données",
"description": "Vos enregistrements en CSV ou JSON, chiffrés par phrase secrète si vous voulez.",
"description": "Vos enregistrements en CSV ou JSON, ou un dossier complet pour votre médecin.",
"openExport": "Export et sauvegarde",
"openHealthRecord": "Dossier médical"
},
@@ -7988,7 +7993,8 @@
"customize": "Personnaliser les analyses",
"localOcr": {
"heading": "Lire les scans sur cet appareil",
"description": "Permet de scanner un compte rendu de laboratoire même si votre fournisseur d'IA ne peut pas lire les images : la photo est lue sur votre appareil et seul le texte extrait est envoyé à votre fournisseur. Moins précis — vérifiez chaque valeur avant d'enregistrer."
"description": "Lit la photo sur votre appareil et n'envoie que le texte extrait à votre fournisseur.",
"detail": "Utile quand votre fournisseur d'IA ne lit pas les images. Moins précis — vérifiez chaque valeur avant d'enregistrer."
},
"sort": {
"heading": "Tri",
+16 -10
View File
@@ -2951,7 +2951,8 @@
"valuesBack": "Torna a Insights",
"valuesBackToMetric": "Torna a {metric}",
"scoresBack": "Torna a Insights",
"ecgDescription": "Registrazioni ECG a singola derivazione dal tuo dispositivo, ciascuna con il risultato del dispositivo. HealthLog mostra il tracciato e la registrazione, ma non interpreta l'onda."
"ecgDescription": "Registrazioni ECG a singola derivazione dal tuo dispositivo, ciascuna con il risultato del dispositivo.",
"ecgDisclaimer": "HealthLog mostra il tracciato e la registrazione, ma non interpreta l'onda."
},
"units": {
"steps": "passi",
@@ -4738,13 +4739,15 @@
},
"fullBackup": {
"title": "Backup completo",
"description": "Le tue misurazioni, i farmaci, l'umore, il ciclo, le analisi, gli episodi di malattia, le allergie, l'anamnesi familiare, i nutrienti, gli allenamenti, il tuo profilo di salute e le tue metriche personalizzate in un unico file JSON. Stesso formato del backup automatico settimanale. Alcuni tipi di dato non sono ancora inclusi: il manifest del file indica cosa contiene.",
"description": "L'intera cartella in un unico file JSON.",
"scopeNote": "I metadati dei documenti e il riepilogo IA, se presente, sono inclusi in chiaro; i file originali dei documenti e i dati di percorso GPS/campionamento degli allenamenti non lo sono — scaricali separatamente dall'archivio documenti o dall'esportazione degli allenamenti.",
"encryptToggle": "Cifra con una passphrase",
"passphrasePlaceholder": "Passphrase (min. 12 caratteri)",
"passphraseHelp": "Deriva la chiave dell'archivio con Argon2id e lo sigilla con AES-256-GCM. Salva il file con estensione .hlx.",
"noRecoveryWarning": "Non c'è recupero: se perdi questa passphrase, l'archivio non potrà essere aperto. Conservala in un luogo sicuro.",
"stepUpNeeded": "Conferma il secondo fattore e riprova."
"stepUpNeeded": "Conferma il secondo fattore e riprova.",
"detail": "Stesso formato del backup automatico settimanale. Alcuni tipi di dato non sono ancora inclusi: il manifest del file indica cosa contiene.",
"contents": "Le tue misurazioni, i farmaci, l'umore, il ciclo, le analisi, gli episodi di malattia, le allergie, l'anamnesi familiare, i nutrienti, gli allenamenti, il tuo profilo di salute e le tue metriche personalizzate in un unico file JSON."
}
},
"import": {
@@ -4798,7 +4801,7 @@
},
"csv": {
"title": "File CSV",
"description": "Importa misurazioni da un foglio di calcolo o dall'esportazione di un dispositivo. Una riga per lettura.",
"description": "Importa misurazioni da un foglio di calcolo o dall'esportazione di un dispositivo.",
"pasteLabel": "Incolla CSV",
"fileInputLabel": "Scegli un file CSV da importare",
"schemaHint": "Colonne: type, value, unit, measuredAt (con offset del fuso orario) e, facoltative, glucoseContext, notes, externalId.",
@@ -4838,11 +4841,12 @@
"reasonExternalIdTooLong": "l'ID esterno è troppo lungo",
"reasonUnstableExternalId": "l'ID esterno non è stabile tra un export e l'altro",
"reasonDuplicate": "già importata",
"reasonUnknown": "saltata"
"reasonUnknown": "saltata",
"detail": "Una riga per lettura."
},
"doseHistory": {
"title": "Storico delle assunzioni",
"description": "Uno storico di assunzioni esportato da un'altra app di monitoraggio, letto così com'è. Un file copre tutti i farmaci che contiene.",
"description": "Uno storico di assunzioni esportato da un'altra app di monitoraggio, letto così com'è.",
"pasteLabel": "Incolla l'esportazione",
"fileInputLabel": "Scegli un file con lo storico delle assunzioni",
"uploadFile": "Scegli file",
@@ -4892,7 +4896,8 @@
"jsonNotAnArray": "Quel JSON non contiene alcun elenco di dosi.",
"jsonNoIntakeTime": "L'esportazione JSON registra quando una dose era prevista, non quando è stata assunta, e un orario previsto non verrà scritto come se fosse l'assunzione. Esporta il CSV, che porta entrambi.",
"tooManyRows": "Quel file contiene più righe di quante un'importazione possa accoglierne. Dividilo e importa le parti."
}
},
"detail": "Un file copre tutti i farmaci che contiene."
}
},
"subtitle": "Esporta i tuoi dati o importa da un backup."
@@ -5796,7 +5801,7 @@
"errorModuleDisabled": "L'esportazione della cartella è disattivata per questo account."
},
"globalExcludedInjectionSitesLabel": "Siti di iniezione esclusi globalmente",
"globalExcludedInjectionSitesHint": "Questi siti non vengono mai proposti e sono rifiutati quando registri una dose.",
"globalExcludedInjectionSitesHint": "Mai proposti e rifiutati alla registrazione, anche se un farmaco li preferisce.",
"sharing": {
"createTitle": "Nuovo link di condivisione",
"createDescription": "Il link è di sola lettura e a tempo.",
@@ -6089,7 +6094,7 @@
},
"export": {
"title": "Scarica i miei dati",
"description": "I tuoi record in CSV o JSON, se vuoi cifrati con una passphrase.",
"description": "I tuoi record in CSV o JSON, o una cartella completa per il medico.",
"openExport": "Esportazione e backup",
"openHealthRecord": "Fascicolo sanitario"
},
@@ -7988,7 +7993,8 @@
"customize": "Personalizza esami",
"localOcr": {
"heading": "Leggi le scansioni su questo dispositivo",
"description": "Consente di scansionare un referto di laboratorio anche se il tuo provider di IA non può leggere le immagini: la foto viene letta sul tuo dispositivo e al provider viene inviato solo il testo estratto. Meno preciso: controlla ogni valore prima di salvare."
"description": "Legge la foto sul tuo dispositivo e invia al provider solo il testo estratto.",
"detail": "Utile quando il tuo provider IA non legge le immagini. Meno preciso — controlla ogni valore prima di salvare."
},
"sort": {
"heading": "Ordinamento",
+16 -10
View File
@@ -2951,7 +2951,8 @@
"valuesBack": "Powrót do Insights",
"valuesBackToMetric": "Powrót do {metric}",
"scoresBack": "Powrót do Insights",
"ecgDescription": "Zapisy EKG jednoodprowadzeniowe z Twojego urządzenia, każdy z wynikiem samego urządzenia. HealthLog pokazuje przebieg i zapis — nie interpretuje krzywej."
"ecgDescription": "Zapisy EKG jednoodprowadzeniowe z Twojego urządzenia, każdy z wynikiem samego urządzenia.",
"ecgDisclaimer": "HealthLog pokazuje przebieg i zapis — nie interpretuje krzywej."
},
"units": {
"steps": "kroki",
@@ -4738,13 +4739,15 @@
},
"fullBackup": {
"title": "Pełna kopia zapasowa",
"description": "Twoje pomiary, leki, nastrój, cykl, wyniki badań, epizody chorobowe, alergie, wywiad rodzinny, składniki odżywcze, treningi, Twój profil zdrowia i własne wskaźniki w jednym pliku JSON. Ten sam kształt co cotygodniowa kopia automatyczna. Niektóre rodzaje wpisów nie są jeszcze przenoszone — manifest pliku wskazuje, co zawiera.",
"description": "Cała dokumentacja w jednym pliku JSON.",
"scopeNote": "Metadane dokumentów oraz podsumowanie AI, jeśli istnieje, są uwzględnione jako czysty tekst; oryginalne pliki dokumentów oraz dane tras GPS/próbek treningów nie są uwzględnione — pobierz je osobno z archiwum dokumentów lub eksportu treningów.",
"encryptToggle": "Zaszyfruj hasłem-frazą",
"passphrasePlaceholder": "Hasło-fraza (min. 12 znaków)",
"passphraseHelp": "Wyprowadza klucz archiwum za pomocą Argon2id i pieczętuje je AES-256-GCM. Zapisz plik z rozszerzeniem .hlx.",
"noRecoveryWarning": "Nie ma odzyskiwania — jeśli zgubisz tę frazę, archiwum nie da się otworzyć. Przechowuj ją bezpiecznie.",
"stepUpNeeded": "Potwierdź drugi składnik i spróbuj ponownie."
"stepUpNeeded": "Potwierdź drugi składnik i spróbuj ponownie.",
"detail": "Ten sam kształt co cotygodniowa kopia automatyczna. Niektóre rodzaje wpisów nie są jeszcze przenoszone — manifest pliku wskazuje, co zawiera.",
"contents": "Twoje pomiary, leki, nastrój, cykl, wyniki badań, epizody chorobowe, alergie, wywiad rodzinny, składniki odżywcze, treningi, Twój profil zdrowia i własne wskaźniki w jednym pliku JSON."
}
},
"import": {
@@ -4798,7 +4801,7 @@
},
"csv": {
"title": "Plik CSV",
"description": "Importuj pomiary z arkusza lub eksportu z urządzenia. Jeden wiersz na odczyt.",
"description": "Importuj pomiary z arkusza lub eksportu z urządzenia.",
"pasteLabel": "Wklej CSV",
"fileInputLabel": "Wybierz plik CSV do zaimportowania",
"schemaHint": "Kolumny: type, value, unit, measuredAt (z przesunięciem strefy czasowej) oraz opcjonalnie glucoseContext, notes, externalId.",
@@ -4838,11 +4841,12 @@
"reasonExternalIdTooLong": "identyfikator zewnętrzny jest zbyt długi",
"reasonUnstableExternalId": "identyfikator zewnętrzny nie jest stabilny między eksportami",
"reasonDuplicate": "już zaimportowano",
"reasonUnknown": "pominięto"
"reasonUnknown": "pominięto",
"detail": "Jeden wiersz na odczyt."
},
"doseHistory": {
"title": "Historia dawek",
"description": "Historia dawek wyeksportowana z innej aplikacji, czytana wprost. Jeden plik obejmuje wszystkie leki, które zawiera.",
"description": "Historia dawek wyeksportowana z innej aplikacji, czytana wprost.",
"pasteLabel": "Wklej eksport",
"fileInputLabel": "Wybierz plik z historią dawek",
"uploadFile": "Wybierz plik",
@@ -4892,7 +4896,8 @@
"jsonNotAnArray": "Ten JSON nie zawiera listy dawek.",
"jsonNoIntakeTime": "Eksport JSON zapisuje, kiedy dawka była zaplanowana, a nie kiedy została przyjęta, i zaplanowana godzina nie zostanie zapisana jako godzina przyjęcia. Wyeksportuj CSV, który zawiera obie.",
"tooManyRows": "Ten plik ma więcej wierszy, niż może przyjąć jeden import. Podziel go i zaimportuj części."
}
},
"detail": "Jeden plik obejmuje wszystkie leki, które zawiera."
}
},
"subtitle": "Wyeksportuj dane lub zaimportuj z kopii zapasowej."
@@ -5796,7 +5801,7 @@
"errorModuleDisabled": "Eksport dokumentacji jest wyłączony dla tego konta."
},
"globalExcludedInjectionSitesLabel": "Globalnie wykluczone miejsca iniekcji",
"globalExcludedInjectionSitesHint": "Te miejsca nigdy nie są proponowane i są odrzucane przy rejestrowaniu dawki.",
"globalExcludedInjectionSitesHint": "Nigdy nieproponowane i odrzucane przy rejestracji, nawet jeśli lek je preferuje.",
"sharing": {
"createTitle": "Nowy link udostępniania",
"createDescription": "Link jest tylko do odczytu i ograniczony czasowo.",
@@ -6089,7 +6094,7 @@
},
"export": {
"title": "Pobierz moje dane",
"description": "Twoje rekordy jako CSV lub JSON, opcjonalnie zaszyfrowane hasłem.",
"description": "Twoje rekordy jako CSV lub JSON albo pełna dokumentacja dla lekarza.",
"openExport": "Eksport i kopia zapasowa",
"openHealthRecord": "Dokumentacja medyczna"
},
@@ -7988,7 +7993,8 @@
"customize": "Dostosuj wyniki badań",
"localOcr": {
"heading": "Odczytuj skany na tym urządzeniu",
"description": "Pozwala zeskanować wynik laboratoryjny nawet wtedy, gdy Twój dostawca AI nie potrafi odczytywać obrazów: zdjęcie jest odczytywane na Twoim urządzeniu, a do dostawcy wysyłany jest tylko wyodrębniony tekst. Mniej dokładne — sprawdź każdą wartość przed zapisaniem."
"description": "Odczytuje zdjęcie na Twoim urządzeniu i wysyła do dostawcy tylko rozpoznany tekst.",
"detail": "Przydatne, gdy dostawca AI nie odczytuje obrazów. Mniej dokładne — sprawdź każdą wartość przed zapisem."
},
"sort": {
"heading": "Sortowanie",
+5
View File
@@ -76,6 +76,11 @@ export default function InsightsEcgPage() {
title={t("insights.ecg.sectionTitle")}
description={t("insights.subPage.ecgDescription")}
>
{/* What the app does and does not claim about a trace. Its own
paragraph, so the page explainer above stays one sentence. */}
<p className="text-muted-foreground text-sm">
{t("insights.subPage.ecgDisclaimer")}
</p>
<EcgSection enabled={isAuthenticated} hideHeading />
</SubPageShell>
);
+3
View File
@@ -144,6 +144,9 @@ export function LabsSettings() {
/>
}
/>
{/* "Less accurate — review every value" is safety copy, so it reads
foreground in the body rather than muted beside the switch. */}
<p className="text-sm">{t("labs.localOcr.detail")}</p>
</SettingsCard>
{/* Biomarker catalog — define / edit / delete. */}
@@ -7,10 +7,10 @@ import { join } from "node:path";
* (design standards §3).
*
* Scope is deliberately the strings that actually reach a card header's
* description slot — `<SettingsCardHeader description={t("…")}>` — because
* that is the slot the rule is about. A hint in a card body is prose and is
* allowed to be prose; `scripts/measure-description-lengths.mjs` sweeps those
* for review without gating them.
* description slot, whether the prop sits on the header or on a shell that
* forwards it. That is the slot the rule is about. A hint in a card BODY is
* prose and is allowed to be prose; `scripts/measure-description-lengths.mjs`
* sweeps those for review without gating them.
*
* Both EN and DE are checked. DE runs 15-25% longer than EN, and a rule that
* only holds in English produces a slot that wraps to two lines in half the
@@ -46,19 +46,57 @@ function walk(dir: string, out: string[] = []): string[] {
return out;
}
/** Every `description={t("…")}` inside a `<SettingsCardHeader …>` element. */
/**
* Elements whose `description` prop reaches `SettingsCardHeader`'s slot.
*
* The first cut of this guard matched `<SettingsCardHeader` only, and only
* under three component folders. That left five real card descriptions
* invisible to it — one of them the 351-character, three-sentence full-backup
* text that the audit's own shortlist had named. Two ways in were missing:
* a card header rendered from a folder the walk never entered, and a
* description FORWARDED to the header by a shell (`ExportCardShell`,
* `ImportCardShell`), where the prop is on the shell's tag rather than the
* header's.
*
* `SubPageShell` is deliberately NOT here. Its description renders as
* `<p className="text-foreground text-sm">` — the body tier, where the
* standards allow prose. Gating it would enforce a rule §3 does not make for
* that tier.
*/
const DESCRIPTION_HOSTS = [
"SettingsCardHeader",
"ExportCardShell",
"ImportCardShell",
];
/** Component roots that can host a card header, directly or via a shell. */
const ROOTS = [
join("src", "components", "settings"),
join("src", "components", "admin"),
join("src", "components", "measurement-reminders"),
join("src", "components", "labs"),
join("src", "components", "insights"),
join("src", "components", "medications"),
join("src", "components", "records"),
];
/** Every `description={t("…")}` on an element that owns the header's slot. */
function describedKeys(): Map<string, string[]> {
const keys = new Map<string, string[]>();
for (const dir of ["settings", "admin", "measurement-reminders"]) {
for (const file of walk(join(ROOT, "src", "components", dir))) {
for (const root of ROOTS) {
for (const file of walk(join(ROOT, root))) {
const source = readFileSync(file, "utf8");
const header = /<SettingsCardHeader\b/g;
const host = new RegExp(`<(?:${DESCRIPTION_HOSTS.join("|")})\\b`, "g");
let match: RegExpExecArray | null;
while ((match = header.exec(source))) {
while ((match = host.exec(source))) {
// The element ends at the first `/>` or `>` that closes the opening
// tag; a `description={t("x")}` after that belongs to something else.
const end = source.indexOf("/>", match.index);
const slice = source.slice(match.index, end === -1 ? undefined : end);
const selfClose = source.indexOf("/>", match.index);
const childrenOpen = source.indexOf("\n >", match.index);
const end = [selfClose, childrenOpen]
.filter((i) => i !== -1)
.sort((a, b) => a - b)[0];
const slice = source.slice(match.index, end ?? undefined);
const desc = /description=\{t\(\s*"([^"]+)"/.exec(slice);
if (!desc) continue;
const list = keys.get(desc[1]) ?? [];
@@ -96,7 +134,7 @@ describe("card header descriptions — one sentence, ≤120 characters", () => {
it("finds the description slots it is meant to check", () => {
// A matcher that quietly matches nothing passes every assertion below it.
expect(keys.size).toBeGreaterThan(25);
expect(keys.size).toBeGreaterThan(35);
});
for (const locale of ["en", "de"] as const) {
+10 -1
View File
@@ -484,7 +484,16 @@ function FullBackupCard() {
</Button>
}
>
{/* v1.28 backup-completeness — the description above lists what's
{/* What is in the file, and the shape it has. This used to be the
card's description — twelve enumerated record types in a muted
`text-xs` meta slot, which is neither the tier nor the length that
slot is for. */}
<p className="text-sm">
{t("settings.sections.export.cards.fullBackup.contents")}{" "}
{t("settings.sections.export.cards.fullBackup.detail")}
</p>
{/* v1.28 backup-completeness — the paragraph above lists what's
included; this line honestly discloses what isn't (document
binaries, workout GPS/sample series) rather than implying
"everything" is in the file. Always visible, not gated on the
@@ -113,6 +113,10 @@ export function CsvImportCard() {
title={t("settings.sections.export.import.csv.title")}
description={t("settings.sections.export.import.csv.description")}
>
<p className="text-sm">
{t("settings.sections.export.import.csv.detail")}
</p>
<div className="space-y-1.5">
<Label htmlFor={textareaId} className="text-xs">
{t("settings.sections.export.import.csv.pasteLabel")}
@@ -216,6 +216,10 @@ export function DoseHistoryImportCard() {
title={t("settings.sections.export.import.doseHistory.title")}
description={t("settings.sections.export.import.doseHistory.description")}
>
<p className="text-sm">
{t("settings.sections.export.import.doseHistory.detail")}
</p>
<div className="space-y-1.5">
<Label htmlFor={textareaId} className="text-xs">
{t("settings.sections.export.import.doseHistory.pasteLabel")}