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