diff --git a/messages/de.json b/messages/de.json
index c28878446..0b1a9ee93 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -2407,6 +2407,7 @@
"days7Label": "7T",
"days7Title": "Letzte 7 Tage",
"days30Label": "30T",
+ "daysNLabel": "{days}T",
"days30Title": "Letzte 30 Tage",
"days90Label": "90T",
"days90Title": "Letzte 90 Tage",
@@ -4745,6 +4746,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": {
@@ -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",
diff --git a/messages/en.json b/messages/en.json
index 7198453d6..be600a3c7 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -2407,6 +2407,7 @@
"days7Label": "7d",
"days7Title": "Last 7 days",
"days30Label": "30d",
+ "daysNLabel": "{days}d",
"days30Title": "Last 30 days",
"days90Label": "90d",
"days90Title": "Last 90 days",
@@ -4745,6 +4746,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": {
@@ -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",
diff --git a/messages/es.json b/messages/es.json
index fea607c2d..5c65d82e0 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -2407,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",
@@ -4745,6 +4746,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": {
@@ -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",
diff --git a/messages/fr.json b/messages/fr.json
index 7462c5d28..be8f0932c 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -2407,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",
@@ -4745,6 +4746,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": {
@@ -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",
diff --git a/messages/it.json b/messages/it.json
index 145d58031..5186016ef 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -2407,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",
@@ -4745,6 +4746,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": {
@@ -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",
diff --git a/messages/ko.json b/messages/ko.json
index 821498bbf..3d7594b2e 100644
--- a/messages/ko.json
+++ b/messages/ko.json
@@ -2498,6 +2498,7 @@
"days7Label": "7일",
"days7Title": "최근 7일",
"days30Label": "30일",
+ "daysNLabel": "{days}일",
"days30Title": "최근 30일",
"days90Label": "90일",
"days90Title": "최근 90일",
@@ -4745,6 +4746,12 @@
"settingsBody": "HealthLog는 AI 없이도 모든 기능이 작동합니다. 일일 브리핑, 측정값에 대한 짧은 메모, 코치를 원하시면 아래에서 제공자를 연결하세요.",
"action": "AI 설정",
"dismiss": "나중에"
+ },
+ "bmiHeight": {
+ "coachPrefill": "아직 키를 입력하지 않았어요. BMI가 왜 중요하고, 입력하기 전에 무엇을 알아야 하나요?"
+ },
+ "medicationsEmpty": {
+ "coachPrefill": "아직 약을 추가하지 않았어요. 여기서 복약 순응도를 기록하기 전에 무엇을 알아야 하나요?"
}
},
"thresholds": {
@@ -8497,6 +8504,10 @@
"currentValue": "현재: {value} {unit}",
"targetRangeValue": "목표: {min}–{max} {unit}",
"sourceLabel": "출처: {source}",
+ "sourceNames": {
+ "moodEntries": "기분 기록",
+ "sevenDays": "7일"
+ },
"inTarget": "목표 범위 안",
"aboveTarget": "목표보다 {delta} {unit} 높음",
"belowTarget": "목표보다 {delta} {unit} 낮음",
diff --git a/messages/pl.json b/messages/pl.json
index 20eee9b10..e35278cec 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -2407,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",
@@ -4745,6 +4746,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": {
@@ -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",
diff --git a/src/app/insights/bmi/page.tsx b/src/app/insights/bmi/page.tsx
index c4e91cb5a..cdc5ea3c6 100644
--- a/src/app/insights/bmi/page.tsx
+++ b/src/app/insights/bmi/page.tsx
@@ -95,7 +95,7 @@ export default function InsightsBmiPage() {
}
/>
-
+
);
}
diff --git a/src/app/insights/medications/page.tsx b/src/app/insights/medications/page.tsx
index 99aa76753..e6492bf1c 100644
--- a/src/app/insights/medications/page.tsx
+++ b/src/app/insights/medications/page.tsx
@@ -208,7 +208,9 @@ export default function InsightsMedikamentePage() {
}
/>
-
+
);
}
diff --git a/src/components/charts/health-chart.tsx b/src/components/charts/health-chart.tsx
index 3f539c146..beedff5eb 100644
--- a/src/components/charts/health-chart.tsx
+++ b/src/components/charts/health-chart.tsx
@@ -76,7 +76,7 @@ import {
type MetricWindowStats,
} from "@/lib/charts/window-stats";
import { shouldFireDataReady } from "@/lib/charts/data-ready-latch";
-import { axisUnitSuffix } from "@/lib/charts/axis-unit";
+import { axisTickUnitSuffix } from "@/lib/charts/axis-unit";
import { chartPointDayKey } from "@/components/day/chart-day";
import {
ChartDayFooter,
@@ -2078,7 +2078,7 @@ export function HealthChart({
}
unit={
showYAxisUnit
- ? axisUnitSuffix(yAxisUnit ?? unit)
+ ? axisTickUnitSuffix(yAxisUnit ?? unit)
: undefined
}
/>
diff --git a/src/components/insights/__tests__/metric-target-source-name.test.ts b/src/components/insights/__tests__/metric-target-source-name.test.ts
new file mode 100644
index 000000000..e8077ac9c
--- /dev/null
+++ b/src/components/insights/__tests__/metric-target-source-name.test.ts
@@ -0,0 +1,16 @@
+import { describe, expect, it } from "vitest";
+
+import { sourceName } from "../metric-target-summary";
+
+describe("sourceName", () => {
+ const t = (key: string) => `t:${key}`;
+ it("names the plain-description sources in the reader's language", () => {
+ expect(sourceName("Mood entries", t)).toBe(
+ "t:targets.sourceNames.moodEntries",
+ );
+ expect(sourceName("7-day", t)).toBe("t:targets.sourceNames.sevenDays");
+ });
+ it("leaves a citation as it is", () => {
+ expect(sourceName("ESH 2023", t)).toBe("ESH 2023");
+ });
+});
diff --git a/src/components/insights/__tests__/sleep-stage-stacked-bar.test.tsx b/src/components/insights/__tests__/sleep-stage-stacked-bar.test.tsx
index 0abdb5b3c..6ea7b026f 100644
--- a/src/components/insights/__tests__/sleep-stage-stacked-bar.test.tsx
+++ b/src/components/insights/__tests__/sleep-stage-stacked-bar.test.tsx
@@ -88,6 +88,9 @@ describe("", () => {
const html = render(, "de");
expect(html).toContain("Phasen-Verteilung");
expect(html).toContain("Letzte 7 Nächte");
+ // The window toggle speaks the app's range vocabulary, not "7d".
+ expect(html).toContain(">7T<");
+ expect(html).not.toContain(">7d<");
});
it("does not crash when the breakdown carries unknown stage keys", () => {
diff --git a/src/components/insights/metric-target-summary.tsx b/src/components/insights/metric-target-summary.tsx
index 845b1b8d6..513e2bfdd 100644
--- a/src/components/insights/metric-target-summary.tsx
+++ b/src/components/insights/metric-target-summary.tsx
@@ -421,12 +421,18 @@ function TargetReferencePanel({
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1 text-xs"
>
- {t("targets.sourceLabel", { source: target.source })}
+
+ {t("targets.sourceLabel", {
+ source: sourceName(target.source, t),
+ })}
+
) : (
- {t("targets.sourceLabel", { source: target.source })}
+ {t("targets.sourceLabel", {
+ source: sourceName(target.source, t),
+ })}
)}
@@ -434,3 +440,19 @@ function TargetReferencePanel({
);
}
+
+/**
+ * Two target sources are plain descriptions rather than a citation ("Mood
+ * entries", "7-day"); the server sends them in English, so they are named in
+ * the reader's language here. Every other source is a citation (ESH 2023,
+ * WHO, AASM/SRS) and reads as it is.
+ */
+const SOURCE_NAME_KEYS: Record = {
+ "Mood entries": "targets.sourceNames.moodEntries",
+ "7-day": "targets.sourceNames.sevenDays",
+};
+
+export function sourceName(source: string, t: (key: string) => string): string {
+ const key = SOURCE_NAME_KEYS[source];
+ return key ? t(key) : source;
+}
diff --git a/src/components/insights/sleep-stage-stacked-bar.tsx b/src/components/insights/sleep-stage-stacked-bar.tsx
index ccf2e94c0..7bb21701d 100644
--- a/src/components/insights/sleep-stage-stacked-bar.tsx
+++ b/src/components/insights/sleep-stage-stacked-bar.tsx
@@ -396,7 +396,9 @@ export function SleepStageStackedBar({ breakdown }: SleepStageStackedBarProps) {
aria-pressed={windowDays === w}
data-slot={`sleep-stage-window-${w}`}
>
- {w}d
+ {/* The page's other range controls read "7T / 30T"; this
+ one printed an English "7d" beside them. */}
+ {t("charts.daysNLabel", { days: w })}
))}
diff --git a/src/lib/charts/__tests__/axis-unit.test.ts b/src/lib/charts/__tests__/axis-unit.test.ts
index 6ff292885..121a0cce3 100644
--- a/src/lib/charts/__tests__/axis-unit.test.ts
+++ b/src/lib/charts/__tests__/axis-unit.test.ts
@@ -7,7 +7,7 @@ import {
stripComments,
walkSourceFiles,
} from "@/__tests__/helpers/source-files";
-import { axisUnitSuffix } from "../axis-unit";
+import { axisTickUnitSuffix, axisUnitSuffix } from "../axis-unit";
const COMPONENTS = join(process.cwd(), "src", "components");
@@ -54,3 +54,15 @@ describe("axis unit suffix", () => {
expect(offenders).toEqual([]);
});
});
+
+describe("axisTickUnitSuffix", () => {
+ it("keeps a short unit on the ticks", () => {
+ expect(axisTickUnitSuffix("bpm")).toBe(axisUnitSuffix("bpm"));
+ expect(axisTickUnitSuffix("mg/dL")).toBe(axisUnitSuffix("mg/dL"));
+ });
+ it("keeps a word-length unit off the ticks, where it was clipped", () => {
+ expect(axisTickUnitSuffix("Schritte")).toBeUndefined();
+ expect(axisTickUnitSuffix("Atemzüge/min")).toBeUndefined();
+ expect(axisTickUnitSuffix("mL/(kg·min)")).toBeUndefined();
+ });
+});
diff --git a/src/lib/charts/axis-unit.ts b/src/lib/charts/axis-unit.ts
index 3b7c81811..abdd2ac50 100644
--- a/src/lib/charts/axis-unit.ts
+++ b/src/lib/charts/axis-unit.ts
@@ -13,3 +13,20 @@ export function axisUnitSuffix(
): string | undefined {
return unit ? ` ${unit}` : undefined;
}
+
+/**
+ * The longest unit a tick label carries. A word-length unit ("Schritte",
+ * "Atemzüge/min", "mL/(kg·min)") made every tick wider than the fixed y-axis
+ * gutter, and the label was clipped at its left edge (".744 Schritte"). Such
+ * a unit stays off the ticks; the stat strip and the tile above the chart
+ * already name it.
+ */
+export const MAX_TICK_UNIT_LENGTH = 5;
+
+/** The tick suffix for `unit`, or none when the unit is too long to fit. */
+export function axisTickUnitSuffix(
+ unit: string | null | undefined,
+): string | undefined {
+ if (!unit || unit.length > MAX_TICK_UNIT_LENGTH) return undefined;
+ return axisUnitSuffix(unit);
+}