Update custome translations to i18n and various improvements

This commit is contained in:
2025-12-03 21:53:04 +00:00
parent a54c729e46
commit 6fb6583ae3
16 changed files with 364 additions and 195 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>
) )
} }

View File

@@ -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>
) )
} }

View File

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

View File

@@ -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
)
}; };
}) })
})); }));

View File

@@ -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
View 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;

View File

@@ -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');

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;