From 4f2c90b66cd93ac710a5764117393bede9f577a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 23 Aug 2026 14:58:51 +0200 Subject: [PATCH] Fold the side-effect tag through the shared helper The three locale matchers were built in parallel and each grew its own fold. Two of them are the same function: the side-effect tag fold and foldForMatch agree character for character, so one now calls the other. The third stays. foldLabel decomposes with NFKD, maps sharp s to ss and joins with underscores; foldForMatch decomposes with NFD, leaves sharp s alone and joins with spaces. That is a disagreement about who typed the text, not an accident: one side of a lab comparison is a shipped label retyped or prose a lab printed, where tolerating a different spelling claims more than the fold can prove, while someone naming a metric to an assistant is composing rather than reproducing. Both docblocks now say so and name the other. --- src/lib/i18n/fold-for-match.ts | 18 ++++++++++++++++++ src/lib/medications/glp1-side-effect-tags.ts | 11 +++-------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/lib/i18n/fold-for-match.ts b/src/lib/i18n/fold-for-match.ts index db0815e91..733dad6d5 100644 --- a/src/lib/i18n/fold-for-match.ts +++ b/src/lib/i18n/fold-for-match.ts @@ -18,6 +18,24 @@ * Client-safe: no message bundles are imported here, so a client component may * use these. Building an index OVER the bundles is a server concern and lives * in `./shared-resolve`. + * + * ## Why there is a second fold, and when to reach for it + * + * `./localised-label-index` carries `foldLabel`, and the two disagree ON + * PURPOSE rather than by accident. That one decomposes with NFKD, maps "ß" to + * "ss" and "œ" to "oe", and joins with "_"; this one decomposes with NFD, + * leaves "ß" alone, and joins with spaces. + * + * The difference is who typed the text. Here the other side of the comparison + * is a shipped label a person retyped, or prose a lab printed, so tolerating a + * DIFFERENT SPELLING would be reaching beyond what the fold can honestly + * claim. There, someone is naming a metric to an assistant in their own words, + * and "Suessstoff" for "Süßstoff" is the same request; the wider fold earns + * its keep because the caller is composing, not reproducing. + * + * Keep them apart. Whichever you pick, apply it to BOTH sides of the + * comparison — a fold on one side only is an exact-string match in a + * disguise. */ /** Curly, modifier and backtick apostrophes a keyboard or word processor emits. */ diff --git a/src/lib/medications/glp1-side-effect-tags.ts b/src/lib/medications/glp1-side-effect-tags.ts index 3566729dc..065755469 100644 --- a/src/lib/medications/glp1-side-effect-tags.ts +++ b/src/lib/medications/glp1-side-effect-tags.ts @@ -21,6 +21,8 @@ * therefore lives server-side in `./glp1-side-effect-tag-match`. */ +import { foldForMatch } from "@/lib/i18n/fold-for-match"; + /** * The catalogue, in display order. `key` is the stable machine value that * crosses the API and the Coach prompt; `labelKey` is the i18n key the chip @@ -85,14 +87,7 @@ export const GLP1_SIDE_EFFECT_TAG_LABEL_KEYS: Record< * the label the matcher indexes. */ export function normaliseSideEffectTag(raw: string): string { - return raw - .toLowerCase() - .normalize("NFD") - .replace(/[\u0300-\u036f]/g, "") - .replace(/[\u2018\u2019\u02bc'`]/g, " ") - .replace(/[-_/]/g, " ") - .replace(/\s+/g, " ") - .trim(); + return foldForMatch(raw); } /**