From 4166cf04607cd3ac4ee4aad612680c0d9f6c717b Mon Sep 17 00:00:00 2001 From: Andreas Weyer Date: Sat, 18 Oct 2025 21:44:18 +0200 Subject: [PATCH] Add localization support and docs --- docs/2025-10-18_MODULAR_STRUCTURE.md | 82 ++++++++++++++ .../README.create-react-app.md | 0 src/App.js | 100 ++++++++++-------- src/components/DeviationList.js | 19 ++-- src/components/DoseSchedule.js | 8 +- src/components/LanguageSelector.js | 19 ++++ src/components/Settings.js | 41 +++---- src/components/SimulationChart.js | 33 +++--- src/components/SuggestionPanel.js | 12 +-- src/components/TimeInput.js | 4 +- src/constants/defaults.js | 14 +-- src/hooks/useLanguage.js | 24 +++++ src/locales/de.js | 77 ++++++++++++++ src/locales/en.js | 77 ++++++++++++++ src/locales/index.js | 24 +++++ 15 files changed, 424 insertions(+), 110 deletions(-) create mode 100644 docs/2025-10-18_MODULAR_STRUCTURE.md rename README.create-react-app.md => docs/README.create-react-app.md (100%) create mode 100644 src/components/LanguageSelector.js create mode 100644 src/hooks/useLanguage.js create mode 100644 src/locales/de.js create mode 100644 src/locales/en.js create mode 100644 src/locales/index.js diff --git a/docs/2025-10-18_MODULAR_STRUCTURE.md b/docs/2025-10-18_MODULAR_STRUCTURE.md new file mode 100644 index 0000000..810fc57 --- /dev/null +++ b/docs/2025-10-18_MODULAR_STRUCTURE.md @@ -0,0 +1,82 @@ +# Medication Plan Assistant - Modular Structure + +This application has been successfully modularized to simplify maintenance and further development. + +## 📁 New Project Structure + +```text +src/ +├── App.js # Main component (greatly simplified) +├── components/ # UI components +│ ├── DoseSchedule.js # Dosage schedule input +│ ├── DeviationList.js # Deviations from the schedule +│ ├── SuggestionPanel.js # Correction suggestions +│ ├── SimulationChart.js # Chart component +│ ├── Settings.js # Settings panel +│ ├── TimeInput.js # Time input with picker +│ └── NumericInput.js # Numeric input with +/- buttons +├── hooks/ # Custom React hooks +│ ├── useAppState.js # State management & local storage +│ └── useSimulation.js # Simulation logic & calculations +├── utils/ # Utility functions +│ ├── timeUtils.js # Time utility functions +│ ├── pharmacokinetics.js # PK calculations +│ ├── calculations.js # Concentration calculations +│ └── suggestions.js # Correction suggestion algorithm +└── constants/ + └── defaults.js # Constants & default values +``` + +## 🎯 Advantages of the new structure + +### ✅ Better maintainability + +- **Small, focused modules**: Each file has a clear responsibility +- **Easier debugging**: Problems can be localized more quickly +- **Clearer code organization**: Related functions are grouped together + +### ✅ Reusability + +- **UI components**: `TimeInput`, `NumericInput` can be used anywhere +- **Utility functions**: PK calculations are isolated and testable +- **Custom hooks**: State logic is reusable + +### ✅ Development friendliness + +- **Parallel development**: Teams can work on different modules +- **Simpler testing**: Each module can be tested in isolation +- **Better IDE support**: Smaller files = better performance + +### ✅ Scalability + +- **New features**: Easy to add as new modules +- **Code splitting**: Possible for better performance +- **Refactoring**: Changes are limited locally + +## 🔧 Technical details + +### State management + +- **useAppState**: Manages global app state and LocalStorage +- **useSimulation**: Manages all simulation-related calculations + +### Component architecture + +- **Dumb components**: UI components receive props and call callbacks +- **Smart Components**: Hooks manage state and business logic +- **Separation of Concerns**: UI, state, and business logic are separated + +### Utils & Calculations + +- **Pure functions**: All calculations are side-effect-free +- **Modular exports**: Functions can be imported individually +- **Typed interfaces**: Clear input/output definitions + +## 🚀 Migration complete + +The application works identically to the original version, but now: + +- Split into **350+ lines** across **several small modules** +- **Easier to understand** and modify +- **Ready for further features** and improvements +- **More test-friendly** for unit and integration tests diff --git a/README.create-react-app.md b/docs/README.create-react-app.md similarity index 100% rename from README.create-react-app.md rename to docs/README.create-react-app.md diff --git a/src/App.js b/src/App.js index f0b03ae..26a3b6b 100644 --- a/src/App.js +++ b/src/App.js @@ -6,36 +6,40 @@ import DeviationList from './components/DeviationList.js'; import SuggestionPanel from './components/SuggestionPanel.js'; import SimulationChart from './components/SimulationChart.js'; import Settings from './components/Settings.js'; +import LanguageSelector from './components/LanguageSelector.js'; // Custom Hooks import { useAppState } from './hooks/useAppState.js'; import { useSimulation } from './hooks/useSimulation.js'; +import { useLanguage } from './hooks/useLanguage.js'; // --- Main Component --- const MedPlanAssistant = () => { - const { - appState, - updateState, - updateNestedState, - updateUiSetting, - handleReset + const { currentLanguage, t, changeLanguage } = useLanguage(); + + const { + appState, + updateState, + updateNestedState, + updateUiSetting, + handleReset } = useAppState(); - const { - pkParams, - doses, - therapeuticRange, - doseIncrement, - uiSettings + const { + pkParams, + doses, + therapeuticRange, + doseIncrement, + uiSettings } = appState; - - const { - showDayTimeXAxis, - chartView, - yAxisMin, - yAxisMax, - simulationDays, - displayedDays + + const { + showDayTimeXAxis, + chartView, + yAxisMin, + yAxisMax, + simulationDays, + displayedDays } = uiSettings; const { @@ -54,57 +58,65 @@ const MedPlanAssistant = () => {
-

Medikationsplan-Assistent

-

Simulation für Lisdexamfetamin (LDX) und d-Amphetamin (d-amph)

+
+
+

{t.appTitle}

+

{t.appSubtitle}

+
+ +
- +
{/* Left Column - Controls */}
- updateState('doses', newDoses)} + t={t} /> - -
- + {/* Center Column - Chart */}
- - -
- + { displayedDays={displayedDays} yAxisMin={yAxisMin} yAxisMax={yAxisMax} + t={t} />
- + {/* Right Column - Settings */}
- { onUpdateTherapeuticRange={(key, value) => updateNestedState('therapeuticRange', key, { [key]: value })} onUpdateUiSetting={updateUiSetting} onReset={handleReset} + t={t} />
- +
-

Wichtiger Hinweis

-

Dieses Tool dient ausschließlich zu Illustrations- und Informationszwecken. Es ist kein medizinisches Gerät und ersetzt nicht die Beratung durch einen Arzt oder Apotheker. Alle Berechnungen sind Simulationen, die auf allgemeinen pharmakokinetischen Modellen basieren und von individuellen Faktoren erheblich abweichen können. Bitte konsultiere deinen behandelnden Arzt, bevor du Anpassungen an deiner Medikation vornimmst.

+

{t.importantNote}

+

{t.disclaimer}

); }; -export default MedPlanAssistant; \ No newline at end of file +export default MedPlanAssistant; diff --git a/src/components/DeviationList.js b/src/components/DeviationList.js index fd60637..6fb3409 100644 --- a/src/components/DeviationList.js +++ b/src/components/DeviationList.js @@ -8,11 +8,12 @@ const DeviationList = ({ simulationDays, onAddDeviation, onRemoveDeviation, - onDeviationChange + onDeviationChange, + t }) => { return (
-

Abweichungen vom Plan

+

{t.deviationsFromPlan}

{deviations.map((dev, index) => (
onDeviationChange(index, 'dose', newDose)} increment={doseIncrement} min={0} - unit="mg" + unit={t.mg} />
-
+
@@ -61,10 +62,8 @@ const DeviationList = ({ onClick={onAddDeviation} className="mt-2 w-full bg-amber-500 text-white py-2 rounded-md hover:bg-amber-600 text-sm" > - Abweichung hinzufügen + {t.addDeviation}
); -}; - -export default DeviationList; +};export default DeviationList; diff --git a/src/components/DoseSchedule.js b/src/components/DoseSchedule.js index b78d996..dddae6e 100644 --- a/src/components/DoseSchedule.js +++ b/src/components/DoseSchedule.js @@ -2,10 +2,10 @@ import React from 'react'; import TimeInput from './TimeInput.js'; import NumericInput from './NumericInput.js'; -const DoseSchedule = ({ doses, doseIncrement, onUpdateDoses }) => { +const DoseSchedule = ({ doses, doseIncrement, onUpdateDoses, t }) => { return (
-

Mein Plan

+

{t.myPlan}

{doses.map((dose, index) => (
{ onChange={newDose => onUpdateDoses(doses.map((d, i) => i === index ? {...d, dose: newDose} : d))} increment={doseIncrement} min={0} - unit="mg" + unit={t.mg} />
- {dose.label} + {t[dose.label] || dose.label}
))} diff --git a/src/components/LanguageSelector.js b/src/components/LanguageSelector.js new file mode 100644 index 0000000..cb6f292 --- /dev/null +++ b/src/components/LanguageSelector.js @@ -0,0 +1,19 @@ +import React from 'react'; + +const LanguageSelector = ({ currentLanguage, onLanguageChange, t }) => { + return ( +
+ + +
+ ); +}; + +export default LanguageSelector; diff --git a/src/components/Settings.js b/src/components/Settings.js index 154830c..71169bb 100644 --- a/src/components/Settings.js +++ b/src/components/Settings.js @@ -8,13 +8,14 @@ const Settings = ({ onUpdatePkParams, onUpdateTherapeuticRange, onUpdateUiSetting, - onReset + onReset, + t }) => { const { showDayTimeXAxis, yAxisMin, yAxisMax, simulationDays, displayedDays } = uiSettings; return (
-

Erweiterte Einstellungen

+

{t.advancedSettings}

- +
- +
- +
onUpdateUiSetting('yAxisMin', val)} increment={'5'} min={0} - placeholder="Auto" + placeholder={t.auto} unit="ng/ml" />
@@ -72,13 +73,13 @@ const Settings = ({ onChange={val => onUpdateUiSetting('yAxisMax', val)} increment={'5'} min={0} - placeholder="Auto" + placeholder={t.auto} unit="ng/ml" />
- +
onUpdateTherapeuticRange('min', val)} increment={'0.5'} min={0} - placeholder="Min" + placeholder={t.min} unit="ng/ml" />
@@ -97,15 +98,15 @@ const Settings = ({ onChange={val => onUpdateTherapeuticRange('max', val)} increment={'0.5'} min={0} - placeholder="Max" + placeholder={t.max} unit="ng/ml" />
-

d-Amphetamin Parameter

+

{t.dAmphetamineParameters}

- + onUpdatePkParams('damph', { halfLife: val })} @@ -115,9 +116,9 @@ const Settings = ({ />
-

Lisdexamfetamin Parameter

+

{t.lisdexamfetamineParameters}

- + onUpdatePkParams('ldx', { halfLife: val })} @@ -127,13 +128,13 @@ const Settings = ({ />
- + onUpdatePkParams('ldx', { absorptionRate: val })} increment={'0.1'} min={0.1} - unit="(schneller >)" + unit={t.faster} />
@@ -142,7 +143,7 @@ const Settings = ({ onClick={onReset} className="w-full bg-red-600 text-white py-2 rounded-md hover:bg-red-700 text-sm" > - Alle Einstellungen zurücksetzen + {t.resetAllSettings} diff --git a/src/components/SimulationChart.js b/src/components/SimulationChart.js index 33de73c..ad1ab44 100644 --- a/src/components/SimulationChart.js +++ b/src/components/SimulationChart.js @@ -11,7 +11,8 @@ const SimulationChart = ({ simulationDays, displayedDays, yAxisMin, - yAxisMax + yAxisMax, + t }) => { const totalHours = (parseInt(simulationDays, 10) || 3) * 24; const chartTicks = Array.from({length: Math.floor(totalHours / 6) + 1}, (_, i) => i * 6); @@ -36,7 +37,7 @@ const SimulationChart = ({ type="number" domain={[0, totalHours]} ticks={chartTicks} - tickFormatter={(h) => `${h}h`} + tickFormatter={(h) => `${h}${t.hour}`} xAxisId="continuous" /> {showDayTimeXAxis && ( @@ -45,26 +46,26 @@ const SimulationChart = ({ type="number" domain={[0, totalHours]} ticks={chartTicks} - tickFormatter={(h) => `${h % 24}h`} + tickFormatter={(h) => `${h % 24}${t.hour}`} xAxisId="daytime" orientation="top" /> )} [`${value.toFixed(1)} ng/ml`, name]} - labelFormatter={(label) => `Stunde: ${label}h`} + formatter={(value, name) => [`${value.toFixed(1)} ${t.ngml}`, name]} + labelFormatter={(label) => `${t.hour.replace('h', 'Hour')}: ${label}${t.hour}`} /> {(chartView === 'damph' || chartView === 'both') && ( ); -}; - -export default SimulationChart; +};export default SimulationChart; diff --git a/src/components/SuggestionPanel.js b/src/components/SuggestionPanel.js index 38b7e39..86f1a23 100644 --- a/src/components/SuggestionPanel.js +++ b/src/components/SuggestionPanel.js @@ -1,21 +1,21 @@ import React from 'react'; -const SuggestionPanel = ({ suggestion, onApplySuggestion }) => { +const SuggestionPanel = ({ suggestion, onApplySuggestion, t }) => { if (!suggestion) return null; return (
-

Was wäre wenn?

+

{t.whatIf}

{suggestion.dose ? ( <>

- Vorschlag: {suggestion.dose}mg (statt {suggestion.originalDose}mg) um {suggestion.time}. + {t.suggestion}: {suggestion.dose}{t.mg} ({t.instead} {suggestion.originalDose}{t.mg}) {t.at} {suggestion.time}.

) : ( @@ -23,6 +23,4 @@ const SuggestionPanel = ({ suggestion, onApplySuggestion }) => { )}
); -}; - -export default SuggestionPanel; +};export default SuggestionPanel; diff --git a/src/components/TimeInput.js b/src/components/TimeInput.js index 9e34d03..040d0b8 100644 --- a/src/components/TimeInput.js +++ b/src/components/TimeInput.js @@ -67,7 +67,7 @@ const TimeInput = ({ value, onChange }) => {
{value}
-
Stunde:
+
Hour:
{[...Array(24).keys()].map(h => (
)} diff --git a/src/constants/defaults.js b/src/constants/defaults.js index 09cc0e8..2facd99 100644 --- a/src/constants/defaults.js +++ b/src/constants/defaults.js @@ -1,19 +1,19 @@ -// --- Constants --- +// Application constants export const LOCAL_STORAGE_KEY = 'medPlanAssistantState_v5'; export const LDX_TO_DAMPH_CONVERSION_FACTOR = 0.2948; -// --- Default State --- +// Default application state export const getDefaultState = () => ({ pkParams: { damph: { halfLife: '11' }, ldx: { halfLife: '0.8', absorptionRate: '1.5' }, }, doses: [ - { time: '06:30', dose: '25', label: 'Morgens' }, - { time: '12:30', dose: '10', label: 'Mittags' }, - { time: '17:00', dose: '10', label: 'Nachmittags' }, - { time: '21:00', dose: '10', label: 'Abends' }, - { time: '01:00', dose: '0', label: 'Nachts' }, + { time: '06:30', dose: '25', label: 'morning' }, + { time: '12:30', dose: '10', label: 'midday' }, + { time: '17:00', dose: '10', label: 'afternoon' }, + { time: '21:00', dose: '10', label: 'evening' }, + { time: '01:00', dose: '0', label: 'night' }, ], steadyStateConfig: { daysOnMedication: '7' }, therapeuticRange: { min: '11.5', max: '14' }, diff --git a/src/hooks/useLanguage.js b/src/hooks/useLanguage.js new file mode 100644 index 0000000..ce1a796 --- /dev/null +++ b/src/hooks/useLanguage.js @@ -0,0 +1,24 @@ +import React from 'react'; +import { translations, getInitialLanguage } from '../locales/index.js'; + +export const useLanguage = () => { + const [currentLanguage, setCurrentLanguage] = React.useState(getInitialLanguage); + + // Get current translations + const t = translations[currentLanguage] || translations.en; + + // Change language and save to localStorage + const changeLanguage = (lang) => { + if (translations[lang]) { + setCurrentLanguage(lang); + localStorage.setItem('medPlanAssistant_language', lang); + } + }; + + return { + currentLanguage, + changeLanguage, + t, + availableLanguages: Object.keys(translations) + }; +}; diff --git a/src/locales/de.js b/src/locales/de.js new file mode 100644 index 0000000..cb8ab89 --- /dev/null +++ b/src/locales/de.js @@ -0,0 +1,77 @@ +// German translations +export const de = { + // Header + appTitle: "Medikationsplan-Assistent", + appSubtitle: "Simulation für Lisdexamfetamin (LDX) und d-Amphetamin (d-amph)", + + // Chart view buttons + dAmphetamine: "d-Amphetamin", + lisdexamfetamine: "Lisdexamfetamin", + both: "Beide", + + // Language selector + language: "Sprache", + english: "English", + german: "Deutsch", + + // Dose Schedule + myPlan: "Mein Plan", + morning: "Morgens", + midday: "Mittags", + afternoon: "Nachmittags", + evening: "Abends", + night: "Nachts", + + // Deviations + deviationsFromPlan: "Abweichungen vom Plan", + addDeviation: "Abweichung hinzufügen", + day: "Tag", + additional: "Zusätzlich", + additionalTooltip: "Markiere dies, wenn es eine zusätzliche Dosis war anstatt eines Ersatzes für eine geplante.", + + // Suggestions + whatIf: "Was wäre wenn?", + suggestion: "Vorschlag", + instead: "statt", + at: "um", + applySuggestion: "Vorschlag als Abweichung übernehmen", + noSignificantCorrection: "Keine signifikante Korrektur notwendig.", + noSuitableNextDose: "Keine passende nächste Dosis für Korrektur gefunden.", + + // Chart + concentration: "Konzentration (ng/ml)", + hour: "h", + min: "Min", + max: "Max", + + // Settings + advancedSettings: "Erweiterte Einstellungen", + show24hTimeAxis: "24h-Zeitachse anzeigen", + simulationDuration: "Simulationsdauer", + days: "Tage", + displayedDays: "Angezeigte Tage", + yAxisRange: "Y-Achsen-Bereich", + auto: "Auto", + therapeuticRange: "Therapeutischer Bereich", + dAmphetamineParameters: "d-Amphetamin Parameter", + halfLife: "Halbwertszeit", + hours: "h", + lisdexamfetamineParameters: "Lisdexamfetamin Parameter", + conversionHalfLife: "Umwandlungs-Halbwertszeit", + absorptionRate: "Absorptionsrate", + faster: "(schneller >)", + resetAllSettings: "Alle Einstellungen zurücksetzen", + + // Units + mg: "mg", + ngml: "ng/ml", + + // Reset confirmation + resetConfirmation: "Bist du sicher, dass du alle Einstellungen auf die Standardwerte zurücksetzen möchtest? Dies kann nicht rückgängig gemacht werden.", + + // Footer disclaimer + importantNote: "Wichtiger Hinweis", + disclaimer: "Dieses Tool dient ausschließlich zu Illustrations- und Informationszwecken. Es ist kein medizinisches Gerät und ersetzt nicht die Beratung durch einen Arzt oder Apotheker. Alle Berechnungen sind Simulationen, die auf allgemeinen pharmakokinetischen Modellen basieren und von individuellen Faktoren erheblich abweichen können. Bitte konsultiere deinen behandelnden Arzt, bevor du Anpassungen an deiner Medikation vornimmst." +}; + +export default de; diff --git a/src/locales/en.js b/src/locales/en.js new file mode 100644 index 0000000..339a74b --- /dev/null +++ b/src/locales/en.js @@ -0,0 +1,77 @@ +// English translations +export const en = { + // App header and navigation + appTitle: "Medication Plan Assistant", + appSubtitle: "Simulation for Lisdexamfetamine (LDX) and d-Amphetamine (d-amph)", + + // Chart view buttons + dAmphetamine: "d-Amphetamine", + lisdexamfetamine: "Lisdexamfetamine", + both: "Both", + + // Language selector + language: "Language", + english: "English", + german: "Deutsch", + + // Dose Schedule + myPlan: "My Plan", + morning: "Morning", + midday: "Midday", + afternoon: "Afternoon", + evening: "Evening", + night: "Night", + + // Deviations + deviationsFromPlan: "Deviations from Plan", + addDeviation: "Add Deviation", + day: "Day", + additional: "Additional", + additionalTooltip: "Mark this if it was an extra dose instead of a replacement for a planned one.", + + // Suggestions + whatIf: "What if?", + suggestion: "Suggestion", + instead: "instead", + at: "at", + applySuggestion: "Apply suggestion as deviation", + noSignificantCorrection: "No significant correction necessary.", + noSuitableNextDose: "No suitable next dose found for correction.", + + // Chart + concentration: "Concentration (ng/ml)", + hour: "h", + min: "Min", + max: "Max", + + // Settings + advancedSettings: "Advanced Settings", + show24hTimeAxis: "Show 24h time axis", + simulationDuration: "Simulation Duration", + days: "Days", + displayedDays: "Displayed Days", + yAxisRange: "Y-Axis Range", + auto: "Auto", + therapeuticRange: "Therapeutic Range", + dAmphetamineParameters: "d-Amphetamine Parameters", + halfLife: "Half-life", + hours: "h", + lisdexamfetamineParameters: "Lisdexamfetamine Parameters", + conversionHalfLife: "Conversion Half-life", + absorptionRate: "Absorption Rate", + faster: "(faster >)", + resetAllSettings: "Reset All Settings", + + // Units + mg: "mg", + ngml: "ng/ml", + + // Reset confirmation + resetConfirmation: "Are you sure you want to reset all settings to default values? This cannot be undone.", + + // Footer disclaimer + importantNote: "Important Notice", + disclaimer: "This tool is for illustration and information purposes only. It is not a medical device and does not replace consultation with a doctor or pharmacist. All calculations are simulations based on general pharmacokinetic models and may differ significantly from individual factors. Please consult your treating physician before making adjustments to your medication." +}; + +export default en; diff --git a/src/locales/index.js b/src/locales/index.js new file mode 100644 index 0000000..f34006e --- /dev/null +++ b/src/locales/index.js @@ -0,0 +1,24 @@ +import en from './en.js'; +import de from './de.js'; + +export const translations = { + en, + de +}; + +// Get browser language preference +export const getBrowserLanguage = () => { + const browserLang = navigator.language || navigator.userLanguage; + return browserLang.startsWith('de') ? 'de' : 'en'; +}; + +// Get stored language or fall back to browser preference or English +export const getInitialLanguage = () => { + const stored = localStorage.getItem('medPlanAssistant_language'); + if (stored && translations[stored]) { + return stored; + } + return getBrowserLanguage(); +}; + +export default translations;