Add contentFormatter for tooltips and error/warning bubbles, format i18n texts, add missing high-dose warning

This commit is contained in:
2026-02-07 15:45:42 +00:00
parent ed79247223
commit 651097b3fb
8 changed files with 456 additions and 128 deletions

View File

@@ -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__` → <u>text</u>
- **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';
<TooltipContent>
<p className="text-xs max-w-xs">
{formatContent(t('myTooltip'))}
</p>
</TooltipContent>
```
**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
<FormNumericInput
errorMessage="Value must be between 5 and 50"
warningMessage="Value is outside typical range"
/>
```
**With formatting** (enhanced):
```tsx
import { formatContent } from '../utils/contentFormatter';
// In FormNumericInput component (form-numeric-input.tsx):
{hasError && isFocused && errorMessage && (
<div className="absolute top-full left-0 w-full mt-1 p-2 bg-red-100 dark:bg-red-900/20 border border-red-300 dark:border-red-700 rounded text-xs text-red-800 dark:text-red-200 z-50">
{formatContent(errorMessage)}
</div>
)}
```
**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
<p className="text-yellow-800 dark:text-yellow-200">
{t('advancedSettingsWarning')}
</p>
```
**With formatting:**
```tsx
<div className="text-yellow-800 dark:text-yellow-200">
{formatContent(t('advancedSettingsWarning'))}
</div>
```
**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
<DialogDescription>
{formatContent(t('deleteConfirmation'))}
</DialogDescription>
// 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

View File

@@ -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<DayScheduleProps> = ({
// 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 (
<div key={dose.id} className="space-y-2">
@@ -252,8 +255,8 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
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'))}
/>
<FormNumericInput
value={dose.ldx}
@@ -263,9 +266,13 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
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]"
/>
<div className="flex gap-2 sm:contents">

View File

@@ -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<string,
return result;
};
/**
* Helper function to render tooltip content with inline source links.
* Parses [link text](url) markdown-style syntax and renders as clickable links.
* @example "See [this study](https://example.com)" → clickable link within tooltip
*/
const renderTooltipWithLinks = (text: string): React.ReactNode => {
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(
<a
key={`link-${match.index}`}
href={match[2]}
target="_blank"
rel="noopener noreferrer"
className="underline italic text-yellow-300 hover:text-yellow-200 cursor-pointer"
>
{match[1]}
</a>
);
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 = ({
</button>
</TooltipTrigger>
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{tWithDefaults(t, 'showTemplateDayTooltip', defaultsForT)}</p>
<p className="text-xs max-w-xs">{formatContent(tWithDefaults(t, 'showTemplateDayTooltip', defaultsForT))}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -348,7 +310,7 @@ const Settings = ({
</button>
</TooltipTrigger>
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{tWithDefaults(t, 'showDayReferenceLinesTooltip', defaultsForT)}</p>
<p className="text-xs max-w-xs">{formatContent(tWithDefaults(t, 'showDayReferenceLinesTooltip', defaultsForT))}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -377,7 +339,7 @@ const Settings = ({
</button>
</TooltipTrigger>
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{tWithDefaults(t, 'showTherapeuticRangeLinesTooltip', defaultsForT)}</p>
<p className="text-xs max-w-xs">{formatContent(tWithDefaults(t, 'showTherapeuticRangeLinesTooltip', defaultsForT))}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -400,7 +362,7 @@ const Settings = ({
</button>
</TooltipTrigger>
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{tWithDefaults(t, 'therapeuticRangeTooltip', defaultsForT)}</p>
<p className="text-xs max-w-xs">{formatContent(tWithDefaults(t, 'therapeuticRangeTooltip', defaultsForT))}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -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 = ({
</button>
</TooltipTrigger>
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{tWithDefaults(t, 'displayedDaysTooltip', defaultsForT)}</p>
<p className="text-xs max-w-xs">{formatContent(tWithDefaults(t, 'displayedDaysTooltip', defaultsForT))}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -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 = ({
</button>
</TooltipTrigger>
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{tWithDefaults(t, 'yAxisRangeTooltip', defaultsForT)}</p>
<p className="text-xs max-w-xs">{formatContent(tWithDefaults(t, 'yAxisRangeTooltip', defaultsForT))}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -506,7 +468,7 @@ const Settings = ({
showResetButton={true}
defaultValue={defaultsForT.yAxisMin}
warning={!!yAxisRangeError}
warningMessage={yAxisRangeError}
warningMessage={formatText(yAxisRangeError)}
/>
<span className="text-muted-foreground">-</span>
<FormNumericInput
@@ -521,7 +483,7 @@ const Settings = ({
showResetButton={true}
defaultValue={defaultsForT.yAxisMax}
warning={!!yAxisRangeError}
warningMessage={yAxisRangeError}
warningMessage={formatText(yAxisRangeError)}
/>
</div>
</div>
@@ -600,7 +562,7 @@ const Settings = ({
</button>
</TooltipTrigger>
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{tWithDefaults(t, 'simulationDurationTooltip', defaultsForT)}</p>
<p className="text-xs max-w-xs">{formatContent(tWithDefaults(t, 'simulationDurationTooltip', defaultsForT))}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -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 = ({
</button>
</TooltipTrigger>
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{tWithDefaults(t, 'steadyStateDaysTooltip', defaultsForT)}</p>
<p className="text-xs max-w-xs">{formatContent(tWithDefaults(t, 'steadyStateDaysTooltip', defaultsForT))}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -702,7 +664,7 @@ const Settings = ({
</button>
</TooltipTrigger>
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{renderTooltipWithLinks(tWithDefaults(t, 'halfLifeTooltip', defaultsForT))}</p>
<p className="text-xs max-w-xs">{formatContent(tWithDefaults(t, 'halfLifeTooltip', defaultsForT))}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -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 = ({
</button>
</TooltipTrigger>
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{tWithDefaults(t, 'conversionHalfLifeTooltip', defaultsForT)}</p>
<p className="text-xs max-w-xs">{formatContent(tWithDefaults(t, 'conversionHalfLifeTooltip', defaultsForT))}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -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 = ({
</button>
</TooltipTrigger>
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{tWithDefaults(t, 'absorptionHalfLifeTooltip', defaultsForT)}</p>
<p className="text-xs max-w-xs">{formatContent(tWithDefaults(t, 'absorptionHalfLifeTooltip', defaultsForT))}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -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 = ({
</button>
</TooltipTrigger>
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{renderTooltipWithLinks(tWithDefaults(t, 'standardVdTooltip', {
<p className="text-xs max-w-xs">{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 = ({
</button>
</TooltipTrigger>
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{renderTooltipWithLinks(tWithDefaults(t, 'bodyWeightTooltip', defaultsForT))}</p>
<p className="text-xs max-w-xs">{formatContent(tWithDefaults(t, 'bodyWeightTooltip', defaultsForT))}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -929,7 +891,7 @@ const Settings = ({
</button>
</TooltipTrigger>
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{renderTooltipWithLinks(tWithDefaults(t, 'tmaxDelayTooltip', defaultsForT))}</p>
<p className="text-xs max-w-xs">{formatContent(tWithDefaults(t, 'tmaxDelayTooltip', defaultsForT))}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -967,7 +929,7 @@ const Settings = ({
</button>
</TooltipTrigger>
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{tWithDefaults(t, 'urinePHTooltip', defaultsForT)}</p>
<p className="text-xs max-w-xs">{formatContent(tWithDefaults(t, 'urinePHTooltip', defaultsForT))}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -1009,7 +971,7 @@ const Settings = ({
</button>
</TooltipTrigger>
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{renderTooltipWithLinks(tWithDefaults(t, 'ageGroupTooltip', defaultsForT))}</p>
<p className="text-xs max-w-xs">{formatContent(tWithDefaults(t, 'ageGroupTooltip', defaultsForT))}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -1061,7 +1023,7 @@ const Settings = ({
</button>
</TooltipTrigger>
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{renderTooltipWithLinks(tWithDefaults(t, 'renalFunctionTooltip', defaultsForT))}</p>
<p className="text-xs max-w-xs">{formatContent(tWithDefaults(t, 'renalFunctionTooltip', defaultsForT))}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -1111,7 +1073,7 @@ const Settings = ({
</button>
</TooltipTrigger>
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{renderTooltipWithLinks(tWithDefaults(t, 'oralBioavailabilityTooltip', defaultsForT))}</p>
<p className="text-xs max-w-xs">{formatContent(tWithDefaults(t, 'oralBioavailabilityTooltip', defaultsForT))}</p>
</TooltipContent>
</Tooltip>
</div>

View File

@@ -30,8 +30,8 @@ interface NumericInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElem
error?: boolean
warning?: boolean
required?: boolean
errorMessage?: string
warningMessage?: string
errorMessage?: React.ReactNode
warningMessage?: React.ReactNode
inputWidth?: string // Custom width for the input field (e.g., 'w-16', 'w-20')
}

View File

@@ -24,8 +24,8 @@ interface TimeInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement
error?: boolean
warning?: boolean
required?: boolean
errorMessage?: string
warningMessage?: string
errorMessage?: React.ReactNode
warningMessage?: React.ReactNode
}
const FormTimeInput = React.forwardRef<HTMLInputElement, TimeInputProps>(

View File

@@ -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)",

View File

@@ -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",

View File

@@ -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(
<strong key={`${keyPrefix}-bolditalic-${partIndex++}`} className="font-semibold italic">
{boldItalicMatch[1]}
</strong>
);
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(
<strong key={`${keyPrefix}-bold-${boldMatch.index}`} className="font-semibold">
{boldMatch[1]}
</strong>
);
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 = /(?<!\*)\*(?!\*)([^*]+)\*(?!\*)/g;
let lastIdx = 0;
let italicMatch;
while ((italicMatch = italicRegex.exec(segment)) !== null) {
// Add text before italic (process underline in it)
if (italicMatch.index > lastIdx) {
const beforeItalic = segment.substring(lastIdx, italicMatch.index);
parts.push(...processUnderline(beforeItalic, `${keyPrefix}-i${lastIdx}`));
}
// Add italic text
parts.push(
<em key={`${keyPrefix}-italic-${italicMatch.index}`} className="italic">
{italicMatch[1]}
</em>
);
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(
<span key={`${keyPrefix}-underline-${underlineMatch.index}`} className="underline">
{underlineMatch[1]}
</span>
);
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(
<a
key={`line${lineIndex}-link-${match.index}`}
href={match[2]}
target="_blank"
rel="noopener noreferrer"
className="underline italic text-yellow-300 hover:text-yellow-200 cursor-pointer"
>
{match[1]}
</a>
);
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(<br key={`br-${lineIndex}`} />);
}
});
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