mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
Keep chart ticks readable and insights strings in the reader's language
A word-length unit on the y-axis ticks ('Schritte', 'Atemzüge/min',
'mL/(kg·min)') was wider than the axis gutter and lost its leading
digits; such units stay off the ticks, where the stat strip above
already names them. The sleep stage toggle reads '7T / 14T / 30T' like
every other range control, the 'Mood entries' and '7-day' target sources
are named in the reader's language, and the BMI and medication empty
states ask the coach in that language too.
This commit is contained in:
1 parent
3f00d2effd
commit
0f51a828c4
16 files changed
+159
-8
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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} 낮음",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -95,7 +95,7 @@ export default function InsightsBmiPage() {
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<CoachLaunchButton prefill="I haven't set my height yet — why does BMI matter, and what should I know before I configure it?" />
|
||||
<CoachLaunchButton prefill={t("insights.bmiHeight.coachPrefill")} />
|
||||
</SubPageShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -208,7 +208,9 @@ export default function InsightsMedikamentePage() {
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<CoachLaunchButton prefill="I haven't added any medications yet — what should I know before I start tracking medication compliance here?" />
|
||||
<CoachLaunchButton
|
||||
prefill={t("insights.medicationsEmpty.coachPrefill")}
|
||||
/>
|
||||
</SubPageShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -88,6 +88,9 @@ describe("<SleepStageStackedBar>", () => {
|
||||
const html = render(<SleepStageStackedBar breakdown={breakdown} />, "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", () => {
|
||||
|
||||
@@ -421,12 +421,18 @@ function TargetReferencePanel({
|
||||
rel="noopener noreferrer"
|
||||
className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1 text-xs"
|
||||
>
|
||||
<span>{t("targets.sourceLabel", { source: target.source })}</span>
|
||||
<span>
|
||||
{t("targets.sourceLabel", {
|
||||
source: sourceName(target.source, t),
|
||||
})}
|
||||
</span>
|
||||
<ExternalLink className="size-3" aria-hidden="true" />
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{t("targets.sourceLabel", { source: target.source })}
|
||||
{t("targets.sourceLabel", {
|
||||
source: sourceName(target.source, t),
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -434,3 +440,19 @@ function TargetReferencePanel({
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<string, string> = {
|
||||
"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;
|
||||
}
|
||||
@@ -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 })}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user