mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
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:
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("");
|
||||
});
|
||||
});
|
||||
@@ -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];
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user