Files
HealthLog/src/lib/encounters/kind-label.ts
T
Marc-André Bombeck 29ab274775 fix(i18n): name a visit kind instead of printing its enum
Three surfaces built the label key by interpolation — the doctor-report
PDF's visit table, the daily digest's upcoming-visit line and the
clinician share view. `EncounterKind` is `ROUTINE` and the bundle leaf is
`routine`, so all three rendered `encounters.kind.ROUTINE` verbatim: into
a document a practice files, onto a lock screen, and onto the page.

Route them through `encounterKindLabelKey`, a literal switch beside the
existing `encounterKindLabel`, so the mapping is written down once.

The enum-derived i18n guard could not have caught this: it only knows
key spaces of the form `prefix + member`, and this one is not. It gains a
second arm for spaces whose mapping is a function, with `encounters.kind`
as its first entry. Break-proof: point one arm of the resolver back at
the uppercase key and six locale cases go red.
2026-08-24 01:02:20 +02:00

80 lines
2.8 KiB
TypeScript

/**
* The human name of a visit kind, in the record owner's language.
*
* Spelled out with literal `t()` calls rather than an interpolated key, for the
* same reason the restore-skip catalogue labels are: an interpolated key is
* invisible to `i18n-call-site-coverage`, so a renamed or missing kind would
* ship as raw dot notation instead of failing CI.
*
* This is not decoration. The value becomes a `MeasurementReminder.label`, and
* that label IS the push body — so the fallback for a visit with no practice
* attached is read off a lock screen. A raw enum constant there is the schema
* leaking into the product.
*/
import type { EncounterKind } from "@/generated/prisma/client";
import { getServerTranslator } from "@/lib/i18n/server-translator";
import { defaultLocale, locales, type Locale } from "@/lib/i18n/config";
function resolveLocale(locale: string | null | undefined): Locale {
return locales.includes(locale as Locale)
? (locale as Locale)
: defaultLocale;
}
export function encounterKindLabel(
kind: EncounterKind,
locale: string | null | undefined,
): string {
const { t } = getServerTranslator(resolveLocale(locale));
switch (kind) {
case "ROUTINE":
return t("encounters.kind.routine");
case "ACUTE":
return t("encounters.kind.acute");
case "SPECIALIST":
return t("encounters.kind.specialist");
case "PREVENTIVE":
return t("encounters.kind.preventive");
case "EMERGENCY":
return t("encounters.kind.emergency");
case "HOSPITAL":
return t("encounters.kind.hospital");
case "THERAPY":
return t("encounters.kind.therapy");
default:
return t("encounters.kind.other");
}
}
/**
* The same mapping as a KEY, for callers that already hold a translator and
* cannot reach for a second one — the doctor-report PDF, the daily digest and
* the clinician share view.
*
* Those three built the key by interpolation (`encounters.kind.${kind}`), and
* the enum is `ROUTINE` while the bundle leaf is `routine`, so all three
* printed raw dot notation into a clinical document. The interpolated form was
* invisible to `i18n-call-site-coverage`, which is the reason it survived: the
* key it asked for never existed and nothing said so.
*/
export function encounterKindLabelKey(kind: EncounterKind): string {
switch (kind) {
case "ROUTINE":
return "encounters.kind.routine";
case "ACUTE":
return "encounters.kind.acute";
case "SPECIALIST":
return "encounters.kind.specialist";
case "PREVENTIVE":
return "encounters.kind.preventive";
case "EMERGENCY":
return "encounters.kind.emergency";
case "HOSPITAL":
return "encounters.kind.hospital";
case "THERAPY":
return "encounters.kind.therapy";
default:
return "encounters.kind.other";
}
}