diff --git a/e2e/sidebar-expand-no-scrollbar.spec.ts b/e2e/sidebar-expand-no-scrollbar.spec.ts
index 71b8d73e8..cb136e1df 100644
--- a/e2e/sidebar-expand-no-scrollbar.spec.ts
+++ b/e2e/sidebar-expand-no-scrollbar.spec.ts
@@ -133,7 +133,43 @@ test.describe("sidebar width transition", () => {
});
// With room to spare there is no edge: the default rail is unchanged.
+ // No native scrollbar in the rail (a classic one with arrow buttons
+ // read as a foreign body); a fade marks the edge that has more.
+ expect(
+ await nav.evaluate((el) => getComputedStyle(el).scrollbarWidth),
+ ).toBe("none");
+ expect(
+ await nav.evaluate(
+ (el) => (el as HTMLElement).offsetWidth - el.clientWidth,
+ ),
+ ).toBe(0);
+ await expect(nav).toHaveAttribute("data-fade-top", "true");
+ await expect(nav).not.toHaveAttribute("data-fade-bottom", "true");
+ await nav.evaluate((el) => {
+ el.scrollTop = 0;
+ });
+ await expect(nav).toHaveAttribute("data-fade-bottom", "true");
+ await expect(nav).not.toHaveAttribute("data-fade-top", "true");
+
await page.setViewportSize({ width: 1280, height: 1400 });
await expect(footer).not.toHaveAttribute("data-nav-overflows", "true");
+ await expect(nav).not.toHaveAttribute("data-fade-bottom", "true");
+ });
+
+ test("a 1366 × 768 window fits the rail without scrolling, collapsed and expanded", async ({
+ page,
+ }) => {
+ await page.setViewportSize({ width: 1366, height: 768 });
+ await page.goto("/", { waitUntil: "domcontentloaded" });
+ const nav = page.locator("aside nav").first();
+ const toggle = page.locator('[data-slot="sidebar-collapse-toggle"]');
+ await expect(nav).toBeVisible();
+ for (let i = 0; i < 2; i++) {
+ await expect
+ .poll(() => nav.evaluate((el) => el.scrollHeight - el.clientHeight))
+ .toBeLessThanOrEqual(0);
+ await toggle.click();
+ await page.waitForTimeout(300);
+ }
});
});
diff --git a/messages/de.json b/messages/de.json
index a65aa5e32..ee6731cd2 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -463,7 +463,6 @@
"signOut": "Abmelden und Einladung annehmen"
},
"createAccount": "Konto erstellen",
- "newAccount": "Neues Konto erstellen",
"backToPasskey": "Zurück zu Passkey",
"passkeySetupLater": "Passkeys kannst du jederzeit in den Einstellungen einrichten.",
"passwordMinLength": "Min. 12 Zeichen",
@@ -583,7 +582,6 @@
"quickAddLifeEvent": "Lebensereignis erfassen",
"customizeDashboard": "Dashboard anpassen",
"medicationIntakeQuickAdd": {
- "sheetTitle": "Medikamenteneinnahme erfassen",
"sheetDescription": "Dosis, Zeitpunkt und Medikament in wenigen Tipps protokollieren.",
"medicationLabel": "Medikament",
"medicationPlaceholder": "Medikament auswählen",
@@ -764,7 +762,7 @@
"valueWithUnit": "Wert ({unit})",
"timestamp": "Zeitpunkt",
"timestampBackdateHint": "Datum und Uhrzeit anpassen, um einen vergangenen Wert zu erfassen.",
- "notesPlaceholder": "z.B. nach dem Essen",
+ "notesPlaceholder": "z. B. nach dem Essen",
"formReset": "Formular zurücksetzen",
"saveError": "Fehler beim Speichern",
"duplicateTimestamp": "Zum gewählten Zeitpunkt existiert bereits ein Messwert dieses Typs.",
@@ -895,7 +893,7 @@
"factorNo": "Nein",
"factorRatingOption": "{label}: {value} von {max}",
"tags": "Tags",
- "tagsPlaceholder": "z.B. Sport, Arbeit, Familie",
+ "tagsPlaceholder": "z. B. Sport, Arbeit, Familie",
"tagsHelp": "Kommagetrennt eingeben",
"timestamp": "Zeitpunkt",
"date": "Datum",
@@ -2409,6 +2407,7 @@
"days7Label": "7T",
"days7Title": "Letzte 7 Tage",
"days30Label": "30T",
+ "daysNLabel": "{days}T",
"days30Title": "Letzte 30 Tage",
"days90Label": "90T",
"days90Title": "Letzte 90 Tage",
@@ -4743,6 +4742,12 @@
"settingsBody": "HealthLog funktioniert vollständig ohne KI. Verbinde unten einen Anbieter, wenn du ein tägliches Briefing, kurze Notizen zu deinen Werten und den Coach möchtest.",
"action": "KI einrichten",
"dismiss": "Nicht jetzt"
+ },
+ "bmiHeight": {
+ "coachPrefill": "Ich habe meine Körpergröße noch nicht eingetragen — warum ist der BMI wichtig, und was sollte ich wissen, bevor ich sie eintrage?"
+ },
+ "medicationsEmpty": {
+ "coachPrefill": "Ich habe noch keine Medikamente angelegt — was sollte ich wissen, bevor ich hier anfange, meine Therapietreue zu verfolgen?"
}
},
"thresholds": {
@@ -6554,7 +6559,7 @@
"createFailed": "Das Token konnte nicht erstellt werden. Bitte versuche es erneut."
},
"tokenMintMovedDescription": "Tokens werden dort ausgestellt, wo sie gelten: oben für Messwerte und Dokumente, oder im API-Endpoint eines Medikaments für genau dieses Medikament.",
- "tokenNamePlaceholder": "Token-Name (z.B. iPhone Shortcut)",
+ "tokenNamePlaceholder": "Token-Name (z. B. iPhone Shortcut)",
"tokenCreated": "Token erstellt — kopiere ihn jetzt! Er wird nicht erneut angezeigt.",
"tokenCopy": "Token kopieren",
"tokenCopied": "Token in die Zwischenablage kopiert",
@@ -8017,7 +8022,9 @@
"noneUnlockedYet": "Noch keine Erfolge freigeschaltet.",
"pointsValue": "{points} Punkte",
"metricCount": "{count}",
- "metricDays": "{count} Tage",
+ "metricDaysOne": "{count} Tag",
+ "metricDaysFew": "{count} Tage",
+ "metricDaysOther": "{count} Tage",
"metricPercent": "{count}%",
"badges": {
"intakeTotal1": {
@@ -8497,6 +8504,10 @@
"currentValue": "Aktuell: {value} {unit}",
"targetRangeValue": "Zielbereich: {min}–{max} {unit}",
"sourceLabel": "Quelle: {source}",
+ "sourceNames": {
+ "moodEntries": "Stimmungseinträge",
+ "sevenDays": "7 Tage"
+ },
"inTarget": "Im Zielbereich",
"aboveTarget": "{delta} {unit} über Zielbereich",
"belowTarget": "{delta} {unit} unter Zielbereich",
@@ -8588,7 +8599,7 @@
"noActiveMedications": "Keine aktiven Medikamente gefunden.",
"whichMedication": "Welches Medikament eingenommen?",
"cancelButton": "Abbrechen",
- "askMedicationName": "Bitte Medikament angeben, z.B. genommen Ramipril.",
+ "askMedicationName": "Bitte Medikament angeben, z. B. genommen Ramipril.",
"medicationNotFoundExact": "Medikament nicht gefunden. Bitte Namen exakt wie in HealthLog senden.",
"helpHeader": "Verfügbare Befehle:",
"helpBody": "/help — Diese Hilfe anzeigen\n/add — Einnahme erfassen\n/start — Bot starten\n\nTextbefehle:\ngenommen <Name> — Einnahme bestätigen\n\nÜber die Buttons in Erinnerungen:\n• Genommen — Einnahme bestätigen\n• 🕐 1h / 🕐 3h — Erinnerung verschieben\n• ⏭ Überspringen — Einnahme überspringen\n• ✓ Bestätigen — Verpasste Einnahme bestätigen",
@@ -9931,7 +9942,7 @@
"loadError": "Diese Episode konnte nicht geladen werden.",
"backToList": "Zurück zum Journal",
"detail": {
- "note": "Note"
+ "note": "Notiz"
},
"edit": {
"title": "Episode bearbeiten",
@@ -10747,7 +10758,7 @@
},
"filter": {
"searchLabel": "Dokumente durchsuchen",
- "searchPlaceholder": "Titel oder Dateiname suchen…",
+ "searchPlaceholder": "Titel, Dateiname",
"typeGroup": "Dokumenttyp",
"conditionGroup": "Erkrankung",
"procedureGroup": "Eingriffe und Operationen",
@@ -12036,8 +12047,7 @@
"title": "Praxen und Ärzte",
"description": "Dein eigenes Adressbuch — einmal tippen, überall auswählen.",
"linkLabel": "Adressbuch",
- "add": "Eintrag",
- "searchPlaceholder": "Name, Praxis oder Fachrichtung suchen",
+ "searchPlaceholder": "Name, Praxis oder Fach suchen",
"loadError": "Adressbuch konnte nicht geladen werden.",
"emptyTitle": "Noch kein Eintrag",
"emptyDescription": "Lege eine Praxis an, damit du sie beim Termin auswählen kannst.",
@@ -12133,7 +12143,6 @@
"unreadableLabel": "Unlesbares Symptom",
"intensityPill": "{value}/10",
"entry": {
- "sheetTitle": "Symptom erfassen",
"sheetDescription": "Eines deiner eigenen Symptome, wie stark und wann.",
"symptom": "Symptom",
"symptomRequired": "Wähle ein Symptom.",
diff --git a/messages/en.json b/messages/en.json
index 941d5f2b7..06a2e9b36 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -463,7 +463,6 @@
"signOut": "Sign out and accept the invitation"
},
"createAccount": "Create account",
- "newAccount": "Create new account",
"backToPasskey": "Back to Passkey",
"passkeySetupLater": "You can set up passkeys at any time in Settings.",
"passwordMinLength": "Min. 12 characters",
@@ -583,7 +582,6 @@
"quickAddLifeEvent": "Log life event",
"customizeDashboard": "Customize dashboard",
"medicationIntakeQuickAdd": {
- "sheetTitle": "Log medication intake",
"sheetDescription": "Record dose, time, and medication in a few taps.",
"medicationLabel": "Medication",
"medicationPlaceholder": "Select a medication",
@@ -2409,6 +2407,7 @@
"days7Label": "7d",
"days7Title": "Last 7 days",
"days30Label": "30d",
+ "daysNLabel": "{days}d",
"days30Title": "Last 30 days",
"days90Label": "90d",
"days90Title": "Last 90 days",
@@ -4743,6 +4742,12 @@
"settingsBody": "HealthLog works fully without AI. Connect a provider below if you want a daily briefing, short notes on your readings and the Coach.",
"action": "Set up AI",
"dismiss": "Not now"
+ },
+ "bmiHeight": {
+ "coachPrefill": "I haven’t set my height yet — why does BMI matter, and what should I know before I configure it?"
+ },
+ "medicationsEmpty": {
+ "coachPrefill": "I haven’t added any medications yet — what should I know before I start tracking medication compliance here?"
}
},
"thresholds": {
@@ -8017,7 +8022,9 @@
"noneUnlockedYet": "No achievements unlocked yet.",
"pointsValue": "{points} points",
"metricCount": "{count}",
- "metricDays": "{count} days",
+ "metricDaysOne": "{count} day",
+ "metricDaysFew": "{count} days",
+ "metricDaysOther": "{count} days",
"metricPercent": "{count}%",
"badges": {
"intakeTotal1": {
@@ -8497,6 +8504,10 @@
"currentValue": "Current: {value} {unit}",
"targetRangeValue": "Target: {min}–{max} {unit}",
"sourceLabel": "Source: {source}",
+ "sourceNames": {
+ "moodEntries": "Mood entries",
+ "sevenDays": "7 days"
+ },
"inTarget": "Within target range",
"aboveTarget": "{delta} {unit} above target",
"belowTarget": "{delta} {unit} below target",
@@ -12036,7 +12047,6 @@
"title": "Practices and doctors",
"description": "Your own address book — type a practice once, pick it everywhere.",
"linkLabel": "Address book",
- "add": "Entry",
"searchPlaceholder": "Search name, practice or specialty",
"loadError": "The address book could not be loaded.",
"emptyTitle": "No entry yet",
@@ -12133,7 +12143,6 @@
"unreadableLabel": "Unreadable symptom",
"intensityPill": "{value}/10",
"entry": {
- "sheetTitle": "Log symptom",
"sheetDescription": "One of your own symptoms, how strong and when.",
"symptom": "Symptom",
"symptomRequired": "Choose a symptom.",
diff --git a/messages/es.json b/messages/es.json
index 1178466e9..ddf55c374 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -463,7 +463,6 @@
"signOut": "Cerrar sesión y aceptar la invitación"
},
"createAccount": "Crear cuenta",
- "newAccount": "Crear cuenta nueva",
"backToPasskey": "Volver a passkey",
"passkeySetupLater": "Puedes configurar passkeys más tarde en los ajustes.",
"passwordMinLength": "Mín. 12 caracteres",
@@ -583,7 +582,6 @@
"quickAddLifeEvent": "Registrar acontecimiento vital",
"customizeDashboard": "Personalizar panel",
"medicationIntakeQuickAdd": {
- "sheetTitle": "Registrar toma de medicación",
"sheetDescription": "Registra dosis, hora y medicamento en unos pocos toques.",
"medicationLabel": "Medicamento",
"medicationPlaceholder": "Selecciona un medicamento",
@@ -2409,6 +2407,7 @@
"days7Label": "7 d",
"days7Title": "Últimos 7 días",
"days30Label": "30 d",
+ "daysNLabel": "{days} d",
"days30Title": "Últimos 30 días",
"days90Label": "90 d",
"days90Title": "Últimos 90 días",
@@ -4743,6 +4742,12 @@
"settingsBody": "HealthLog funciona por completo sin IA. Conecta un proveedor abajo si quieres un resumen diario, notas breves sobre tus mediciones y el Coach.",
"action": "Configurar IA",
"dismiss": "Ahora no"
+ },
+ "bmiHeight": {
+ "coachPrefill": "Todavía no he indicado mi altura: ¿por qué importa el IMC y qué debería saber antes de configurarla?"
+ },
+ "medicationsEmpty": {
+ "coachPrefill": "Todavía no he añadido medicamentos: ¿qué debería saber antes de empezar a seguir aquí la adherencia?"
}
},
"thresholds": {
@@ -8017,7 +8022,9 @@
"noneUnlockedYet": "Aún no has desbloqueado ningún logro.",
"pointsValue": "{points} puntos",
"metricCount": "{count}",
- "metricDays": "{count} días",
+ "metricDaysOne": "{count} día",
+ "metricDaysFew": "{count} días",
+ "metricDaysOther": "{count} días",
"metricPercent": "{count}%",
"badges": {
"intakeTotal1": {
@@ -8497,6 +8504,10 @@
"currentValue": "Actual: {value} {unit}",
"targetRangeValue": "Rango objetivo: {min}–{max} {unit}",
"sourceLabel": "Fuente: {source}",
+ "sourceNames": {
+ "moodEntries": "Entradas de ánimo",
+ "sevenDays": "7 días"
+ },
"inTarget": "En rango objetivo",
"aboveTarget": "{delta} {unit} por encima del rango",
"belowTarget": "{delta} {unit} por debajo del rango",
@@ -9931,7 +9942,7 @@
"loadError": "No se pudo cargar este episodio.",
"backToList": "Volver al diario",
"detail": {
- "note": "Note"
+ "note": "Nota"
},
"edit": {
"title": "Editar episodio",
@@ -12036,7 +12047,6 @@
"title": "Consultas y médicos",
"description": "Tu propia agenda: escribe una consulta una vez y elígela en todas partes.",
"linkLabel": "Agenda",
- "add": "Entrada",
"searchPlaceholder": "Buscar nombre, consulta o especialidad",
"loadError": "No se pudo cargar la agenda.",
"emptyTitle": "Todavía no hay entradas",
@@ -12133,7 +12143,6 @@
"unreadableLabel": "Síntoma ilegible",
"intensityPill": "{value}/10",
"entry": {
- "sheetTitle": "Registrar síntoma",
"sheetDescription": "Uno de tus propios síntomas, su intensidad y cuándo.",
"symptom": "Síntoma",
"symptomRequired": "Elige un síntoma.",
diff --git a/messages/fr.json b/messages/fr.json
index ee8fa291a..c1ff33246 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -463,7 +463,6 @@
"signOut": "Se déconnecter et accepter l'invitation"
},
"createAccount": "Créer un compte",
- "newAccount": "Créer un nouveau compte",
"backToPasskey": "Revenir à la passkey",
"passkeySetupLater": "Tu peux configurer les passkeys plus tard dans les paramètres.",
"passwordMinLength": "Min. 12 caractères",
@@ -583,7 +582,6 @@
"quickAddLifeEvent": "Enregistrer un événement de vie",
"customizeDashboard": "Personnaliser le tableau de bord",
"medicationIntakeQuickAdd": {
- "sheetTitle": "Saisir une prise de médicament",
"sheetDescription": "Enregistre la dose, l’heure et le médicament en quelques touches.",
"medicationLabel": "Médicament",
"medicationPlaceholder": "Sélectionner un médicament",
@@ -2409,6 +2407,7 @@
"days7Label": "7 j",
"days7Title": "7 derniers jours",
"days30Label": "30 j",
+ "daysNLabel": "{days} j",
"days30Title": "30 derniers jours",
"days90Label": "90 j",
"days90Title": "90 derniers jours",
@@ -4743,6 +4742,12 @@
"settingsBody": "HealthLog fonctionne entièrement sans IA. Connectez un fournisseur ci-dessous si vous voulez un briefing quotidien, de courtes notes sur vos mesures et le Coach.",
"action": "Configurer l'IA",
"dismiss": "Pas maintenant"
+ },
+ "bmiHeight": {
+ "coachPrefill": "Je n’ai pas encore indiqué ma taille — pourquoi l’IMC compte-t-il, et que dois-je savoir avant de la renseigner ?"
+ },
+ "medicationsEmpty": {
+ "coachPrefill": "Je n’ai encore ajouté aucun médicament — que dois-je savoir avant de suivre ici l’observance ?"
}
},
"thresholds": {
@@ -8017,7 +8022,9 @@
"noneUnlockedYet": "Aucune réussite débloquée pour l’instant.",
"pointsValue": "{points} points",
"metricCount": "{count}",
- "metricDays": "{count} jours",
+ "metricDaysOne": "{count} jour",
+ "metricDaysFew": "{count} jours",
+ "metricDaysOther": "{count} jours",
"metricPercent": "{count}%",
"badges": {
"intakeTotal1": {
@@ -8497,6 +8504,10 @@
"currentValue": "Actuel : {value} {unit}",
"targetRangeValue": "Plage cible : {min}–{max} {unit}",
"sourceLabel": "Source : {source}",
+ "sourceNames": {
+ "moodEntries": "Entrées d’humeur",
+ "sevenDays": "7 jours"
+ },
"inTarget": "Dans la plage cible",
"aboveTarget": "{delta} {unit} au-dessus de la plage",
"belowTarget": "{delta} {unit} en dessous de la plage",
@@ -12036,7 +12047,6 @@
"title": "Cabinets et médecins",
"description": "Votre carnet d'adresses : saisissez un cabinet une fois, choisissez-le partout.",
"linkLabel": "Carnet d'adresses",
- "add": "Entrée",
"searchPlaceholder": "Rechercher un nom, un cabinet ou une spécialité",
"loadError": "Impossible de charger le carnet d'adresses.",
"emptyTitle": "Aucune entrée",
@@ -12133,7 +12143,6 @@
"unreadableLabel": "Symptôme illisible",
"intensityPill": "{value}/10",
"entry": {
- "sheetTitle": "Noter un symptôme",
"sheetDescription": "L'un de tes propres symptômes, son intensité et quand.",
"symptom": "Symptôme",
"symptomRequired": "Choisis un symptôme.",
diff --git a/messages/it.json b/messages/it.json
index 6f6c924f0..d6cd9c4f4 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -463,7 +463,6 @@
"signOut": "Esci e accetta l'invito"
},
"createAccount": "Crea account",
- "newAccount": "Crea un nuovo account",
"backToPasskey": "Torna a passkey",
"passkeySetupLater": "Puoi configurare le passkey più tardi dalle impostazioni.",
"passwordMinLength": "Min. 12 caratteri",
@@ -583,7 +582,6 @@
"quickAddLifeEvent": "Registra evento di vita",
"customizeDashboard": "Personalizza dashboard",
"medicationIntakeQuickAdd": {
- "sheetTitle": "Registra assunzione del farmaco",
"sheetDescription": "Registra dose, orario e farmaco con pochi tap.",
"medicationLabel": "Farmaco",
"medicationPlaceholder": "Seleziona un farmaco",
@@ -2409,6 +2407,7 @@
"days7Label": "7 g",
"days7Title": "Ultimi 7 giorni",
"days30Label": "30 g",
+ "daysNLabel": "{days} g",
"days30Title": "Ultimi 30 giorni",
"days90Label": "90 g",
"days90Title": "Ultimi 90 giorni",
@@ -4743,6 +4742,12 @@
"settingsBody": "HealthLog funziona completamente senza IA. Collega un fornitore qui sotto se vuoi un riepilogo giornaliero, brevi note sulle tue misurazioni e il Coach.",
"action": "Configura IA",
"dismiss": "Non ora"
+ },
+ "bmiHeight": {
+ "coachPrefill": "Non ho ancora inserito la mia altezza: perché conta il BMI e cosa dovrei sapere prima di impostarla?"
+ },
+ "medicationsEmpty": {
+ "coachPrefill": "Non ho ancora aggiunto farmaci: cosa dovrei sapere prima di iniziare a monitorare qui l’aderenza?"
}
},
"thresholds": {
@@ -8017,7 +8022,9 @@
"noneUnlockedYet": "Nessun obiettivo ancora sbloccato.",
"pointsValue": "{points} punti",
"metricCount": "{count}",
- "metricDays": "{count} giorni",
+ "metricDaysOne": "{count} giorno",
+ "metricDaysFew": "{count} giorni",
+ "metricDaysOther": "{count} giorni",
"metricPercent": "{count}%",
"badges": {
"intakeTotal1": {
@@ -8497,6 +8504,10 @@
"currentValue": "Attuale: {value} {unit}",
"targetRangeValue": "Range obiettivo: {min}–{max} {unit}",
"sourceLabel": "Fonte: {source}",
+ "sourceNames": {
+ "moodEntries": "Voci dell’umore",
+ "sevenDays": "7 giorni"
+ },
"inTarget": "Nel range obiettivo",
"aboveTarget": "{delta} {unit} sopra il range",
"belowTarget": "{delta} {unit} sotto il range",
@@ -9931,7 +9942,7 @@
"loadError": "Impossibile caricare questo episodio.",
"backToList": "Torna al diario",
"detail": {
- "note": "Note"
+ "note": "Nota"
},
"edit": {
"title": "Modifica episodio",
@@ -12036,7 +12047,6 @@
"title": "Studi e medici",
"description": "La tua rubrica: scrivi uno studio una volta e selezionalo ovunque.",
"linkLabel": "Rubrica",
- "add": "Voce",
"searchPlaceholder": "Cerca nome, studio o specializzazione",
"loadError": "Impossibile caricare la rubrica.",
"emptyTitle": "Nessuna voce",
@@ -12133,7 +12143,6 @@
"unreadableLabel": "Sintomo illeggibile",
"intensityPill": "{value}/10",
"entry": {
- "sheetTitle": "Registra sintomo",
"sheetDescription": "Uno dei tuoi sintomi, quanto forte e quando.",
"symptom": "Sintomo",
"symptomRequired": "Scegli un sintomo.",
diff --git a/messages/ko.json b/messages/ko.json
index 381dd916d..56aec5aa9 100644
--- a/messages/ko.json
+++ b/messages/ko.json
@@ -463,7 +463,6 @@
"signOut": "로그아웃하고 초대 수락하기"
},
"createAccount": "계정 만들기",
- "newAccount": "새 계정 만들기",
"backToPasskey": "패스키로 돌아가기",
"passkeySetupLater": "패스키는 설정에서 언제든 등록할 수 있어요.",
"passwordMinLength": "최소 12자",
@@ -583,7 +582,6 @@
"quickAddLifeEvent": "삶의 사건 기록",
"customizeDashboard": "대시보드 맞춤 설정",
"medicationIntakeQuickAdd": {
- "sheetTitle": "복용 기록",
"sheetDescription": "몇 번의 탭으로 약과 용량, 시각을 기록해요.",
"medicationLabel": "약",
"medicationPlaceholder": "약 선택",
@@ -2500,6 +2498,7 @@
"days7Label": "7일",
"days7Title": "최근 7일",
"days30Label": "30일",
+ "daysNLabel": "{days}일",
"days30Title": "최근 30일",
"days90Label": "90일",
"days90Title": "최근 90일",
@@ -4743,6 +4742,12 @@
"settingsBody": "HealthLog는 AI 없이도 모든 기능이 작동합니다. 일일 브리핑, 측정값에 대한 짧은 메모, 코치를 원하시면 아래에서 제공자를 연결하세요.",
"action": "AI 설정",
"dismiss": "나중에"
+ },
+ "bmiHeight": {
+ "coachPrefill": "아직 키를 입력하지 않았어요. BMI가 왜 중요하고, 입력하기 전에 무엇을 알아야 하나요?"
+ },
+ "medicationsEmpty": {
+ "coachPrefill": "아직 약을 추가하지 않았어요. 여기서 복약 순응도를 기록하기 전에 무엇을 알아야 하나요?"
}
},
"thresholds": {
@@ -8017,7 +8022,9 @@
"noneUnlockedYet": "아직 달성한 업적이 없어요.",
"pointsValue": "{points}포인트",
"metricCount": "{count}",
- "metricDays": "{count}일",
+ "metricDaysOne": "{count}일",
+ "metricDaysFew": "{count}일",
+ "metricDaysOther": "{count}일",
"metricPercent": "{count}%",
"badges": {
"intakeTotal1": {
@@ -8497,6 +8504,10 @@
"currentValue": "현재: {value} {unit}",
"targetRangeValue": "목표: {min}–{max} {unit}",
"sourceLabel": "출처: {source}",
+ "sourceNames": {
+ "moodEntries": "기분 기록",
+ "sevenDays": "7일"
+ },
"inTarget": "목표 범위 안",
"aboveTarget": "목표보다 {delta} {unit} 높음",
"belowTarget": "목표보다 {delta} {unit} 낮음",
@@ -12036,7 +12047,6 @@
"title": "병원과 의사",
"description": "나만의 주소록이에요 — 병원을 한 번만 입력하면 어디서든 골라 쓸 수 있어요.",
"linkLabel": "주소록",
- "add": "항목",
"searchPlaceholder": "이름, 병원, 진료과 검색",
"loadError": "주소록을 불러오지 못했어요.",
"emptyTitle": "아직 항목이 없어요",
@@ -12133,7 +12143,6 @@
"unreadableLabel": "읽을 수 없는 증상",
"intensityPill": "{value}/10",
"entry": {
- "sheetTitle": "증상 기록",
"sheetDescription": "직접 정한 증상과 강도, 시각을 기록합니다.",
"symptom": "증상",
"symptomRequired": "증상을 선택하세요.",
diff --git a/messages/pl.json b/messages/pl.json
index dc1e0bfcd..f0a04ad32 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -463,7 +463,6 @@
"signOut": "Wyloguj się i przyjmij zaproszenie"
},
"createAccount": "Utwórz konto",
- "newAccount": "Utwórz nowe konto",
"backToPasskey": "Wróć do passkey",
"passkeySetupLater": "Passkey możesz skonfigurować później w ustawieniach.",
"passwordMinLength": "Min. 12 znaków",
@@ -583,7 +582,6 @@
"quickAddLifeEvent": "Zapisz wydarzenie życiowe",
"customizeDashboard": "Dostosuj pulpit",
"medicationIntakeQuickAdd": {
- "sheetTitle": "Zarejestruj przyjęcie leku",
"sheetDescription": "Zapisz dawkę, godzinę i lek w kilku stuknięciach.",
"medicationLabel": "Lek",
"medicationPlaceholder": "Wybierz lek",
@@ -2409,6 +2407,7 @@
"days7Label": "7 dni",
"days7Title": "Ostatnie 7 dni",
"days30Label": "30 dni",
+ "daysNLabel": "{days} dni",
"days30Title": "Ostatnie 30 dni",
"days90Label": "90 dni",
"days90Title": "Ostatnie 90 dni",
@@ -4743,6 +4742,12 @@
"settingsBody": "HealthLog działa w pełni bez AI. Podłącz dostawcę poniżej, jeśli chcesz codzienne podsumowanie, krótkie notatki o pomiarach i Coacha.",
"action": "Skonfiguruj AI",
"dismiss": "Nie teraz"
+ },
+ "bmiHeight": {
+ "coachPrefill": "Nie podałem jeszcze wzrostu — dlaczego BMI ma znaczenie i co powinienem wiedzieć, zanim go ustawię?"
+ },
+ "medicationsEmpty": {
+ "coachPrefill": "Nie dodałem jeszcze żadnych leków — co powinienem wiedzieć, zanim zacznę tu śledzić przestrzeganie zaleceń?"
}
},
"thresholds": {
@@ -8017,7 +8022,9 @@
"noneUnlockedYet": "Nie odblokowano jeszcze żadnego osiągnięcia.",
"pointsValue": "{points} punktów",
"metricCount": "{count}",
- "metricDays": "{count} dni",
+ "metricDaysOne": "{count} dzień",
+ "metricDaysFew": "{count} dni",
+ "metricDaysOther": "{count} dni",
"metricPercent": "{count}%",
"badges": {
"intakeTotal1": {
@@ -8497,6 +8504,10 @@
"currentValue": "Aktualnie: {value} {unit}",
"targetRangeValue": "Zakres docelowy: {min}–{max} {unit}",
"sourceLabel": "Źródło: {source}",
+ "sourceNames": {
+ "moodEntries": "Wpisy nastroju",
+ "sevenDays": "7 dni"
+ },
"inTarget": "W zakresie docelowym",
"aboveTarget": "{delta} {unit} powyżej zakresu",
"belowTarget": "{delta} {unit} poniżej zakresu",
@@ -9931,7 +9942,7 @@
"loadError": "Nie udało się wczytać tego epizodu.",
"backToList": "Powrót do dziennika",
"detail": {
- "note": "Note"
+ "note": "Notatka"
},
"edit": {
"title": "Edytuj epizod",
@@ -12036,7 +12047,6 @@
"title": "Gabinety i lekarze",
"description": "Twoja własna książka adresowa — wpisz gabinet raz, wybieraj go wszędzie.",
"linkLabel": "Książka adresowa",
- "add": "Wpis",
"searchPlaceholder": "Szukaj nazwiska, gabinetu lub specjalizacji",
"loadError": "Nie udało się wczytać książki adresowej.",
"emptyTitle": "Brak wpisów",
@@ -12133,7 +12143,6 @@
"unreadableLabel": "Nieczytelny objaw",
"intensityPill": "{value}/10",
"entry": {
- "sheetTitle": "Zapisz objaw",
"sheetDescription": "Jeden z twoich objawów, jak silny i kiedy.",
"symptom": "Objaw",
"symptomRequired": "Wybierz objaw.",
diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx
index 45d3e0e4e..9d039b5e6 100644
--- a/src/app/about/page.tsx
+++ b/src/app/about/page.tsx
@@ -60,7 +60,7 @@ function Section({
{title}
-
+
{children}
@@ -117,7 +117,7 @@ export default function AboutPage() {
href="https://github.com/MBombeck/HealthLog"
target="_blank"
rel="noopener noreferrer"
- className="text-primary hover:underline"
+ className="text-primary underline underline-offset-2"
>
github.com/MBombeck/HealthLog
{" "}
@@ -145,7 +145,7 @@ export default function AboutPage() {
href="https://www.maxmind.com"
target="_blank"
rel="noopener noreferrer"
- className="text-primary hover:underline"
+ className="text-primary underline underline-offset-2"
>
www.maxmind.com
@@ -164,7 +164,7 @@ export default function AboutPage() {
href="https://www.maxmind.com/en/geolite/eula"
target="_blank"
rel="noopener noreferrer"
- className="text-primary hover:underline"
+ className="text-primary underline underline-offset-2"
>
GeoLite End User License Agreement
@@ -173,7 +173,7 @@ export default function AboutPage() {
href="https://creativecommons.org/licenses/by-sa/4.0/"
target="_blank"
rel="noopener noreferrer"
- className="text-primary hover:underline"
+ className="text-primary underline underline-offset-2"
>
Creative Commons Attribution-ShareAlike 4.0 International License
{" "}
diff --git a/src/app/achievements/page.tsx b/src/app/achievements/page.tsx
index 57d1d4f8a..6c0fe99b0 100644
--- a/src/app/achievements/page.tsx
+++ b/src/app/achievements/page.tsx
@@ -35,7 +35,7 @@ import { PageHeader } from "@/components/ui/page-header";
import { PageAuthGate } from "@/components/ui/page-auth-gate";
import { QueryErrorCard } from "@/components/ui/query-error-card";
import { useAuth } from "@/hooks/use-auth";
-import { useTranslations } from "@/lib/i18n/context";
+import { useFormatters, useTranslations } from "@/lib/i18n/context";
import { formatDate } from "@/lib/format";
import { useAchievementsQuery } from "@/lib/queries/use-achievements-query";
import {
@@ -81,12 +81,13 @@ const CATEGORY_LABEL_KEY: Record
= {
function formatMetric(
format: AchievementProgress["format"],
value: number,
- t: (key: string, params?: Record) => string,
+ i18n: Pick, "t" | "tCount">,
) {
if (format === "percent")
- return t("achievements.metricPercent", { count: value });
- if (format === "days") return t("achievements.metricDays", { count: value });
- return t("achievements.metricCount", { count: value });
+ return i18n.t("achievements.metricPercent", { count: value });
+ // "1 Tag", not "1 Tage": the day count takes the plural form.
+ if (format === "days") return i18n.tCount("achievements.metricDays", value);
+ return i18n.t("achievements.metricCount", { count: value });
}
/**
@@ -127,9 +128,11 @@ export function groupByCategory(
interface AchievementCardProps {
achievement: AchievementProgress;
t: ReturnType["t"];
+ tCount: ReturnType["tCount"];
}
-function AchievementCard({ achievement, t }: AchievementCardProps) {
+function AchievementCard({ achievement, t, tCount }: AchievementCardProps) {
+ const i18n = { t, tCount };
const Icon = iconMap[achievement.icon] ?? Star;
const unlocked = achievement.unlocked;
@@ -175,7 +178,7 @@ function AchievementCard({ achievement, t }: AchievementCardProps) {
className={
unlocked
? "border-primary/30 from-primary/8 to-primary/0 rounded-xl border bg-gradient-to-br p-3"
- : "border-border bg-card/50 rounded-xl border p-3 opacity-70"
+ : "border-border bg-card/50 rounded-xl border p-3"
}
>
@@ -191,7 +194,7 @@ function AchievementCard({ achievement, t }: AchievementCardProps) {
{t(achievement.titleKey)}
-
+
{t(achievement.descriptionKey)}
@@ -215,8 +218,16 @@ function AchievementCard({ achievement, t }: AchievementCardProps) {
{t("achievements.criterionHint", {
- current: formatMetric(achievement.format, achievement.current, t),
- target: formatMetric(achievement.format, achievement.target, t),
+ current: formatMetric(
+ achievement.format,
+ achievement.current,
+ i18n,
+ ),
+ target: formatMetric(
+ achievement.format,
+ achievement.target,
+ i18n,
+ ),
})}
{/* v1.4.33 IW9 — aria-label so the bar carries an accessible name. */}
@@ -224,8 +235,16 @@ function AchievementCard({ achievement, t }: AchievementCardProps) {
value={achievement.progressPercent}
className="h-1.5"
aria-label={t("achievements.criterionHint", {
- current: formatMetric(achievement.format, achievement.current, t),
- target: formatMetric(achievement.format, achievement.target, t),
+ current: formatMetric(
+ achievement.format,
+ achievement.current,
+ i18n,
+ ),
+ target: formatMetric(
+ achievement.format,
+ achievement.target,
+ i18n,
+ ),
})}
/>
@@ -259,7 +278,9 @@ function AchievementCard({ achievement, t }: AchievementCardProps) {
export default function AchievementsPage() {
const { user, isAuthenticated, isLoading: authLoading } = useAuth();
- const { t } = useTranslations();
+ const { t, tCount } = useTranslations();
+ const fmt = useFormatters();
+ const i18n = { t, tCount };
// v1.18.0 — the achievements module gate. When the account has the
// module turned off the whole page disappears: an empty state stands in
@@ -335,9 +356,9 @@ export default function AchievementsPage() {
{t("achievements.points")}
- {summary?.earnedPoints ?? 0}
+ {fmt.integer(summary?.earnedPoints ?? 0)}
- / {summary?.totalPoints ?? 0}
+ / {fmt.integer(summary?.totalPoints ?? 0)}
diff --git a/src/app/auth/register/page.tsx b/src/app/auth/register/page.tsx
index 8b1bc9c34..e8cb2498f 100644
--- a/src/app/auth/register/page.tsx
+++ b/src/app/auth/register/page.tsx
@@ -175,14 +175,11 @@ export default function RegisterPage() {
-
-
- {t("auth.createAccount")}
-
-
- {t("auth.newAccount")}
-
-
+ {/* The title says it; a subtitle repeating "new account" said it
+ twice. */}
+
+ {t("auth.createAccount")}
+
{inviteToken && (
diff --git a/src/app/coach/conversations/page.tsx b/src/app/coach/conversations/page.tsx
index bda8faa42..32f0e0ce8 100644
--- a/src/app/coach/conversations/page.tsx
+++ b/src/app/coach/conversations/page.tsx
@@ -219,7 +219,9 @@ function CoachConversationsBody({ readOnly }: { readOnly: boolean }) {
className={cn(
"group relative flex flex-wrap items-center gap-1.5 rounded-xl px-3 py-2.5",
"text-sm transition-colors",
- "text-muted-foreground hover:bg-muted/40 hover:text-foreground",
+ // The title is the reader's own content: foreground,
+ // with the timestamp below as the muted meta line.
+ "text-foreground hover:bg-muted/40",
)}
>