diff --git a/docs/README.content-formater.md b/docs/README.content-formater.md new file mode 100644 index 0000000..a6ef04e --- /dev/null +++ b/docs/README.content-formater.md @@ -0,0 +1,129 @@ +# Content Formatting Usage Guide + +The `contentFormatter` utility (`src/utils/contentFormatter.tsx`) provides markdown-style formatting for various UI content throughout the application. + +## Supported Formatting + +- **Bold:** `**text**` → **text** +- **Italic:** `*text*` → *text* +- **Bold + Italic:** `***text***` → ***text*** +- **Underline:** `__text__` → text +- **Line breaks:** `\n` (use `\\n` in translation strings) +- **Links:** `[text](url)` → clickable link with yellow underline + +## Current Usage + +### 1. Tooltips (✅ Already Implemented) + +All tooltips in `settings.tsx` use `formatContent()`: + +```tsx +import { formatContent } from '../utils/contentFormatter'; + + +

+ {formatContent(t('myTooltip'))} +

+
+``` + +**Example translation:** +```typescript +myTooltip: "This is a tooltip.\\n\\n**Important:** Some key info.\\n\\n***Default:*** 11h." +``` + +## Potential Future Usage + +### 2. Error/Warning Messages in Form Fields + +The formatter can be applied to `errorMessage` and `warningMessage` props in form components: + +**Current implementation** (plain text): +```tsx + +``` + +**With formatting** (enhanced): +```tsx +import { formatContent } from '../utils/contentFormatter'; + +// In FormNumericInput component (form-numeric-input.tsx): +{hasError && isFocused && errorMessage && ( +
+ {formatContent(errorMessage)} +
+)} +``` + +**Example with formatting:** +```typescript +errorMessage={t('errorEliminationHalfLife')} + +// In translations: +errorEliminationHalfLife: "**Invalid value.**\\n\\nHalf-life must be between **5h** and **50h**.\\n\\nSee [reference ranges](https://example.com)." +``` + +### 3. Info Boxes + +Static info boxes (like `advancedSettingsWarning`) could support formatting: + +**Current:** +```tsx +

+ {t('advancedSettingsWarning')} +

+``` + +**With formatting:** +```tsx +
+ {formatContent(t('advancedSettingsWarning'))} +
+``` + +**Example translation:** +```typescript +advancedSettingsWarning: "⚠️ **Warning:**\\n\\nThese parameters affect simulation accuracy.\\n\\nOnly adjust if you have ***specific clinical data*** or research references." +``` + +### 4. Modal Content + +Dialog/modal descriptions could use formatting for better readability: + +```tsx + + {formatContent(t('deleteConfirmation'))} + + +// Translation: +deleteConfirmation: "Are you sure you want to delete this data?\\n\\n**This action cannot be undone.**\\n\\nConsider [exporting a backup](export) first." +``` + +## Implementation Checklist + +To add formatting support to a component: + +1. ✅ Import the formatter: `import { formatContent } from '../utils/contentFormatter'` +2. ✅ Wrap the content: `{formatContent(text)}` +3. ✅ Update translations to use `\\n`, `**bold**`, `*italic*`, etc. +4. ✅ Test in both light and dark themes +5. ✅ Ensure links open in new tabs (already handled by formatter) + +## Notes + +- The formatter returns React nodes, so it should replace the content, not be nested inside `{}` +- Links automatically get `target="_blank"` and `rel="noopener noreferrer"` +- Link color is yellow (`text-yellow-300`) to maintain visibility in dark themes +- Line breaks use `\\n` in translation files (double backslash for escaping) +- The formatter is safe for user-generated content (doesn't execute scripts) + +## Benefits + +- **Improved readability:** Structure complex information with line breaks and emphasis +- **Consistency:** Unified formatting across tooltips, errors, warnings, and info boxes +- **Accessibility:** Links and emphasis improve screen reader experience +- **Maintainability:** Simple markdown-style syntax in translation files +- **I18n friendly:** All formatting stays in translation strings, easy to translate diff --git a/src/components/day-schedule.tsx b/src/components/day-schedule.tsx index 4228877..86b62da 100644 --- a/src/components/day-schedule.tsx +++ b/src/components/day-schedule.tsx @@ -19,6 +19,7 @@ import { IconButtonWithTooltip } from './ui/icon-button-with-tooltip'; import CollapsibleCardHeader from './ui/collapsible-card-header'; import { Plus, Copy, Trash2, ArrowDownAZ, TrendingUp, TrendingDown, Utensils } from 'lucide-react'; import type { DayGroup } from '../constants/defaults'; +import { formatText } from '../utils/contentFormatter'; interface DayScheduleProps { days: DayGroup[]; @@ -243,6 +244,8 @@ const DaySchedule: React.FC = ({ // Check for zero dose const isZeroDose = dose.ldx === '0' || dose.ldx === '0.0'; + // Check for dose > 70 mg + const isHighDose = parseFloat(dose.ldx) > 70; return (
@@ -252,8 +255,8 @@ const DaySchedule: React.FC = ({ onChange={(value) => onUpdateDose(day.id, dose.id, 'time', value)} required={true} warning={hasDuplicateTime} - errorMessage={t('errorTimeRequired')} - warningMessage={t('warningDuplicateTime')} + errorMessage={formatText(t('errorTimeRequired'))} + warningMessage={formatText(t('warningDuplicateTime'))} /> = ({ max={200} //unit="mg" required={true} - warning={isZeroDose} - errorMessage={t('errorNumberRequired')} - warningMessage={t('warningZeroDose')} + warning={isZeroDose || isHighDose} + errorMessage={formatText(t('errorNumberRequired'))} + warningMessage={ + isZeroDose ? formatText(t('warningZeroDose')) + : isHighDose ? formatText(t('warningDoseAbove70mg')) + : undefined // should not happen since warning is false + } inputWidth="w-[72px]" />
diff --git a/src/components/settings.tsx b/src/components/settings.tsx index d885626..0f6814b 100644 --- a/src/components/settings.tsx +++ b/src/components/settings.tsx @@ -22,6 +22,7 @@ import { FormSelect } from './ui/form-select'; import CollapsibleCardHeader from './ui/collapsible-card-header'; import { Info } from 'lucide-react'; import { getDefaultState } from '../constants/defaults'; +import { formatContent, formatText } from '../utils/contentFormatter'; /** * Helper function to create translation interpolation values for defaults. @@ -76,45 +77,6 @@ const tWithDefaults = (translationFn: any, key: string, defaults: Record { - const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g; - const parts: React.ReactNode[] = []; - let lastIndex = 0; - let match; - - while ((match = linkRegex.exec(text)) !== null) { - // Add text before link - if (match.index > lastIndex) { - parts.push(text.substring(lastIndex, match.index)); - } - // Add link - parts.push( - - {match[1]} - - ); - lastIndex = linkRegex.lastIndex; - } - - // Add remaining text - if (lastIndex < text.length) { - parts.push(text.substring(lastIndex)); - } - - return parts.length > 0 ? parts : text; -}; - const Settings = ({ pkParams, therapeuticRange, @@ -319,7 +281,7 @@ const Settings = ({ -

{tWithDefaults(t, 'showTemplateDayTooltip', defaultsForT)}

+

{formatContent(tWithDefaults(t, 'showTemplateDayTooltip', defaultsForT))}

@@ -348,7 +310,7 @@ const Settings = ({ -

{tWithDefaults(t, 'showDayReferenceLinesTooltip', defaultsForT)}

+

{formatContent(tWithDefaults(t, 'showDayReferenceLinesTooltip', defaultsForT))}

@@ -377,7 +339,7 @@ const Settings = ({ -

{tWithDefaults(t, 'showTherapeuticRangeLinesTooltip', defaultsForT)}

+

{formatContent(tWithDefaults(t, 'showTherapeuticRangeLinesTooltip', defaultsForT))}

@@ -400,7 +362,7 @@ const Settings = ({ -

{tWithDefaults(t, 'therapeuticRangeTooltip', defaultsForT)}

+

{formatContent(tWithDefaults(t, 'therapeuticRangeTooltip', defaultsForT))}

@@ -414,7 +376,7 @@ const Settings = ({ placeholder={t('min')} required={true} error={!!therapeuticRangeError || !therapeuticRange.min} - errorMessage={therapeuticRangeError || t('errorTherapeuticRangeMinRequired') || 'Minimum therapeutic range is required'} + errorMessage={formatText(therapeuticRangeError || t('errorTherapeuticRangeMinRequired') || 'Minimum therapeutic range is required')} showResetButton={true} defaultValue={defaultsForT.therapeuticRangeMin} /> @@ -429,7 +391,7 @@ const Settings = ({ unit="ng/ml" required={true} error={!!therapeuticRangeError || !therapeuticRange.max} - errorMessage={therapeuticRangeError || t('errorTherapeuticRangeMaxRequired') || 'Maximum therapeutic range is required'} + errorMessage={formatText(therapeuticRangeError || t('errorTherapeuticRangeMaxRequired') || 'Maximum therapeutic range is required')} showResetButton={true} defaultValue={defaultsForT.therapeuticRangeMax} /> @@ -456,7 +418,7 @@ const Settings = ({ -

{tWithDefaults(t, 'displayedDaysTooltip', defaultsForT)}

+

{formatContent(tWithDefaults(t, 'displayedDaysTooltip', defaultsForT))}

@@ -468,7 +430,7 @@ const Settings = ({ max={parseInt(simulationDays, 10) || 3} unit={t('unitDays')} required={true} - errorMessage={t('errorNumberRequired')} + errorMessage={formatText(t('errorNumberRequired'))} showResetButton={true} defaultValue={defaultsForT.displayedDays} /> @@ -490,7 +452,7 @@ const Settings = ({ -

{tWithDefaults(t, 'yAxisRangeTooltip', defaultsForT)}

+

{formatContent(tWithDefaults(t, 'yAxisRangeTooltip', defaultsForT))}

@@ -506,7 +468,7 @@ const Settings = ({ showResetButton={true} defaultValue={defaultsForT.yAxisMin} warning={!!yAxisRangeError} - warningMessage={yAxisRangeError} + warningMessage={formatText(yAxisRangeError)} /> - @@ -600,7 +562,7 @@ const Settings = ({ -

{tWithDefaults(t, 'simulationDurationTooltip', defaultsForT)}

+

{formatContent(tWithDefaults(t, 'simulationDurationTooltip', defaultsForT))}

@@ -612,7 +574,7 @@ const Settings = ({ max={7} unit={t('unitDays')} required={true} - errorMessage={t('errorNumberRequired')} + errorMessage={formatText(t('errorNumberRequired'))} showResetButton={true} defaultValue={defaultsForT.simulationDays} /> @@ -652,7 +614,7 @@ const Settings = ({ -

{tWithDefaults(t, 'steadyStateDaysTooltip', defaultsForT)}

+

{formatContent(tWithDefaults(t, 'steadyStateDaysTooltip', defaultsForT))}

@@ -702,7 +664,7 @@ const Settings = ({ -

{renderTooltipWithLinks(tWithDefaults(t, 'halfLifeTooltip', defaultsForT))}

+

{formatContent(tWithDefaults(t, 'halfLifeTooltip', defaultsForT))}

@@ -716,8 +678,8 @@ const Settings = ({ required={true} warning={eliminationWarning && !eliminationExtreme} error={eliminationExtreme} - warningMessage={t('warningEliminationOutOfRange')} - errorMessage={t('errorEliminationHalfLifeRequired')} + warningMessage={formatText(t('warningEliminationOutOfRange'))} + errorMessage={formatText(t('errorEliminationHalfLifeRequired'))} showResetButton={true} defaultValue={defaultsForT.damphHalfLife} /> @@ -742,7 +704,7 @@ const Settings = ({ -

{tWithDefaults(t, 'conversionHalfLifeTooltip', defaultsForT)}

+

{formatContent(tWithDefaults(t, 'conversionHalfLifeTooltip', defaultsForT))}

@@ -755,8 +717,8 @@ const Settings = ({ unit="h" required={true} warning={conversionWarning} - warningMessage={t('warningConversionOutOfRange')} - errorMessage={t('errorConversionHalfLifeRequired')} + warningMessage={formatText(t('warningConversionOutOfRange'))} + errorMessage={formatText(t('errorConversionHalfLifeRequired'))} showResetButton={true} defaultValue={defaultsForT.ldxHalfLife} /> @@ -778,7 +740,7 @@ const Settings = ({ -

{tWithDefaults(t, 'absorptionHalfLifeTooltip', defaultsForT)}

+

{formatContent(tWithDefaults(t, 'absorptionHalfLifeTooltip', defaultsForT))}

@@ -791,8 +753,8 @@ const Settings = ({ unit="h" required={true} warning={absorptionWarning} - warningMessage={t('warningAbsorptionOutOfRange')} - errorMessage={t('errorAbsorptionRateRequired')} + warningMessage={formatText(t('warningAbsorptionOutOfRange'))} + errorMessage={formatText(t('errorAbsorptionRateRequired'))} showResetButton={true} defaultValue={defaultsForT.ldxAbsorptionHalfLife} /> @@ -831,7 +793,7 @@ const Settings = ({ -

{renderTooltipWithLinks(tWithDefaults(t, 'standardVdTooltip', { +

{formatContent(tWithDefaults(t, 'standardVdTooltip', { ...defaultsForT, standardVdValue: pkParams.advanced.standardVd?.preset === 'adult' ? '377' : pkParams.advanced.standardVd?.preset === 'child' ? '175' : pkParams.advanced.standardVd?.customValue || '377', standardVdPreset: t(`standardVdPreset${pkParams.advanced.standardVd?.preset?.charAt(0).toUpperCase()}${pkParams.advanced.standardVd?.preset?.slice(1)}` || 'standardVdPresetAdult') @@ -891,7 +853,7 @@ const Settings = ({ -

{renderTooltipWithLinks(tWithDefaults(t, 'bodyWeightTooltip', defaultsForT))}

+

{formatContent(tWithDefaults(t, 'bodyWeightTooltip', defaultsForT))}

@@ -929,7 +891,7 @@ const Settings = ({ -

{renderTooltipWithLinks(tWithDefaults(t, 'tmaxDelayTooltip', defaultsForT))}

+

{formatContent(tWithDefaults(t, 'tmaxDelayTooltip', defaultsForT))}

@@ -967,7 +929,7 @@ const Settings = ({ -

{tWithDefaults(t, 'urinePHTooltip', defaultsForT)}

+

{formatContent(tWithDefaults(t, 'urinePHTooltip', defaultsForT))}

@@ -1009,7 +971,7 @@ const Settings = ({ -

{renderTooltipWithLinks(tWithDefaults(t, 'ageGroupTooltip', defaultsForT))}

+

{formatContent(tWithDefaults(t, 'ageGroupTooltip', defaultsForT))}

@@ -1061,7 +1023,7 @@ const Settings = ({ -

{renderTooltipWithLinks(tWithDefaults(t, 'renalFunctionTooltip', defaultsForT))}

+

{formatContent(tWithDefaults(t, 'renalFunctionTooltip', defaultsForT))}

@@ -1111,7 +1073,7 @@ const Settings = ({ -

{renderTooltipWithLinks(tWithDefaults(t, 'oralBioavailabilityTooltip', defaultsForT))}

+

{formatContent(tWithDefaults(t, 'oralBioavailabilityTooltip', defaultsForT))}

diff --git a/src/components/ui/form-numeric-input.tsx b/src/components/ui/form-numeric-input.tsx index 7c2890a..f6eacca 100644 --- a/src/components/ui/form-numeric-input.tsx +++ b/src/components/ui/form-numeric-input.tsx @@ -30,8 +30,8 @@ interface NumericInputProps extends Omit( diff --git a/src/locales/de.ts b/src/locales/de.ts index b47a9ba..adc9e3a 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -67,7 +67,7 @@ export const de = { refLineMax: "Max", pinChart: "Diagramm oben fixieren", unpinChart: "Diagramm freigeben", - stickyChartTooltip: "Diagramm beim Scrollen durch die Einstellungen sichtbar halten, um Änderungen in Echtzeit zu sehen. Standard: aus.", + stickyChartTooltip: "Diagramm beim Scrollen durch die Einstellungen sichtbar halten, um Änderungen in Echtzeit zu sehen.\\n\\n__Standard:__ **aus**", chartViewDamphTooltip: "Nur den aktiven Metaboliten (d-Amphetamin) im Konzentrationsverlauf anzeigen", chartViewLdxTooltip: "Nur das Prodrug (Lisdexamfetamin) im Konzentrationsverlauf anzeigen", chartViewBothTooltip: "Sowohl d-Amphetamin als auch Lisdexamfetamin gemeinsam anzeigen", @@ -79,13 +79,13 @@ export const de = { advancedSettings: "Erweiterte Einstellungen", advancedSettingsWarning: "⚠️ Diese Parameter beeinflussen die Simulationsgenauigkeit und können von Bevölkerungsdurchschnitten abweichen. Nur anpassen, wenn spezifische klinische Daten oder Forschungsreferenzen vorliegen.", standardVolumeOfDistribution: "Verteilungsvolumen (Vd)", - standardVdTooltip: "Definiert wie sich der Wirkstoff im Körper verteilt. Erwachsene: 377L (Roberts 2015), Kinder: ~150-200L. Gewichtsbasierte Skalierung: ~5,4 L/kg (für Erwachsene >18 Jahre basierend auf [Populations-Pharmakokinetik](https://pmc.ncbi.nlm.nih.gov/articles/PMC5572767/)). Beeinflusst alle Konzentrationsberechnungen. Nur für pädiatrische oder spezialisierte Simulationen ändern. Standard: {{standardVdValue}}L ({{standardVdPreset}}).", + standardVdTooltip: "Definiert wie sich der Wirkstoff im Körper verteilt.\\n\\n__Voreinstellungen:__\\n• Erwachsene: 377L (Roberts 2015)\\n• Kinder: ~150-200L\\n• Gewichtsbasiert: ~5,4 L/kg (für Erwachsene >18 Jahre basierend auf [Populations-Pharmakokinetik](https://pmc.ncbi.nlm.nih.gov/articles/PMC5572767/))\\n\\nBeeinflusst alle Konzentrationsberechnungen. Nur für pädiatrische oder spezialisierte Simulationen ändern.\\n\\n__Standard:__ **{{standardVdValue}}L** ({{standardVdPreset}})", standardVdPresetAdult: "Erwachsene (377L)", standardVdPresetChild: "Kinder (175L)", standardVdPresetCustom: "Benutzerdefiniert", standardVdPresetWeightBased: "Gewichtsbasiert (~5,4 L/kg)", customVdValue: "Benutzerdefiniertes Vd (L)", - weightBasedVdInfo: "Gewichtsbasiertes Vd passt Plasmakonzentrationen basierend auf Körpergewicht an (~5,4 L/kg). Leichtere Personen → höhere Spitzen, schwerere → niedrigere Spitzen. Diese Option ist für Erwachsene (>18 Jahre) basierend auf der Populations-PK-Studie vorgesehen. Für pädiatrische Patienten verwenden Sie die Voreinstellung 'Kinder'.", + weightBasedVdInfo: "Gewichtsbasiertes Vd passt Plasmakonzentrationen basierend auf Körpergewicht an (~5,4 L/kg). Leichtere Personen → höhere Spitzen, schwerere → niedrigere Spitzen.\\n\\nDiese Option ist für Erwachsene (>18 Jahre) basierend auf der Populations-PK-Studie vorgesehen.\\n\\nFür pädiatrische Patienten verwenden Sie die Voreinstellung 'Kinder'.", xAxisTimeFormat: "Zeitformat", xAxisFormatContinuous: "Fortlaufend", xAxisFormatContinuousDesc: "Endlose Sequenz (0h, 6h, 12h...)", @@ -94,75 +94,75 @@ export const de = { xAxisFormat12h: "Tageszeit (12h AM/PM)", xAxisFormat12hDesc: "Wiederholend 12h Zyklus im AM/PM Format", showTemplateDayInChart: "Regulären Plan kontinuierlich anzeigen", - showTemplateDayTooltip: "Medikationsplan als Referenz-Overlay jederzeit anzeigen (Standard: aktiviert).", + showTemplateDayTooltip: "Medikationsplan als Referenz-Overlay jederzeit anzeigen.\\n\\n__Standard:__ **aktiviert**", simulationSettings: "Simulations-Einstellungen", showDayReferenceLines: "Tagestrenner anzeigen", - showDayReferenceLinesTooltip: "Vertikale Linien und Statusanzeigen zwischen Tagen anzeigen (Standard: aktiviert).", + showDayReferenceLinesTooltip: "Vertikale Linien und Statusanzeigen zwischen Tagen anzeigen.\\n\\n__Standard:__ **aktiviert**", showTherapeuticRangeLines: "Therapeutischen Bereich anzeigen ", - showTherapeuticRangeLinesTooltip: "Horizontale Referenzlinien für therapeutisches Min/Max anzeigen (Standard: aktiviert).", + showTherapeuticRangeLinesTooltip: "Horizontale Referenzlinien für therapeutisches Min/Max anzeigen.\\n\\n__Standard:__ **aktiviert**", simulationDuration: "Simulationsdauer", - simulationDurationTooltip: "Anzahl der zu simulierenden Tage. Längere Zeiträume zeigen Steady-State. Standard: {{simulationDays}} Tage.", + simulationDurationTooltip: "Anzahl der zu simulierenden Tage. Längere Zeiträume zeigen Steady-State.\\n\\n__Standard:__ **{{simulationDays}} Tage**", displayedDays: "Sichtbare Tage (im Fokus)", - displayedDaysTooltip: "Wie viele Tage auf einmal angezeigt werden. Kleinere Werte zoomen in Details. Standard: {{displayedDays}} Tag(e).", + displayedDaysTooltip: "Wie viele Tage auf einmal angezeigt werden. Kleinere Werte zoomen in Details.\\n\\n__Standard:__ **{{displayedDays}} Tag(e)**", yAxisRange: "Y-Achsen-Bereich (Konzentrations-Zoom)", - yAxisRangeTooltip: "Vertikale Achse manuell festlegen (Konzentrationsskala). Leer lassen für automatische Anpassung. Standard: auto.", + yAxisRangeTooltip: "Vertikale Achse manuell festlegen (Konzentrationsskala). Leer lassen für automatische Anpassung.\\n\\n__Standard:__ **auto**", yAxisRangeAutoButton: "A", yAxisRangeAutoButtonTitle: "Bereich automatisch anhand des Datenbereichs bestimmen", auto: "Auto", therapeuticRange: "Therapeutischer Bereich (d-Amphetamin)", - therapeuticRangeTooltip: "Personalisierte Konzentrationsziele basierend auf DEINER individuellen Reaktion. Setze diese nachdem du beobachtet hast, welche Werte Symptomkontrolle vs. Nebenwirkungen bieten. Referenzbereiche (stark variabel): Erwachsene ~10-80 ng/mL, Kinder ~20-120 ng/mL (aufgrund geringeren Körpergewichts/Vd). Leer lassen wenn unsicher. Konsultiere deinen Arzt.", + therapeuticRangeTooltip: "Personalisierte Konzentrationsziele basierend auf DEINER individuellen Reaktion. Setze diese nachdem du beobachtet hast, welche Werte Symptomkontrolle vs. Nebenwirkungen bieten.\\n\\n**Referenzbereiche** (stark variabel):\\n• __Erwachsene:__ **~10-80 ng/mL**\\n• __Kinder:__ **~20-120 ng/mL** (aufgrund geringeren Körpergewichts/Vd)\\n\\nLeer lassen wenn unsicher.\\n\\n***Konsultiere deinen Arzt.***", dAmphetamineParameters: "d-Amphetamin Parameter", halfLife: "Eliminations-Halbwertszeit", - halfLifeTooltip: "Zeit bis der Körper die Hälfte des d-Amphetamins aus dem Blut ausscheidet. Beeinflusst durch Urin-pH: sauer (<6) → 7-9h, neutral (6-7,5) → 10-12h, alkalisch (>7,5) → 13-15h. Siehe [therapeutische Referenzbereiche](https://www.thieme-connect.com/products/ejournals/pdf/10.1055/a-2689-4911.pdf). Standard: {{damphHalfLife}}h.", + halfLifeTooltip: "Zeit bis der Körper die Hälfte des d-Amphetamins aus dem Blut ausscheidet.\\n\\n__Beeinflusst durch Urin-pH:__\\n• __Sauer (<6):__ **7-9h**\\n• __Neutral (6-7,5)__ → **10-12h**\\n• __Alkalisch (>7,5)__ → **13-15h**\\n\\nSiehe [therapeutische Referenzbereiche](https://www.thieme-connect.com/products/ejournals/pdf/10.1055/a-2689-4911.pdf).\\n\\n__Standard:__ **{{damphHalfLife}}h**", lisdexamfetamineParameters: "Lisdexamfetamin (LDX) Parameter", conversionHalfLife: "LDX→d-Amph Umwandlungs-Halbwertszeit", - conversionHalfLifeTooltip: "Zeit bis rote Blutkörperchen die Hälfte des inaktiven LDX-Prodrugs in aktives d-Amphetamin umwandeln. Typisch: 0,7-1,2h. Standard: {{ldxHalfLife}}h.", + conversionHalfLifeTooltip: "Zeit bis rote Blutkörperchen die Hälfte des inaktiven LDX-Prodrugs in aktives d-Amphetamin umwandeln.\\n\\nTypischer Bereich: **0,7-1,2h**.\\n\\n__Standard:__ **{{ldxHalfLife}}h**", absorptionHalfLife: "Absorptions-Halbwertszeit", - absorptionHalfLifeTooltip: "Zeit bis der Darm die Hälfte des LDX vom Magen ins Blut aufnimmt. Durch Nahrung verzögert (~1h Verschiebung). Typisch: 0,7-1,2h. Standard: {{ldxAbsorptionHalfLife}}h.", + absorptionHalfLifeTooltip: "Zeit bis der Darm die Hälfte des LDX vom Magen ins Blut aufnimmt.\\n\\nDurch Nahrung verzögert (**~1h Verschiebung**).\\n\\nTypischer Bereich: **0,7-1,2h**.\\n\\n__Standard:__ **{{ldxAbsorptionHalfLife}}h**", faster: "(schneller >)", // Advanced Settings weightBasedVdScaling: "Gewichtsbasiertes Verteilungsvolumen", - weightBasedVdTooltip: "Passt Plasmakonzentrationen basierend auf Körpergewicht an (proportional zu ~5,4 L/kg). Leichtere → höhere Spitzen, schwerere → niedrigere. Bei Deaktivierung: 70 kg Erwachsener.", + weightBasedVdTooltip: "Passt Plasmakonzentrationen basierend auf Körpergewicht an (proportional zu **~5,4 L/kg**).\\n\\n__Effekte:__\\n• Leichtere Personen → ***höhere*** Konzentrationsspitzen\\n• __Schwerere Personen__ → ***niedrigere*** Konzentrationsspitzen\\n\\n__Bei Deaktivierung:__ **70 kg Erwachsene Person**", bodyWeight: "Körpergewicht", - bodyWeightTooltip: "Dein Körpergewicht für Konzentrationsanpassung. Verwendet zur Berechnung des Verteilungsvolumens (Vd = Gewicht × 5,4). Siehe [Populations-Pharmakokinetik](https://pmc.ncbi.nlm.nih.gov/articles/PMC5572767/). Standard: {{bodyWeight}} kg.", + bodyWeightTooltip: "Dein Körpergewicht für Konzentrationsanpassung. Verwendet zur Berechnung des Verteilungsvolumens:\\n\\n**Vd = Gewicht × 5,4**\\n\\nSiehe [Populations-Pharmakokinetik](https://pmc.ncbi.nlm.nih.gov/articles/PMC5572767/).\\n\\n__Standard:__ **{{bodyWeight}} kg**", bodyWeightUnit: "kg", foodEffectEnabled: "Mit Mahlzeit eingenommen", foodEffectDelay: "Nahrungseffekt-Verzögerung", - foodEffectTooltip: "Fettreiche Mahlzeiten verzögern die Absorption ohne die Gesamtaufnahme zu ändern. Verlangsamt Wirkungseintritt (~1h Verzögerung). Hinweis: Die in dieser Studie verwendete fettreiche Mahlzeit bestand aus 1 englischem Muffin mit Butter, 1 Spiegelei, 1 Scheibe amerikanischem Käse, 1 Scheibe kanadischem Speck, 57 g Bratkartoffeln und 240 ml Vollmilch. Deaktiviert nimmt nüchternen Zustand an.", + foodEffectTooltip: "Fettreiche Mahlzeiten verzögern die Absorption **ohne die Gesamtaufnahme zu ändern**.\\n\\nVerlangsamt Wirkungseintritt um **~1 Stunde**.\\n\\nDeaktiviert nimmt nüchternen Zustand an.", tmaxDelay: "Absorptions-Verzögerung", - tmaxDelayTooltip: "Zeitverzögerung bei Einnahme mit fettreicher Mahlzeit. Wird durch Einzel-Dosis Nahrungsschalter (🍴 Symbol) im Zeitplan angewendet. Forschung zeigt ~1h Verzögerung ohne Spitzenreduktion. Siehe [Studie](https://pmc.ncbi.nlm.nih.gov/articles/PMC4823324/). Standard: {{tmaxDelay}}h.", + tmaxDelayTooltip: "Zeitverzögerung bei Einnahme mit **fettreicher Mahlzeit**. Wird durch Einzel-Dosis Nahrungsschalter (🍴 Symbol) im Zeitplan angewendet.\\n\\nForschung zeigt ~1h Verzögerung ohne Spitzenreduktion. Siehe [Studie](https://pmc.ncbi.nlm.nih.gov/articles/PMC4823324/).\\n\\n__Hinweis:__ Die in dieser Studie verwendete fettreiche Mahlzeit bestand aus 1 englischem Muffin mit Butter, 1 Spiegelei, 1 Scheibe amerikanischem Käse, 1 Scheibe kanadischem Speck, 57 g Bratkartoffeln und 240 ml Vollmilch.\\n\\n__Standard:__ **{{tmaxDelay}}h**", tmaxDelayUnit: "h", urinePHTendency: "Urin-pH-Effekte", - urinePHTooltip: "Urin-pH beeinflusst Nierenrückresorption von Amphetamin. Saurer Urin (<6) erhöht Elimination (schnellere Ausscheidung, t½ ~7-9h). Normaler pH (6-7,5) hält Basis-Elimination (~11h). Alkalischer Urin (>7,5) reduziert Elimination (langsamere Ausscheidung, t½ ~13-15h). Typischer Bereich: 5,5-8,0. Standard: Normaler pH (6-7,5).", + urinePHTooltip: "Urin-pH beeinflusst Nierenrückresorption von Amphetamin.\\n\\n__Effekte auf die Elimination:__\\n• __Sauer__ (<6): ***Erhöhte*** Elimination (***schnellere*** Ausscheidung), **t½ ~7-9h**\\n• __Normal__ (6-7,5): ***Basis***-Elimination (**t½ ~11h**)\\n• __Alkalisch__ (>7,5) → ***Reduzierte*** Elimination (***langsamere*** Ausscheidung), **t½ ~13-15h**\\n\\nTypischer Bereich: 5,5-8,0.\\n\\n__Standard:__ **Normaler pH** (6-7,5)", urinePHMode: "pH-Effekt", urinePHModeNormal: "Normal (pH 6-7,5, t½ 11h)", urinePHModeAcidic: "Sauer (pH <6, schnellere Elimination)", urinePHModeAlkaline: "Alkalisch (pH >7,5, langsamere Elimination)", urinePHValue: "pH-Wert", - urinePHValueTooltip: "Dein typischer Urin-pH (sauer=schnellere Ausscheidung, alkalisch=langsamer). Standard: {{phTendency}}. Bereich: 5,5-8,0.", + urinePHValueTooltip: "Dein typischer Urin-pH (sauer=schnellere Ausscheidung, alkalisch=langsamer).\\n\\nBereich: **5,5-8,0**.\\n\\n__Standard:__ **{{phTendency}}**", phValue: "pH-Wert", phUnit: "(5,5-8,0)", oralBioavailability: "Orale Bioverfügbarkeit", - oralBioavailabilityTooltip: "Anteil der LDX-Dosis, der ins Blut gelangt. Siehe [Bioverfügbarkeitsstudie](https://www.frontiersin.org/journals/pharmacology/articles/10.3389/fphar.2022.881198/full) (FDA-Label: 96,4%). Selten Anpassung nötig, außer bei dokumentierten Absorptionsproblemen. Standard: {{fOral}} ({{fOralPercent}}%).", + oralBioavailabilityTooltip: "Anteil der LDX-Dosis, der ins Blut gelangt.\\n\\nSiehe [Bioverfügbarkeitsstudie](https://www.frontiersin.org/journals/pharmacology/articles/10.3389/fphar.2022.881198/full) — **FDA-Label: 96,4%**.\\n\\nSelten Anpassung nötig, außer bei dokumentierten Absorptionsproblemen.\\n\\n__Standard:__ **{{fOral}} ({{fOralPercent}}%)**", steadyStateDays: "Medikationshistorie", - steadyStateDaysTooltip: "Anzahl vorheriger Tage stabiler Medikamentendosis zur Simulation der Akkumulation/Steady-State. 0 setzen für \"erster Tag ohne Vorgeschichte.\" Standard: {{steadyStateDays}} Tage. Max: 7.", + steadyStateDaysTooltip: "Anzahl vorheriger Tage stabiler Medikamentendosis zur Simulation der Akkumulation/Steady-State.\\n\\nWird diese Option ausgeschaltet, beginnt die Simulation an Tag eins ohne vorherige Medikationshistorie. Dasselbe gilt für den Wert **0**.\\n\\nMax: **7 Tage**.\\n\\n__Standard:__ **{{steadyStateDays}} Tage**", // Age-specific pharmacokinetics ageGroup: "Altersgruppe", - ageGroupTooltip: "Pädiatrische Personen (6-12 J.) zeigen schnellere d-Amphetamin-Elimination (t½ ~9h) verglichen mit Erwachsenen (~11h) aufgrund höherer gewichtsnormalisierter Stoffwechselrate. Siehe [Forschungsdokument](https://git.11001001.org/cbaoth/med-plan-assistant/src/branch/main/docs/2026-01-17_AI-Reseach_SimulatingLDXandD-AmphetaminePlasmaLevels.md#52-pediatric-vs-adult-modeling) Abschnitt 5.2. 'Benutzerdefiniert' wählen, um manuell konfigurierte Halbwertszeit zu verwenden. Standard: Erwachsener.", + ageGroupTooltip: "Pädiatrische Personen (6-12 J.) zeigen **schnellere d-Amphetamin-Elimination** (t½ ~9h) verglichen mit Erwachsenen (~11h) aufgrund höherer gewichtsnormalisierter Stoffwechselrate.\\n\\nSiehe [Forschungsdokument](https://git.11001001.org/cbaoth/med-plan-assistant/src/branch/main/docs/2026-01-17_AI-Reseach_SimulatingLDXandD-AmphetaminePlasmaLevels.md#52-pediatric-vs-adult-modeling) Abschnitt 5.2.\\n\\n'Benutzerdefiniert' wählen, um manuell konfigurierte Halbwertszeit zu verwenden.\\n\\n__Standard:__ **Erwachsener**", ageGroupAdult: "Erwachsener (t½ 11h)", ageGroupChild: "Kind 6-12 J. (t½ 9h)", ageGroupCustom: "Benutzerdefiniert (manuelle t½)", // Renal function effects renalFunction: "Niereninsuffizienz", - renalFunctionTooltip: "Schwere Niereninsuffizienz verlängert d-Amphetamin-Halbwertszeit um ~50% (von 11h auf 16,5h). FDA-Label empfiehlt Dosierungsobergrenzen: 50mg bei schwerer Insuffizienz, 30mg bei Nierenversagen (ESRD). Siehe [FDA-Label Abschnitt 8.6](https://www.accessdata.fda.gov/drugsatfda_docs/label/2017/021977s049lbl.pdf) und [Forschungsdokument](https://git.11001001.org/cbaoth/med-plan-assistant/src/branch/main/docs/2026-01-17_AI-Reseach_SimulatingLDXandD-AmphetaminePlasmaLevels.md#82-renal-function) Abschnitt 8.2. Standard: deaktiviert.", + renalFunctionTooltip: "Schwere Niereninsuffizienz verlängert d-Amphetamin-Halbwertszeit um **~50%** (von 11h auf 16,5h).\\n\\n__FDA-Label Dosierungsobergrenzen:__\\n• __Schwere Insuffizienz:__ **50mg**\\n• __Nierenversagen (ESRD):__ **30mg**\\n\\nSiehe [FDA-Label Abschnitt 8.6](https://www.accessdata.fda.gov/drugsatfda_docs/label/2017/021977s049lbl.pdf) und [Forschungsdokument](https://git.11001001.org/cbaoth/med-plan-assistant/src/branch/main/docs/2026-01-17_AI-Reseach_SimulatingLDXandD-AmphetaminePlasmaLevels.md#82-renal-function) Abschnitt 8.2.\\n\\n__Standard:__ **deaktiviert**", renalFunctionSeverity: "Schweregrad der Insuffizienz", renalFunctionNormal: "Normal (keine Anpassung)", renalFunctionMild: "Leicht (keine Anpassung)", diff --git a/src/locales/en.ts b/src/locales/en.ts index b4ea73d..b256c07 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -67,7 +67,7 @@ export const en = { refLineMax: "Max", pinChart: "Pin chart to top", unpinChart: "Unpin chart", - stickyChartTooltip: "Keep chart visible while scrolling through settings for real-time feedback. Default: off.", + stickyChartTooltip: "Keep chart visible while scrolling through settings for real-time feedback.\\n\\n__Default:__ **off**", chartViewDamphTooltip: "Show only the active metabolite (d-Amphetamine) concentration profile", chartViewLdxTooltip: "Show only the prodrug (Lisdexamfetamine) concentration profile", chartViewBothTooltip: "Show both d-Amphetamine and Lisdexamfetamine profiles together", @@ -78,13 +78,13 @@ export const en = { advancedSettings: "Advanced Settings", advancedSettingsWarning: "⚠️ These parameters affect simulation accuracy and may deviate from population averages. Adjust only if you have specific clinical data or research references.", standardVolumeOfDistribution: "Volume of Distribution (Vd)", - standardVdTooltip: "Defines how drug disperses in body. Adult: 377L (Roberts 2015), Child: ~150-200L. Weight-based scaling: ~5.4 L/kg (intended for adults >18 years based on [population PK analysis](https://pmc.ncbi.nlm.nih.gov/articles/PMC5572767/)). Affects all concentration calculations. Change only for pediatric or specialized simulations. Default: {{standardVdValue}}L ({{standardVdPreset}}).", + standardVdTooltip: "Defines how drug disperses in body.\\n\\n__Presets:__\\n• __Adult:__ **377L** (Roberts 2015)\\n• __Child:__ **~150-200L**\\n• __Weight-based:__ **~5.4 L/kg** (intended for adults >18 years based on [population PK analysis](https://pmc.ncbi.nlm.nih.gov/articles/PMC5572767/))\\n\\nAffects all concentration calculations. Change only for pediatric or specialized simulations.\\n\\n__Default:__ **{{standardVdValue}}L** ({{standardVdPreset}})", standardVdPresetAdult: "Adult (377L)", standardVdPresetChild: "Child (175L)", standardVdPresetCustom: "Custom", standardVdPresetWeightBased: "Weight-Based (~5.4 L/kg)", customVdValue: "Custom Vd (L)", - weightBasedVdInfo: "Weight-based Vd adjusts plasma concentrations based on body weight (~5.4 L/kg). Lighter persons → higher peaks, heavier → lower peaks. This option is intended for adults (>18 years) based on the population PK study. For pediatric patients, use the 'Child' preset.", + weightBasedVdInfo: "Weight-based Vd adjusts plasma concentrations based on body weight (~5.4 L/kg).\\n\\nLighter persons → higher peaks, heavier → lower peaks.\\n\\nThis option is intended for adults (>18 years) based on the population PK study. For pediatric patients, use the 'Child' preset.", xAxisTimeFormat: "Time Format", xAxisFormatContinuous: "Continuous", xAxisFormatContinuousDesc: "Endless sequence (0h, 6h, 12h...)", @@ -93,74 +93,74 @@ export const en = { xAxisFormat12h: "Time of Day (12h AM/PM)", xAxisFormat12hDesc: "Repeating 12h cycle in AM/PM format", showTemplateDayInChart: "Continuously Show Regular Plan", - showTemplateDayTooltip: "Display the regular medication plan as reference overlay at all times (default: enabled).", + showTemplateDayTooltip: "Display the regular medication plan as reference overlay at all times.\\n\\n__Default:__ **enabled**", simulationSettings: "Simulation Settings", showDayReferenceLines: "Show Day Separators", - showDayReferenceLinesTooltip: "Display vertical lines and status indicators separating days (default: enabled).", + showDayReferenceLinesTooltip: "Display vertical lines and status indicators separating days.\\n\\n__Default:__ **enabled**", showTherapeuticRangeLines: "Show Therapeutic Range", - showTherapeuticRangeLinesTooltip: "Display horizontal reference lines for therapeutic min/max concentrations (default: enabled).", + showTherapeuticRangeLinesTooltip: "Display horizontal reference lines for therapeutic min/max concentrations.\\n\\n__Default:__ **enabled**", simulationDuration: "Simulation Duration", - simulationDurationTooltip: "Number of days to simulate. Longer periods allow steady-state observation. Default: {{simulationDays}} days.", + simulationDurationTooltip: "Number of days to simulate. Longer periods allow steady-state observation.\\n\\n__Default:__ **{{simulationDays}} days**", displayedDays: "Visible Days (in Focus)", - displayedDaysTooltip: "How many days to display on screen at once. Smaller values zoom in on details. Default: {{displayedDays}} day(s).", + displayedDaysTooltip: "How many days to display on screen at once. Smaller values zoom in on details.\\n\\n__Default:__ **{{displayedDays}} day(s)**", yAxisRange: "Y-Axis Range (Concentration Zoom)", - yAxisRangeTooltip: "Manually set vertical axis limits (concentration scale). Leave empty for automatic scaling based on data. Default: auto.", + yAxisRangeTooltip: "Manually set vertical axis limits (concentration scale). Leave empty for automatic scaling based on data.\\n\\n__Default:__ **auto**", yAxisRangeAutoButton: "A", yAxisRangeAutoButtonTitle: "Determine range automatically based on data range", auto: "Auto", therapeuticRange: "Therapeutic Range (d-Amphetamine)", - therapeuticRangeTooltip: "Personalized concentration targets based on YOUR individual response. Set these after observing which levels provide symptom control vs. side effects. Reference ranges (highly variable): Adults ~10-80 ng/mL, Children ~20-120 ng/mL (due to lower body weight/Vd). Leave empty if unsure. Consult your physician.", + therapeuticRangeTooltip: "Personalized concentration targets based on **YOUR individual response**.\\n\\nSet these after observing which levels provide symptom control vs. side effects.\\n\\n**Reference ranges** (highly variable):\\n• __Adults:__ **~10-80 ng/mL**\\n• __Children:__ **~20-120 ng/mL** (due to lower body weight/Vd)\\n\\nLeave empty if unsure. ***Consult your physician.***", dAmphetamineParameters: "d-Amphetamine Parameters", halfLife: "Elimination Half-life", - halfLifeTooltip: "Time for body to clear half the d-amphetamine from blood. Affected by urine pH: acidic (<6) → 7-9h, neutral (6-7.5) → 10-12h, alkaline (>7.5) → 13-15h. See [therapeutic reference ranges](https://www.thieme-connect.com/products/ejournals/pdf/10.1055/a-2689-4911.pdf). Default: {{damphHalfLife}}h.", + halfLifeTooltip: "Time for body to clear half the d-amphetamine from blood.\\n\\n__Affected by urine pH:__\\n• __Acidic__ (<6) → **7-9h**\\n• __Neutral__ (6-7.5) → **10-12h**\\n• __Alkaline__ (>7.5) → **13-15h**\\n\\n*See* [therapeutic reference ranges](https://www.thieme-connect.com/products/ejournals/pdf/10.1055/a-2689-4911.pdf).\\n\\n__Default:__ **{{damphHalfLife}}h**", lisdexamfetamineParameters: "Lisdexamfetamine (LDX) Parameters", conversionHalfLife: "LDX→d-Amph Conversion Half-life", - conversionHalfLifeTooltip: "Time for red blood cells to convert half the inactive LDX prodrug into active d-amphetamine. Typical: 0.7-1.2h. Default: {{ldxHalfLife}}h.", + conversionHalfLifeTooltip: "Time for red blood cells to convert half the inactive LDX prodrug into active d-amphetamine.\\n\\n__Typical range:__ **0.7-1.2h**.\\n__Default:__ **{{ldxHalfLife}}h**", absorptionHalfLife: "Absorption Half-life", - absorptionHalfLifeTooltip: "Time for intestines to absorb half the LDX from stomach to blood. Delayed by food (~1h shift). Typical: 0.7-1.2h. Default: {{ldxAbsorptionHalfLife}}h.", + absorptionHalfLifeTooltip: "Time for intestines to absorb half the LDX from stomach to blood.\\n\\nDelayed by food (**~1h shift**).\\n\\n__Typical range:__ **0.7-1.2h**.\\n__Default:__ **{{ldxAbsorptionHalfLife}}h**", faster: "(faster >)", // Advanced Settings weightBasedVdScaling: "Weight-Based Volume of Distribution", - weightBasedVdTooltip: "Adjusts plasma concentrations based on body weight (proportional to ~5.4 L/kg). Lighter persons → higher peaks, heavier → lower peaks. When disabled, assumes 70 kg adult.", + weightBasedVdTooltip: "Adjusts plasma concentrations based on body weight (proportional to **~5.4 L/kg**).\\n\\n__Effects:__\\n• __Lighter persons__ → ***higher*** concentration peaks\\n• __Heavier persons__ → ***lower*** concentration peaks\\n\\n__When disabled:__ assumes **70 kg adult**", bodyWeight: "Body Weight", - bodyWeightTooltip: "Your body weight for concentration scaling. Used to calculate volume of distribution (Vd = weight × 5.4). See [population PK analysis](https://pmc.ncbi.nlm.nih.gov/articles/PMC5572767/). Default: {{bodyWeight}} kg.", + bodyWeightTooltip: "Your body weight for concentration scaling.\\n\\nUsed to calculate volume of distribution:\\n**Vd = weight × 5.4**\\n\\nSee [population PK analysis](https://pmc.ncbi.nlm.nih.gov/articles/PMC5572767/).\\n\\n__Default:__ **{{bodyWeight}} kg**", bodyWeightUnit: "kg", foodEffectEnabled: "Taken With Meal", foodEffectDelay: "Food Effect Delay", - foodEffectTooltip: "High-fat meals delay absorption without changing total exposure. Slows onset of effects (~1h delay). When disabled, assumes fasted state.", + foodEffectTooltip: "High-fat meals delay absorption **without changing total exposure**.\\n\\nSlows onset of effects by **~1 hour**.\\n\\nWhen disabled, assumes fasted state.", tmaxDelay: "Absorption Delay", - tmaxDelayTooltip: "Time delay when dose is taken with high-fat meal. Applied using per-dose food toggles (🍴 icon) in schedule. Research shows ~1h delay without peak reduction. See [study](https://pmc.ncbi.nlm.nih.gov/articles/PMC4823324/). Note: The high-fat meal used in this study consisted of 1 English muffin with butter, 1 fried egg, 1 slice of American cheese, 1 slice of Canadian bacon, 2 oz (57 g) of hash brown potatoes, and 8 fl oz (240 mL) of whole milk. Default: {{tmaxDelay}}h.", + tmaxDelayTooltip: "Time delay when dose is taken with **high-fat meal**. Applied using per-dose food toggles (🍴 icon) in schedule.\\n\\nResearch shows ~1h delay without peak reduction. *See* [study](https://pmc.ncbi.nlm.nih.gov/articles/PMC4823324/).\\n\\n__Note:__ The high-fat meal used in this study consisted of 1 English muffin with butter, 1 fried egg, 1 slice of American cheese, 1 slice of Canadian bacon, 2 oz (57 g) of hash brown potatoes, and 8 fl oz (240 mL) of whole milk.\\n\\n__Default:__ **{{tmaxDelay}}h**", tmaxDelayUnit: "h", urinePHTendency: "Urine pH Effects", - urinePHTooltip: "Urine pH affects kidney reabsorption of amphetamine. Acidic urine (<6) increases elimination (faster clearance, t½ ~7-9h). Normal pH (6-7.5) maintains baseline elimination (~11h). Alkaline urine (>7.5) reduces elimination (slower clearance, t½ ~13-15h). Typical range: 5.5-8.0. Default: Normal pH (6-7.5).", + urinePHTooltip: "Urine pH affects kidney reabsorption of amphetamine.\\n\\n__Effects on elimination:__\\n• __Acidic__ (<6) → ***Faster*** clearance, **t½ ~7-9h**\\n• __Normal__ (6-7.5) → ***Baseline*** elimination **~11h**\\n• __Alkaline__ (>7.5) → ***Slower*** clearance, **t½ ~13-15h**\\n\\n__Typical range:__ **5.5-8.0**\\n\\n__Default:__ **Normal pH** (6-7.5)", urinePHMode: "pH Effect", urinePHModeNormal: "Normal (pH 6-7.5, t½ 11h)", urinePHModeAcidic: "Acidic (pH <6, faster elimination)", urinePHModeAlkaline: "Alkaline (pH >7.5, slower elimination)", urinePHValue: "pH Value", - urinePHValueTooltip: "Your typical urine pH (acidic=faster clearance, alkaline=slower). Default: {{phTendency}}. Range: 5.5-8.0.", + urinePHValueTooltip: "Your typical urine pH (acidic=faster clearance, alkaline=slower).\\n\\nRange: **5.5-8.0**.\\n\\n__Default:__ **{{phTendency}}**", phValue: "pH Value", phUnit: "(5.5-8.0)", oralBioavailability: "Oral Bioavailability", - oralBioavailabilityTooltip: "Fraction of LDX dose that reaches bloodstream. See [bioavailability study](https://www.frontiersin.org/journals/pharmacology/articles/10.3389/fphar.2022.881198/full) (FDA label: 96.4%). Rarely needs adjustment unless you have documented absorption issues. Default: {{fOral}} ({{fOralPercent}}%).", + oralBioavailabilityTooltip: "Fraction of LDX dose that reaches bloodstream.\\n\\n*See* [bioavailability study](https://www.frontiersin.org/journals/pharmacology/articles/10.3389/fphar.2022.881198/full) — **FDA label: 96.4%**.\\n\\nRarely needs adjustment unless you have documented absorption issues.\\n\\n__Default:__ **{{fOral}} ({{fOralPercent}}%)**", steadyStateDays: "Medication History", - steadyStateDaysTooltip: "Number of prior days on stable medication dose to simulate accumulation/steady-state. Set 0 for \"first day from scratch.\" Default: {{steadyStateDays}} days. Max: 7.", + steadyStateDaysTooltip: "Number of prior days on stable medication dose to simulate accumulation/steady-state.\\n\\If this option is disabled, the simulation will begin from day one with no prior medication history. The same applies for the value is **0**.\\n\\nMax: **7 days**.\\n\\n__Default:__ **{{steadyStateDays}} days**.", // Age-specific pharmacokinetics ageGroup: "Age Group", - ageGroupTooltip: "Pediatric subjects (6-12y) exhibit faster d-amphetamine elimination (t½ ~9h) compared to adults (~11h) due to higher weight-normalized metabolic rate. See [research document](https://git.11001001.org/cbaoth/med-plan-assistant/src/branch/main/docs/2026-01-17_AI-Reseach_SimulatingLDXandD-AmphetaminePlasmaLevels.md#52-pediatric-vs-adult-modeling) Section 5.2. Select 'custom' to use your manually configured half-life. Default: adult.", + ageGroupTooltip: "Pediatric subjects (6-12y) exhibit **faster d-amphetamine elimination** (t½ ~9h) compared to adults (~11h) due to higher weight-normalized metabolic rate.\\n\\n*See* [research document](https://git.11001001.org/cbaoth/med-plan-assistant/src/branch/main/docs/2026-01-17_AI-Reseach_SimulatingLDXandD-AmphetaminePlasmaLevels.md#52-pediatric-vs-adult-modeling) *Section 5.2.*\\n\\nSelect 'custom' to use your manually configured half-life.\\n\\n__Default:__ **adult**.", ageGroupAdult: "Adult (t½ 11h)", ageGroupChild: "Child 6-12y (t½ 9h)", ageGroupCustom: "Custom (use manual t½)", // Renal function effects renalFunction: "Renal Impairment", - renalFunctionTooltip: "Severe renal impairment extends d-amphetamine half-life by ~50% (from 11h to 16.5h). FDA label recommends dose caps: 50mg for severe impairment, 30mg for ESRD. See [FDA Label Section 8.6](https://www.accessdata.fda.gov/drugsatfda_docs/label/2017/021977s049lbl.pdf) and [research document](https://git.11001001.org/cbaoth/med-plan-assistant/src/branch/main/docs/2026-01-17_AI-Reseach_SimulatingLDXandD-AmphetaminePlasmaLevels.md#82-renal-function) Section 8.2. Default: disabled.", + renalFunctionTooltip: "Severe renal impairment extends d-amphetamine half-life by **~50%** (from 11h to 16.5h).\\n\\n__FDA label dose caps:__\\n• __Severe impairment__: **50mg**\\n• __ESRD__: **30mg**\\n*See* [FDA Label Section 8.6](https://www.accessdata.fda.gov/drugsatfda_docs/label/2017/021977s049lbl.pdf) *and* [research document](https://git.11001001.org/cbaoth/med-plan-assistant/src/branch/main/docs/2026-01-17_AI-Reseach_SimulatingLDXandD-AmphetaminePlasmaLevels.md#82-renal-function) *Section 8.2.*\\n\\n__Default:__ **disabled**.", renalFunctionSeverity: "Impairment Severity", renalFunctionNormal: "Normal (no adjustment)", renalFunctionMild: "Mild (no adjustment)", @@ -292,10 +292,10 @@ export const en = { // Field validation - Warnings warningDuplicateTime: "⚠️ Multiple doses at same time.", warningZeroDose: "⚠️ Zero dose has no effect on simulation.", - warningAbsorptionOutOfRange: "⚠️ Typical range: 0.7-1.2h. Current value may be outside clinical norms.", - warningConversionOutOfRange: "⚠️ Typical range: 0.7-1.2h. Current value may be outside clinical norms.", - warningEliminationOutOfRange: "⚠️ Typical range: 9-12h (normal pH). Extended range 7-15h (pH effects). Current value is unusual.", - warningDoseAbove70mg: "⚠️ FDA-approved maximum: 70 mg. Higher doses lack safety data and increase cardiovascular risk.", + warningAbsorptionOutOfRange: "⚠️ Current value may be outside clinical norms.\\n\\n__Typical range:__ **0.7-1.2h**.", + warningConversionOutOfRange: "⚠️ Current value may be outside clinical norms.\\n\\n__Typical range:__ **0.7-1.2h**.", + warningEliminationOutOfRange: "⚠️ Current value may be outside clinical norms.\\n\\n__Typical range:__ **9-12h** (normal pH).\\nExtended range 7-15h (pH effects).", + warningDoseAbove70mg: "⚠️ Higher doses lack safety data and increase cardiovascular risk.\\n\\n__FDA-approved maximum:__ **70 mg**.\\n\\nConsult your physician before exceeding this dose.", // Time picker timePickerHour: "Hour", diff --git a/src/utils/contentFormatter.tsx b/src/utils/contentFormatter.tsx new file mode 100644 index 0000000..8a48035 --- /dev/null +++ b/src/utils/contentFormatter.tsx @@ -0,0 +1,230 @@ +/** + * Content Formatting Utilities + * + * Provides markdown-style formatting capabilities for various UI content including: + * - Tooltips + * - Error/warning messages + * - Info boxes + * - Help text + * + * Supported formatting (processed in this order): + * 1. Links: [text](url) + * 2. Bold+Italic: ***text*** + * 3. Bold: **text** + * 4. Italic: *text* + * 5. Underline: __text__ + * 6. Line breaks: \n + * + * @author Andreas Weyer + * @license MIT + */ + +import * as React from 'react'; + +/** + * Renders formatted formatContent with markdown-style formatting support. + * Can be used for tooltips, error messages, info boxes, and other UI text. + * + * Processing order: Links → Bold+Italic (***) → Bold (**) → Italic (*) → Underline (__) → Line breaks (\n) + * + * @example + * ```typescript + * // In tooltip + * formatContent("See [study](https://example.com)\\n__Important:__ **Take with food**.") + * + * // In error message + * formatContent("**Error:** Value must be between *5* and *50*.") + * + * // In info box + * formatContent("***Note:*** This setting affects accuracy.\\n\\nSee [docs](https://example.com).") + * ``` + * + * @param text - The text to format with markdown-style syntax + * @returns Formatted React nodes ready for rendering + */ +export const formatContent = (text: string): React.ReactNode => { + // Helper to process text segments with bold/italic/underline formatting + const processFormatting = (segment: string, keyPrefix: string): React.ReactNode[] => { + const parts: React.ReactNode[] = []; + let remaining = segment; + let partIndex = 0; + + // Process bold+italic first (***text***) + const boldItalicRegex = /\*\*\*([^*]+)\*\*\*/g; + let lastIdx = 0; + let boldItalicMatch; + + while ((boldItalicMatch = boldItalicRegex.exec(remaining)) !== null) { + // Add text before bold+italic + if (boldItalicMatch.index > lastIdx) { + const beforeBoldItalic = remaining.substring(lastIdx, boldItalicMatch.index); + parts.push(...processBoldItalicAndUnderline(beforeBoldItalic, `${keyPrefix}-bi${partIndex++}`)); + } + // Add bold+italic text + parts.push( + + {boldItalicMatch[1]} + + ); + lastIdx = boldItalicRegex.lastIndex; + } + + // Add remaining text with bold/italic/underline processing + if (lastIdx < remaining.length) { + parts.push(...processBoldItalicAndUnderline(remaining.substring(lastIdx), `${keyPrefix}-bi${partIndex++}`)); + } + + return parts.length > 0 ? parts : [remaining]; + }; + + // Helper to process bold/italic/underline (after bold+italic ***) + const processBoldItalicAndUnderline = (segment: string, keyPrefix: string): React.ReactNode[] => { + const parts: React.ReactNode[] = []; + const boldRegex = /\*\*([^*]+)\*\*/g; + let lastIdx = 0; + let boldMatch; + + while ((boldMatch = boldRegex.exec(segment)) !== null) { + // Add text before bold + if (boldMatch.index > lastIdx) { + const beforeBold = segment.substring(lastIdx, boldMatch.index); + parts.push(...processItalicAndUnderline(beforeBold, `${keyPrefix}-b${lastIdx}`)); + } + // Add bold text + parts.push( + + {boldMatch[1]} + + ); + lastIdx = boldRegex.lastIndex; + } + + // Add remaining text with italic/underline processing + if (lastIdx < segment.length) { + parts.push(...processItalicAndUnderline(segment.substring(lastIdx), `${keyPrefix}-b${lastIdx}`)); + } + + return parts.length > 0 ? parts : [segment]; + }; + + // Helper to process italic and underline (*text* and __text__) + const processItalicAndUnderline = (segment: string, keyPrefix: string): React.ReactNode[] => { + const parts: React.ReactNode[] = []; + // Match single * that's not part of ** or inside links + const italicRegex = /(? lastIdx) { + const beforeItalic = segment.substring(lastIdx, italicMatch.index); + parts.push(...processUnderline(beforeItalic, `${keyPrefix}-i${lastIdx}`)); + } + // Add italic text + parts.push( + + {italicMatch[1]} + + ); + lastIdx = italicRegex.lastIndex; + } + + // Add remaining text with underline processing + if (lastIdx < segment.length) { + parts.push(...processUnderline(segment.substring(lastIdx), `${keyPrefix}-i${lastIdx}`)); + } + + return parts.length > 0 ? parts : [segment]; + }; + + // Helper to process underline (__text__) - final level of formatting + const processUnderline = (segment: string, keyPrefix: string): React.ReactNode[] => { + const parts: React.ReactNode[] = []; + const underlineRegex = /__([^_]+)__/g; + let lastIdx = 0; + let underlineMatch; + + while ((underlineMatch = underlineRegex.exec(segment)) !== null) { + // Add text before underline (plain text) + if (underlineMatch.index > lastIdx) { + parts.push(segment.substring(lastIdx, underlineMatch.index)); + } + // Add underlined text + parts.push( + + {underlineMatch[1]} + + ); + lastIdx = underlineRegex.lastIndex; + } + + // Add remaining plain text + if (lastIdx < segment.length) { + parts.push(segment.substring(lastIdx)); + } + + return parts.length > 0 ? parts : [segment]; + }; + + // Split by line breaks first + const lines = text.split('\\n'); + const result: React.ReactNode[] = []; + + lines.forEach((line, lineIndex) => { + const lineParts: React.ReactNode[] = []; + const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g; + let lastIndex = 0; + let match; + + while ((match = linkRegex.exec(line)) !== null) { + // Add text before link with formatting + if (match.index > lastIndex) { + const beforeLink = line.substring(lastIndex, match.index); + lineParts.push(...processFormatting(beforeLink, `line${lineIndex}-seg${lastIndex}`)); + } + // Add link + lineParts.push( + + {match[1]} + + ); + lastIndex = linkRegex.lastIndex; + } + + // Add remaining text with formatting + if (lastIndex < line.length) { + const remaining = line.substring(lastIndex); + lineParts.push(...processFormatting(remaining, `line${lineIndex}-seg${lastIndex}`)); + } + + // Add line content + if (lineParts.length > 0) { + result.push(...lineParts); + } else { + result.push(line); + } + + // Add line break if not the last line + if (lineIndex < lines.length - 1) { + result.push(
); + } + }); + + return result.length > 0 ? result : text; +}; + +/** + * Alias for renderContent for use in non-tooltip contexts (error messages, info boxes, etc.). + * Provides the same markdown-style formatting capabilities. + * + * @param text - The text to format with markdown-style syntax + * @returns Formatted React nodes ready for rendering + */ +export const formatText = formatContent; // Alias for non-tooltip contexts