diff --git a/web/themes/default/app.js b/web/themes/default/app.js
index 158dc44..fb20486 100644
--- a/web/themes/default/app.js
+++ b/web/themes/default/app.js
@@ -108,10 +108,10 @@ function _langToggleLabel(lang){
function _mapSupportedLang(lang){
if(!lang)return '';
var l=String(lang).toLowerCase().replace(/_/g,'-').trim();
- if(l==='de'||l==='en'||l==='es'||l==='zh-cn')return l;
+ if(l==='de'||l==='en'||l==='es'||l==='fr'||l==='zh-cn')return l;
var base=l.split('-')[0];
- if(base==='de'||base==='en'||base==='es')return base;
+ if(base==='de'||base==='en'||base==='es'||base==='fr')return base;
if(base==='zh'){
if(l.indexOf('cn')>=0||l.indexOf('hans')>=0||l==='zh')return 'zh-cn';
diff --git a/web/themes/default/index.html b/web/themes/default/index.html
index 5023291..861b61c 100644
--- a/web/themes/default/index.html
+++ b/web/themes/default/index.html
@@ -38,6 +38,7 @@
+
diff --git a/web/translations/fr.json b/web/translations/fr.json
new file mode 100644
index 0000000..010db4a
--- /dev/null
+++ b/web/translations/fr.json
@@ -0,0 +1,247 @@
+{
+ "header_status_standby": "Prêt",
+ "header_status_printing": "Impression",
+ "header_status_complete": "Terminé",
+ "header_status_error": "Erreur",
+ "kobra_free": "Disponible",
+ "kobra_busy": "Occupé",
+ "kobra_printing": "Impression",
+ "kobra_preheating": "Préchauffage",
+ "kobra_auto_leveling": "Mise à niveau auto",
+ "kobra_checking": "Vérification",
+ "kobra_updated": "Mise à jour",
+ "kobra_init": "Initialisation",
+ "kobra_pausing": "Pause en cours…",
+ "kobra_paused": "En pause",
+ "kobra_resuming": "Reprise en cours…",
+ "kobra_resumed": "Repris",
+ "kobra_stopping": "Arrêt en cours…",
+ "kobra_stoped": "Arrêté",
+ "kobra_finished": "Terminé",
+ "kobra_failed": "Erreur",
+ "kobra_canceled": "Annulé",
+ "kobra_offline": "Hors ligne",
+ "nav_dashboard": "Tableau de bord",
+ "nav_print": "Impression",
+ "nav_temps": "Températures",
+ "nav_motion": "Mouvement",
+ "nav_ams": "AMS",
+ "nav_extras": "Lumière / Ventilateur",
+ "nav_console": "Console",
+ "card_progress": "Progression",
+ "card_temps": "Températures",
+ "card_light_fan": "Ventilateur",
+ "card_speed": "Vitesse d'impression",
+ "card_cam": "Caméra",
+ "lbl_elapsed": "Écoulé :",
+ "lbl_remaining": "Restant :",
+ "lbl_slicer_time": "Estimation slicer :",
+ "lbl_layers": "Couche",
+ "speed_silent": "🐢 Silencieux",
+ "speed_normal": "⚡ Normal",
+ "speed_sport": "🚀 Sport",
+ "lbl_light": "💡 Lumière",
+ "lbl_feed": "Charger",
+ "lbl_unload": "Décharger",
+ "card_ace_dry": "Séchage ACE",
+ "ace_dry_dryer": "Séchoir",
+ "ace_dry_status_off": "Statut : Arrêté",
+ "ace_dry_status_on": "Statut : Actif",
+ "ace_dry_status_remaining": "Restant",
+ "ace_dry_humidity": "Humidité",
+ "ace_dry_current_temp": "Température",
+ "ace_dry_chart": "Historique (Temp/Humidité)",
+ "ace_dry_temp": "Température (°C)",
+ "ace_dry_duration": "Durée (min)",
+ "ace_dry_start": "▶ Démarrer",
+ "ace_dry_stop": "■ Arrêter",
+ "ace_dry_auto_refill": "Remplissage auto",
+ "ace_dry_enable": "Activer le séchage",
+ "ace_dry_temp_line": "Température de séchage",
+ "ace_dry_time_line": "Durée de séchage",
+ "ace_dry_ui_pending": "(Interface seule, backend suivant)",
+ "ace_dry_dialog_title": "Réglages Temp/Durée du séchoir",
+ "ace_dry_dialog_temp": "Température (30-80°C)",
+ "ace_dry_dialog_time": "Temps restant (h:m:s)",
+ "ace_dry_dialog_confirm": "Confirmer",
+ "ace_dry_dialog_cancel": "Annuler",
+ "ace_dry_dialog_save_restart": "Enregistrer et redémarrer",
+ "ace_dry_dialog_custom_name": "Nom personnalisé",
+ "ace_dry_dialog_reset_default": "Réinitialiser",
+ "cam_placeholder": "📷 Caméra non démarrée",
+ "cam_stream_unavailable": "Flux indisponible",
+ "btn_cam_start": "▶ Caméra",
+ "btn_cam_stop": "◼ Caméra",
+ "btn_pause": "⏸ Pause",
+ "btn_resume": "▶ Reprendre",
+ "btn_cancel": "✕ Arrêter",
+ "label_nozzle": "Buse",
+ "label_bed": "Plateau",
+ "label_fan": "🌀 Ventilateur",
+ "label_light": "💡 Lumière",
+ "label_on_off": "On / Off",
+ "label_speed": "Vitesse",
+ "panel_print_title": "Contrôle impression",
+ "panel_print_btn_pause": "⏸ Pause",
+ "panel_print_btn_resume": "▶ Reprendre",
+ "panel_print_btn_cancel": "✕ Annuler",
+ "panel_print_temps_live": "Températures (en direct)",
+ "label_set": "Définir",
+ "label_off": "Éteint",
+ "panel_temps_nozzle": "Buse",
+ "panel_temps_bed": "Plateau chauffant",
+ "panel_temps_chart": "Historique (60 dernières valeurs)",
+ "label_target_c": "Cible :",
+ "panel_motion_xy": "Axes XY",
+ "panel_motion_z": "Axe Z",
+ "label_step": "Pas :",
+ "btn_home_z": "Origine Z",
+ "btn_home_xy": "Origine XY",
+ "btn_home_all": "Origine Tout",
+ "btn_disable_motors": "Moteurs Off",
+ "panel_ams_title": "Filament",
+ "card_ams": "Filament",
+ "ams_no_data": "Aucune donnée AMS reçue",
+ "label_slot": "Slot",
+ "ams_empty": "Vide",
+ "panel_extras_light": "Lumière",
+ "panel_extras_fan": "Ventilateur",
+ "panel_extras_camera": "Caméra",
+ "btn_cam_start2": "▶ Démarrer",
+ "btn_cam_stop2": "◼ Arrêter",
+ "panel_console_title": "Journal d'événements",
+ "log_light_on": "Lumière allumée",
+ "log_light_off": "Lumière éteinte",
+ "log_fan": "Ventilateur →",
+ "log_nozzle": "Buse →",
+ "log_bed": "Plateau →",
+ "log_axis": "Axe",
+ "log_home": "Origine",
+ "log_home_all": "Origine Tout",
+ "log_cam_start": "Caméra démarrée :",
+ "log_cam_stop": "Caméra arrêtée",
+ "log_poll_error": "Erreur de sondage :",
+ "log_error": "Erreur :",
+ "confirm_cancel": "Vraiment annuler l'impression ?",
+ "settings_title": "Paramètres",
+ "settings_connection": "Connexion",
+ "settings_print": "Paramètres d'impression",
+ "settings_poll": "Intervalle de sondage",
+ "settings_version": "Version",
+ "settings_save": "Enregistrer et redémarrer",
+ "settings_printer_name": "Nom de l'imprimante",
+ "settings_printer_ip": "IP de l'imprimante",
+ "settings_mqtt_port": "Port MQTT",
+ "settings_username": "Nom d'utilisateur MQTT",
+ "settings_password": "Mot de passe MQTT",
+ "settings_device_id": "ID de l'appareil",
+ "settings_mode_id": "ID du mode",
+ "hint_ip_no_port": "Adresse IP uniquement, sans port (ex. 192.168.1.102)",
+ "settings_default_slot": "Slot par défaut (couleur unique)",
+ "settings_slot_auto": "Auto (tous les slots chargés)",
+ "settings_auto_leveling": "Mise à niveau auto avant impression",
+ "settings_camera_on_print": "Activer la caméra au démarrage de l'impression",
+ "settings_web_upload_warning": "Afficher un avertissement lors de l'impression de fichiers web",
+ "update_check": "Vérifier les mises à jour",
+ "update_checking": "Vérification…",
+ "update_available": "disponible",
+ "update_none": "Déjà à jour",
+ "update_apply": "Installer maintenant",
+ "update_applying": "Téléchargement…",
+ "update_restarting": "Redémarrage…",
+ "update_error": "Erreur",
+ "btn_connect": "⚡ Connecter",
+ "btn_disconnect": "✕ Déconnecter",
+ "lbl_conn_error": "Erreur de connexion :",
+ "slot_edit_title": "Modifier le slot",
+ "slot_edit_color": "Couleur",
+ "slot_edit_material": "Matériau",
+ "slot_edit_load": "⬇ Charger",
+ "slot_edit_unload": "⬆ Décharger",
+ "slot_edit_save": "💾 Enregistrer",
+ "slot_edit_custom": "ex. PLA, PETG, ABS…",
+ "slot_edit_ok": "Slot AMS",
+ "slot_edit_profile": "Profil OrcaSlicer",
+ "slot_edit_profile_hint": "Envoyé lors de la synchronisation OrcaSlicer comme marque spécifique au lieu de \"Générique\"",
+ "slot_edit_profile_default": "— Générique (défaut) —",
+ "orca_profile_section": "Profils OrcaSlicer",
+ "orca_profile_hint": "Importez vos propres profils de filament OrcaSlicer (ouvrez le dossier utilisateur via Aide → Afficher le dossier de configuration)",
+ "orca_profile_import_btn": "Importer des profils",
+ "orca_profile_import_link": "★ Importer mes profils…",
+ "orca_profile_import_title": "Importer vos profils OrcaSlicer",
+ "orca_profile_help_html": "Déposez un ZIP de votre dossier filament OrcaSlicer ou des fichiers .json individuels.
Dans OrcaSlicer : Aide → Afficher le dossier de configuration → user/<id>/filament/",
+ "orca_profile_dropmsg": "Déposez ici ou cliquez",
+ "orca_profile_list_label": "Profils importés",
+ "orca_profile_user_label": "Mes profils",
+ "orca_profile_user_empty": "– aucun –",
+ "orca_profile_uploading": "Envoi en cours…",
+ "orca_profile_done": "Importé",
+ "orca_profile_skipped": "ignoré",
+ "log_dir_all": "Tout",
+ "log_lvl_label": "Niveau :",
+ "file_ready_btn": "▶ Lancer l'impression",
+ "file_slots_btn": "🎨 Choisir les slots",
+ "file_cancel_btn": "✕ Annuler",
+ "nav_printers": "Imprimantes",
+ "skip_title": "✂ Ignorer des objets",
+ "skip_hint": "Décochez les objets que vous ne souhaitez plus imprimer :",
+ "skip_btn_label": "Objets",
+ "skip_no_objects": "Aucun objet dans cette impression.",
+ "skip_already": "ignoré",
+ "skip_select_at_least_one": "Veuillez sélectionner au moins un objet.",
+ "skip_sending": "Envoi …",
+ "skip_success": "Les objets seront ignorés.",
+ "fd_objects_hint": "Ignorer des objets (optionnel) :",
+ "fd_slots_hint": "Associer le canal GCode au slot AMS :",
+ "fd_cancel": "Annuler",
+ "fd_print": "▶ Imprimer",
+ "fd_no_slots_msg": "Aucun slot AMS chargé.{br}Lancer l'impression quand même ?",
+ "fd_slot": "Slot",
+ "fd_no_matching_material": "Aucun matériau correspondant",
+ "fd_used": "UTILISÉ",
+ "add_printer": "Ajouter une imprimante",
+ "apd_lbl_ip": "IP de l'imprimante",
+ "apd_lbl_name": "Nom (optionnel)",
+ "apd_placeholder_name": "ex. Kobra X Salon",
+ "apd_cancel": "Annuler",
+ "apd_confirm": "Ajouter",
+ "apd_fetching": "Récupération des données de l'imprimante…",
+ "apd_success": "Imprimante ajoutée, redémarrage du bridge…",
+ "apd_err_ip": "Veuillez saisir une adresse IP",
+ "printers_remove": "Supprimer l'imprimante",
+ "printers_remove_confirm": "Supprimer l'imprimante \"{name}\" ? Le bridge va redémarrer.",
+ "printers_active": "● actif",
+ "printers_switch": "Changer →",
+ "printers_current": "Imprimante actuelle",
+ "printers_loading": "Chargement…",
+ "printers_none": "Aucune imprimante configurée.",
+ "printers_empty_hint": "Aucune imprimante configurée.",
+ "nav_browser": "Navigateur",
+ "panel_browser_title": "Explorateur de fichiers",
+ "store_search_placeholder": "🔍 Rechercher…",
+ "store_empty": "Aucun fichier uploadé.",
+ "store_refresh": "↻ Actualiser",
+ "store_print": "▶ Imprimer",
+ "store_download": "⬇ Télécharger",
+ "store_delete_confirm": "Supprimer le fichier ?",
+ "store_print_confirm": "Imprimer le fichier ?",
+ "store_web_verify_title": "Vérifier le fichier",
+ "store_web_verify_msg": "Veuillez vérifier que ce fichier a été créé pour l'Anycubic Kobra X.",
+ "store_web_verify_confirm": "Confirmer",
+ "store_web_verify_abort": "Annuler",
+ "store_no_results": "Aucun fichier trouvé.",
+ "store_never": "jamais imprimé",
+ "store_estimate": "Estimation",
+ "store_upload_label_prefix": "Déposez un GCode ici ou ",
+ "store_upload_label_browse": "parcourir",
+ "store_upload_busy": "⏳ Envoi en cours…",
+ "store_upload_success": "✓ {file}",
+ "store_upload_error": "✗ {error}",
+ "sf_all": "Tout",
+ "sf_ok": "✓ Terminés",
+ "sf_err": "✗ Échoués",
+ "sf_new": "Nouveau",
+ "ss_date": "↓ Date",
+ "ss_name": "A–Z Nom",
+ "ss_dur": "⏱ Durée d'impression"
+}