Update custome translations to i18n and various improvements
This commit is contained in:
@@ -12,9 +12,12 @@
|
|||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"i18next": "^25.7.1",
|
||||||
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
"lucide-react": "^0.554.0",
|
"lucide-react": "^0.554.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
|
"react-i18next": "^16.3.5",
|
||||||
"react-is": "^19.2.0",
|
"react-is": "^19.2.0",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"recharts": "^3.3.0",
|
"recharts": "^3.3.0",
|
||||||
|
|||||||
18
src/App.tsx
18
src/App.tsx
@@ -54,7 +54,8 @@ const MedPlanAssistant = () => {
|
|||||||
yAxisMax,
|
yAxisMax,
|
||||||
showTemplateDay,
|
showTemplateDay,
|
||||||
simulationDays,
|
simulationDays,
|
||||||
displayedDays
|
displayedDays,
|
||||||
|
showDayReferenceLines
|
||||||
} = uiSettings;
|
} = uiSettings;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -68,8 +69,8 @@ const MedPlanAssistant = () => {
|
|||||||
<header className="mb-8">
|
<header className="mb-8">
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl md:text-4xl font-bold tracking-tight">{t.appTitle}</h1>
|
<h1 className="text-3xl md:text-4xl font-bold tracking-tight">{t('appTitle')}</h1>
|
||||||
<p className="text-muted-foreground mt-1">{t.appSubtitle}</p>
|
<p className="text-muted-foreground mt-1">{t('appSubtitle')}</p>
|
||||||
</div>
|
</div>
|
||||||
<LanguageSelector currentLanguage={currentLanguage} onLanguageChange={changeLanguage} t={t} />
|
<LanguageSelector currentLanguage={currentLanguage} onLanguageChange={changeLanguage} t={t} />
|
||||||
</div>
|
</div>
|
||||||
@@ -84,19 +85,19 @@ const MedPlanAssistant = () => {
|
|||||||
onClick={() => updateUiSetting('chartView', 'damph')}
|
onClick={() => updateUiSetting('chartView', 'damph')}
|
||||||
variant={chartView === 'damph' ? 'default' : 'secondary'}
|
variant={chartView === 'damph' ? 'default' : 'secondary'}
|
||||||
>
|
>
|
||||||
{t.dAmphetamine}
|
{t('dAmphetamine')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => updateUiSetting('chartView', 'ldx')}
|
onClick={() => updateUiSetting('chartView', 'ldx')}
|
||||||
variant={chartView === 'ldx' ? 'default' : 'secondary'}
|
variant={chartView === 'ldx' ? 'default' : 'secondary'}
|
||||||
>
|
>
|
||||||
{t.lisdexamfetamine}
|
{t('lisdexamfetamine')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => updateUiSetting('chartView', 'both')}
|
onClick={() => updateUiSetting('chartView', 'both')}
|
||||||
variant={chartView === 'both' ? 'default' : 'secondary'}
|
variant={chartView === 'both' ? 'default' : 'secondary'}
|
||||||
>
|
>
|
||||||
{t.both}
|
{t('both')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -105,6 +106,7 @@ const MedPlanAssistant = () => {
|
|||||||
templateProfile={showTemplateDay ? templateProfile : null}
|
templateProfile={showTemplateDay ? templateProfile : null}
|
||||||
chartView={chartView}
|
chartView={chartView}
|
||||||
showDayTimeOnXAxis={showDayTimeOnXAxis}
|
showDayTimeOnXAxis={showDayTimeOnXAxis}
|
||||||
|
showDayReferenceLines={showDayReferenceLines}
|
||||||
therapeuticRange={therapeuticRange}
|
therapeuticRange={therapeuticRange}
|
||||||
simulationDays={simulationDays}
|
simulationDays={simulationDays}
|
||||||
displayedDays={displayedDays}
|
displayedDays={displayedDays}
|
||||||
@@ -145,8 +147,8 @@ const MedPlanAssistant = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer className="mt-8 p-4 bg-muted rounded-lg text-sm text-muted-foreground border">
|
<footer className="mt-8 p-4 bg-muted rounded-lg text-sm text-muted-foreground border">
|
||||||
<h3 className="font-semibold mb-2 text-foreground">{t.importantNote}</h3>
|
<h3 className="font-semibold mb-2 text-foreground">{t('importantNote')}</h3>
|
||||||
<p>{t.disclaimer}</p>
|
<p>{t('disclaimer')}</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -48,13 +48,11 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<CardTitle className="text-lg">
|
<CardTitle className="text-lg">
|
||||||
{day.isTemplate ? t.regularPlan : t.dayNumber.replace('{{number}}', String(dayIndex + 1))}
|
{day.isTemplate ? t('regularPlan') : t('deviatingPlan')}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
{day.isTemplate && (
|
<Badge variant="secondary" className="text-xs">
|
||||||
<Badge variant="secondary" className="text-xs">
|
{t('day')} {dayIndex + 1}
|
||||||
{t.day} 1
|
</Badge>
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{canAddDay && (
|
{canAddDay && (
|
||||||
@@ -62,7 +60,7 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
|
|||||||
onClick={() => onAddDay(day.id)}
|
onClick={() => onAddDay(day.id)}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
title={t.cloneDay}
|
title={t('cloneDay')}
|
||||||
>
|
>
|
||||||
<Copy className="h-4 w-4" />
|
<Copy className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -73,7 +71,7 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="border-destructive text-destructive hover:bg-destructive hover:text-destructive-foreground"
|
className="border-destructive text-destructive hover:bg-destructive hover:text-destructive-foreground"
|
||||||
title={t.removeDay}
|
title={t('removeDay')}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -84,41 +82,49 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
|
|||||||
<CardContent className="space-y-3">
|
<CardContent className="space-y-3">
|
||||||
{/* Dose table header */}
|
{/* Dose table header */}
|
||||||
<div className="grid grid-cols-[120px_1fr_auto] gap-3 text-sm font-medium text-muted-foreground">
|
<div className="grid grid-cols-[120px_1fr_auto] gap-3 text-sm font-medium text-muted-foreground">
|
||||||
<div>{t.time}</div>
|
<div>{t('time')}</div>
|
||||||
<div>{t.ldx} (mg)</div>
|
<div>{t('ldx')} (mg)</div>
|
||||||
<div></div>
|
<div></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Dose rows */}
|
{/* Dose rows */}
|
||||||
{day.doses.map((dose) => (
|
{day.doses.map((dose) => {
|
||||||
<div key={dose.id} className="grid grid-cols-[120px_1fr_auto] gap-3 items-center">
|
// Check for duplicate times
|
||||||
<FormTimeInput
|
const duplicateTimeCount = day.doses.filter(d => d.time === dose.time).length;
|
||||||
value={dose.time}
|
const hasDuplicateTime = duplicateTimeCount > 1;
|
||||||
onChange={(value) => onUpdateDose(day.id, dose.id, 'time', value)}
|
|
||||||
required={true}
|
return (
|
||||||
errorMessage={t.errorTimeRequired}
|
<div key={dose.id} className="grid grid-cols-[120px_1fr_auto] gap-3 items-center">
|
||||||
/>
|
<FormTimeInput
|
||||||
<FormNumericInput
|
value={dose.time}
|
||||||
value={dose.ldx}
|
onChange={(value) => onUpdateDose(day.id, dose.id, 'time', value)}
|
||||||
onChange={(value) => onUpdateDose(day.id, dose.id, 'ldx', value)}
|
required={true}
|
||||||
increment={doseIncrement}
|
warning={hasDuplicateTime}
|
||||||
min={0}
|
errorMessage={t('errorTimeRequired')}
|
||||||
unit="mg"
|
warningMessage={t('warningDuplicateTime')}
|
||||||
required={true}
|
/>
|
||||||
errorMessage={t.errorNumberRequired}
|
<FormNumericInput
|
||||||
/>
|
value={dose.ldx}
|
||||||
<Button
|
onChange={(value) => onUpdateDose(day.id, dose.id, 'ldx', value)}
|
||||||
onClick={() => onRemoveDose(day.id, dose.id)}
|
increment={doseIncrement}
|
||||||
size="sm"
|
min={0}
|
||||||
variant="ghost"
|
unit="mg"
|
||||||
disabled={day.isTemplate && day.doses.length === 1}
|
required={true}
|
||||||
className="h-9 w-9 p-0"
|
errorMessage={t('errorNumberRequired')}
|
||||||
title={t.removeDose}
|
/>
|
||||||
>
|
<Button
|
||||||
<Trash2 className="h-4 w-4" />
|
onClick={() => onRemoveDose(day.id, dose.id)}
|
||||||
</Button>
|
size="sm"
|
||||||
</div>
|
variant="ghost"
|
||||||
))}
|
disabled={day.isTemplate && day.doses.length === 1}
|
||||||
|
className="h-9 w-9 p-0"
|
||||||
|
title={t('removeDose')}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
{/* Add dose button */}
|
{/* Add dose button */}
|
||||||
{day.doses.length < 5 && (
|
{day.doses.length < 5 && (
|
||||||
@@ -129,7 +135,7 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
|
|||||||
className="w-full mt-2"
|
className="w-full mt-2"
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
{t.addDose}
|
{t('addDose')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -144,7 +150,7 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
{t.addDay}
|
{t('addDay')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,14 +15,14 @@ import { Label } from './ui/label';
|
|||||||
const LanguageSelector = ({ currentLanguage, onLanguageChange, t }: any) => {
|
const LanguageSelector = ({ currentLanguage, onLanguageChange, t }: any) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Label className="text-sm font-medium">{t.language}:</Label>
|
<Label className="text-sm font-medium">{t('language')}:</Label>
|
||||||
<Select value={currentLanguage} onValueChange={onLanguageChange}>
|
<Select value={currentLanguage} onValueChange={onLanguageChange}>
|
||||||
<SelectTrigger className="w-32">
|
<SelectTrigger className="w-32">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="en">{t.english}</SelectItem>
|
<SelectItem value="en">{t('english')}</SelectItem>
|
||||||
<SelectItem value="de">{t.german}</SelectItem>
|
<SelectItem value="de">{t('german')}</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import { Button } from './ui/button';
|
|||||||
import { Switch } from './ui/switch';
|
import { Switch } from './ui/switch';
|
||||||
import { Label } from './ui/label';
|
import { Label } from './ui/label';
|
||||||
import { Separator } from './ui/separator';
|
import { Separator } from './ui/separator';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip';
|
||||||
|
|
||||||
const Settings = ({
|
const Settings = ({
|
||||||
pkParams,
|
pkParams,
|
||||||
@@ -28,72 +30,119 @@ const Settings = ({
|
|||||||
t
|
t
|
||||||
}: any) => {
|
}: any) => {
|
||||||
const { showDayTimeOnXAxis, yAxisMin, yAxisMax, showTemplateDay, simulationDays, displayedDays } = uiSettings;
|
const { showDayTimeOnXAxis, yAxisMin, yAxisMax, showTemplateDay, simulationDays, displayedDays } = uiSettings;
|
||||||
|
const showDayReferenceLines = (uiSettings as any).showDayReferenceLines ?? true;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{t.advancedSettings}</CardTitle>
|
<CardTitle className="text-lg">{t('diagramSettings')}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="showDayTimeOnXAxis" className="font-medium">
|
<Label className="font-medium">{t('xAxisTimeFormat')}</Label>
|
||||||
{t.show24hTimeAxis}
|
<TooltipProvider>
|
||||||
</Label>
|
<Select
|
||||||
<Switch
|
value={showDayTimeOnXAxis}
|
||||||
id="showDayTimeOnXAxis"
|
onValueChange={value => onUpdateUiSetting('showDayTimeOnXAxis', value)}
|
||||||
checked={showDayTimeOnXAxis}
|
>
|
||||||
onCheckedChange={checked => onUpdateUiSetting('showDayTimeOnXAxis', checked)}
|
<SelectTrigger className="w-[240px]">
|
||||||
/>
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<SelectItem value="continuous">
|
||||||
|
{t('xAxisFormatContinuous')}
|
||||||
|
</SelectItem>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right">
|
||||||
|
<p className="text-xs">{t('xAxisFormatContinuousDesc')}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<SelectItem value="24h">
|
||||||
|
{t('xAxisFormat24h')}
|
||||||
|
</SelectItem>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right">
|
||||||
|
<p className="text-xs">{t('xAxisFormat24hDesc')}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<SelectItem value="12h">
|
||||||
|
{t('xAxisFormat12h')}
|
||||||
|
</SelectItem>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right">
|
||||||
|
<p className="text-xs">{t('xAxisFormat12hDesc')}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Label htmlFor="showTemplateDay" className="font-medium">
|
<Switch
|
||||||
{t.showTemplateDayInChart}
|
id="showDayReferenceLines"
|
||||||
|
checked={showDayReferenceLines}
|
||||||
|
onCheckedChange={checked => onUpdateUiSetting('showDayReferenceLines', checked)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="showDayReferenceLines" className="font-regular">
|
||||||
|
{t('showDayReferenceLines')}
|
||||||
</Label>
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
<Switch
|
<Switch
|
||||||
id="showTemplateDay"
|
id="showTemplateDay"
|
||||||
checked={showTemplateDay}
|
checked={showTemplateDay}
|
||||||
onCheckedChange={checked => onUpdateUiSetting('showTemplateDay', checked)}
|
onCheckedChange={checked => onUpdateUiSetting('showTemplateDay', checked)}
|
||||||
/>
|
/>
|
||||||
|
<Label htmlFor="showTemplateDay" className="font-regular">
|
||||||
|
{t('showTemplateDayInChart')}
|
||||||
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="font-medium">{t.simulationDuration}</Label>
|
<Label className="font-medium">{t('simulationDuration')}</Label>
|
||||||
<FormNumericInput
|
<FormNumericInput
|
||||||
value={simulationDays}
|
value={simulationDays}
|
||||||
onChange={val => onUpdateUiSetting('simulationDays', val)}
|
onChange={val => onUpdateUiSetting('simulationDays', val)}
|
||||||
increment={1}
|
increment={1}
|
||||||
min={3}
|
min={3}
|
||||||
max={7}
|
max={7}
|
||||||
unit={t.days}
|
unit={t('days')}
|
||||||
required={true}
|
required={true}
|
||||||
errorMessage={t.errorNumberRequired}
|
errorMessage={t('errorNumberRequired')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="font-medium">{t.displayedDays}</Label>
|
<Label className="font-medium">{t('displayedDays')}</Label>
|
||||||
<FormNumericInput
|
<FormNumericInput
|
||||||
value={displayedDays}
|
value={displayedDays}
|
||||||
onChange={val => onUpdateUiSetting('displayedDays', val)}
|
onChange={val => onUpdateUiSetting('displayedDays', val)}
|
||||||
increment={1}
|
increment={1}
|
||||||
min={1}
|
min={1}
|
||||||
max={parseInt(simulationDays, 10) || 3}
|
max={parseInt(simulationDays, 10) || 3}
|
||||||
unit={t.days}
|
unit={t('days')}
|
||||||
required={true}
|
required={true}
|
||||||
errorMessage={t.errorNumberRequired}
|
errorMessage={t('errorNumberRequired')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="font-medium">{t.yAxisRange}</Label>
|
<Label className="font-medium">{t('yAxisRange')}</Label>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FormNumericInput
|
<FormNumericInput
|
||||||
value={yAxisMin}
|
value={yAxisMin}
|
||||||
onChange={val => onUpdateUiSetting('yAxisMin', val)}
|
onChange={val => onUpdateUiSetting('yAxisMin', val)}
|
||||||
increment={5}
|
increment={5}
|
||||||
min={0}
|
min={0}
|
||||||
placeholder={t.auto}
|
placeholder={t('auto')}
|
||||||
allowEmpty={true}
|
allowEmpty={true}
|
||||||
clearButton={true}
|
clearButton={true}
|
||||||
/>
|
/>
|
||||||
@@ -103,7 +152,7 @@ const Settings = ({
|
|||||||
onChange={val => onUpdateUiSetting('yAxisMax', val)}
|
onChange={val => onUpdateUiSetting('yAxisMax', val)}
|
||||||
increment={5}
|
increment={5}
|
||||||
min={0}
|
min={0}
|
||||||
placeholder={t.auto}
|
placeholder={t('auto')}
|
||||||
unit="ng/ml"
|
unit="ng/ml"
|
||||||
allowEmpty={true}
|
allowEmpty={true}
|
||||||
clearButton={true}
|
clearButton={true}
|
||||||
@@ -112,16 +161,16 @@ const Settings = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="font-medium">{t.therapeuticRange}</Label>
|
<Label className="font-medium">{t('therapeuticRange')}</Label>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FormNumericInput
|
<FormNumericInput
|
||||||
value={therapeuticRange.min}
|
value={therapeuticRange.min}
|
||||||
onChange={val => onUpdateTherapeuticRange('min', val)}
|
onChange={val => onUpdateTherapeuticRange('min', val)}
|
||||||
increment={0.5}
|
increment={0.5}
|
||||||
min={0}
|
min={0}
|
||||||
placeholder={t.min}
|
placeholder={t('min')}
|
||||||
required={true}
|
required={true}
|
||||||
errorMessage={t.therapeuticRangeMinRequired || 'Minimum therapeutic range is required'}
|
errorMessage={t('therapeuticRangeMinRequired') || 'Minimum therapeutic range is required'}
|
||||||
/>
|
/>
|
||||||
<span className="text-muted-foreground">-</span>
|
<span className="text-muted-foreground">-</span>
|
||||||
<FormNumericInput
|
<FormNumericInput
|
||||||
@@ -129,19 +178,19 @@ const Settings = ({
|
|||||||
onChange={val => onUpdateTherapeuticRange('max', val)}
|
onChange={val => onUpdateTherapeuticRange('max', val)}
|
||||||
increment={0.5}
|
increment={0.5}
|
||||||
min={0}
|
min={0}
|
||||||
placeholder={t.max}
|
placeholder={t('max')}
|
||||||
unit="ng/ml"
|
unit="ng/ml"
|
||||||
required={true}
|
required={true}
|
||||||
errorMessage={t.therapeuticRangeMaxRequired || 'Maximum therapeutic range is required'}
|
errorMessage={t('therapeuticRangeMaxRequired') || 'Maximum therapeutic range is required'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Separator className="my-4" />
|
<Separator className="my-4" />
|
||||||
|
|
||||||
<h3 className="text-lg font-semibold">{t.dAmphetamineParameters}</h3>
|
<h3 className="text-lg font-semibold">{t('dAmphetamineParameters')}</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="font-medium">{t.halfLife}</Label>
|
<Label className="font-medium">{t('halfLife')}</Label>
|
||||||
<FormNumericInput
|
<FormNumericInput
|
||||||
value={pkParams.damph.halfLife}
|
value={pkParams.damph.halfLife}
|
||||||
onChange={val => onUpdatePkParams('damph', { halfLife: val })}
|
onChange={val => onUpdatePkParams('damph', { halfLife: val })}
|
||||||
@@ -149,15 +198,15 @@ const Settings = ({
|
|||||||
min={0.1}
|
min={0.1}
|
||||||
unit="h"
|
unit="h"
|
||||||
required={true}
|
required={true}
|
||||||
errorMessage={t.halfLifeRequired || 'Half-life is required'}
|
errorMessage={t('halfLifeRequired') || 'Half-life is required'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Separator className="my-4" />
|
<Separator className="my-4" />
|
||||||
|
|
||||||
<h3 className="text-lg font-semibold">{t.lisdexamfetamineParameters}</h3>
|
<h3 className="text-lg font-semibold">{t('lisdexamfetamineParameters')}</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="font-medium">{t.conversionHalfLife}</Label>
|
<Label className="font-medium">{t('conversionHalfLife')}</Label>
|
||||||
<FormNumericInput
|
<FormNumericInput
|
||||||
value={pkParams.ldx.halfLife}
|
value={pkParams.ldx.halfLife}
|
||||||
onChange={val => onUpdatePkParams('ldx', { halfLife: val })}
|
onChange={val => onUpdatePkParams('ldx', { halfLife: val })}
|
||||||
@@ -165,20 +214,20 @@ const Settings = ({
|
|||||||
min={0.1}
|
min={0.1}
|
||||||
unit="h"
|
unit="h"
|
||||||
required={true}
|
required={true}
|
||||||
errorMessage={t.conversionHalfLifeRequired || 'Conversion half-life is required'}
|
errorMessage={t('conversionHalfLifeRequired') || 'Conversion half-life is required'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="font-medium">{t.absorptionRate}</Label>
|
<Label className="font-medium">{t('absorptionRate')}</Label>
|
||||||
<FormNumericInput
|
<FormNumericInput
|
||||||
value={pkParams.ldx.absorptionRate}
|
value={pkParams.ldx.absorptionRate}
|
||||||
onChange={val => onUpdatePkParams('ldx', { absorptionRate: val })}
|
onChange={val => onUpdatePkParams('ldx', { absorptionRate: val })}
|
||||||
increment={0.1}
|
increment={0.1}
|
||||||
min={0.1}
|
min={0.1}
|
||||||
unit={t.faster}
|
unit={t('faster')}
|
||||||
required={true}
|
required={true}
|
||||||
errorMessage={t.absorptionRateRequired || 'Absorption rate is required'}
|
errorMessage={t('absorptionRateRequired') || 'Absorption rate is required'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -190,7 +239,7 @@ const Settings = ({
|
|||||||
variant="destructive"
|
variant="destructive"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
{t.resetAllSettings}
|
{t('resetAllSettings')}
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ const CHART_COLORS = {
|
|||||||
correctedLdx: '#059669', // emerald-600 (success, dash-dot)
|
correctedLdx: '#059669', // emerald-600 (success, dash-dot)
|
||||||
|
|
||||||
// Reference lines
|
// Reference lines
|
||||||
|
regularPlanDivider: '#22c55e', // green-500
|
||||||
|
deviationDayDivider: '#9ca3af', // gray-400
|
||||||
therapeuticMin: '#22c55e', // green-500
|
therapeuticMin: '#22c55e', // green-500
|
||||||
therapeuticMax: '#ef4444', // red-500
|
therapeuticMax: '#ef4444', // red-500
|
||||||
dayDivider: '#9ca3af', // gray-400
|
dayDivider: '#9ca3af', // gray-400
|
||||||
@@ -38,6 +40,7 @@ const SimulationChart = ({
|
|||||||
templateProfile,
|
templateProfile,
|
||||||
chartView,
|
chartView,
|
||||||
showDayTimeOnXAxis,
|
showDayTimeOnXAxis,
|
||||||
|
showDayReferenceLines,
|
||||||
therapeuticRange,
|
therapeuticRange,
|
||||||
simulationDays,
|
simulationDays,
|
||||||
displayedDays,
|
displayedDays,
|
||||||
@@ -46,15 +49,27 @@ const SimulationChart = ({
|
|||||||
t
|
t
|
||||||
}: any) => {
|
}: any) => {
|
||||||
const totalHours = (parseInt(simulationDays, 10) || 3) * 24;
|
const totalHours = (parseInt(simulationDays, 10) || 3) * 24;
|
||||||
|
const dispDays = parseInt(displayedDays, 10) || 2;
|
||||||
|
|
||||||
// Generate ticks for continuous time axis (every 6 hours)
|
// Dynamically calculate tick interval based on displayed days
|
||||||
|
// Aim for ~40-50 pixels per tick for readability
|
||||||
|
const xTickInterval = React.useMemo(() => {
|
||||||
|
// Scale interval with displayed days: 1 day = 1h, 2 days = 2h, 3-4 days = 3h, 5+ days = 6h
|
||||||
|
if (dispDays <= 1) return 1;
|
||||||
|
if (dispDays <= 2) return 2;
|
||||||
|
if (dispDays <= 4) return 3;
|
||||||
|
if (dispDays <= 6) return 4;
|
||||||
|
return 6;
|
||||||
|
}, [dispDays]);
|
||||||
|
|
||||||
|
// Generate ticks for continuous time axis
|
||||||
const chartTicks = React.useMemo(() => {
|
const chartTicks = React.useMemo(() => {
|
||||||
const ticks = [];
|
const ticks = [];
|
||||||
for (let i = 0; i <= totalHours; i += 6) {
|
for (let i = 0; i <= totalHours; i += xTickInterval) {
|
||||||
ticks.push(i);
|
ticks.push(i);
|
||||||
}
|
}
|
||||||
return ticks;
|
return ticks;
|
||||||
}, [totalHours]);
|
}, [totalHours, xTickInterval]);
|
||||||
|
|
||||||
const chartDomain = React.useMemo(() => {
|
const chartDomain = React.useMemo(() => {
|
||||||
const numMin = parseFloat(yAxisMin);
|
const numMin = parseFloat(yAxisMin);
|
||||||
@@ -107,7 +122,6 @@ const SimulationChart = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const simDays = parseInt(simulationDays, 10) || 3;
|
const simDays = parseInt(simulationDays, 10) || 3;
|
||||||
const dispDays = parseInt(displayedDays, 10) || 2;
|
|
||||||
|
|
||||||
// Y-axis takes ~80px, scrollable area gets the rest
|
// Y-axis takes ~80px, scrollable area gets the rest
|
||||||
const yAxisWidth = 80;
|
const yAxisWidth = 80;
|
||||||
@@ -134,7 +148,7 @@ const SimulationChart = ({
|
|||||||
{(chartView === 'damph' || chartView === 'both') && (
|
{(chartView === 'damph' || chartView === 'both') && (
|
||||||
<Line
|
<Line
|
||||||
dataKey="combinedDamph"
|
dataKey="combinedDamph"
|
||||||
name={`${t.dAmphetamine}`}
|
name={`${t('dAmphetamine')}`}
|
||||||
stroke={CHART_COLORS.idealDamph}
|
stroke={CHART_COLORS.idealDamph}
|
||||||
strokeWidth={2.5}
|
strokeWidth={2.5}
|
||||||
dot={false}
|
dot={false}
|
||||||
@@ -144,7 +158,7 @@ const SimulationChart = ({
|
|||||||
{(chartView === 'ldx' || chartView === 'both') && (
|
{(chartView === 'ldx' || chartView === 'both') && (
|
||||||
<Line
|
<Line
|
||||||
dataKey="combinedLdx"
|
dataKey="combinedLdx"
|
||||||
name={`${t.lisdexamfetamine}`}
|
name={`${t('lisdexamfetamine')}`}
|
||||||
stroke={CHART_COLORS.idealLdx}
|
stroke={CHART_COLORS.idealLdx}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
strokeDasharray="3 3"
|
strokeDasharray="3 3"
|
||||||
@@ -155,7 +169,7 @@ const SimulationChart = ({
|
|||||||
{templateProfile && (chartView === 'damph' || chartView === 'both') && (
|
{templateProfile && (chartView === 'damph' || chartView === 'both') && (
|
||||||
<Line
|
<Line
|
||||||
dataKey="templateDamph"
|
dataKey="templateDamph"
|
||||||
name={`${t.dAmphetamine} (${t.regularPlan} ${t.continuation || 'continuation'})`}
|
name={`${t('dAmphetamine')} (${t('regularPlan')} ${t('continuation')})`}
|
||||||
stroke={CHART_COLORS.idealDamph}
|
stroke={CHART_COLORS.idealDamph}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
strokeDasharray="3 3"
|
strokeDasharray="3 3"
|
||||||
@@ -166,7 +180,7 @@ const SimulationChart = ({
|
|||||||
{templateProfile && (chartView === 'ldx' || chartView === 'both') && (
|
{templateProfile && (chartView === 'ldx' || chartView === 'both') && (
|
||||||
<Line
|
<Line
|
||||||
dataKey="templateLdx"
|
dataKey="templateLdx"
|
||||||
name={`${t.lisdexamfetamine} (${t.regularPlan} ${t.continuation || 'continuation'})`}
|
name={`${t('lisdexamfetamine')} (${t('regularPlan')} ${t('continuation')})`}
|
||||||
stroke={CHART_COLORS.idealLdx}
|
stroke={CHART_COLORS.idealLdx}
|
||||||
strokeWidth={1.5}
|
strokeWidth={1.5}
|
||||||
strokeDasharray="3 3"
|
strokeDasharray="3 3"
|
||||||
@@ -190,6 +204,8 @@ const SimulationChart = ({
|
|||||||
syncId="medPlanChart"
|
syncId="medPlanChart"
|
||||||
>
|
>
|
||||||
<XAxis
|
<XAxis
|
||||||
|
xAxisId="hours"
|
||||||
|
label={{ value: showDayTimeOnXAxis === 'continuous' ? t('axisLabelHours') : t('axisLabelTimeOfDay'), position: 'insideBottom', offset: -10, style: { fontStyle: 'italic', color: '#666' } }}
|
||||||
dataKey="timeHours"
|
dataKey="timeHours"
|
||||||
type="number"
|
type="number"
|
||||||
domain={[0, totalHours]}
|
domain={[0, totalHours]}
|
||||||
@@ -197,28 +213,35 @@ const SimulationChart = ({
|
|||||||
tickCount={chartTicks.length}
|
tickCount={chartTicks.length}
|
||||||
interval={0}
|
interval={0}
|
||||||
tickFormatter={(h) => {
|
tickFormatter={(h) => {
|
||||||
if (showDayTimeOnXAxis) {
|
if (showDayTimeOnXAxis === '24h') {
|
||||||
// Show 24h repeating format (0-23h)
|
// Show 24h repeating format (0-23h)
|
||||||
return `${h % 24}${t.hour}`;
|
return `${h % 24}${t('hour')}`;
|
||||||
|
} else if (showDayTimeOnXAxis === '12h') {
|
||||||
|
// Show 12h AM/PM format
|
||||||
|
const hour12 = h % 24;
|
||||||
|
if (hour12 === 12) return t('tickNoon');
|
||||||
|
const displayHour = hour12 === 0 ? 12 : hour12 > 12 ? hour12 - 12 : hour12;
|
||||||
|
const period = hour12 < 12 ? 'a' : 'p';
|
||||||
|
return `${displayHour}${period}`;
|
||||||
} else {
|
} else {
|
||||||
// Show continuous time (0, 6, 12, 18, 24, 30, 36, ...)
|
// Show continuous time (0, 6, 12, 18, 24, 30, 36, ...)
|
||||||
return `${h}${t.hour}`;
|
return `${h}`;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
xAxisId="hours"
|
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis
|
||||||
yAxisId="concentration"
|
yAxisId="concentration"
|
||||||
//label={{ value: t.concentration, angle: -90, position: 'insideLeft', offset: -10 }}
|
label={{ value: t('axisLabelConcentration'), angle: -90, position: 'insideLeft', offset: '0 -10', style: { fontStyle: 'italic', color: '#666' } }}
|
||||||
domain={chartDomain as any}
|
domain={chartDomain as any}
|
||||||
allowDecimals={false}
|
allowDecimals={false}
|
||||||
/>
|
tickCount={20}
|
||||||
|
/>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
formatter={(value: any, name) => [`${typeof value === 'number' ? value.toFixed(1) : value} ${t.ngml}`, name]}
|
formatter={(value: any, name) => [`${typeof value === 'number' ? value.toFixed(1) : value} ${t('ngml')}`, name]}
|
||||||
labelFormatter={(label, payload) => {
|
labelFormatter={(label, payload) => {
|
||||||
// Extract timeHours from the payload data point
|
// Extract timeHours from the payload data point
|
||||||
const timeHours = payload?.[0]?.payload?.timeHours ?? label;
|
const timeHours = payload?.[0]?.payload?.timeHours ?? label;
|
||||||
return `${t.hour.replace('h', 'Hour')}: ${timeHours}${t.hour}`;
|
return `${t('hour').replace('h', 'Hour')}: ${timeHours}${t('hour')}`;
|
||||||
}}
|
}}
|
||||||
wrapperStyle={{ pointerEvents: 'none', zIndex: 200 }}
|
wrapperStyle={{ pointerEvents: 'none', zIndex: 200 }}
|
||||||
allowEscapeViewBox={{ x: false, y: false }}
|
allowEscapeViewBox={{ x: false, y: false }}
|
||||||
@@ -227,11 +250,29 @@ const SimulationChart = ({
|
|||||||
/>
|
/>
|
||||||
<CartesianGrid strokeDasharray="1 1" xAxisId="hours" yAxisId="concentration" />
|
<CartesianGrid strokeDasharray="1 1" xAxisId="hours" yAxisId="concentration" />
|
||||||
|
|
||||||
|
{showDayReferenceLines !== false && [...Array(dispDays).keys()].map(day => (
|
||||||
|
<ReferenceLine
|
||||||
|
key={`day-${day+1}`}
|
||||||
|
x={24 * (day+1)}
|
||||||
|
label={{
|
||||||
|
value: (day === 0 ? t('refLineRegularPlan') : t('refLineDeviatingPlan')) + ' (' + t('refLineDayX', { x: day+1 }) + ')',
|
||||||
|
position: 'insideTopRight',
|
||||||
|
style: {
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
fontStyle: 'italic',
|
||||||
|
fill: day === 0 ? CHART_COLORS.regularPlanDivider : CHART_COLORS.deviationDayDivider
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
stroke={day === 0 ? CHART_COLORS.regularPlanDivider : CHART_COLORS.deviationDayDivider}
|
||||||
|
//strokeDasharray="0 0"
|
||||||
|
xAxisId="hours"
|
||||||
|
yAxisId="concentration"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
{(chartView === 'damph' || chartView === 'both') && (
|
{(chartView === 'damph' || chartView === 'both') && (
|
||||||
<ReferenceLine
|
<ReferenceLine
|
||||||
y={parseFloat(therapeuticRange.min) || 0}
|
y={parseFloat(therapeuticRange.min) || 0}
|
||||||
label={{ value: t.min, position: 'insideTopLeft' }}
|
label={{ value: t('refLineMin'), position: 'insideTopLeft' }}
|
||||||
stroke={CHART_COLORS.therapeuticMin}
|
stroke={CHART_COLORS.therapeuticMin}
|
||||||
strokeDasharray="3 3"
|
strokeDasharray="3 3"
|
||||||
xAxisId="hours"
|
xAxisId="hours"
|
||||||
@@ -241,7 +282,7 @@ const SimulationChart = ({
|
|||||||
{(chartView === 'damph' || chartView === 'both') && (
|
{(chartView === 'damph' || chartView === 'both') && (
|
||||||
<ReferenceLine
|
<ReferenceLine
|
||||||
y={parseFloat(therapeuticRange.max) || 0}
|
y={parseFloat(therapeuticRange.max) || 0}
|
||||||
label={{ value: t.max, position: 'insideTopLeft' }}
|
label={{ value: t('refLineMax'), position: 'insideTopLeft' }}
|
||||||
stroke={CHART_COLORS.therapeuticMax}
|
stroke={CHART_COLORS.therapeuticMax}
|
||||||
strokeDasharray="3 3"
|
strokeDasharray="3 3"
|
||||||
xAxisId="hours"
|
xAxisId="hours"
|
||||||
@@ -265,7 +306,7 @@ const SimulationChart = ({
|
|||||||
<Line
|
<Line
|
||||||
type="monotone"
|
type="monotone"
|
||||||
dataKey="combinedDamph"
|
dataKey="combinedDamph"
|
||||||
name={`${t.dAmphetamine}`}
|
name={`${t('dAmphetamine')}`}
|
||||||
stroke={CHART_COLORS.idealDamph}
|
stroke={CHART_COLORS.idealDamph}
|
||||||
strokeWidth={2.5}
|
strokeWidth={2.5}
|
||||||
dot={false}
|
dot={false}
|
||||||
@@ -278,7 +319,7 @@ const SimulationChart = ({
|
|||||||
<Line
|
<Line
|
||||||
type="monotone"
|
type="monotone"
|
||||||
dataKey="combinedLdx"
|
dataKey="combinedLdx"
|
||||||
name={`${t.lisdexamfetamine}`}
|
name={`${t('lisdexamfetamine')}`}
|
||||||
stroke={CHART_COLORS.idealLdx}
|
stroke={CHART_COLORS.idealLdx}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
dot={false}
|
dot={false}
|
||||||
@@ -293,7 +334,7 @@ const SimulationChart = ({
|
|||||||
<Line
|
<Line
|
||||||
type="monotone"
|
type="monotone"
|
||||||
dataKey="templateDamph"
|
dataKey="templateDamph"
|
||||||
name={`${t.dAmphetamine} (${t.regularPlan} ${t.continuation || 'continuation'})`}
|
name={`${t('dAmphetamine')} (${t('regularPlan')} ${t('continuation')})`}
|
||||||
stroke={CHART_COLORS.idealDamph}
|
stroke={CHART_COLORS.idealDamph}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
strokeDasharray="3 3"
|
strokeDasharray="3 3"
|
||||||
@@ -308,7 +349,7 @@ const SimulationChart = ({
|
|||||||
<Line
|
<Line
|
||||||
type="monotone"
|
type="monotone"
|
||||||
dataKey="templateLdx"
|
dataKey="templateLdx"
|
||||||
name={`${t.lisdexamfetamine} (${t.regularPlan} ${t.continuation || 'continuation'})`}
|
name={`${t('lisdexamfetamine')} (${t('regularPlan')} ${t('continuation')})`}
|
||||||
stroke={CHART_COLORS.idealLdx}
|
stroke={CHART_COLORS.idealLdx}
|
||||||
strokeWidth={1.5}
|
strokeWidth={1.5}
|
||||||
strokeDasharray="3 3"
|
strokeDasharray="3 3"
|
||||||
|
|||||||
@@ -25,8 +25,10 @@ interface NumericInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElem
|
|||||||
allowEmpty?: boolean
|
allowEmpty?: boolean
|
||||||
clearButton?: boolean
|
clearButton?: boolean
|
||||||
error?: boolean
|
error?: boolean
|
||||||
|
warning?: boolean
|
||||||
required?: boolean
|
required?: boolean
|
||||||
errorMessage?: string
|
errorMessage?: string
|
||||||
|
warningMessage?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const FormNumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
|
const FormNumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
|
||||||
@@ -41,18 +43,22 @@ const FormNumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
|
|||||||
allowEmpty = false,
|
allowEmpty = false,
|
||||||
clearButton = false,
|
clearButton = false,
|
||||||
error = false,
|
error = false,
|
||||||
|
warning = false,
|
||||||
required = false,
|
required = false,
|
||||||
errorMessage = 'This field is required',
|
errorMessage = 'Time is required',
|
||||||
|
warningMessage,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const [showError, setShowError] = React.useState(false)
|
const [showError, setShowError] = React.useState(false)
|
||||||
|
const [showWarning, setShowWarning] = React.useState(false)
|
||||||
const [touched, setTouched] = React.useState(false)
|
const [touched, setTouched] = React.useState(false)
|
||||||
const containerRef = React.useRef<HTMLDivElement>(null)
|
const containerRef = React.useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
// Check if value is invalid (check validity regardless of touch state)
|
// Check if value is invalid (check validity regardless of touch state)
|
||||||
const isInvalid = required && !allowEmpty && (value === '' || value === null || value === undefined)
|
const isInvalid = required && !allowEmpty && (value === '' || value === null || value === undefined)
|
||||||
const hasError = error || isInvalid
|
const hasError = error || isInvalid
|
||||||
|
const hasWarning = warning && !hasError
|
||||||
|
|
||||||
// Check validity on mount and when value changes
|
// Check validity on mount and when value changes
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -123,6 +129,7 @@ const FormNumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
|
|||||||
|
|
||||||
const handleFocus = () => {
|
const handleFocus = () => {
|
||||||
setShowError(hasError)
|
setShowError(hasError)
|
||||||
|
setShowWarning(hasWarning)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAlignmentClass = () => {
|
const getAlignmentClass = () => {
|
||||||
@@ -197,11 +204,16 @@ const FormNumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{unit && <span className="text-sm text-muted-foreground whitespace-nowrap">{unit}</span>}
|
{unit && <span className="text-sm text-muted-foreground whitespace-nowrap">{unit}</span>}
|
||||||
{hasError && showError && (
|
{hasError && showError && errorMessage && (
|
||||||
<div className="absolute top-full left-0 mt-1 z-50 w-64 bg-destructive text-destructive-foreground text-xs p-2 rounded-md shadow-lg">
|
<div className="absolute top-full left-0 mt-1 z-50 w-64 bg-destructive text-destructive-foreground text-xs p-2 rounded-md shadow-lg">
|
||||||
{errorMessage}
|
{errorMessage}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{hasWarning && showWarning && warningMessage && (
|
||||||
|
<div className="absolute top-full left-0 mt-1 z-50 w-48 bg-yellow-500 text-yellow-950 text-xs p-2 rounded-md shadow-lg">
|
||||||
|
{warningMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,10 @@ interface TimeInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement
|
|||||||
unit?: string
|
unit?: string
|
||||||
align?: 'left' | 'center' | 'right'
|
align?: 'left' | 'center' | 'right'
|
||||||
error?: boolean
|
error?: boolean
|
||||||
|
warning?: boolean
|
||||||
required?: boolean
|
required?: boolean
|
||||||
errorMessage?: string
|
errorMessage?: string
|
||||||
|
warningMessage?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const FormTimeInput = React.forwardRef<HTMLInputElement, TimeInputProps>(
|
const FormTimeInput = React.forwardRef<HTMLInputElement, TimeInputProps>(
|
||||||
@@ -32,20 +34,24 @@ const FormTimeInput = React.forwardRef<HTMLInputElement, TimeInputProps>(
|
|||||||
unit,
|
unit,
|
||||||
align = 'center',
|
align = 'center',
|
||||||
error = false,
|
error = false,
|
||||||
|
warning = false,
|
||||||
required = false,
|
required = false,
|
||||||
errorMessage = 'Time is required',
|
errorMessage = 'Time is required',
|
||||||
|
warningMessage,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const [displayValue, setDisplayValue] = React.useState(value)
|
const [displayValue, setDisplayValue] = React.useState(value)
|
||||||
const [isPickerOpen, setIsPickerOpen] = React.useState(false)
|
const [isPickerOpen, setIsPickerOpen] = React.useState(false)
|
||||||
const [showError, setShowError] = React.useState(false)
|
const [showError, setShowError] = React.useState(false)
|
||||||
|
const [showWarning, setShowWarning] = React.useState(false)
|
||||||
const containerRef = React.useRef<HTMLDivElement>(null)
|
const containerRef = React.useRef<HTMLDivElement>(null)
|
||||||
const [pickerHours, pickerMinutes] = (value || "00:00").split(':').map(Number)
|
const [pickerHours, pickerMinutes] = (value || "00:00").split(':').map(Number)
|
||||||
|
|
||||||
// Check if value is invalid (check validity regardless of touch state)
|
// Check if value is invalid (check validity regardless of touch state)
|
||||||
const isInvalid = required && (!value || value.trim() === '')
|
const isInvalid = required && (!value || value.trim() === '')
|
||||||
const hasError = error || isInvalid
|
const hasError = error || isInvalid
|
||||||
|
const hasWarning = warning && !hasError
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setDisplayValue(value)
|
setDisplayValue(value)
|
||||||
@@ -93,6 +99,7 @@ const FormTimeInput = React.forwardRef<HTMLInputElement, TimeInputProps>(
|
|||||||
|
|
||||||
const handleFocus = () => {
|
const handleFocus = () => {
|
||||||
setShowError(hasError)
|
setShowError(hasError)
|
||||||
|
setShowWarning(hasWarning)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePickerChange = (part: 'h' | 'm', val: number) => {
|
const handlePickerChange = (part: 'h' | 'm', val: number) => {
|
||||||
@@ -130,7 +137,8 @@ const FormTimeInput = React.forwardRef<HTMLInputElement, TimeInputProps>(
|
|||||||
"w-20 h-9 z-20",
|
"w-20 h-9 z-20",
|
||||||
"rounded-r-none",
|
"rounded-r-none",
|
||||||
getAlignmentClass(),
|
getAlignmentClass(),
|
||||||
hasError && "border-destructive focus-visible:ring-destructive"
|
hasError && "border-destructive focus-visible:ring-destructive",
|
||||||
|
hasWarning && !hasError && "border-yellow-500 focus-visible:ring-yellow-500"
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@@ -196,11 +204,16 @@ const FormTimeInput = React.forwardRef<HTMLInputElement, TimeInputProps>(
|
|||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
{unit && <span className="text-sm text-muted-foreground whitespace-nowrap">{unit}</span>}
|
{unit && <span className="text-sm text-muted-foreground whitespace-nowrap">{unit}</span>}
|
||||||
{hasError && showError && (
|
{hasError && showError && errorMessage && (
|
||||||
<div className="absolute top-full left-0 mt-1 z-50 w-48 bg-destructive text-destructive-foreground text-xs p-2 rounded-md shadow-lg">
|
<div className="absolute top-full left-0 mt-1 z-50 w-48 bg-destructive text-destructive-foreground text-xs p-2 rounded-md shadow-lg">
|
||||||
{errorMessage}
|
{errorMessage}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{hasWarning && showWarning && warningMessage && (
|
||||||
|
<div className="absolute top-full left-0 mt-1 z-50 w-48 bg-yellow-500 text-yellow-950 text-xs p-2 rounded-md shadow-lg">
|
||||||
|
{warningMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,13 +40,14 @@ export interface TherapeuticRange {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UiSettings {
|
export interface UiSettings {
|
||||||
showDayTimeOnXAxis: boolean;
|
showDayTimeOnXAxis: 'continuous' | '24h' | '12h';
|
||||||
showTemplateDay: boolean;
|
showTemplateDay: boolean;
|
||||||
chartView: 'ldx' | 'damph' | 'both';
|
chartView: 'ldx' | 'damph' | 'both';
|
||||||
yAxisMin: string;
|
yAxisMin: string;
|
||||||
yAxisMax: string;
|
yAxisMax: string;
|
||||||
simulationDays: string;
|
simulationDays: string;
|
||||||
displayedDays: string;
|
displayedDays: string;
|
||||||
|
showDayReferenceLines?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppState {
|
export interface AppState {
|
||||||
@@ -98,7 +99,7 @@ export const getDefaultState = (): AppState => ({
|
|||||||
therapeuticRange: { min: '10.5', max: '11.5' },
|
therapeuticRange: { min: '10.5', max: '11.5' },
|
||||||
doseIncrement: '2.5',
|
doseIncrement: '2.5',
|
||||||
uiSettings: {
|
uiSettings: {
|
||||||
showDayTimeOnXAxis: true,
|
showDayTimeOnXAxis: 'continuous',
|
||||||
showTemplateDay: false,
|
showTemplateDay: false,
|
||||||
chartView: 'both',
|
chartView: 'both',
|
||||||
yAxisMin: '0',
|
yAxisMin: '0',
|
||||||
|
|||||||
@@ -22,12 +22,19 @@ export const useAppState = () => {
|
|||||||
if (savedState) {
|
if (savedState) {
|
||||||
const parsedState = JSON.parse(savedState);
|
const parsedState = JSON.parse(savedState);
|
||||||
const defaults = getDefaultState();
|
const defaults = getDefaultState();
|
||||||
|
|
||||||
|
// Migrate old boolean showDayTimeOnXAxis to new string enum
|
||||||
|
let migratedUiSettings = {...defaults.uiSettings, ...parsedState.uiSettings};
|
||||||
|
if (typeof migratedUiSettings.showDayTimeOnXAxis === 'boolean') {
|
||||||
|
migratedUiSettings.showDayTimeOnXAxis = migratedUiSettings.showDayTimeOnXAxis ? '24h' : 'continuous';
|
||||||
|
}
|
||||||
|
|
||||||
setAppState({
|
setAppState({
|
||||||
...defaults,
|
...defaults,
|
||||||
...parsedState,
|
...parsedState,
|
||||||
pkParams: {...defaults.pkParams, ...parsedState.pkParams},
|
pkParams: {...defaults.pkParams, ...parsedState.pkParams},
|
||||||
days: parsedState.days || defaults.days,
|
days: parsedState.days || defaults.days,
|
||||||
uiSettings: {...defaults.uiSettings, ...parsedState.uiSettings},
|
uiSettings: migratedUiSettings,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -178,11 +185,24 @@ export const useAppState = () => {
|
|||||||
...prev,
|
...prev,
|
||||||
days: prev.days.map(day => {
|
days: prev.days.map(day => {
|
||||||
if (day.id !== dayId) return day;
|
if (day.id !== dayId) return day;
|
||||||
|
|
||||||
|
// Update the dose field
|
||||||
|
const updatedDoses = day.doses.map(dose =>
|
||||||
|
dose.id === doseId ? { ...dose, [field]: value } : dose
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sort by time if time field was changed
|
||||||
|
if (field === 'time') {
|
||||||
|
updatedDoses.sort((a, b) => {
|
||||||
|
const timeA = a.time || '00:00';
|
||||||
|
const timeB = b.time || '00:00';
|
||||||
|
return timeA.localeCompare(timeB);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...day,
|
...day,
|
||||||
doses: day.doses.map(dose =>
|
doses: updatedDoses
|
||||||
dose.id === doseId ? { ...dose, [field]: value } : dose
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,34 +1,25 @@
|
|||||||
/**
|
/**
|
||||||
* Language Hook
|
* Language Hook using react-i18next
|
||||||
*
|
*
|
||||||
* Manages application language state and provides translation access.
|
* Provides internationalization with substitution capabilities using react-i18next.
|
||||||
* Persists language preference to localStorage.
|
|
||||||
*
|
*
|
||||||
* @author Andreas Weyer
|
* @author Andreas Weyer
|
||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { translations, getInitialLanguage } from '../locales/index';
|
|
||||||
|
|
||||||
export const useLanguage = () => {
|
export const useLanguage = () => {
|
||||||
const [currentLanguage, setCurrentLanguage] = React.useState(getInitialLanguage);
|
const { t, i18n } = useTranslation();
|
||||||
|
|
||||||
// Get current translations
|
|
||||||
const t = translations[currentLanguage as keyof typeof translations] || translations.en;
|
|
||||||
|
|
||||||
// Change language and save to localStorage
|
|
||||||
const changeLanguage = (lang: string) => {
|
const changeLanguage = (lang: string) => {
|
||||||
if (translations[lang as keyof typeof translations]) {
|
i18n.changeLanguage(lang);
|
||||||
setCurrentLanguage(lang);
|
|
||||||
localStorage.setItem('medPlanAssistant_language', lang);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentLanguage,
|
currentLanguage: i18n.language,
|
||||||
changeLanguage,
|
changeLanguage,
|
||||||
t,
|
t,
|
||||||
availableLanguages: Object.keys(translations)
|
availableLanguages: Object.keys(i18n.services.resourceStore.data),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
36
src/i18n.ts
Normal file
36
src/i18n.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import i18n from 'i18next';
|
||||||
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||||
|
|
||||||
|
import en from './locales/en';
|
||||||
|
import de from './locales/de';
|
||||||
|
|
||||||
|
const resources = {
|
||||||
|
en: {
|
||||||
|
translation: en,
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
translation: de,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
i18n
|
||||||
|
.use(LanguageDetector)
|
||||||
|
.use(initReactI18next)
|
||||||
|
.init({
|
||||||
|
resources,
|
||||||
|
fallbackLng: 'en',
|
||||||
|
debug: process.env.NODE_ENV === 'development',
|
||||||
|
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false, // React already escapes values
|
||||||
|
},
|
||||||
|
|
||||||
|
detection: {
|
||||||
|
order: ['localStorage', 'navigator', 'htmlTag'],
|
||||||
|
caches: ['localStorage'],
|
||||||
|
lookupLocalStorage: 'medPlanAssistant_language',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default i18n;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import './styles/global.css';
|
import './styles/global.css';
|
||||||
|
import './i18n'; // Initialize i18n
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
|
||||||
const rootElement = document.getElementById('root');
|
const rootElement = document.getElementById('root');
|
||||||
|
|||||||
@@ -39,22 +39,35 @@ export const de = {
|
|||||||
noSuitableNextDose: "Keine passende nächste Dosis für Korrektur gefunden.",
|
noSuitableNextDose: "Keine passende nächste Dosis für Korrektur gefunden.",
|
||||||
|
|
||||||
// Chart
|
// Chart
|
||||||
concentration: "Konzentration (ng/ml)",
|
axisLabelConcentration: "Konzentration (ng/ml)",
|
||||||
hour: "h",
|
axisLabelHours: "Stunden (h)",
|
||||||
min: "Min",
|
axisLabelTimeOfDay: "Tageszeit (h)",
|
||||||
max: "Max",
|
tickNoon: "Mittag",
|
||||||
|
refLineRegularPlan: "Regulärer Plan",
|
||||||
|
refLineDeviatingPlan: "Abweichung",
|
||||||
|
refLineDayX: "Tag {{x}}",
|
||||||
|
refLineMin: "Min",
|
||||||
|
refLineMax: "Max",
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
advancedSettings: "Erweiterte Einstellungen",
|
diagramSettings: "Diagramm-Einstellungen",
|
||||||
show24hTimeAxis: "24h-Zeitachse anzeigen",
|
xAxisTimeFormat: "Zeitformat",
|
||||||
|
xAxisFormatContinuous: "Fortlaufend",
|
||||||
|
xAxisFormatContinuousDesc: "Endlose Sequenz (0h, 6h, 12h...)",
|
||||||
|
xAxisFormat24h: "24h",
|
||||||
|
xAxisFormat24hDesc: "Wiederholender 0-24h Zyklus",
|
||||||
|
xAxisFormat12h: "12h AM/PM",
|
||||||
|
xAxisFormat12hDesc: "Wiederholend 12h mit AM/PM",
|
||||||
|
showTemplateDayInChart: "Regulären Plan kontinuierlich im Diagramm anzeigen",
|
||||||
|
showDayReferenceLines: "Tagestrenner anzeigen",
|
||||||
simulationDuration: "Simulationsdauer",
|
simulationDuration: "Simulationsdauer",
|
||||||
days: "Tage",
|
days: "Tage",
|
||||||
displayedDays: "Angezeigte Tage",
|
displayedDays: "Sichtbare Tage (im Fokus)",
|
||||||
yAxisRange: "Y-Achsen-Bereich",
|
yAxisRange: "Y-Achsen-Bereich (Zoom)",
|
||||||
yAxisRangeAutoButton: "A",
|
yAxisRangeAutoButton: "A",
|
||||||
yAxisRangeAutoButtonTitle: "Bereich automatisch anhand des Datenbereichs bestimmen",
|
yAxisRangeAutoButtonTitle: "Bereich automatisch anhand des Datenbereichs bestimmen",
|
||||||
auto: "Auto",
|
auto: "Auto",
|
||||||
therapeuticRange: "Therapeutischer Bereich",
|
therapeuticRange: "Therapeutischer Bereich (Referenzlinien)",
|
||||||
dAmphetamineParameters: "d-Amphetamin Parameter",
|
dAmphetamineParameters: "d-Amphetamin Parameter",
|
||||||
halfLife: "Halbwertszeit",
|
halfLife: "Halbwertszeit",
|
||||||
hours: "h",
|
hours: "h",
|
||||||
@@ -78,9 +91,11 @@ export const de = {
|
|||||||
// Field validation
|
// Field validation
|
||||||
errorNumberRequired: "Bitte gib eine gültige Zahl ein.",
|
errorNumberRequired: "Bitte gib eine gültige Zahl ein.",
|
||||||
errorTimeRequired: "Bitte gib eine gültige Zeitangabe ein.",
|
errorTimeRequired: "Bitte gib eine gültige Zeitangabe ein.",
|
||||||
|
warningDuplicateTime: "Mehrere Dosen zur gleichen Zeit.",
|
||||||
|
|
||||||
// Day-based schedule
|
// Day-based schedule
|
||||||
regularPlan: "Regulärer Plan",
|
regularPlan: "Regulärer Plan",
|
||||||
|
deviatingPlan: "Abweichender Plan",
|
||||||
continuation: "Fortsetzung",
|
continuation: "Fortsetzung",
|
||||||
dayNumber: "Tag {{number}}",
|
dayNumber: "Tag {{number}}",
|
||||||
cloneDay: "Tag klonen",
|
cloneDay: "Tag klonen",
|
||||||
@@ -97,8 +112,7 @@ export const de = {
|
|||||||
viewingSharedPlan: "Du siehst einen geteilten Plan",
|
viewingSharedPlan: "Du siehst einen geteilten Plan",
|
||||||
saveAsMyPlan: "Als meinen Plan speichern",
|
saveAsMyPlan: "Als meinen Plan speichern",
|
||||||
discardSharedPlan: "Verwerfen",
|
discardSharedPlan: "Verwerfen",
|
||||||
planCopiedToClipboard: "Plan-Link in Zwischenablage kopiert!",
|
planCopiedToClipboard: "Plan-Link in Zwischenablage kopiert!"
|
||||||
showTemplateDayInChart: "Regulären Plan im Diagramm anzeigen"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default de;
|
export default de;
|
||||||
|
|||||||
@@ -39,22 +39,35 @@ export const en = {
|
|||||||
noSuitableNextDose: "No suitable next dose found for correction.",
|
noSuitableNextDose: "No suitable next dose found for correction.",
|
||||||
|
|
||||||
// Chart
|
// Chart
|
||||||
concentration: "Concentration (ng/ml)",
|
axisLabelConcentration: "Concentration (ng/ml)",
|
||||||
hour: "h",
|
axisLabelHours: "Hours (h)",
|
||||||
min: "Min",
|
axisLabelTimeOfDay: "Time of Day (h)",
|
||||||
max: "Max",
|
tickNoon: "Noon",
|
||||||
|
refLineRegularPlan: "Regular Plan",
|
||||||
|
refLineDeviatingPlan: "Deviation",
|
||||||
|
refLineDayX: "Day {{x}}",
|
||||||
|
refLineMin: "Min",
|
||||||
|
refLineMax: "Max",
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
advancedSettings: "Advanced Settings",
|
diagramSettings: "Diagram Settings",
|
||||||
show24hTimeAxis: "Show 24h time axis",
|
xAxisTimeFormat: "Time Format",
|
||||||
|
xAxisFormatContinuous: "Continuous",
|
||||||
|
xAxisFormatContinuousDesc: "Endless sequence (0h, 6h, 12h...)",
|
||||||
|
xAxisFormat24h: "24h",
|
||||||
|
xAxisFormat24hDesc: "Repeating 0-24h cycle",
|
||||||
|
xAxisFormat12h: "12h AM/PM",
|
||||||
|
xAxisFormat12hDesc: "Repeating 12h with AM/PM",
|
||||||
|
showTemplateDayInChart: "Overlay regular plan in chart",
|
||||||
|
showDayReferenceLines: "Show day separators",
|
||||||
simulationDuration: "Simulation Duration",
|
simulationDuration: "Simulation Duration",
|
||||||
days: "Days",
|
days: "Days",
|
||||||
displayedDays: "Displayed Days",
|
displayedDays: "Visible Days (in Focus)",
|
||||||
yAxisRange: "Y-Axis Range",
|
yAxisRange: "Y-Axis Range (Zoom)",
|
||||||
yAxisRangeAutoButton: "A",
|
yAxisRangeAutoButton: "A",
|
||||||
yAxisRangeAutoButtonTitle: "Determine range automatically based on data range",
|
yAxisRangeAutoButtonTitle: "Determine range automatically based on data range",
|
||||||
auto: "Auto",
|
auto: "Auto",
|
||||||
therapeuticRange: "Therapeutic Range",
|
therapeuticRange: "Therapeutic Range (Reference Lines)",
|
||||||
dAmphetamineParameters: "d-Amphetamine Parameters",
|
dAmphetamineParameters: "d-Amphetamine Parameters",
|
||||||
halfLife: "Half-life",
|
halfLife: "Half-life",
|
||||||
hours: "h",
|
hours: "h",
|
||||||
@@ -78,9 +91,11 @@ export const en = {
|
|||||||
// Field validation
|
// Field validation
|
||||||
errorNumberRequired: "Please enter a valid number.",
|
errorNumberRequired: "Please enter a valid number.",
|
||||||
errorTimeRequired: "Please enter a valid time.",
|
errorTimeRequired: "Please enter a valid time.",
|
||||||
|
warningDuplicateTime: "Multiple doses at same time.",
|
||||||
|
|
||||||
// Day-based schedule
|
// Day-based schedule
|
||||||
regularPlan: "Regular Plan",
|
regularPlan: "Regular Plan",
|
||||||
|
deviatingPlan: "Deviating Plan",
|
||||||
continuation: "continuation",
|
continuation: "continuation",
|
||||||
dayNumber: "Day {{number}}",
|
dayNumber: "Day {{number}}",
|
||||||
cloneDay: "Clone day",
|
cloneDay: "Clone day",
|
||||||
@@ -98,7 +113,6 @@ export const en = {
|
|||||||
saveAsMyPlan: "Save as My Plan",
|
saveAsMyPlan: "Save as My Plan",
|
||||||
discardSharedPlan: "Discard",
|
discardSharedPlan: "Discard",
|
||||||
planCopiedToClipboard: "Plan link copied to clipboard!",
|
planCopiedToClipboard: "Plan link copied to clipboard!",
|
||||||
showTemplateDayInChart: "Show regular plan in chart"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default en;
|
export default en;
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
/**
|
|
||||||
* Internationalization (i18n) Configuration
|
|
||||||
*
|
|
||||||
* Manages application translations and language detection.
|
|
||||||
* Supports English and German with browser language preference detection.
|
|
||||||
*
|
|
||||||
* @author Andreas Weyer
|
|
||||||
* @license MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
import en from './en';
|
|
||||||
import de from './de';
|
|
||||||
|
|
||||||
export const translations = {
|
|
||||||
en,
|
|
||||||
de
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get browser language preference
|
|
||||||
export const getBrowserLanguage = () => {
|
|
||||||
const browserLang = navigator.language;
|
|
||||||
return browserLang.startsWith('de') ? 'de' : 'en';
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get initial language from localStorage or browser preference
|
|
||||||
export const getInitialLanguage = () => {
|
|
||||||
const stored = localStorage.getItem('medPlanAssistant_language');
|
|
||||||
if (stored && translations[stored as keyof typeof translations]) {
|
|
||||||
return stored;
|
|
||||||
}
|
|
||||||
return getBrowserLanguage();
|
|
||||||
};
|
|
||||||
|
|
||||||
export default translations;
|
|
||||||
Reference in New Issue
Block a user