fix(nutrients): name the form an upper limit covers and phrase it per locale

This commit is contained in:
Marc-André Bombeck committed 2026-10-16 05:31:33 +02:00
1 parent d66d3969cd
commit 8ba4d1b0d0
14 files changed
+208 -32

No files matched your search

@@ -0,0 +1,74 @@
import { describe, expect, it, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import { I18nProvider } from "@/lib/i18n/context";
import { NUTRIENT_CATALOG } from "@/lib/nutrients/catalog";
import type { NutrientOverviewRow } from "../types";
const state = vi.hoisted(() => ({ rows: [] as NutrientOverviewRow[] }));
vi.mock("@tanstack/react-query", () => ({
useQuery: () => ({
data: { nutrients: state.rows },
isLoading: false,
isError: false,
refetch: () => undefined,
}),
}));
vi.mock("@/hooks/use-auth", () => ({
useAuth: () => ({ user: { gender: "MALE" } }),
}));
import { MicronutrientsCard } from "../micronutrients-card";
function render(rows: NutrientOverviewRow[], locale: "en" | "de" = "en") {
state.rows = rows;
return renderToStaticMarkup(
<I18nProvider initialLocale={locale}>
<MicronutrientsCard />
</I18nProvider>,
);
}
function row(nutrient: "vitamin_a" | "vitamin_d"): NutrientOverviewRow {
return {
nutrient,
unit: "ug",
latestDay: "2026-10-15",
latestAmount: 4000,
daysWithData: 3,
upperLimit: NUTRIENT_CATALOG[nutrient].upperLimit,
latestAboveUpperLimit: true,
};
}
/**
* The upper limit of vitamin A is set for preformed retinol; beta-carotene
* does not count toward it. The amount on file cannot say which form it came
* in, so the note says what the level covers, in the reader's language, and
* the citation stays a bare citation.
*/
describe("MicronutrientsCard upper-limit note", () => {
it("says the vitamin A level covers retinol only", () => {
const html = render([row("vitamin_a")]);
expect(html).toContain('data-slot="nutrients-upper-limit-note"');
expect(html).toContain("preformed vitamin A (retinol) only");
expect(html).toContain("beta-carotene");
expect(html).toContain("(EFSA UL overview 2024 (SCF 2002))");
});
it("phrases the note in the reader's language, not the citation's", () => {
const html = render([row("vitamin_a")], "de");
expect(html).toContain("Höchstmenge für Erwachsene");
expect(html).toContain("Beta-Carotin");
expect(html).not.toContain("adults");
});
it("adds no form sentence where the level covers the nutrient as such", () => {
const html = render([row("vitamin_d")]);
expect(html).toContain('data-slot="nutrients-upper-limit-note"');
expect(html).not.toContain("The level covers");
});
});
@@ -131,6 +131,9 @@ function UpperLimitNote({
source: limit.source,
},
)}
{limit.form
? ` ${t(`nutrients.micronutrients.upperLimitForm.${limit.form}`)}`
: null}
</span>
</p>
);
@@ -37,6 +37,9 @@ export interface NutrientUpperLimitDto {
value: number;
unit: string;
scope: "total" | "supplemental";
/** The chemical form the level covers; `null` for the nutrient as such. */
form?:
"retinol" | "folic_acid" | "magnesium_salts" | "alpha_tocopherol" | null;
source: string;
}
+18 -2
View File
@@ -62,7 +62,10 @@ import {
NUTRIENT_LABELS,
type NutrientsDailyResult,
} from "@/lib/mcp/nutrients-read";
import { NUTRIENT_CODES } from "@/lib/nutrients/catalog";
import {
NUTRIENT_CODES,
type NutrientUpperLimit,
} from "@/lib/nutrients/catalog";
import { loadIntradayPulse } from "@/lib/analytics/intraday-pulse-io";
import { DEFAULT_TIMEZONE, userDayKey } from "@/lib/tz/format";
import { encodeOffsetCursor, decodeOffsetCursor } from "@/lib/mcp/pagination";
@@ -371,6 +374,18 @@ function plainClinicalText(
return `${head}${band}`.trim();
}
/** What the EFSA level covers where it is narrower than the nutrient. */
const UPPER_LIMIT_FORM_NOTE: Record<
NonNullable<NutrientUpperLimit["form"]>,
string
> = {
retinol:
" (preformed vitamin A, retinol, only; beta-carotene does not count toward it)",
folic_acid: " (folic acid from supplements and fortified foods)",
magnesium_salts: " (readily dissociable magnesium salts in supplements)",
alpha_tocopherol: " (alpha-tocopherol)",
};
/**
* Render a `fetch` result for a nutrient code as a plain-text citation
* sentence: the most recent non-zero day's total plus the resolved EFSA
@@ -407,8 +422,9 @@ function plainNutrientText(
if (limit && latestDay?.aboveUpperLimit) {
const share =
limit.scope === "supplemental" ? " for the supplement share" : "";
const form = limit.form ? UPPER_LIMIT_FORM_NOTE[limit.form] : "";
parts.push(
`that day is above the EFSA upper intake level of ${round1(limit.value)}${unit}${share}`,
`that day is above the EFSA upper intake level of ${round1(limit.value)}${unit}${share}${form}`,
);
}
return parts.length > 0
@@ -269,10 +269,16 @@ describe("upper intake levels (v1.43)", () => {
expect(def.upperLimit.unit, def.code).toBe(def.unit);
expect(def.upperLimit.value).toBeGreaterThan(0);
expect(def.upperLimit.source).toContain("EFSA");
// A bare citation: what the level covers is `form`, phrased per locale.
expect(def.upperLimit.source, def.code).not.toMatch(/adults|:/);
}
expect(NUTRIENT_CATALOG.vitamin_d.upperLimit?.value).toBe(100);
expect(NUTRIENT_CATALOG.magnesium.upperLimit?.scope).toBe("supplemental");
expect(NUTRIENT_CATALOG.vitamin_c.upperLimit).toBeNull();
// Vitamin A's level is set for preformed retinol; beta-carotene does not
// count toward it.
expect(NUTRIENT_CATALOG.vitamin_a.upperLimit?.form).toBe("retinol");
expect(NUTRIENT_CATALOG.vitamin_d.upperLimit?.form).toBeNull();
expect(NUTRIENT_CATALOG.dha.upperLimit).toBeNull();
});
+37 -15
View File
@@ -130,9 +130,22 @@ export interface NutrientUpperLimit {
value: number;
unit: NutrientUnit;
scope: "total" | "supplemental";
/**
* The chemical form the level is set for, where it is narrower than the
* nutrient: vitamin A's level covers preformed retinol only (beta-carotene
* does not count), folate's the folic acid, magnesium's the dissociable
* salts in supplements, vitamin E's alpha-tocopherol. `null` where the
* level covers the nutrient as such. Clients phrase it in the reader's
* language; `source` stays the bare citation.
*/
form: NutrientUpperLimitForm | null;
/** The citation (EFSA overview and the opinion it rests on), verbatim. */
source: string;
}
export type NutrientUpperLimitForm =
"retinol" | "folic_acid" | "magnesium_salts" | "alpha_tocopherol";
export interface NutrientReference {
/** EFSA value type, named in Coach prose later (Slice B). */
kind: "PRI" | "AI" | "safeLevel";
@@ -222,76 +235,85 @@ const UPPER_LIMITS: Partial<Record<NutrientCode, NutrientUpperLimit>> = {
value: 3000,
unit: "ug",
scope: "total",
source:
"EFSA UL overview 2024 (SCF 2002): preformed vitamin A (retinol), adults",
form: "retinol",
source: "EFSA UL overview 2024 (SCF 2002)",
},
vitamin_b6: {
value: 12,
unit: "mg",
scope: "total",
source: "EFSA UL overview 2024 (NDA Panel 2023): adults",
form: null,
source: "EFSA UL overview 2024 (NDA Panel 2023)",
},
folate: {
value: 1000,
unit: "ug",
scope: "supplemental",
source:
"EFSA UL overview 2024 (NDA Panel 2023): folic acid from supplements and fortified foods, adults",
form: "folic_acid",
source: "EFSA UL overview 2024 (NDA Panel 2023)",
},
vitamin_d: {
value: 100,
unit: "ug",
scope: "total",
source: "EFSA UL overview 2024 (NDA Panel 2023): adults",
form: null,
source: "EFSA UL overview 2024 (NDA Panel 2023)",
},
vitamin_e: {
value: 300,
unit: "mg",
scope: "total",
source: "EFSA UL overview 2024 (NDA Panel 2024): alpha-tocopherol, adults",
form: "alpha_tocopherol",
source: "EFSA UL overview 2024 (NDA Panel 2024)",
},
calcium: {
value: 2500,
unit: "mg",
scope: "total",
source: "EFSA UL overview 2024 (NDA Panel 2012): adults",
form: null,
source: "EFSA UL overview 2024 (NDA Panel 2012)",
},
magnesium: {
value: 250,
unit: "mg",
scope: "supplemental",
source:
"EFSA UL overview 2024 (SCF 2001): readily dissociable magnesium salts in supplements, adults",
form: "magnesium_salts",
source: "EFSA UL overview 2024 (SCF 2001)",
},
zinc: {
value: 25,
unit: "mg",
scope: "total",
source: "EFSA UL overview 2024 (SCF 2003): adults",
form: null,
source: "EFSA UL overview 2024 (SCF 2003)",
},
copper: {
value: 5,
unit: "mg",
scope: "total",
source: "EFSA UL overview 2024 (SCF 2003): adults",
form: null,
source: "EFSA UL overview 2024 (SCF 2003)",
},
selenium: {
value: 255,
unit: "ug",
scope: "total",
source: "EFSA UL overview 2024 (NDA Panel 2023): adults",
form: null,
source: "EFSA UL overview 2024 (NDA Panel 2023)",
},
molybdenum: {
value: 600,
unit: "ug",
scope: "total",
source: "EFSA UL overview 2024 (SCF 2000): adults",
form: null,
source: "EFSA UL overview 2024 (SCF 2000)",
},
iodine: {
value: 600,
unit: "ug",
scope: "total",
source: "EFSA UL overview 2024 (SCF 2002): adults",
form: null,
source: "EFSA UL overview 2024 (SCF 2002)",
},
};
+11 -1
View File
@@ -111,7 +111,17 @@ export const nutrientUpperLimitSchema = z
value: z.number(),
unit: z.string(),
scope: z.enum(["total", "supplemental"]),
source: z.string(),
form: z
.enum(["retinol", "folic_acid", "magnesium_salts", "alpha_tocopherol"])
.nullable()
.describe(
"v1.43 — the chemical form the level is set for, where narrower than the nutrient: `retinol` (vitamin A: preformed retinol only, beta-carotene does not count), `folic_acid`, `magnesium_salts` (readily dissociable salts in supplements), `alpha_tocopherol`. `null` where the level covers the nutrient as such. Phrase it in the reader's language; skip an unknown value.",
),
source: z
.string()
.describe(
"The bare citation (EFSA overview and the opinion it rests on), not prose. Every level here is the adult one.",
),
})
.meta({
id: "NutrientUpperLimit",