Merge pull request 'feat: Comments translation to EN' (#1) from English-version into main

Reviewed-on: #1
This commit is contained in:
2026-08-29 22:24:27 +02:00
9 changed files with 48 additions and 48 deletions
+2 -2
View File
@@ -8,10 +8,10 @@ const PORT = process.env.PORT || 3000;
app.use(cors()); app.use(cors());
app.use(express.json()); app.use(express.json());
// Service du moteur de calcul situé à la racine // Serve the calculation engine located at the root
app.use('/pkCore.js', express.static(path.join(__dirname, '../pkCore.js'))); app.use('/pkCore.js', express.static(path.join(__dirname, '../pkCore.js')));
// Service des fichiers statiques frontend // Serve frontend static files
app.use(express.static(path.join(__dirname, '../frontend'))); app.use(express.static(path.join(__dirname, '../frontend')));
app.listen(PORT, () => { app.listen(PORT, () => {
+2 -2
View File
@@ -1,8 +1,8 @@
@charset "UTF-8"; @charset "UTF-8";
/* ============================================================ /* ============================================================
Thèmes (dark / light / monokai) — variables CSS consommées Themes (dark / light / monokai) — CSS variables consumed
par des classes Tailwind arbitraires : bg-[var(--kt-bg)] etc. by arbitrary Tailwind classes: bg-[var(--kt-bg)] etc.
============================================================ */ ============================================================ */
:root, :root,
html[data-theme="dark"] { html[data-theme="dark"] {
+13 -13
View File
@@ -6,13 +6,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KétaPK - Simulation Pharmacocinétique</title> <title>KétaPK - Simulation Pharmacocinétique</title>
<!-- CSS Tailwind compilé --> <!-- Compiled Tailwind CSS -->
<link rel="stylesheet" href="css/tailwind.css"> <link rel="stylesheet" href="css/tailwind.css">
<!-- CSS Personnalisé --> <!-- Custom CSS -->
<link rel="stylesheet" href="css/styles.css"> <link rel="stylesheet" href="css/styles.css">
<!-- Librairies JS Statiques --> <!-- Static JS Libraries -->
<script src="vendor/chart.umd.min.js"></script> <script src="vendor/chart.umd.min.js"></script>
<script src="vendor/chartjs-plugin-annotation.min.js"></script> <script src="vendor/chartjs-plugin-annotation.min.js"></script>
</head> </head>
@@ -95,18 +95,18 @@
<!-- SIDEBAR --> <!-- SIDEBAR -->
<aside class="lg:col-span-3 xl:col-span-2 space-y-5"> <aside class="lg:col-span-3 xl:col-span-2 space-y-5">
<!-- Panneau Patient --> <!-- Patient Panel -->
<div class="bg-[var(--kt-surface-2)] p-4 rounded-xl border border-[var(--kt-border)] shadow-md mb-6"> <div class="bg-[var(--kt-surface-2)] p-4 rounded-xl border border-[var(--kt-border)] shadow-md mb-6">
<div class="grid grid-cols-1 gap-4 items-center"> <div class="grid grid-cols-1 gap-4 items-center">
<!-- Nom du Patient --> <!-- Patient Name -->
<div> <div>
<label data-i18n="patient.name_label" class="block text-xs text-[var(--kt-text-3)] mb-1 font-medium">Nom du patient</label> <label data-i18n="patient.name_label" class="block text-xs text-[var(--kt-text-3)] mb-1 font-medium">Nom du patient</label>
<input type="text" id="patientName" data-i18n-placeholder="patient.name_placeholder" placeholder="ex: DUPONT Jean" <input type="text" id="patientName" data-i18n-placeholder="patient.name_placeholder" placeholder="ex: DUPONT Jean"
class="w-full bg-[var(--kt-bg)] border border-[var(--kt-border)] rounded-lg px-3 py-2 text-[var(--kt-text-1)] font-bold focus:border-cyan-500 focus:outline-none"> class="w-full bg-[var(--kt-bg)] border border-[var(--kt-border)] rounded-lg px-3 py-2 text-[var(--kt-text-1)] font-bold focus:border-cyan-500 focus:outline-none">
</div> </div>
<!-- Poids + Avertissement --> <!-- Weight + Warning -->
<div> <div>
<label data-i18n="patient.weight_label" class="block text-xs text-[var(--kt-text-3)] mb-1 font-medium">Poids du patient (kg)</label> <label data-i18n="patient.weight_label" class="block text-xs text-[var(--kt-text-3)] mb-1 font-medium">Poids du patient (kg)</label>
<input type="number" id="patientWeight" value="70" min="10" max="250" step="any" <input type="number" id="patientWeight" value="70" min="10" max="250" step="any"
@@ -118,7 +118,7 @@
</div> </div>
</div> </div>
<!-- Choix Unité Perfusion --> <!-- Infusion Unit Choice -->
<div> <div>
<label data-i18n="patient.infusion_unit_label" class="block text-xs text-[var(--kt-text-3)] mb-1 font-medium">Unité de perfusion continue</label> <label data-i18n="patient.infusion_unit_label" class="block text-xs text-[var(--kt-text-3)] mb-1 font-medium">Unité de perfusion continue</label>
<select id="infusionUnitSelect" <select id="infusionUnitSelect"
@@ -129,7 +129,7 @@
</select> </select>
</div> </div>
<!-- Résolution temporelle de la timeline --> <!-- Timeline time resolution -->
<div> <div>
<label data-i18n="patient.timeline_resolution_label" class="block text-xs text-[var(--kt-text-3)] mb-1 font-medium">Résolution de la timeline</label> <label data-i18n="patient.timeline_resolution_label" class="block text-xs text-[var(--kt-text-3)] mb-1 font-medium">Résolution de la timeline</label>
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-2 gap-2">
@@ -150,7 +150,7 @@
</div> </div>
</div> </div>
<!-- Bouton d'exportation CSV Lateral --> <!-- Lateral CSV export button -->
<div class="flex justify-end gap-3 mb-4"> <div class="flex justify-end gap-3 mb-4">
<button id="btnExportCSV" data-i18n="buttons.export_csv" <button id="btnExportCSV" data-i18n="buttons.export_csv"
class="bg-[var(--kt-btn-bg)] hover:bg-[var(--kt-btn-bg-hover)] text-[var(--kt-text-1)] text-xs font-semibold px-3 py-2 rounded-lg flex items-center gap-2 transition w-full justify-center"> class="bg-[var(--kt-btn-bg)] hover:bg-[var(--kt-btn-bg-hover)] text-[var(--kt-text-1)] text-xs font-semibold px-3 py-2 rounded-lg flex items-center gap-2 transition w-full justify-center">
@@ -191,7 +191,7 @@
<!-- CONTENT --> <!-- CONTENT -->
<section class="lg:col-span-9 xl:col-span-10 space-y-5 flex flex-col"> <section class="lg:col-span-9 xl:col-span-10 space-y-5 flex flex-col">
<!-- Graphique dynamique --> <!-- Dynamic chart -->
<div <div
class="bg-[var(--kt-surface-2)] border border-[var(--kt-border)] rounded-2xl p-4 shadow-md backdrop-blur-sm flex flex-col h-[calc(100vh-370px)] min-h-[450px] relative"> class="bg-[var(--kt-surface-2)] border border-[var(--kt-border)] rounded-2xl p-4 shadow-md backdrop-blur-sm flex flex-col h-[calc(100vh-370px)] min-h-[450px] relative">
<div class="flex-1 w-full h-full relative"> <div class="flex-1 w-full h-full relative">
@@ -211,11 +211,11 @@
<div id="timelineGrid" <div id="timelineGrid"
class="flex gap-2 overflow-x-auto pb-2 scrollbar-thin scrollbar-thumb-[var(--kt-scroll-thumb)] scrollbar-track-[var(--kt-scroll-track)]"> class="flex gap-2 overflow-x-auto pb-2 scrollbar-thin scrollbar-thumb-[var(--kt-scroll-thumb)] scrollbar-track-[var(--kt-scroll-track)]">
<!-- Injecté via app.js --> <!-- Injected via app.js -->
</div> </div>
</div> </div>
<!-- Tableau de contrôle des concentrations (0-300 min) --> <!-- Concentration control table (0-300 min) -->
<details class="bg-[var(--kt-surface-2)] border border-[var(--kt-border)] rounded-xl mb-6"> <details class="bg-[var(--kt-surface-2)] border border-[var(--kt-border)] rounded-xl mb-6">
<summary <summary
class="p-4 cursor-pointer text-[var(--kt-text-2)] font-semibold text-sm hover:text-[var(--kt-text-1)] flex justify-between items-center"> class="p-4 cursor-pointer text-[var(--kt-text-2)] font-semibold text-sm hover:text-[var(--kt-text-1)] flex justify-between items-center">
@@ -235,7 +235,7 @@
</tr> </tr>
</thead> </thead>
<tbody id="dataTableBody" class="divide-y divide-[var(--kt-border-soft)] font-mono"> <tbody id="dataTableBody" class="divide-y divide-[var(--kt-border-soft)] font-mono">
<!-- Généré dynamiquement en JS --> <!-- Dynamically generated in JS -->
</tbody> </tbody>
</table> </table>
</div> </div>
+2 -2
View File
@@ -69,7 +69,7 @@ function refreshChartTheme() {
pkChart.options.scales.x.grid.color = gridColor; pkChart.options.scales.x.grid.color = gridColor;
pkChart.options.plugins.legend.labels.color = legendColor; pkChart.options.plugins.legend.labels.color = legendColor;
// Les couleurs de texte des zones cliniques dépendent aussi du thème (cf. chartManager.js) // The clinical zone text colors also depend on the theme (see chartManager.js)
updateChartAnnotations(pkChart, currentMode, pkChart.options.scales.y.max); updateChartAnnotations(pkChart, currentMode, pkChart.options.scales.y.max);
pkChart.update(); pkChart.update();
} }
@@ -189,7 +189,7 @@ function updateSimulation() {
const weight = parseFloat(weightInput?.value) || 70; const weight = parseFloat(weightInput?.value) || 70;
const unit = document.getElementById('infusionUnitSelect')?.value || 'mg_kg_h'; const unit = document.getElementById('infusionUnitSelect')?.value || 'mg_kg_h';
// Avertissement de poids (30-120 kg) // Weight warning (30-120 kg)
const weightWarning = document.getElementById('weightWarning'); const weightWarning = document.getElementById('weightWarning');
if (weightWarning) { if (weightWarning) {
if (weight < 30 || weight > 120) { if (weight < 30 || weight > 120) {
+1 -1
View File
@@ -8,7 +8,7 @@ export function initCalculatorUI() {
if (!modal || !btnOpen || !btnClose) return; if (!modal || !btnOpen || !btnClose) return;
btnOpen.addEventListener('click', () => { btnOpen.addEventListener('click', () => {
// Utilise patientWeight à la place d'inputWeight // Uses patientWeight instead of inputWeight
const mainWeight = document.getElementById('patientWeight')?.value || 70; const mainWeight = document.getElementById('patientWeight')?.value || 70;
const calcWeightInput = document.getElementById('calcWeight'); const calcWeightInput = document.getElementById('calcWeight');
if (calcWeightInput) calcWeightInput.value = mainWeight; if (calcWeightInput) calcWeightInput.value = mainWeight;
+5 -5
View File
@@ -1,9 +1,9 @@
import { CLINICAL_THRESHOLDS } from './pkCore.js'; import { CLINICAL_THRESHOLDS } from './pkCore.js';
import { t } from './i18n.js'; import { t } from './i18n.js';
// Les couleurs de zone sont volontairement fixes (indépendantes du thème d'interface) : // The zone colors are deliberately fixed (independent of the interface theme):
// elles reproduisent le dégradé de référence de l'outil KetaPK original. Seul le texte // they reproduce the reference gradient of the original KetaPK tool. Only the text
// (zone.textHex) est choisi pour rester lisible sur cette couleur de fond opaque. // (zone.textHex) is chosen to remain readable on this opaque background color.
export function updateChartAnnotations(chart, mode, currentYMax) { export function updateChartAnnotations(chart, mode, currentYMax) {
const thresholds = CLINICAL_THRESHOLDS[mode]; const thresholds = CLINICAL_THRESHOLDS[mode];
const annotations = {}; const annotations = {};
@@ -36,14 +36,14 @@ export function updateChartAnnotations(chart, mode, currentYMax) {
export function adjustChartYScale(chart, mode, datasets) { export function adjustChartYScale(chart, mode, datasets) {
let highestValue = 0; let highestValue = 0;
// Recherche de la valeur maximale atteinte parmi tous les modèles affichés // Find the maximum value reached among all displayed models
datasets.forEach(ds => { datasets.forEach(ds => {
ds.data.forEach(val => { ds.data.forEach(val => {
if (val > highestValue) highestValue = val; if (val > highestValue) highestValue = val;
}); });
}); });
// Ajustement dynamique à 110 % de la valeur pic (ou plancher à 50 ng/mL si vide) // Dynamic adjustment to 110% of the peak value (or floor at 50 ng/mL if empty)
let targetYMax = highestValue > 0 ? Math.ceil(highestValue * 1.1) : 50; let targetYMax = highestValue > 0 ? Math.ceil(highestValue * 1.1) : 50;
chart.options.scales.y.max = targetYMax; chart.options.scales.y.max = targetYMax;
+4 -4
View File
@@ -28,7 +28,7 @@ export async function exportSimulationToCSV(simulationData, patientInfo) {
let csvContent = "\uFEFF"; let csvContent = "\uFEFF";
// Métadonnées du rapport // Report metadata
csvContent += `${t('csv.title')}\n`; csvContent += `${t('csv.title')}\n`;
if (patientInfo.name) { if (patientInfo.name) {
csvContent += `${t('patient.name_label') || 'Patient'}:,${patientInfo.name}\n`; csvContent += `${t('patient.name_label') || 'Patient'}:,${patientInfo.name}\n`;
@@ -36,10 +36,10 @@ export async function exportSimulationToCSV(simulationData, patientInfo) {
csvContent += `${t('csv.weight')}:,${patientInfo.weight}\n`; csvContent += `${t('csv.weight')}:,${patientInfo.weight}\n`;
csvContent += `${t('csv.unit')}:,${patientInfo.infusionUnit}\n`; csvContent += `${t('csv.unit')}:,${patientInfo.infusionUnit}\n`;
// Ligne de Traçabilité de Version // Version traceability line
csvContent += `${t('csv.version') || 'Version'}:,v${APP_VERSION}\n\n`; csvContent += `${t('csv.version') || 'Version'}:,v${APP_VERSION}\n\n`;
// En-têtes du tableau // Table headers
const headers = [ const headers = [
t('csv.col_time'), t('csv.col_time'),
t('csv.col_bolus'), t('csv.col_bolus'),
@@ -50,7 +50,7 @@ export async function exportSimulationToCSV(simulationData, patientInfo) {
]; ];
csvContent += headers.join(",") + "\n"; csvContent += headers.join(",") + "\n";
// Lignes de données // Data rows
simulationData.timeline.forEach((item, index) => { simulationData.timeline.forEach((item, index) => {
const dominoCp = (simulationData.domino && simulationData.domino[index]) ? Math.round(simulationData.domino[index].cp) : "N/A"; const dominoCp = (simulationData.domino && simulationData.domino[index]) ? Math.round(simulationData.domino[index].cp) : "N/A";
const clementsCp = (simulationData.clements && simulationData.clements[index]) ? Math.round(simulationData.clements[index].cp) : "N/A"; const clementsCp = (simulationData.clements && simulationData.clements[index]) ? Math.round(simulationData.clements[index].cp) : "N/A";
+4 -4
View File
@@ -4,7 +4,7 @@ export function generatePDFReport(pkChart, patient, mode) {
const { jsPDF } = window.jspdf; const { jsPDF } = window.jspdf;
const doc = new jsPDF('landscape', 'mm', 'a4'); const doc = new jsPDF('landscape', 'mm', 'a4');
// En-tête // Header
doc.setFillColor(30, 41, 59); doc.setFillColor(30, 41, 59);
doc.rect(0, 0, 297, 25, 'F'); doc.rect(0, 0, 297, 25, 'F');
@@ -13,7 +13,7 @@ export function generatePDFReport(pkChart, patient, mode) {
doc.setFont("helvetica", "bold"); doc.setFont("helvetica", "bold");
doc.text(t('pdf.title'), 14, 16); doc.text(t('pdf.title'), 14, 16);
// Informations Patient // Patient information
doc.setTextColor(50, 50, 50); doc.setTextColor(50, 50, 50);
doc.setFontSize(10); doc.setFontSize(10);
doc.setFont("helvetica", "normal"); doc.setFont("helvetica", "normal");
@@ -25,11 +25,11 @@ export function generatePDFReport(pkChart, patient, mode) {
doc.text(`${t('pdf.mode')}${mode}`, 14, 42); doc.text(`${t('pdf.mode')}${mode}`, 14, 42);
doc.text(t('pdf.patient_info', patient), 14, 49); doc.text(t('pdf.patient_info', patient), 14, 49);
// Image du Graphique // Chart image
const chartImage = pkChart.toBase64Image('image/jpeg', 1.0); const chartImage = pkChart.toBase64Image('image/jpeg', 1.0);
doc.addImage(chartImage, 'JPEG', 14, 55, 268, 120); doc.addImage(chartImage, 'JPEG', 14, 55, 268, 120);
// Pied de page // Footer
doc.setFont("helvetica", "italic"); doc.setFont("helvetica", "italic");
doc.setFontSize(8); doc.setFontSize(8);
doc.setTextColor(150, 150, 150); doc.setTextColor(150, 150, 150);
+15 -15
View File
@@ -1,12 +1,12 @@
/** /**
* ============================================================================ * ============================================================================
* KétaPK - Moteur Pharmacocinétique (PK/PD) * KétaPK - Pharmacokinetic Engine (PK/PD)
* Modèles : Domino (1982), Clements (1981), Kamp (2020) * Models: Domino (1982), Clements (1981), Kamp (2020)
* ============================================================================ * ============================================================================
*/ */
/** /**
* Version unique de l'application (Centralisée) * Single application version (centralized)
*/ */
export const APP_VERSION = '0.19.0'; export const APP_VERSION = '0.19.0';
@@ -15,7 +15,7 @@ export const MODES = {
RACEMIQUE: 'RACEMIQUE' RACEMIQUE: 'RACEMIQUE'
}; };
// Modèles Multi-exponentiels // Multi-exponential models
export const PK_MODELS = { export const PK_MODELS = {
DOMINO: { DOMINO: {
name: "Domino (1982)", name: "Domino (1982)",
@@ -43,7 +43,7 @@ export const PK_MODELS = {
} }
}; };
// Zones d'effets cliniques et seuils (ng/mL) // Clinical effect zones and thresholds (ng/mL)
export const CLINICAL_THRESHOLDS = { export const CLINICAL_THRESHOLDS = {
[MODES.ESKETAMINE]: { [MODES.ESKETAMINE]: {
maxScale: 500, maxScale: 500,
@@ -73,7 +73,7 @@ export const CLINICAL_THRESHOLDS = {
} }
}; };
// Unités de perfusion supportées // Supported infusion units
export const INFUSION_UNITS = { export const INFUSION_UNITS = {
MG_KG_H: 'mg_kg_h', // mg/kg/h MG_KG_H: 'mg_kg_h', // mg/kg/h
MCG_KG_MIN: 'mcg_kg_min',// µg/kg/min MCG_KG_MIN: 'mcg_kg_min',// µg/kg/min
@@ -81,7 +81,7 @@ export const INFUSION_UNITS = {
}; };
/** /**
* Calcule la dose en mg administrée sur un intervalle de 5 minutes * Calculates the dose in mg administered over a 5-minute interval
*/ */
export function calculate5MinInfusionDose(rate, unit, weight) { export function calculate5MinInfusionDose(rate, unit, weight) {
if (!rate || rate <= 0) return 0; if (!rate || rate <= 0) return 0;
@@ -99,7 +99,7 @@ export function calculate5MinInfusionDose(rate, unit, weight) {
} }
/** /**
* Calcul d'un bolus unique à l'instant t avec prise en compte de refDose * Calculation of a single bolus at time t, accounting for refDose
*/ */
export function calculateBolusCp(model, doseMg, weightKg, timeElapsedMin) { export function calculateBolusCp(model, doseMg, weightKg, timeElapsedMin) {
if (timeElapsedMin < 0 || weightKg <= 0 || !model.refDose) return 0; if (timeElapsedMin < 0 || weightKg <= 0 || !model.refDose) return 0;
@@ -111,12 +111,12 @@ export function calculateBolusCp(model, doseMg, weightKg, timeElapsedMin) {
const termB = B * Math.exp(-beta * timeElapsedMin); const termB = B * Math.exp(-beta * timeElapsedMin);
const termC = C * (gamma ? Math.exp(-gamma * timeElapsedMin) : 0); const termC = C * (gamma ? Math.exp(-gamma * timeElapsedMin) : 0);
// Division par refDose pour respecter la formule exacte de l'Excel // Division by refDose to match the exact Excel formula
return (dosePerKg / refDose) * (termA + termB + termC); return (dosePerKg / refDose) * (termA + termB + termC);
} }
/** /**
* Propage la perfusion active sur la timeline (pas de 5 min) * Propagates the active infusion across the timeline (5-min steps)
*/ */
export function processTimelineEvents(timelineInputs, weightKg, infusionUnit) { export function processTimelineEvents(timelineInputs, weightKg, infusionUnit) {
let currentRate = 0; let currentRate = 0;
@@ -142,7 +142,7 @@ export function processTimelineEvents(timelineInputs, weightKg, infusionUnit) {
} }
/** /**
* Convertit le débit de perfusion courant en mg/kg/min, quelle que soit l'unité saisie * Converts the current infusion rate to mg/kg/min, regardless of the input unit
*/ */
function infusionRateToMgKgPerMin(rate, unit, weightKg) { function infusionRateToMgKgPerMin(rate, unit, weightKg) {
if (!rate || rate <= 0) return 0; if (!rate || rate <= 0) return 0;
@@ -160,8 +160,8 @@ function infusionRateToMgKgPerMin(rate, unit, weightKg) {
} }
/** /**
* solution exacte (méthode récursive discrétisée, cf. formule KétaPK) des bolus et * Exact resolution (discretized recursive method, cf. KétaPK formula) of boluses and
* perfusions (à débit variable) sur la timeline, ng/mL arrondis à l'entier. * infusions (with variable rate) across the timeline, ng/mL rounded to the nearest integer.
*/ */
export function generateSimulationData({ model, weightKg, timelineInputs, infusionUnit = INFUSION_UNITS.MG_KG_H }) { export function generateSimulationData({ model, weightKg, timelineInputs, infusionUnit = INFUSION_UNITS.MG_KG_H }) {
const processedTimeline = processTimelineEvents(timelineInputs, weightKg, infusionUnit); const processedTimeline = processTimelineEvents(timelineInputs, weightKg, infusionUnit);
@@ -191,7 +191,7 @@ export function generateSimulationData({ model, weightKg, timelineInputs, infusi
if (!nextEvent) return; if (!nextEvent) return;
const deltaT = nextEvent.time - event.time; const deltaT = nextEvent.time - event.time;
// Excel applique le débit déclaré à la fin de l'intervalle (colonne d'arrivée), pas celui du départ // Excel applies the declared rate at the end of the interval (arrival column), not the starting one
const Rn = infusionRateToMgKgPerMin(nextEvent.infusionRate, infusionUnit, weightKg) / refDose; const Rn = infusionRateToMgKgPerMin(nextEvent.infusionRate, infusionUnit, weightKg) / refDose;
const Falpha = Math.exp(-alpha * deltaT); const Falpha = Math.exp(-alpha * deltaT);
@@ -209,7 +209,7 @@ export function generateSimulationData({ model, weightKg, timelineInputs, infusi
return cpResults; return cpResults;
} }
// Moteur de conversion de doses // Dose conversion engine
export const CalculatorEngine = { export const CalculatorEngine = {
mgToMgKg: (mg, weight) => (weight > 0 ? mg / weight : 0), mgToMgKg: (mg, weight) => (weight > 0 ? mg / weight : 0),
mgKgToMg: (mgKg, weight) => mgKg * weight, mgKgToMg: (mgKg, weight) => mgKg * weight,