From 6fb6583ae307bcc01add9aa5a749b2508969566c Mon Sep 17 00:00:00 2001 From: Andreas Weyer Date: Wed, 3 Dec 2025 21:53:04 +0000 Subject: [PATCH] Update custome translations to i18n and various improvements --- package.json | 3 + src/App.tsx | 18 ++-- src/components/day-schedule.tsx | 88 +++++++++-------- src/components/language-selector.tsx | 6 +- src/components/settings.tsx | 121 ++++++++++++++++------- src/components/simulation-chart.tsx | 93 ++++++++++++----- src/components/ui/form-numeric-input.tsx | 16 ++- src/components/ui/form-time-input.tsx | 17 +++- src/constants/defaults.ts | 5 +- src/hooks/useAppState.ts | 28 +++++- src/hooks/useLanguage.ts | 23 ++--- src/i18n.ts | 36 +++++++ src/index.tsx | 1 + src/locales/de.ts | 36 ++++--- src/locales/en.ts | 34 +++++-- src/locales/index.ts | 34 ------- 16 files changed, 364 insertions(+), 195 deletions(-) create mode 100644 src/i18n.ts delete mode 100644 src/locales/index.ts diff --git a/package.json b/package.json index 90ed4e9..3b6bb25 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.tsx b/src/App.tsx index 5d0f0ea..1f33f6e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -54,7 +54,8 @@ const MedPlanAssistant = () => { yAxisMax, showTemplateDay, simulationDays, - displayedDays + displayedDays, + showDayReferenceLines } = uiSettings; const { @@ -68,8 +69,8 @@ const MedPlanAssistant = () => {
-

{t.appTitle}

-

{t.appSubtitle}

+

{t('appTitle')}

+

{t('appSubtitle')}

@@ -84,19 +85,19 @@ const MedPlanAssistant = () => { onClick={() => updateUiSetting('chartView', 'damph')} variant={chartView === 'damph' ? 'default' : 'secondary'} > - {t.dAmphetamine} + {t('dAmphetamine')} @@ -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 = () => {
-

{t.importantNote}

-

{t.disclaimer}

+

{t('importantNote')}

+

{t('disclaimer')}

diff --git a/src/components/day-schedule.tsx b/src/components/day-schedule.tsx index 9c5619f..a8f23ca 100644 --- a/src/components/day-schedule.tsx +++ b/src/components/day-schedule.tsx @@ -48,13 +48,11 @@ const DaySchedule: React.FC = ({
- {day.isTemplate ? t.regularPlan : t.dayNumber.replace('{{number}}', String(dayIndex + 1))} + {day.isTemplate ? t('regularPlan') : t('deviatingPlan')} - {day.isTemplate && ( - - {t.day} 1 - - )} + + {t('day')} {dayIndex + 1} +
{canAddDay && ( @@ -62,7 +60,7 @@ const DaySchedule: React.FC = ({ onClick={() => onAddDay(day.id)} size="sm" variant="outline" - title={t.cloneDay} + title={t('cloneDay')} > @@ -73,7 +71,7 @@ const DaySchedule: React.FC = ({ size="sm" variant="outline" className="border-destructive text-destructive hover:bg-destructive hover:text-destructive-foreground" - title={t.removeDay} + title={t('removeDay')} > @@ -84,41 +82,49 @@ const DaySchedule: React.FC = ({ {/* Dose table header */}
-
{t.time}
-
{t.ldx} (mg)
+
{t('time')}
+
{t('ldx')} (mg)
{/* Dose rows */} - {day.doses.map((dose) => ( -
- onUpdateDose(day.id, dose.id, 'time', value)} - required={true} - errorMessage={t.errorTimeRequired} - /> - onUpdateDose(day.id, dose.id, 'ldx', value)} - increment={doseIncrement} - min={0} - unit="mg" - required={true} - errorMessage={t.errorNumberRequired} - /> - -
- ))} + {day.doses.map((dose) => { + // Check for duplicate times + const duplicateTimeCount = day.doses.filter(d => d.time === dose.time).length; + const hasDuplicateTime = duplicateTimeCount > 1; + + return ( +
+ onUpdateDose(day.id, dose.id, 'time', value)} + required={true} + warning={hasDuplicateTime} + errorMessage={t('errorTimeRequired')} + warningMessage={t('warningDuplicateTime')} + /> + onUpdateDose(day.id, dose.id, 'ldx', value)} + increment={doseIncrement} + min={0} + unit="mg" + required={true} + errorMessage={t('errorNumberRequired')} + /> + +
+ ); + })} {/* Add dose button */} {day.doses.length < 5 && ( @@ -129,7 +135,7 @@ const DaySchedule: React.FC = ({ className="w-full mt-2" > - {t.addDose} + {t('addDose')} )}
@@ -144,7 +150,7 @@ const DaySchedule: React.FC = ({ className="w-full" > - {t.addDay} + {t('addDay')} )}
diff --git a/src/components/language-selector.tsx b/src/components/language-selector.tsx index 93f0c03..132996c 100644 --- a/src/components/language-selector.tsx +++ b/src/components/language-selector.tsx @@ -15,14 +15,14 @@ import { Label } from './ui/label'; const LanguageSelector = ({ currentLanguage, onLanguageChange, t }: any) => { return (
- +
diff --git a/src/components/settings.tsx b/src/components/settings.tsx index 6f5da92..a51bfa8 100644 --- a/src/components/settings.tsx +++ b/src/components/settings.tsx @@ -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 ( - {t.advancedSettings} + {t('diagramSettings')} -
- - onUpdateUiSetting('showDayTimeOnXAxis', checked)} - /> +
+ + + +
-
+ +
onUpdateUiSetting('showTemplateDay', checked)} /> +
- + onUpdateUiSetting('simulationDays', val)} increment={1} min={3} max={7} - unit={t.days} + unit={t('days')} required={true} - errorMessage={t.errorNumberRequired} + errorMessage={t('errorNumberRequired')} />
- + 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')} />
- +
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 = ({
- +
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'} /> - 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'} />
-

{t.dAmphetamineParameters}

+

{t('dAmphetamineParameters')}

- + 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'} />
-

{t.lisdexamfetamineParameters}

+

{t('lisdexamfetamineParameters')}

- + 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'} />
- + 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'} />
@@ -190,7 +239,7 @@ const Settings = ({ variant="destructive" className="w-full" > - {t.resetAllSettings} + {t('resetAllSettings')} diff --git a/src/components/simulation-chart.tsx b/src/components/simulation-chart.tsx index a96f4b7..83c9c92 100644 --- a/src/components/simulation-chart.tsx +++ b/src/components/simulation-chart.tsx @@ -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') && ( { - 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" /> + yAxisId="concentration" + label={{ value: t('axisLabelConcentration'), angle: -90, position: 'insideLeft', offset: '0 -10', style: { fontStyle: 'italic', color: '#666' } }} + domain={chartDomain as any} + allowDecimals={false} + tickCount={20} + /> [`${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 = ({ /> - + {showDayReferenceLines !== false && [...Array(dispDays).keys()].map(day => ( + + ))} {(chartView === 'damph' || chartView === 'both') && ( ( @@ -41,18 +43,22 @@ const FormNumericInput = React.forwardRef( 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(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( const handleFocus = () => { setShowError(hasError) + setShowWarning(hasWarning) } const getAlignmentClass = () => { @@ -197,11 +204,16 @@ const FormNumericInput = React.forwardRef( )}
{unit && {unit}} - {hasError && showError && ( + {hasError && showError && errorMessage && (
{errorMessage}
)} + {hasWarning && showWarning && warningMessage && ( +
+ {warningMessage} +
+ )}
) } diff --git a/src/components/ui/form-time-input.tsx b/src/components/ui/form-time-input.tsx index 1f448cf..5deaee0 100644 --- a/src/components/ui/form-time-input.tsx +++ b/src/components/ui/form-time-input.tsx @@ -21,8 +21,10 @@ interface TimeInputProps extends Omit( @@ -32,20 +34,24 @@ const FormTimeInput = React.forwardRef( 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(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( const handleFocus = () => { setShowError(hasError) + setShowWarning(hasWarning) } const handlePickerChange = (part: 'h' | 'm', val: number) => { @@ -130,7 +137,8 @@ const FormTimeInput = React.forwardRef( "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(
{unit && {unit}} - {hasError && showError && ( + {hasError && showError && errorMessage && (
{errorMessage}
)} + {hasWarning && showWarning && warningMessage && ( +
+ {warningMessage} +
+ )} ) } diff --git a/src/constants/defaults.ts b/src/constants/defaults.ts index ef9ee87..1920b91 100644 --- a/src/constants/defaults.ts +++ b/src/constants/defaults.ts @@ -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', diff --git a/src/hooks/useAppState.ts b/src/hooks/useAppState.ts index bcc6506..168cfd5 100644 --- a/src/hooks/useAppState.ts +++ b/src/hooks/useAppState.ts @@ -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 }; }) })); diff --git a/src/hooks/useLanguage.ts b/src/hooks/useLanguage.ts index e3cd76a..7b03ffb 100644 --- a/src/hooks/useLanguage.ts +++ b/src/hooks/useLanguage.ts @@ -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), }; }; diff --git a/src/i18n.ts b/src/i18n.ts new file mode 100644 index 0000000..7d955de --- /dev/null +++ b/src/i18n.ts @@ -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; diff --git a/src/index.tsx b/src/index.tsx index 1536d28..e8494b9 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -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'); diff --git a/src/locales/de.ts b/src/locales/de.ts index ff893be..2a2efa7 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -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; diff --git a/src/locales/en.ts b/src/locales/en.ts index 97c94c8..1e05af6 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -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; diff --git a/src/locales/index.ts b/src/locales/index.ts deleted file mode 100644 index 12b9a0c..0000000 --- a/src/locales/index.ts +++ /dev/null @@ -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;