From 77eac770504a0dd5e8be40518e023b730e58cc46 Mon Sep 17 00:00:00 2001 From: masterdev Date: Wed, 17 Jun 2026 03:10:05 +0100 Subject: [PATCH] Add selectable frontend language and form labels --- frontend/src/App.tsx | 520 ++++++++++++++---------- frontend/src/components/MachineCard.tsx | 49 +-- frontend/src/components/StatusBadge.tsx | 21 +- frontend/src/styles.css | 43 ++ 4 files changed, 377 insertions(+), 256 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3176a7c..6d8b97d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -18,6 +18,7 @@ type ToastKey = "config" | "order" | "downtime" | "scrap"; type ModuleKey = "atelier" | "machine" | "trs" | "orders" | "downtimes" | "scrap" | "config"; type FormErrors = Record>; type FormControl = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement; +type Language = "en" | "fr"; const today = new Date().toISOString().slice(0, 10); const initialFormErrors: FormErrors = { @@ -27,15 +28,15 @@ const initialFormErrors: FormErrors = { scrap: {}, }; -const moduleTabs: Array<{ key: ModuleKey; label: string; description: string }> = [ - { key: "atelier", label: "Atelier", description: "Vue machines" }, - { key: "machine", label: "Machine", description: "Cycles et evenements" }, - { key: "trs", label: "TRS", description: "Performance jour" }, - { key: "orders", label: "OF", description: "Ordres fabrication" }, - { key: "downtimes", label: "Arrets", description: "Qualification" }, - { key: "scrap", label: "Rebuts", description: "Declaration" }, - { key: "config", label: "Config", description: "Parametrage signal" }, -]; +function FieldLabel({ label, error, children }: { label: string; error?: ReactNode; children: ReactNode }) { + return ( + + ); +} export default function App() { const [dashboard, setDashboard] = useState(null); @@ -47,6 +48,7 @@ export default function App() { const [dailyOee, setDailyOee] = useState(null); const [selectedMachineId, setSelectedMachineId] = useState(null); const [selectedOrderId, setSelectedOrderId] = useState(null); + const [language, setLanguage] = useState(() => (localStorage.getItem("plast-track-language") as Language | null) ?? "en"); const [activeModule, setActiveModule] = useState("atelier"); const [machineFilter, setMachineFilter] = useState("all"); const [machineSort, setMachineSort] = useState("code"); @@ -71,6 +73,20 @@ export default function App() { const refreshTimer = useRef(null); const toastTimers = useRef>({}); + function t(en: string, fr: string) { + return language === "fr" ? fr : en; + } + + const moduleTabs: Array<{ key: ModuleKey; label: string; description: string }> = [ + { key: "atelier", label: t("Workshop", "Atelier"), description: t("Machines view", "Vue machines") }, + { key: "machine", label: "Machine", description: t("Cycles and events", "Cycles et evenements") }, + { key: "trs", label: "OEE/TRS", description: t("Daily performance", "Performance jour") }, + { key: "orders", label: t("Orders", "OF"), description: t("Production orders", "Ordres fabrication") }, + { key: "downtimes", label: t("Stops", "Arrets"), description: "Qualification" }, + { key: "scrap", label: t("Scrap", "Rebuts"), description: t("Declaration", "Declaration") }, + { key: "config", label: "Config", description: t("Signal setup", "Parametrage signal") }, + ]; + async function loadDashboard() { const snapshot = await fetchJson("/api/dashboard"); setDashboard(snapshot); @@ -128,6 +144,10 @@ export default function App() { }); }, []); + useEffect(() => { + localStorage.setItem("plast-track-language", language); + }, [language]); + useEffect(() => { const timer = window.setInterval(() => setNowMs(Date.now()), 30000); return () => window.clearInterval(timer); @@ -291,17 +311,17 @@ export default function App() { form.reset(); setSelectedOrderId(createdOrder.id); setFormErrors((current) => ({ ...current, order: {} })); - showToast("order", "Production order created."); + showToast("order", t("Production order created.", "Ordre de fabrication cree.")); await loadAll(); } catch (error) { - setActionErrors((current) => ({ ...current, order: error instanceof Error ? error.message : "Order creation failed." })); + setActionErrors((current) => ({ ...current, order: error instanceof Error ? error.message : t("Order creation failed.", "Creation OF echouee.") })); } } async function submitOrderUpdate(event: FormEvent) { event.preventDefault(); if (!selectedOrder) { - setActionErrors((current) => ({ ...current, order: "Select an OF before updating." })); + setActionErrors((current) => ({ ...current, order: t("Select an OF before updating.", "Selectionner un OF avant modification.") })); return; } const form = event.currentTarget; @@ -325,10 +345,10 @@ export default function App() { }); setFormErrors((current) => ({ ...current, order: {} })); - showToast("order", "Production order updated."); + showToast("order", t("Production order updated.", "Ordre de fabrication modifie.")); await loadAll(); } catch (error) { - setActionErrors((current) => ({ ...current, order: error instanceof Error ? error.message : "Order update failed." })); + setActionErrors((current) => ({ ...current, order: error instanceof Error ? error.message : t("Order update failed.", "Modification OF echouee.") })); } } @@ -350,20 +370,20 @@ export default function App() { form.reset(); setFormErrors((current) => ({ ...current, downtime: {} })); - showToast("downtime", "Downtime qualified."); + showToast("downtime", t("Downtime qualified.", "Arret qualifie.")); await loadAll(); if (selectedMachineId !== null) { await loadDetail(selectedMachineId); } } catch (error) { - setActionErrors((current) => ({ ...current, downtime: error instanceof Error ? error.message : "Downtime qualification failed." })); + setActionErrors((current) => ({ ...current, downtime: error instanceof Error ? error.message : t("Downtime qualification failed.", "Qualification arret echouee.") })); } } async function submitScrap(event: FormEvent) { event.preventDefault(); if (!selectedMachine) { - setActionErrors((current) => ({ ...current, scrap: "Select a machine before declaring scrap." })); + setActionErrors((current) => ({ ...current, scrap: t("Select a machine before declaring scrap.", "Selectionner une machine avant de declarer le rebut.") })); return; } const form = event.currentTarget; @@ -385,10 +405,10 @@ export default function App() { form.reset(); setFormErrors((current) => ({ ...current, scrap: {} })); - showToast("scrap", "Scrap declaration recorded."); + showToast("scrap", t("Scrap declaration recorded.", "Declaration rebut enregistree.")); await loadAll(); } catch (error) { - setActionErrors((current) => ({ ...current, scrap: error instanceof Error ? error.message : "Scrap declaration failed." })); + setActionErrors((current) => ({ ...current, scrap: error instanceof Error ? error.message : t("Scrap declaration failed.", "Declaration rebut echouee.") })); } } @@ -398,14 +418,14 @@ export default function App() { await postJson(`/api/production-orders/${orderId}/${action}`); await loadAll(); } catch (error) { - setActionErrors((current) => ({ ...current, order: error instanceof Error ? error.message : "Order status change failed." })); + setActionErrors((current) => ({ ...current, order: error instanceof Error ? error.message : t("Order status change failed.", "Changement statut OF echoue.") })); } } async function submitMachineConfig(event: FormEvent) { event.preventDefault(); if (!selectedMachine) { - setActionErrors((current) => ({ ...current, config: "Select a machine before saving configuration." })); + setActionErrors((current) => ({ ...current, config: t("Select a machine before saving configuration.", "Selectionner une machine avant d'enregistrer la configuration.") })); return; } @@ -430,11 +450,11 @@ export default function App() { }); setFormErrors((current) => ({ ...current, config: {} })); - showToast("config", "Machine configuration saved."); + showToast("config", t("Machine configuration saved.", "Configuration machine enregistree.")); await loadAll(); await loadDetail(selectedMachine.id); } catch (error) { - setActionErrors((current) => ({ ...current, config: error instanceof Error ? error.message : "Machine configuration failed." })); + setActionErrors((current) => ({ ...current, config: error instanceof Error ? error.message : t("Machine configuration failed.", "Configuration machine echouee.") })); } } @@ -468,7 +488,7 @@ export default function App() { } function downtimeOptionLabel(downtime: Downtime) { - const state = downtime.end_time === null ? "Open" : "Ended"; + const state = downtime.end_time === null ? t("Open", "Ouvert") : t("Ended", "Termine"); const parts = [`#${downtime.id}`, state, downtimeMachineLabel(downtime)]; if (downtime.order_number) { parts.push(downtime.order_number); @@ -480,12 +500,12 @@ export default function App() { if (downtime.reason_code) { return downtime.reason_code; } - return downtime.end_time === null ? "Open - waiting qualification" : "Ended - waiting qualification"; + return downtime.end_time === null ? t("Open - waiting qualification", "Ouvert - en attente de qualification") : t("Ended - waiting qualification", "Termine - en attente de qualification"); } function formatDurationSeconds(durationSec: number | null) { if (durationSec === null) { - return "In progress"; + return t("In progress", "En cours"); } if (durationSec < 60) { return `${durationSec} s`; @@ -507,29 +527,45 @@ export default function App() { return machines.find((machine) => machine.id === order.machine_id)?.code ?? `Machine ${order.machine_id}`; } + function connectionLabel(status: string) { + if (status === "Connecting") { + return t("Connecting", "Connexion"); + } + if (status === "Load failed") { + return t("Load failed", "Chargement echoue"); + } + if (status === "Live") { + return t("Live", "Direct"); + } + if (status === "Reconnecting") { + return t("Reconnecting", "Reconnexion"); + } + return status; + } + function renderModule(): ReactNode { if (activeModule === "atelier") { return ( - setMachineFilter(event.target.value)} aria-label={t("Filter machines by status", "Filtrer les machines par etat")}> + {machineStatuses.map((status) => ( ))} - setMachineSort(event.target.value)} aria-label={t("Sort machines", "Trier les machines")}> + + + } @@ -541,6 +577,7 @@ export default function App() { machine={machine} selected={machine.id === selectedMachineId} nowMs={nowMs} + language={language} onSelect={() => selectMachine(machine.id, "machine")} onCreateOrder={() => selectMachine(machine.id, "orders")} onDeclareScrap={() => selectMachine(machine.id, "scrap")} @@ -548,54 +585,54 @@ export default function App() { /> ))} - {visibleMachines.length === 0 ?
No machines match the current filter.
: null} + {visibleMachines.length === 0 ?
{t("No machines match the current filter.", "Aucune machine ne correspond au filtre.")}
: null}
); } if (activeModule === "machine") { return ( - + {selectedMachine ? (
- } /> - - - + } /> + + +
- Machine + {t("Machine", "Machine")} {selectedMachine.brand} {selectedMachine.model}
- Signal setup + {t("Signal setup", "Parametrage signal")} {selectedMachine.config.cycle_signal_edge} edge / {selectedMachine.config.debounce_ms} ms /{" "} {selectedMachine.config.stop_detection_delay_sec} s
- Good parts + {t("Good parts", "Pieces bonnes")} {selectedMachine.produced_qty - selectedMachine.scrap_qty}
-

Recent cycles

- {detail.cycles.length} samples +

{t("Recent cycles", "Cycles recents")}

+ {detail.cycles.length} {t("samples", "echantillons")}
-

Recent events

+

{t("Recent events", "Evenements recents")}

    {detail.events.slice(0, 8).map((item) => (
  • @@ -606,7 +643,7 @@ export default function App() {
-

Downtime history

+

{t("Downtime history", "Historique arrets")}

    {detail.downtimes.slice(0, 8).map((item) => (
  • @@ -622,7 +659,7 @@ export default function App() {
) : ( -

No machine selected.

+

{t("No machine selected.", "Aucune machine selectionnee.")}

)} ); @@ -630,14 +667,14 @@ export default function App() { if (activeModule === "trs") { return ( - + {dailyOee ? (
- - - + + +
{machines.map((machine) => ( @@ -655,7 +692,10 @@ export default function App() { if (activeModule === "orders") { return ( - +
validateForm(event.currentTarget, "order")} noValidate > - - {fieldError("order", "order_number")} - + + + - {fieldError("order", "machine_id")} - - {fieldError("order", "article_ref")} - - {fieldError("order", "article_name")} - - - - {fieldError("order", "planned_qty")} - - {fieldError("order", "cavities")} - - {fieldError("order", "theoretical_cycle_time_sec")} + {machines.map((machine) => ( + + ))} + + + + + + + + + + + + + + + + + + + + + + + {actionError("order")} {toast("order")} @@ -701,12 +752,12 @@ export default function App() { - - - - - - + + + + + + @@ -723,13 +774,13 @@ export default function App() { @@ -742,28 +793,28 @@ export default function App() {
- OF detail + {t("OF detail", "Detail OF")}

{selectedOrder.order_number}

- +
- Machine + {t("Machine", "Machine")} {orderMachineLabel(selectedOrder)}
- Article + {t("Article", "Article")} {selectedOrder.article_ref} / {selectedOrder.article_name}
- Cycle / cavities + {t("Cycle / cavities", "Cycle / empreintes")} {selectedOrder.theoretical_cycle_time_sec} s / {selectedOrder.cavities}
- Dates + {t("Dates", "Dates")} - {selectedOrder.started_at ? new Date(selectedOrder.started_at).toLocaleString() : "Not started"} + {selectedOrder.started_at ? new Date(selectedOrder.started_at).toLocaleString() : t("Not started", "Non demarre")} {selectedOrder.ended_at ? ` -> ${new Date(selectedOrder.ended_at).toLocaleString()}` : ""}
@@ -777,38 +828,48 @@ export default function App() { onChangeCapture={(event) => validateForm(event.currentTarget, "order")} noValidate > - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + {actionError("order")} {toast("order")}
) : ( -
Select an OF to see and update details.
+
{t("Select an OF to see and update details.", "Selectionner un OF pour voir et modifier les details.")}
)} @@ -818,11 +879,11 @@ export default function App() { if (activeModule === "downtimes") { return (
@@ -835,33 +896,38 @@ export default function App() { onChangeCapture={(event) => validateForm(event.currentTarget, "downtime")} noValidate > - + - ))} - - {fieldError("downtime", "downtime_id")} - + + + - {fieldError("downtime", "reason_code")} - - {fieldError("downtime", "qualified_by")} -
OFMachineArticlePlannedStatusActions{t("OF", "OF")}{t("Machine", "Machine")}{t("Article", "Article")}{t("Planned", "Prevu")}{t("Status", "Etat")}{t("Actions", "Actions")}
{order.status}