fix(medications): localise the dose unit and lift it onto its own line

A medication's dose is stored as free text the wizard composes from an
amount and a unit token ("1 pieces", "2 tablets"), and the card rendered
that token verbatim glued onto the name — so the unit showed in English
on every locale and the title read "Test ABC 1 pieces".

Translate the dose at display time: a formatDose helper splits the stored
string and runs a known unit token through the existing unit messages, so
"1 pieces" reads "1 Stück" / "1 piece(s)" per locale while a custom
free-text dose passes through untouched. The card header now carries the
name alone on its title line with the dose on a muted line beneath it,
above the category badge; the detail view, table and intake dialogs
localise the same way. Vorsorge and lab rows pass an empty dose so they
render no extra line.
This commit is contained in:
Marc-André Bombeck committed 2026-06-30 13:39:23 +02:00
1 parent a31711699c
commit 6fe552ffdf
14 files changed
+180 -43

No files matched your search

@@ -0,0 +1,36 @@
import { describe, it, expect } from "vitest";
import { formatDose } from "@/lib/medications/format-dose";
/**
* The unit keys translate via `medications.wizard.steps.step3.unit.<key>`.
* The stub mirrors a German bundle for the keys under test and echoes the
* key for anything else so a wrong lookup is visible.
*/
const t = (key: string): string =>
({
"medications.wizard.steps.step3.unit.pieces": "Stück",
"medications.wizard.steps.step3.unit.mg": "mg",
"medications.wizard.steps.step3.unit.tablets": "Tablette(n)",
})[key] ?? key;
describe("formatDose", () => {
it("translates a raw unit key glued to an amount", () => {
expect(formatDose("1 pieces", t)).toBe("1 Stück");
});
it("keeps a numeric amount and translates the unit", () => {
expect(formatDose("5 mg", t)).toBe("5 mg");
expect(formatDose("2 tablets", t)).toBe("2 Tablette(n)");
});
it("passes a custom free-text dose through unchanged", () => {
expect(formatDose("1 puff morning", t)).toBe("1 puff morning");
});
it("returns an empty string for empty / null / undefined input", () => {
expect(formatDose("", t)).toBe("");
expect(formatDose(null, t)).toBe("");
expect(formatDose(undefined, t)).toBe("");
});
});
+29
View File
@@ -0,0 +1,29 @@
/**
* Canonical dose-unit key list — the single source of truth shared by the
* wizard's dose step (the unit `<Select>`) and the display-time
* {@link formatDose} helper. Keep both in lock-step by importing from here;
* never re-declare the array.
*
* Ordered so mg / ml / IE / µg lead and the device-form units
* (Tablette, Hub, Sprühstoß, …) follow. Mirrors the v1.5.3 list with the
* maintainer-requested order tightening.
*
* Each key resolves to a localised label at
* `medications.wizard.steps.step3.unit.<key>` in every locale bundle.
*/
export const DOSE_UNIT_KEYS = [
"mg",
"ml",
"iu",
"mcg",
"g",
"tablets",
"capsules",
"drops",
"puffs",
"sprays",
"pieces",
"other",
] as const;
export type DoseUnitKey = (typeof DOSE_UNIT_KEYS)[number];
+45
View File
@@ -0,0 +1,45 @@
import { DOSE_UNIT_KEYS } from "./dose-units";
/**
* Display-time dose formatter.
*
* The wizard composes `Medication.dose` as a free-text string
* `"{amount} {unitKey}"` storing the RAW unit key (e.g. "1 pieces",
* "5 mg"). Rendered verbatim that leaks the untranslated English key onto
* the card — "Test ABC 1 pieces" even on a German UI. This helper splits
* the amount from the unit and, when the unit is one of the canonical
* {@link DOSE_UNIT_KEYS}, swaps it for the localised label
* (`medications.wizard.steps.step3.unit.<key>`). A custom free-text dose
* the wizard never produced (e.g. "1 puff morning") passes through
* unchanged.
*
* Pure + SSR-safe: takes the `t` accessor as a parameter, no React hook.
*/
// Mirrors `parseDoseExpression` in wizard-payload.ts — split on a leading
// number ("5 mg" → {amount:"5", unit:"mg"}); no number → {amount:"", unit:raw}.
const DOSE_EXPR_RE = /^\s*(\d+(?:[.,]\d+)?)\s*(.*)$/;
const KNOWN_UNITS = new Set<string>(DOSE_UNIT_KEYS);
export function formatDose(
raw: string | null | undefined,
t: (key: string) => string,
): string {
if (!raw) return "";
const trimmed = raw.trim();
if (!trimmed) return "";
const match = DOSE_EXPR_RE.exec(trimmed);
const amount = match ? (match[1] ?? "") : "";
const unit = match ? (match[2] ?? "").trim() : trimmed;
const key = unit.toLowerCase();
if (KNOWN_UNITS.has(key)) {
const label = t(`medications.wizard.steps.step3.unit.${key}`);
return amount ? `${amount} ${label}` : label;
}
// Custom free-text dose (the wizard never minted this unit) — leave it be.
return raw;
}