]> git.giorgioravera.it Git - network-manager.git/commitdiff
Added translation
authorGiorgio Ravera <giorgio.ravera@gmail.com>
Wed, 5 Aug 2026 18:43:48 +0000 (20:43 +0200)
committerGiorgio Ravera <giorgio.ravera@gmail.com>
Wed, 5 Aug 2026 18:43:48 +0000 (20:43 +0200)
45 files changed:
TODO.md
backend/app.py
backend/backup.py
backend/bootstrap.py
backend/db/aliases.py
backend/db/hosts.py
backend/db/settings.py
backend/routes/about.py [deleted file]
backend/routes/aliases.py
backend/routes/backup.py
backend/routes/devices.py
backend/routes/dhcp.py
backend/routes/dns.py
backend/routes/health.py
backend/routes/hosts.py
backend/routes/localization.py [new file with mode: 0644]
backend/routes/settings.py
backend/routes/system.py
backend/settings/default.py
backend/settings/localization.py [new file with mode: 0644]
backend/settings/settings.py
frontend/aliases.html
frontend/css/layout.css
frontend/devices.html
frontend/hosts.html
frontend/index.html
frontend/js/aliases.js
frontend/js/api.js
frontend/js/backendMessages.js [new file with mode: 0644]
frontend/js/common.js
frontend/js/devices.js
frontend/js/hosts.js
frontend/js/i18n.js [new file with mode: 0644]
frontend/js/index.js
frontend/js/leases.js
frontend/js/login.js
frontend/js/logs.js
frontend/js/services.js
frontend/js/settings.js
frontend/leases.html
frontend/locales/en.json [new file with mode: 0644]
frontend/locales/it.json [new file with mode: 0644]
frontend/logs.html
frontend/modals.html
frontend/settings.html

diff --git a/TODO.md b/TODO.md
index 80061a996dffbd9c40c2e86784d1c299ecadbc14..490eca0bcc999b1f02503206b3f7c3fcd9a42113 100644 (file)
--- a/TODO.md
+++ b/TODO.md
 - [ ] Backup of generated configurations
 
 ### 🌍 Language
-- [ ] Localization
+- [X] Localization
 
 ---
 
index 9d19221d360b8869a16e4910f9432780fc39fd14..889607f21c541579eb51b0d7095960bddbcad975 100644 (file)
@@ -21,6 +21,7 @@ from backend.routes.certificates import router as certificates_router
 from backend.routes.backup import router as backup_router
 from backend.routes.logs import router as logs_router
 from backend.routes.settings import router as settings_router
+from backend.routes.localization import router as localization_router
 
 # Import Security
 from backend.security import is_logged_in, apply_session
@@ -226,6 +227,7 @@ def create_app() -> FastAPI:
     app.include_router(backup_router)
     app.include_router(logs_router)
     app.include_router(settings_router)
+    app.include_router(localization_router)
 
     # CORS
     cors_origins = [
index bba95e851fe1fa1190ef9ba9834cbdc151b80856..053fc33f212c32dfef11821b11d51b2663dbfb2e 100644 (file)
@@ -4,8 +4,8 @@
 from datetime import datetime, timezone
 import hashlib
 import json
-import os
 from pathlib import Path
+import shutil
 import time
 from typing import List, Dict, Any, Optional, Union
 import zipfile
@@ -33,6 +33,15 @@ backup_files = [
  # Set to True to remove individual backup files after creating the archive (optional, can be set to False for debugging)
 remove_backup_files = True
 
+# ---------------------------------------------------------
+# Internal: Cleanup Extract Directory
+# ---------------------------------------------------------
+def cleanup_extract_dir(path: Path) -> None:
+    try:
+        shutil.rmtree(path, ignore_errors=True)
+    except Exception as e:
+        logger.warning("Failed removing temporary folder %s: %s", path, str(e))
+
 # ---------------------------------------------------------
 # Internal: Generate Filestamp
 # ---------------------------------------------------------
@@ -53,6 +62,7 @@ def build_result(operations: Dict[str, Dict[str, Any]]) -> Dict[str, Any]:
         "total": len(operations),
         "success": sum(1 for op in operations.values() if op.get("status") == "success"),
         "failed": sum(1 for op in operations.values() if op.get("status") == "failure"),
+        "not_found": sum(1 for op in operations.values() if op.get("status") == "not_found"),
     }
 
     errors = sum(
@@ -281,7 +291,7 @@ def restore_hosts(
 
     except Exception as e:
         logger.exception("restore_hosts failed applying records: %s", str(e).strip())
-        errors.append(str(e));
+        errors.append(str(e))
 
     took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
 
@@ -293,12 +303,11 @@ def restore_hosts(
             "took_ms": took_ms,
         }
     else:
-        count_stored = count_loaded
         result: Dict[str, Any] = {
             "status": "success",
             "file": str(file),
             "count_loaded": count_loaded,
-            "count_stored": count_stored,
+            "count_restored": count_restored,
             "took_ms": took_ms,
         }
 
@@ -399,7 +408,7 @@ def restore_aliases(
 
     except Exception as e:
         logger.exception("restore_aliases failed applying records: %s", str(e).strip())
-        errors.append(str(e));
+        errors.append(str(e))
 
     took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
 
@@ -411,12 +420,11 @@ def restore_aliases(
             "took_ms": took_ms,
         }
     else:
-        count_stored = count_loaded
         result: Dict[str, Any] = {
             "status": "success",
             "file": str(file),
             "count_loaded": count_loaded,
-            "count_stored": count_stored,
+            "count_restored": count_restored,
             "took_ms": took_ms,
         }
 
@@ -574,6 +582,14 @@ def check_metadata(
 # ---------------------------------------------------------
 def backup_create() -> Dict[str, Any]:
 
+    # Initialization
+    operations = {
+        "metadata": {},
+        "hosts": {},
+        "aliases": {},
+        "archive": {},
+    }
+
     # Ensure backup directory exists
     base_dir = Path(get_config("BACKUP_PATH"))
     base_dir.mkdir(parents=True, exist_ok=True)
@@ -581,21 +597,13 @@ def backup_create() -> Dict[str, Any]:
     # Timestamp used for backup file naming
     ts = generate_timestamps()
     timestamp = ts["iso"]           # per metadata/API
-    file_timestamp = ts["file"]      # per filename
+    file_timestamp = ts["file"]     # per filename
 
     # Create zip folder
     zip_name = f"backup_{file_timestamp}.zip"
     backup_path=base_dir / f"backup_{file_timestamp}"
     backup_path.mkdir(parents=True, exist_ok=True)
 
-    # Init struttura unica
-    operations = {
-        "metadata": {},
-        "hosts": {},
-        "aliases": {},
-        "archive": {},
-    }
-
     # --- STEP ---
     operations["hosts"] = store_hosts(timestamp=timestamp, filepath=backup_path)
     operations["aliases"] = store_aliases(timestamp=timestamp, filepath=backup_path)
@@ -632,7 +640,7 @@ def backup_list() -> List[Dict[str, Any]]:
 # ---------------------------------------------------------
 def backup_restore(backup_id: str, cleanup: bool = True) -> Dict[str, Any]:
 
-    # Init struttura unica
+    # Initialization
     operations = {
         "archive": {},
         "metadata": {},
@@ -647,7 +655,11 @@ def backup_restore(backup_id: str, cleanup: bool = True) -> Dict[str, Any]:
 
     if not backup_file.is_file():
         logger.error(f"Backup file not found: {backup_file}")
-        raise FileNotFoundError("Backup file not found")
+        operations["archive"] = {
+            "status": "not_found",
+            "errors": [f"Backup file not found: {backup_id}"]
+        }
+        return build_result(operations)
 
     # --- ARCHIVE ---
     operations["archive"] = unzip_backup_archive(zip_name=backup_id, extract_dir=extract_dir)
@@ -657,6 +669,7 @@ def backup_restore(backup_id: str, cleanup: bool = True) -> Dict[str, Any]:
     # --- METADATA ---
     operations["metadata"] = check_metadata(filepath=extract_dir, remove_file=remove_backup_files)
     if operations["metadata"].get("status") != "success":
+        cleanup_extract_dir(extract_dir)
         return build_result(operations)
 
     # --- CLEANUP ---
@@ -664,9 +677,17 @@ def backup_restore(backup_id: str, cleanup: bool = True) -> Dict[str, Any]:
         try:
             reset_hosts_db()
             reset_aliases_db()
+            operations["cleanup"] = {
+                "status": "success"
+            }
         except Exception as e:
             logger.exception("Cleanup failed %s", str(e).strip())
-            raise
+            operations["cleanup"] = {
+                "status": "failure",
+                "errors": [str(e)]
+            }
+            cleanup_extract_dir(extract_dir)
+            return build_result(operations)
 
     # --- RESTORE FILES ---
     for f in operations["metadata"]["files"]:
@@ -677,9 +698,7 @@ def backup_restore(backup_id: str, cleanup: bool = True) -> Dict[str, Any]:
             operations["aliases"] = restore_aliases(filepath=extract_dir, filename=f["file"], remove_file=remove_backup_files)
 
     if remove_backup_files:
-        p = Path(extract_dir)
-        if p.is_dir() and not any(p.iterdir()):
-            p.rmdir()
+        cleanup_extract_dir(extract_dir)
 
     return build_result(operations)
 
@@ -689,8 +708,10 @@ def backup_restore(backup_id: str, cleanup: bool = True) -> Dict[str, Any]:
 def backup_delete(backup_id: str) -> Dict[str, Any]:
 
     # Initialization
+    operations = {
+        "delete": {},
+    }
     start_ns = time.monotonic_ns()
-    errors: List[str] = []
 
     backup_dir = Path(get_config("BACKUP_PATH"))
     backup_file = backup_dir / backup_id
@@ -698,20 +719,29 @@ def backup_delete(backup_id: str) -> Dict[str, Any]:
     try:
         # Check if file exists
         if not backup_file.is_file():
-            raise FileNotFoundError(f"Backup file not found: {backup_id}")
+            logger.error(f"Backup file not found: {backup_file}")
+            operations["delete"] = {
+                "status": "not_found",
+                "errors": [f"Backup file not found: {backup_id}"]
+            }
+            operations["delete"]["took_ms"] = (time.monotonic_ns() - start_ns) / 1_000_000
+            return build_result(operations)
 
         # Remove file
         backup_file.unlink()
 
+        operations["delete"] = {
+            "status": "success",
+            "file": str(backup_file),
+        }
+
     except Exception as e:
         logger.exception("delete_backup failed: %s", str(e).strip())
-        errors.append(str(e))
+        operations["delete"] = {
+            "status": "failure",
+            "errors": [str(e)],
+        }
 
-    took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
+    operations["delete"]["took_ms"] = (time.monotonic_ns() - start_ns) / 1_000_000
 
-    return {
-        "status": "failure" if errors else "success",
-        "file": str(backup_file) if not errors else None,
-        "errors": errors,
-        "took_ms": took_ms,
-    }
+    return build_result(operations)
index a09868070dc8b5e8619cb344f51f9b0830cbb311..f869fb1416c36850af576a10278ff5b1073c0a73 100644 (file)
@@ -36,6 +36,10 @@ def print_welcome(logger):
         "Database: file=%s | reset=%s",
         str(settings.DB_FILE), settings.DB_RESET
     )
+    logger.info(
+        "Language: %s",
+        get_config("LANGUAGE")
+    )
     logger.info(
         "Log: level=%s, to_file=%s, file=%s",
         get_config("LOG_LEVEL"), get_config("LOG_TO_FILE"), str(settings.LOG_FILE)
index 73465ef9adeb1b5194d18b453ea6051178ef3db9..a1745b83ff08f5aad04855e176b410fa862a6a5e 100644 (file)
@@ -82,7 +82,7 @@ def get_alias(alias_id: int) -> Optional[Dict[str, Any]]:
 # -----------------------------
 # ADD ALIAS
 # -----------------------------
-def add_alias(data: Dict[str, Any]) -> int:
+def add_alias(data: Dict[str, Any]):
 
     # Validate input
     cleaned = validate_data(data)
@@ -103,7 +103,6 @@ def add_alias(data: Dict[str, Any]) -> int:
             ),
         )
         conn.commit()
-        return cur.lastrowid
 
     except sqlite3.IntegrityError:
         conn.rollback()
index 3498df5f72654ed627b772b611e7036bda9882b3..bae2ece46cf96b0d0c342ee66189c30a69609093 100644 (file)
@@ -53,13 +53,13 @@ def validate_data(data: Dict[str, Any]) -> Dict[str, Any]:
         if not MAC_RE.match(mac):
             raise ValueError(f"Invalid MAC address: {mac}")
 
-    # Check description
+    # Check Description
     description = data.get("description")
 
-    # Normalizzazione boolean per DB (0/1)
+    # Boolean normalization for DB (0/1)
     ssl_enabled = int(bool(data.get("ssl_enabled", 0)))
 
-    # Normalizzazione (0/1/2)
+    # Normalization (0/1/2)
     v = int(data.get("visibility", 0))
     visibility = v if v in (0, 1, 2) else 0
 
@@ -128,7 +128,7 @@ def get_host(host_id: int) -> Optional[Dict[str, Any]]:
 # -----------------------------
 # ADD HOST
 # -----------------------------
-def add_host(data: Dict[str, Any]) -> int:
+def add_host(data: Dict[str, Any]):
 
     # Validate input
     cleaned = validate_data(data)
@@ -151,7 +151,6 @@ def add_host(data: Dict[str, Any]) -> int:
             ),
         )
         conn.commit()
-        return cur.lastrowid
 
     except sqlite3.IntegrityError:
         conn.rollback()
@@ -165,7 +164,7 @@ def add_host(data: Dict[str, Any]) -> int:
 # -----------------------------
 # UPDATE HOST
 # -----------------------------
-def update_host(host_id: int, data: Dict[str, Any]) -> bool:
+def update_host(host_id: int, data: Dict[str, Any]):
 
     # Validate input
     cleaned = validate_data(data)
@@ -190,8 +189,11 @@ def update_host(host_id: int, data: Dict[str, Any]) -> bool:
                 host_id,
             ),
         )
+
+        if cur.rowcount == 0:
+            raise ValueError(f"Host {host_id} not found")
+
         conn.commit()
-        return cur.rowcount > 0
 
     except Exception as err:
         conn.rollback()
@@ -201,7 +203,7 @@ def update_host(host_id: int, data: Dict[str, Any]) -> bool:
 # -----------------------------
 # DELETE HOST
 # -----------------------------
-def delete_host(host_id: int) -> bool:
+def delete_host(host_id: int):
 
     # Validate input
     if host_id is None:
@@ -210,8 +212,11 @@ def delete_host(host_id: int) -> bool:
     conn = get_db()
     try:
         cur = conn.execute("DELETE FROM hosts WHERE id = ?", (host_id,))
+
+        if cur.rowcount == 0:
+            raise ValueError(f"Host {host_id} not found")
+
         conn.commit()
-        return cur.rowcount > 0
 
     except Exception as err:
         conn.rollback()
index 44dc1d6e47bf0f1bc707ee86d820ded85d1a6468..2904b557e06455a656b6f4289a085952639443a7 100644 (file)
@@ -38,6 +38,13 @@ TYPE_CASTERS = {
 # Default Values
 # ---------------------------------------------------------
 CONFIG_DEFAULTS = {
+    "LANGUAGE": {
+        "value": settings.LANGUAGE,
+        "description": "Language for the application",
+        "group_name": "localization",
+        "type": "string",
+        "allowed": ["en", "es", "it", "fr", "de"],
+    },
     "LOG_LEVEL": {
         "value": settings.LOG_LEVEL,
         "description": "Logging verbosity level",
diff --git a/backend/routes/about.py b/backend/routes/about.py
deleted file mode 100644 (file)
index 95ffe31..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-# backend/system.py
-
-# Import standard modules
-from fastapi import APIRouter
-from datetime import datetime, timezone
-import os
-import signal
-import threading
-import time
-
-# Import Settings & Config
-from backend.settings.settings import settings
-from backend.db.settings import get_config
-
-# Create Router
-router = APIRouter()
-
-# ---------------------------------------------------------
-# Get Information
-# ---------------------------------------------------------
-@router.get("/about")
-def about():
-    return {
-        "app": {
-            "name": settings.APP_NAME,
-            "version": settings.APP_VERSION,
-        },
-        "domain": get_config("DOMAIN"),
-        "server_time": datetime.now(timezone.utc).isoformat(),
-    }
-
-# ---------------------------------------------------------
-# Restart Application
-# ---------------------------------------------------------
-@router.post("/api/restart")
-def restart():
-    def do_restart():
-        time.sleep(0.5)
-        os.kill(os.getpid(), signal.SIGTERM)
-
-    threading.Thread(target=do_restart, daemon=True).start()
-
-    return {"message": "Application restarting..."}
index 7569aba084c65a08896f57093104e95d088e919c..e1f26224a48a656657136031652116cde8edcda2 100644 (file)
@@ -1,9 +1,8 @@
 # backend/routes/aliases.py
 
 # import standard modules
-from fastapi import APIRouter, Request, Response, HTTPException, status
+from fastapi import APIRouter, HTTPException, status
 from fastapi.responses import FileResponse
-import ipaddress
 import time
 
 # Import local modules
@@ -31,7 +30,7 @@ router = APIRouter()
 # ---------------------------------------------------------
 # Aliass page
 @router.get("/aliases")
-def aliases_page(request: Request):
+def aliases_page():
     return FileResponse(settings.FRONTEND_PATH / "aliases.html")
 
 # Serve aliases.js
@@ -46,14 +45,12 @@ def aliases_js():
     200: {"description": "Aliass found"},
     500: {"description": "Internal server error"},
 })
-def api_get_aliases(request: Request):
+def api_get_aliases():
+
     try:
         aliases = get_aliases()
         return aliases or []
 
-    except HTTPException:
-        raise
-
     except Exception as err:
         logger.exception("Error getting list aliases %s", str(err).strip())
         raise HTTPException(
@@ -73,31 +70,13 @@ def api_get_aliases(request: Request):
     404: {"description": "Alias not found"},
     500: {"description": "Internal server error"},
 })
-def api_get_alias(request: Request, alias_id: int):
+def api_get_alias(alias_id: int):
 
     # Inizializzazioni
     start_ns = time.monotonic_ns()
 
     try:
         alias = get_alias(alias_id)
-        if not alias:  # None or empty dict
-            took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
-            raise HTTPException(
-                status_code=status.HTTP_404_NOT_FOUND,
-                detail={
-                    "code": "ALIAS_NOT_FOUND",
-                    "status": "failure",
-                    "message": "Alias not found",
-                    "details": {
-                        "alias_id": alias_id,
-                        "took_ms": took_ms,
-                    },
-                },
-            )
-        return alias
-
-    except HTTPException:
-        raise
 
     except Exception as err:
         logger.exception("Error getting alias %s: %s", alias_id, str(err).strip())
@@ -115,34 +94,52 @@ def api_get_alias(request: Request, alias_id: int):
             },
         )
 
+    if not alias:  # None or empty dict
+        took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND,
+            detail={
+                "code": "ALIAS_NOT_FOUND",
+                "status": "failure",
+                "message": "Alias not found",
+                "details": {
+                    "alias_id": alias_id,
+                    "took_ms": took_ms,
+                },
+            },
+        )
+
+    return alias
+
 # ---------------------------------------------------------
 # Add Aliass
 # ---------------------------------------------------------
-@router.post("/api/aliases", status_code=status.HTTP_200_OK, responses={
-    200: {"description": "Alias added"},
+@router.post("/api/aliases", status_code=status.HTTP_201_CREATED, responses={
+    201: {"description": "Alias added"},
     409: {"description": "Alias already present"},
     500: {"description": "Internal server error"},
 })
-def api_add_alias(request: Request, data: dict):
+def api_add_alias(data: dict):
 
     # Inizializzazioni
     start_ns = time.monotonic_ns()
+    alias_id = None
 
     try:
         alias_id = add_alias(data)
-        if(alias_id > 0):
-            took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
-            return {
-                    "code": "ALIAS_ADDED",
-                    "status": "success",
-                    "message": "Alias added successfully",
-                    "details": {
-                        "alias_id": alias_id,
-                        "took_ms": took_ms,
-                    },
-                }
+        took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
+        return {
+                "code": "ALIAS_ADDED",
+                "status": "success",
+                "message": "Alias added successfully",
+                "details": {
+                    "alias_id": alias_id,
+                    "took_ms": took_ms,
+                },
+            }
 
-        # Already present
+    # Not Found
+    except ValueError:
         took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
         raise HTTPException(
             status_code=status.HTTP_409_CONFLICT,
@@ -157,9 +154,6 @@ def api_add_alias(request: Request, data: dict):
             },
         )
 
-    except HTTPException:
-        raise
-
     except Exception as err:
         logger.exception("Error adding alias: %s", str(err).strip())
         took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
@@ -184,26 +178,26 @@ def api_add_alias(request: Request, data: dict):
     404: {"description": "Alias not found"},
     500: {"description": "Internal server error"},
 })
-def api_update_alias(request: Request, data: dict, alias_id: int):
+def api_update_alias(data: dict, alias_id: int):
 
     # Inizializzazioni
     start_ns = time.monotonic_ns()
 
     try:
-        updated = update_alias(alias_id, data)
-        if updated:
-            took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
-            return {
-                    "code": "ALIAS_UPDATED",
-                    "status": "success",
-                    "message": "Alias updated successfully",
-                    "details": {
-                        "alias_id": alias_id,
-                        "took_ms": took_ms,
-                    },
-                }
+        update_alias(alias_id, data)
+        took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
+        return {
+               "code": "ALIAS_UPDATED",
+                "status": "success",
+                "message": "Alias updated successfully",
+                "details": {
+                    "alias_id": alias_id,
+                    "took_ms": took_ms,
+                },
+            }
 
-        # Not Found
+    # Not Found
+    except ValueError:
         took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
         raise HTTPException(
             status_code=status.HTTP_404_NOT_FOUND,
@@ -218,9 +212,6 @@ def api_update_alias(request: Request, data: dict, alias_id: int):
             },
         )
 
-    except HTTPException:
-        raise
-
     except Exception as err:
         logger.exception("Error updating alias %s: %s", alias_id, str(err).strip())
         took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
@@ -245,26 +236,26 @@ def api_update_alias(request: Request, data: dict, alias_id: int):
     404: {"description": "Alias not found"},
     500: {"description": "Internal server error"},
 })
-def api_delete_alias(request: Request, alias_id: int):
+def api_delete_alias(alias_id: int):
 
     # Inizializzazioni
     start_ns = time.monotonic_ns()
 
     try:
-        deleted = delete_alias(alias_id)
-        if deleted:
-            took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
-            return {
-                    "code": "ALIAS_DELETED",
-                    "status": "success",
-                    "message": "Alias deleted successfully",
-                    "details": {
-                        "alias_id": alias_id,
-                        "took_ms": took_ms,
-                    },
-                }
+        delete_alias(alias_id)
+        took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
+        return {
+                "code": "ALIAS_DELETED",
+                "status": "success",
+                "message": "Alias deleted successfully",
+                "details": {
+                    "alias_id": alias_id,
+                    "took_ms": took_ms,
+                },
+            }
 
-        # Not Found
+    # Not Found
+    except ValueError:
         took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
         raise HTTPException(
             status_code=status.HTTP_404_NOT_FOUND,
@@ -279,9 +270,6 @@ def api_delete_alias(request: Request, alias_id: int):
             },
         )
 
-    except HTTPException:
-        raise
-
     except Exception as err:
         logger.exception("Error deleting alias %s: %s", alias_id, str(err).strip())
         took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
index 4bf2326930921f109418ecfab5bfcde8dfa8f705..41edbde63c85667ee5aee2e6b132c1fa4ba6eaee 100644 (file)
@@ -13,8 +13,7 @@ import zipfile
 # Import local modules
 from backend.backup import backup_create, backup_list, backup_restore, backup_delete
 
-# Import Settings & Config
-from backend.settings.settings import settings
+# Import Config
 from backend.db.settings import get_config
 # Import Logging
 from backend.log.log import get_logger
@@ -45,8 +44,10 @@ def to_partial_code(code_ok: str) -> str:
 def build_operation_response(
     *,
     code_ok: str,
+    code_not_found: str,
     code_error: str,
     message_ok: str,
+    message_not_found: str,
     message_partial: str,
     message_error: str,
     result: dict,
@@ -56,9 +57,24 @@ def build_operation_response(
     total = summary.get("total", 0)
     success = summary.get("success", 0)
     failed = summary.get("failed", 0)
+    not_found = summary.get("not_found", 0)
 
     took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
 
+    # Handle not found case
+    if not_found > 0:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND,
+            detail={
+                "code": code_not_found,
+                "status": "not_found",
+                "message": message_not_found,
+                "took_ms": took_ms,
+                "results": result,
+            },
+        )
+
+    # Handle partial success case
     is_partial = failed > 0 or success != total
     if is_partial:
         if success > 0:
@@ -106,10 +122,12 @@ async def api_backup_create():
         result = backup_create()
 
         return build_operation_response(
-            code_ok="BACKUP_CREATE_OK",
+            code_ok="BACKUP_CREATED",
+            code_not_found="BACKUP_NOT_FOUND",
             code_error="BACKUP_CREATE_ERROR",
             message_ok="Backup executed successfully",
             message_partial="Backup completed with some failed operations",
+            message_not_found="Backup not found",
             message_error="Some operations failed",
             result=result,
             start_ns=start_ns,
@@ -144,9 +162,6 @@ async def api_backup_list():
             "backups": backups
         }
 
-    except HTTPException:
-        raise
-
     except Exception as err:
         logger.exception("Error listing backups: %s", str(err))
         raise HTTPException(
@@ -168,10 +183,12 @@ async def api_backup_restore(payload: BackupRestoreRequest):
         result = backup_restore(backup_id=payload.backup_id)
 
         return build_operation_response(
-            code_ok="BACKUP_RESTORE_OK",
+            code_ok="BACKUP_RESTORED",
+            code_not_found="BACKUP_NOT_FOUND",
             code_error="BACKUP_RESTORE_ERROR",
             message_ok="Restore executed successfully",
             message_partial="Restore completed with some failed operations",
+            message_not_found="Backup not found",
             message_error="Some operations failed",
             result=result,
             start_ns=start_ns,
@@ -199,27 +216,18 @@ async def api_backup_delete(payload: BackupDeleteRequest):
     try:
         # Delete Backup
         result = backup_delete(backup_id=payload.backup_id)
-        took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
-
-        if result.get("status") != "success":
-            raise HTTPException(
-                status_code=status.HTTP_404_NOT_FOUND,
-                detail={
-                    "code": "BACKUP_NOT_FOUND",
-                    "status": "failure",
-                    "message": "Backup not found",
-                    "took_ms": took_ms,
-                    "results": result,
-                },
-            )
 
-        return {
-            "code": "BACKUP_DELETED",
-            "status": "success",
-            "message": "Backup deleted successfully",
-            "took_ms": took_ms,
-            "results": result,
-        }
+        return build_operation_response(
+            code_ok="BACKUP_DELETED",
+            code_not_found="BACKUP_NOT_FOUND",
+            code_error="BACKUP_DELETE_ERROR",
+            message_ok="Backup deleted successfully",
+            message_partial="Backup deletion completed with some failed operations",
+            message_not_found="Backup not found",
+            message_error="Some operations failed",
+            result=result,
+            start_ns=start_ns,
+        )
 
     except HTTPException:
         raise
@@ -238,17 +246,17 @@ async def api_backup_delete(payload: BackupDeleteRequest):
 def download_backup(backup_id: str):
     backup_dir = Path(get_config("BACKUP_PATH"))
 
-    zip_path = backup_dir / f"{backup_id}"
+    zip_path = backup_dir / backup_id
 
     if not zip_path.exists():
         raise HTTPException(
-                status_code=status.HTTP_404_NOT_FOUND,
-                detail={
-                    "code": "BACKUP_NOT_FOUND",
-                    "status": "failure",
-                    "message": "Backup not found",
-                },
-            )
+            status_code=status.HTTP_404_NOT_FOUND,
+            detail={
+                "code": "BACKUP_NOT_FOUND",
+                "status": "not_found",
+                "message": "Backup not found",
+            },
+        )
 
     return FileResponse(
         path=zip_path,
@@ -298,7 +306,6 @@ def upload_backup(file: UploadFile = File(...)):
             )
 
     # validate ZIP
-    import zipfile
     try:
         with zipfile.ZipFile(file.file) as z:
             if z.testzip() is not None:
index fae6442d73bb94fa7afed77f056489143722eda0..f1a8322073219a5123c15e0d2eea140ce841074f 100644 (file)
@@ -2,10 +2,8 @@
 
 # import standard modules
 from concurrent.futures import ThreadPoolExecutor
-from fastapi import APIRouter, Request, Response, HTTPException, status
+from fastapi import APIRouter, HTTPException, status
 from fastapi.responses import FileResponse
-import ipaddress
-import time
 
 # Import local modules
 from backend.db.hosts import get_hosts
@@ -30,7 +28,7 @@ router = APIRouter()
 # ---------------------------------------------------------
 # Devices page
 @router.get("/devices")
-def devices_page(request: Request):
+def devices_page():
     return FileResponse(settings.FRONTEND_PATH / "devices.html")
 
 # Serve devices.js
@@ -45,27 +43,31 @@ def devices_js():
     200: {"description": "Devices found"},
     500: {"description": "Internal server error"},
 })
-def api_get_devices(request: Request):
+def api_get_devices():
 
     try:
+        workers = get_config("PING_WORKERS")
         hosts = get_hosts(filter_devices=True)
-        with ThreadPoolExecutor(max_workers=get_config("PING_WORKERS")) as executor:
-            futures = [executor.submit(is_host_active, host["ipv4"]) for host in hosts]
-            for i, future in enumerate(futures):
-                hosts[i]["dhcp_state"] = "static"
-                hosts[i]["active"] = future.result()
-
         leases = get_leases(filter_devices=True)
-        with ThreadPoolExecutor(max_workers=get_config("PING_WORKERS")) as executor:
-            futures = [executor.submit(is_host_active, lease["ipv4"]) for lease in leases]
-            for i, future in enumerate(futures):
-                leases[i]["description"] = None
-                leases[i]["active"] = future.result()
 
-        return hosts+leases or []
+        for host in hosts:
+            host["dhcp_state"] = "static"
+
+        for lease in leases:
+            lease["description"] = None
+
+        devices = hosts + leases
+
+        with ThreadPoolExecutor(max_workers=workers) as executor:
+            futures = [
+                executor.submit(is_host_active, device["ipv4"])
+                for device in devices
+            ]
+
+            for i, future in enumerate(futures):
+                devices[i]["active"] = future.result()
 
-    except HTTPException:
-        raise
+        return devices
 
     except Exception as err:
         logger.exception("Error getting list devices %s", str(err).strip())
index 55e8229ad50189b208e71dd8f65ffe014625fb4f..4a075ed53441496081e3a65698496f1d7d20ac0d 100644 (file)
@@ -1,7 +1,7 @@
 # backend/routes/dhcp.py
 
 # import standard modules
-from fastapi import APIRouter, Request, Response, HTTPException, status
+from fastapi import APIRouter, HTTPException, status
 from fastapi.responses import FileResponse
 import json
 from pathlib import Path
@@ -28,7 +28,7 @@ router = APIRouter()
 # ---------------------------------------------------------
 # Leases page
 @router.get("/leases")
-def leases_page(request: Request):
+def leases_page():
     return FileResponse(settings.FRONTEND_PATH / "leases.html")
 
 # Serve leases.js
@@ -43,7 +43,7 @@ def leases_js():
     200: {"description": "DHCP configuration reload successfully"},
     500: {"description": "Internal server error"},
 })
-async def api_dhcp_reload(request: Request):
+async def api_dhcp_reload():
 
     # Inizializzazioni
     start_ns = time.monotonic_ns()
@@ -121,7 +121,7 @@ async def api_dhcp_reload(request: Request):
     404: {"description": "Leases not found"},
     500: {"description": "Internal server error"},
 })
-def api_dhcp_leases(request: Request):
+def api_dhcp_leases():
 
     try:
         leases = get_leases()
@@ -131,7 +131,7 @@ def api_dhcp_leases(request: Request):
         raise HTTPException(
             status_code=status.HTTP_404_NOT_FOUND,
             detail={
-                "code": "DHCP_LEASES_NOT_FOUND",
+                "code": "DHCP_LEASE_NOT_FOUND",
                 "status": "failure",
                 "message": str(err),
             },
@@ -159,7 +159,7 @@ def api_dhcp_leases(request: Request):
     404: {"description": "Lease not found"},
     500: {"description": "Internal server error"},
 })
-def api_get_lease(request: Request, lease_id: int):
+def api_get_lease(lease_id: int):
 
     # Inizializzazioni
     start_ns = time.monotonic_ns()
@@ -224,7 +224,7 @@ def api_get_lease(request: Request, lease_id: int):
     404: {"description": "Lease not found"},
     500: {"description": "Internal server error"},
 })
-def api_delete_lease(request: Request, lease_id: int):
+def api_delete_lease(lease_id: int):
 
     # Inizializzazioni
     start_ns = time.monotonic_ns()
@@ -248,7 +248,7 @@ def api_delete_lease(request: Request, lease_id: int):
         raise HTTPException(
             status_code=status.HTTP_404_NOT_FOUND,
             detail={
-                "code": "DHCP_LEASES_NOT_FOUND",
+                "code": "DHCP_LEASE_NOT_FOUND",
                 "status": "failure",
                 "message": str(err),
                 "details": {
@@ -263,7 +263,7 @@ def api_delete_lease(request: Request, lease_id: int):
         raise HTTPException(
             status_code=status.HTTP_404_NOT_FOUND,
             detail={
-                "code": "DHCP_LEASES_NOT_FOUND",
+                "code": "DHCP_LEASE_NOT_FOUND",
                 "status": "failure",
                 "message": str(err),
                 "details": {
index b39173c523e1f97a7e2843620640f78bcab69e45..404177f1188b572b7cfca8bd6b630dad3fad0506 100644 (file)
@@ -1,7 +1,7 @@
 # backend/routes/dns.py
 
 # import standard modules
-from fastapi import APIRouter, Request, Response, HTTPException, status
+from fastapi import APIRouter, HTTPException, status
 from fastapi.responses import FileResponse
 import asyncio
 import json
@@ -32,7 +32,7 @@ router = APIRouter()
     200: {"description": "DNS configuration reload successfully"},
     500: {"description": "Internal server error"},
 })
-async def api_dns_reload(request: Request):
+async def api_dns_reload():
 
     # Inizializzazioni
     start_ns = time.monotonic_ns()
index c1dd440cc36e29ae7b26d3df02fe3b0a28180cc7..e8a309e85f1f4b9d6e6e6befbd01d7a957c61a89 100644 (file)
@@ -24,7 +24,7 @@ router = APIRouter()
 def health():
     start = time.time()
 
-    db_status = "ok"
+    db_status = "healthy"
     db_version = None
     db_tables = None
     db_size = None
@@ -51,7 +51,7 @@ def health():
     latency = round((time.time() - start) * 1000, 2)
 
     return {
-        "status": "ok" if db_status == "ok" else "degraded",
+        "status": "healthy" if db_status == "healthy" else "degraded",
         "latency_ms": latency,
         "database": {
             "status": db_status,
index 26f383ab7ee583c41be2024ae742e414c219a7bf..f85ae8077aa308e24c0404494f41c931b48bcac2 100644 (file)
@@ -1,9 +1,8 @@
 # backend/routes/hosts.py
 
 # import standard modules
-from fastapi import APIRouter, Request, Response, HTTPException, status
+from fastapi import APIRouter, HTTPException, status
 from fastapi.responses import FileResponse
-import ipaddress
 import time
 
 # Import local modules
@@ -31,7 +30,7 @@ router = APIRouter()
 # ---------------------------------------------------------
 # Hosts page
 @router.get("/hosts")
-def hosts_page(request: Request):
+def hosts_page():
     return FileResponse(settings.FRONTEND_PATH / "hosts.html")
 
 # Serve hosts.js
@@ -46,15 +45,12 @@ def hosts_js():
     200: {"description": "Hosts found"},
     500: {"description": "Internal server error"},
 })
-def api_get_hosts(request: Request):
+def api_get_hosts():
 
     try:
         hosts = get_hosts()
         return hosts or []
 
-    except HTTPException:
-        raise
-
     except Exception as err:
         logger.exception("Error getting list hosts %s", str(err).strip())
         raise HTTPException(
@@ -62,7 +58,7 @@ def api_get_hosts(request: Request):
             detail={
                 "code": "HOSTS_GET_ERROR",
                 "status": "failure",
-                "message": "Internal error getting host",
+                "message": "Internal error getting hosts",
             },
         )
 
@@ -74,31 +70,13 @@ def api_get_hosts(request: Request):
     404: {"description": "Host not found"},
     500: {"description": "Internal server error"},
 })
-def api_get_host(request: Request, host_id: int):
+def api_get_host(host_id: int):
 
     # Inizializzazioni
     start_ns = time.monotonic_ns()
 
     try:
         host = get_host(host_id)
-        if not host:  # None or empty dict
-            took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
-            raise HTTPException(
-                status_code=status.HTTP_404_NOT_FOUND,
-                detail={
-                    "code": "HOST_NOT_FOUND",
-                    "status": "failure",
-                    "message": "Host not found",
-                    "details": {
-                        "host_id": host_id,
-                        "took_ms": took_ms,
-                    },
-                },
-            )
-        return host
-
-    except HTTPException:
-        raise
 
     except Exception as err:
         logger.exception("Error getting host %s: %s", host_id, str(err).strip())
@@ -116,34 +94,52 @@ def api_get_host(request: Request, host_id: int):
             },
         )
 
+    if not host:  # None or empty dict
+        took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND,
+            detail={
+                "code": "HOST_NOT_FOUND",
+                "status": "failure",
+                "message": "Host not found",
+                "details": {
+                    "host_id": host_id,
+                    "took_ms": took_ms,
+                },
+            },
+        )
+
+    return host
+
 # ---------------------------------------------------------
 # Add Hosts
 # ---------------------------------------------------------
-@router.post("/api/hosts", status_code=status.HTTP_200_OK, responses={
-    200: {"description": "Host added"},
+@router.post("/api/hosts", status_code=status.HTTP_201_CREATED, responses={
+    201: {"description": "Host added"},
     409: {"description": "Host already present"},
     500: {"description": "Internal server error"},
 })
-def api_add_host(request: Request, data: dict):
+def api_add_host(data: dict):
 
     # Inizializzazioni
     start_ns = time.monotonic_ns()
+    host_id = None
 
     try:
         host_id = add_host(data)
-        if(host_id > 0):
-            took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
-            return {
-                    "code": "HOST_ADDED",
-                    "status": "success",
-                    "message": "Host added successfully",
-                    "details": {
-                        "host_id": host_id,
-                        "took_ms": took_ms,
-                    },
-                }
+        took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
+        return {
+                "code": "HOST_ADDED",
+                "status": "success",
+                "message": "Host added successfully",
+                "details": {
+                    "host_id": host_id,
+                    "took_ms": took_ms,
+                },
+            }
 
-        # Already present
+    # Not Found
+    except ValueError:
         took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
         raise HTTPException(
             status_code=status.HTTP_409_CONFLICT,
@@ -158,9 +154,6 @@ def api_add_host(request: Request, data: dict):
             },
         )
 
-    except HTTPException:
-        raise
-
     except Exception as err:
         logger.exception("Error adding host: %s", str(err).strip())
         took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
@@ -185,26 +178,26 @@ def api_add_host(request: Request, data: dict):
     404: {"description": "Host not found"},
     500: {"description": "Internal server error"},
 })
-def api_update_host(request: Request, data: dict, host_id: int):
+def api_update_host(data: dict, host_id: int):
 
     # Inizializzazioni
     start_ns = time.monotonic_ns()
 
     try:
-        updated = update_host(host_id, data)
-        if updated:
-            took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
-            return {
-                    "code": "HOST_UPDATED",
-                    "status": "success",
-                    "message": "Host updated successfully",
-                    "details": {
-                        "host_id": host_id,
-                        "took_ms": took_ms,
-                    },
-                }
+        update_host(host_id, data)
+        took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
+        return {
+                "code": "HOST_UPDATED",
+                "status": "success",
+                "message": "Host updated successfully",
+                "details": {
+                    "host_id": host_id,
+                    "took_ms": took_ms,
+                },
+            }
 
-        # Not Found
+    # Not Found
+    except ValueError:
         took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
         raise HTTPException(
             status_code=status.HTTP_404_NOT_FOUND,
@@ -219,9 +212,6 @@ def api_update_host(request: Request, data: dict, host_id: int):
             },
         )
 
-    except HTTPException:
-        raise
-
     except Exception as err:
         logger.exception("Error updating host %s: %s", host_id, str(err).strip())
         took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
@@ -246,26 +236,26 @@ def api_update_host(request: Request, data: dict, host_id: int):
     404: {"description": "Host not found"},
     500: {"description": "Internal server error"},
 })
-def api_delete_host(request: Request, host_id: int):
+def api_delete_host(host_id: int):
 
     # Inizializzazioni
     start_ns = time.monotonic_ns()
 
     try:
-        deleted = delete_host(host_id)
-        if deleted:
-            took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
-            return {
-                    "code": "HOST_DELETED",
-                    "status": "success",
-                    "message": "Host deleted successfully",
-                    "details": {
-                        "host_id": host_id,
-                        "took_ms": took_ms,
-                    },
-                }
+        delete_host(host_id)
+        took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
+        return {
+                "code": "HOST_DELETED",
+                "status": "success",
+                "message": "Host deleted successfully",
+                "details": {
+                    "host_id": host_id,
+                    "took_ms": took_ms,
+                },
+            }
 
-        # Not Found
+    # Not Found
+    except ValueError:
         took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
         raise HTTPException(
             status_code=status.HTTP_404_NOT_FOUND,
@@ -280,9 +270,6 @@ def api_delete_host(request: Request, host_id: int):
             },
         )
 
-    except HTTPException:
-        raise
-
     except Exception as err:
         logger.exception("Error deleting host %s: %s", host_id, str(err).strip())
         took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
diff --git a/backend/routes/localization.py b/backend/routes/localization.py
new file mode 100644 (file)
index 0000000..17ce50f
--- /dev/null
@@ -0,0 +1,87 @@
+# backend/routes/localization.py
+
+# import standard modules
+from fastapi import APIRouter, Request, Response, HTTPException, status
+from fastapi.responses import FileResponse
+
+# Import Settings & Config
+from backend.settings.settings import settings
+from backend.db.settings import get_config
+# Import Logging
+from backend.log.log import get_logger
+# Import Localization
+from backend.settings.localization import load_language
+
+# Logger initialization
+logger = get_logger(__name__)
+
+# Create Router
+router = APIRouter()
+
+# ---------------------------------------------------------
+# FRONTEND PATHS (absolute paths inside Docker)
+# ---------------------------------------------------------
+# Serve i18n.js
+@router.get("/js/i18n.js")
+def i18n_js(request: Request):
+    return FileResponse(settings.FRONTEND_PATH / "js/i18n.js")
+
+# Serve backendMessages.js
+@router.get("/js/backendMessages.js")
+def i18n_js(request: Request):
+    return FileResponse(settings.FRONTEND_PATH / "js/backendMessages.js")
+
+# ---------------------------------------------------------
+# Returns the translation dictionary
+# ---------------------------------------------------------
+@router.get("/api/i18n",
+    status_code=status.HTTP_200_OK,
+    summary="Get UI translations",
+    description="Returns the translation dictionary for the currently configured application language.",
+    tags=["Localization"]
+)
+def get_translations():
+    lang = get_config("LANGUAGE")
+    return load_language(lang)
+
+# ---------------------------------------------------------
+# Returns a specific translation dictionary
+# ---------------------------------------------------------
+@router.get(
+    "/api/i18n/{lang}",
+    status_code=status.HTTP_200_OK,
+    responses={
+        200: {"description": "Translations loaded"},
+        404: {"description": "Language not found"},
+        500: {"description": "Internal server error"},
+    },
+    summary="Get translations",
+    description="Returns all translations for the specified language.",
+    tags=["Localization"],
+)
+def get_translations(lang: str):
+    try:
+        translations = load_language(lang, fallback=False)
+        return translations
+
+    except FileNotFoundError:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND,
+            detail={
+                "code": "LANGUAGE_NOT_FOUND",
+                "status": "failure",
+                "message": f"Language '{lang}' not found",
+            },
+        )
+
+    except Exception as err:
+        logger.exception("Error loading translations: %s", str(err).strip())
+
+        raise HTTPException(
+            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+            detail={
+                "code": "TRANSLATION_ERROR",
+                "status": "failure",
+                "message": "Internal error loading translations",
+            },
+        )
\ No newline at end of file
index 2802698baa734608beeca7a9babf6ef4e25774c6..c23e295c72b65345b9f8022602876ada8197d04c 100644 (file)
@@ -1,9 +1,8 @@
 # backend/routes/settings.py
 
 # import standard modules
-from fastapi import APIRouter, Request, Response, HTTPException, status
+from fastapi import APIRouter, HTTPException, status
 from fastapi.responses import FileResponse
-import ipaddress
 import time
 
 # Import local modules
@@ -29,7 +28,7 @@ router = APIRouter()
 # ---------------------------------------------------------
 # Settings page
 @router.get("/settings")
-def settings_page(request: Request):
+def settings_page():
     return FileResponse(settings.FRONTEND_PATH / "settings.html")
 
 # Serve settings.js
@@ -44,15 +43,12 @@ def settings_js():
     200: {"description": "Settings found"},
     500: {"description": "Internal server error"},
 })
-def api_get_configs(request: Request):
+def api_get_configs():
 
     try:
         configs = get_configs()
         return configs or []
 
-    except HTTPException:
-        raise
-
     except Exception as err:
         logger.exception("Error getting list of the configuration parameters %s", str(err).strip())
         raise HTTPException(
@@ -72,31 +68,13 @@ def api_get_configs(request: Request):
     404: {"description": "Configuration parameter not found"},
     500: {"description": "Internal server error"},
 })
-def api_get_setting(request: Request, config_key: str):
+def api_get_setting(config_key: str):
 
     # Inizializzazioni
     start_ns = time.monotonic_ns()
 
     try:
         config = get_config(config_key, json_format=True)
-        if not config:  # None or empty dict
-            took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
-            raise HTTPException(
-                status_code=status.HTTP_404_NOT_FOUND,
-                detail={
-                    "code": "CONFIG_NOT_FOUND",
-                    "status": "failure",
-                    "message": "Configuration parameter not found",
-                    "details": {
-                        "config_key": config_key,
-                        "took_ms": took_ms,
-                    },
-                },
-            )
-        return config
-
-    except HTTPException:
-        raise
 
     except Exception as err:
         logger.exception("Error getting configuration parameter %s: %s", config_key, str(err).strip())
@@ -114,16 +92,32 @@ def api_get_setting(request: Request, config_key: str):
             },
         )
 
+    if not config:  # None or empty dict
+        took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND,
+            detail={
+                "code": "CONFIG_NOT_FOUND",
+                "status": "failure",
+                "message": "Configuration parameter not found",
+                "details": {
+                    "config_key": config_key,
+                    "took_ms": took_ms,
+                },
+            },
+        )
+    return config
+
 # ---------------------------------------------------------
 # Update config
 # ---------------------------------------------------------
-@router.put("/api/settings/{config_key}", status_code=status.HTTP_200_OK, responses={
-    200: {"description": "Configuration parameter updated"},
+@router.put("/api/settings/{config_key}", status_code=status.HTTP_201_CREATED, responses={
+    201: {"description": "Configuration parameter updated"},
     400: {"description": "Invalid request"},
     404: {"description": "Configuration parameter not found"},
     500: {"description": "Internal server error"},
 })
-def api_update_setting(request: Request, data: dict, config_key: str):
+def api_update_setting(data: dict, config_key: str):
 
     # Inizializzazioni
     start_ns = time.monotonic_ns()
@@ -165,7 +159,7 @@ def api_update_setting(request: Request, data: dict, config_key: str):
                 raise HTTPException(
                     status_code=status.HTTP_400_BAD_REQUEST,
                     detail={
-                        "code": "CONFIG_RESET_ERROR",
+                        "code": "CONFIG_UPDATE_ERROR",
                         "status": "failure",
                         "message": (result.get("message") if result else None) or "Internal error updating configuration parameter",
                         "details": {
@@ -198,12 +192,12 @@ def api_update_setting(request: Request, data: dict, config_key: str):
 # Reset config to default
 # ---------------------------------------------------------
 @router.post("/api/settings/{config_key}/reset", status_code=status.HTTP_200_OK, responses={
-    200: {"description": "Configuration parameter reset to default"},
+    200: {"description": "Configuration parameter restored to default"},
     400: {"description": "Invalid request"},
     404: {"description": "Configuration parameter not found"},
     500: {"description": "Internal server error"},
 })
-def api_reset_config(request: Request, config_key: str):
+def api_reset_config(config_key: str):
 
     # Inizializzazioni
     start_ns = time.monotonic_ns()
@@ -213,9 +207,9 @@ def api_reset_config(request: Request, config_key: str):
         if result["status"] == "success":
             took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
             return {
-                    "code": "CONFIG_RESET",
+                    "code": "CONFIG_RESTORED",
                     "status": "success",
-                    "message": "Configuration parameter reset to default successfully",
+                    "message": "Configuration parameter restored to default successfully",
                     "details": {
                         "config_key": config_key,
                         "took_ms": took_ms,
@@ -245,9 +239,9 @@ def api_reset_config(request: Request, config_key: str):
                 raise HTTPException(
                     status_code=status.HTTP_400_BAD_REQUEST,
                     detail={
-                        "code": "CONFIG_RESET_ERROR",
+                        "code": "CONFIG_RESTORED_ERROR",
                         "status": "failure",
-                        "message": (result.get("message") if result else None) or "Internal error resetting configuration parameter",
+                        "message": (result.get("message") if result else None) or "Internal error restoring configuration parameter",
                         "details": {
                             "config_key": config_key,
                             "took_ms": took_ms,
@@ -264,9 +258,9 @@ def api_reset_config(request: Request, config_key: str):
         raise HTTPException(
             status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
             detail={
-                "code": "CONFIG_RESET_ERROR",
+                "code": "CONFIG_RESTORED_ERROR",
                 "status": "failure",
-                "message": "Internal error resetting configuration parameter",
+                "message": "Internal error restoring configuration parameter",
                 "details": {
                     "config_key": config_key,
                     "took_ms": took_ms,
index ad20b744b72b602efb8a3e6b0fa8d6450297ea52..b30fe733e7bd299442ff650c03a6ef71e1f8d668 100644 (file)
@@ -34,10 +34,18 @@ def about():
 # ---------------------------------------------------------
 @router.post("/api/restart")
 def restart():
+
     def do_restart():
         time.sleep(0.5)
         os.kill(os.getpid(), signal.SIGTERM)
 
-    threading.Thread(target=do_restart, daemon=True).start()
+    threading.Thread(
+        target=do_restart,
+        daemon=True
+    ).start()
 
-    return {"message": "Application restarting..."}
+    return {
+        "code": "APP_RESTARTING",
+        "status": "success",
+        "message": "Application restarting..."
+    }
index 8e2fe3a63a3bf42beda2143cbf4d10bfb1fce237..d1da8388bc56fc16625e389588902827699ebe34 100644 (file)
@@ -16,6 +16,11 @@ DATA_PATH = "/data"
 DB_FILE = "database.db"
 DB_RESET = False
 
+# ---------------------------------------------------------
+# Language
+# ---------------------------------------------------------
+LANGUAGE = "en"
+
 # ---------------------------------------------------------
 # Log
 # ---------------------------------------------------------
diff --git a/backend/settings/localization.py b/backend/settings/localization.py
new file mode 100644 (file)
index 0000000..2c56493
--- /dev/null
@@ -0,0 +1,41 @@
+# backend/settings/localization.py
+
+# Import standard modules
+import json
+
+# Import Settings
+from backend.settings.settings import settings
+from backend.settings import default
+
+# Import Logging
+from backend.log.log import get_logger
+
+# Logger initialization
+logger = get_logger(__name__)
+
+LOCALES_DIR = settings.FRONTEND_PATH / "locales"
+
+_translations = {}
+
+def load_language(lang: str, fallback: bool = True):
+    if lang in _translations:
+        return _translations[lang]
+
+    file_path = LOCALES_DIR / f"{lang}.json"
+
+    try:
+        with open(file_path, encoding="utf-8") as f:
+            _translations[lang] = json.load(f)
+
+    except (FileNotFoundError, json.JSONDecodeError):
+        if fallback and lang != default.LANGUAGE:
+            logger.warning("Invalid language '%s', using default '%s'", lang, default.LANGUAGE)
+            return load_language(default.LANGUAGE, False)
+
+        raise
+
+    return _translations[lang]
+
+def t(key: str, lang: str):
+    translations = load_language(lang)
+    return translations.get(key, key)
index a5e05f069d73ad760de5cff52c3044c80a62807f..54e63e5b2dc70c70806fd85c5313b137140f6f84 100644 (file)
@@ -79,6 +79,9 @@ class Settings(BaseModel):
     DB_FILE: Path = Field(default_factory=lambda: Path(os.getenv("DB_FILE", default.DB_FILE)))
     DB_RESET: bool = Field(default_factory=lambda: to_bool(os.getenv("DB_RESET"), default.DB_RESET))
 
+    # Language
+    LANGUAGE: str = Field(default_factory=lambda: os.getenv("LANGUAGE", default.LANGUAGE))
+
     # Log
     LOG_LEVEL: str = Field(default_factory=lambda: os.getenv("LOG_LEVEL", default.LOG_LEVEL))
     LOG_TO_FILE: bool = Field(default_factory=lambda: to_bool(os.getenv("LOG_TO_FILE"), default.LOG_TO_FILE))
index 0c87f1f8505906bbd3735beb0b1900194e079d53..911160e7dd323c3160f41111af786608183ce77f 100644 (file)
@@ -20,7 +20,7 @@
     <link rel="stylesheet" href="css/layout.css">
 </head>
 
-<body>
+<body data-page-title="dashboard.title">
     <!-- Topbar -->
     <header class="topbar">
         <div class="topbar-inner">
@@ -33,7 +33,7 @@
                     <line x1="12" y1="6" x2="12" y2="18" stroke="var(--accent)" stroke-width="2"></line>
                     <line x1="6" y1="12" x2="18" y2="12" stroke="var(--accent)" stroke-width="2"></line>
                 </svg>
-                <span>Network Manager</span>
+                <span data-i18n="app.name">Network Manager</span>
             </a>
 
             <nav class="navbar">
 
                 <!-- Desktop nav -->
                 <div class="d-none d-md-flex gap-2 ms-auto">
-                        <a href="/hosts"    class="btn btn-primary">       Hosts</a>
-                        <a href="/aliases"  class="btn btn-primary active">Alias</a>
-                        <a href="/leases"   class="btn btn-primary">       DHCP Leases</a>
-                        <a href="/devices"  class="btn btn-primary">       Devices</a>
-                        <a href="/logs"     class="btn btn-primary">       Logs</a>
-                        <a href="/settings" class="btn btn-primary">       Settings</a>
-                        <button class="btn btn-primary btn-logout">Logout</button>
+                        <a href="/hosts"    class="btn btn-primary"        data-i18n="hosts.title">Hosts</a>
+                        <a href="/aliases"  class="btn btn-primary active" data-i18n="aliases.title">Alias</a>
+                        <a href="/leases"   class="btn btn-primary"        data-i18n="dhcp.title">DHCP Leases</a>
+                        <a href="/devices"  class="btn btn-primary"        data-i18n="devices.title">Devices</a>
+                        <a href="/logs"     class="btn btn-primary"        data-i18n="logs.title">Logs</a>
+                        <a href="/settings" class="btn btn-primary"        data-i18n="settings.title">Settings</a>
+                        <button class="btn btn-primary btn-logout"         data-i18n="auth.logout">Logout</button>
                 </div>
             </nav>
 
             <div class="offcanvas offcanvas-end d-md-none" tabindex="-1" id="mobileMenu">
 
                 <div class="offcanvas-header">
-                    <h5 class="offcanvas-title">Network Manager</h5>
+                    <h5 class="offcanvas-title" data-i18n="app.name">Network Manager</h5>
 
                     <button
                         type="button"
                         class="btn-close"
+                        data-i18n-title="common.close"
+                        data-i18n-aria-label="common.close"
                         data-bs-dismiss="offcanvas">
                     </button>
                 </div>
 
                 <div class="offcanvas-body">
                     <div class="d-grid gap-2">
-                        <a href="/hosts"    class="btn btn-primary">       Hosts</a>
-                        <a href="/aliases"  class="btn btn-primary active">Alias</a>
-                        <a href="/leases"   class="btn btn-primary">       DHCP Leases</a>
-                        <a href="/devices"  class="btn btn-primary">       Devices</a>
-                        <a href="/logs"     class="btn btn-primary">       Logs</a>
-                        <a href="/settings" class="btn btn-primary">       Settings</a>
-                        <button class="btn btn-primary btn-logout">Logout</button>
+                        <a href="/hosts"    class="btn btn-primary"        data-i18n="hosts.title">Hosts</a>
+                        <a href="/aliases"  class="btn btn-primary" active data-i18n="aliases.title">Alias</a>
+                        <a href="/leases"   class="btn btn-primary"        data-i18n="dhcp.title">DHCP Leases</a>
+                        <a href="/devices"  class="btn btn-primary"        data-i18n="devices.title">Devices</a>
+                        <a href="/logs"     class="btn btn-primary"        data-i18n="logs.title">Logs</a>
+                        <a href="/settings" class="btn btn-primary"        data-i18n="settings.title">Settings</a>
+                        <button class="btn btn-primary btn-logout"         data-i18n="auth.logout">Logout</button>
                     </div>
                 </div>
             </div>
                 <div class="col-auto col-md-auto">
                     <h2 class="mb-0 d-flex align-items-center gap-2 lh-1">
                         <span class="title-icon"><i class="bi bi-diagram-2"></i></span>
-                        <span class="section-title">Aliases List</span>
+                        <span class="section-title" data-i18n="aliases.page.title">Aliases List</span>
                     </h2>
                 </div>
 
                         <input
                             type="text"
                             id="searchInput"
-                            placeholder="Search..."
                             class="form-control form-control-sm placeholder-italic"
-                            aria-label="Search alias">
+                            data-i18n-placeholder="common.search.placeholder"
+                            data-i18n-aria-label="aliases.search">
                     </div>
                 </div>
 
                     <!-- Add host Desktop -->
                     <button
                         class="btn btn-primary d-none d-md-flex align-items-center gap-2 px-3"
-                        title="Add Alias"
-                        aria-label="Add Alias"
+                        data-i18n-title="aliases.add"
+                        data-i18n-aria-label="aliases.add"
                         data-bs-toggle="modal" data-bs-target="#addAliasModal">
                         <i class="bi bi-plus-lg"></i>
-                        <span class="label">Add Alias</span>
+                        <span class="label" data-i18n="aliases.add">Add Alias</span>
                     </button>
 
                     <!-- Separator -->
                     <!-- Reload DNS Desktop -->
                     <button
                         class="btn btn-primary d-none d-md-flex align-items-center gap-2 px-3"
-                        title="Reload DNS (BIND)"
-                        aria-label="Reload DNS"
+                        data-i18n-title="dns.reload.description"
+                        data-i18n-aria-label="dns.reload.description"
                         data-action="reloadDns">
                         <i class="bi bi-arrow-repeat"></i>
-                        <span class="label">Reload DNS</span>
+                        <span class="label" data-i18n="dns.reload">Reload DNS</span>
                     </button>
 
                     <!-- Reload DHCP Desktop -->
                     <button
                         class="btn btn-primary d-none d-md-flex align-items-center gap-2 px-3"
-                        title="Reload DHCP (Kea)"
-                        aria-label="Reload DHCP"
+                        data-i18n-title="dhcp.reload.description"
+                        data-i18n-aria-label="dhcp.reload.description"
                         data-action="reloadDhcp">
                         <i class="bi bi-arrow-repeat"></i>
-                        <span class="label">Reload DHCP</span>
+                        <span class="label" data-i18n="dhcp.reload">Reload DHCP</span>
                     </button>
 
                     <!-- Mobile Dropdown -->
                             <li class="px-2 py-1">
                                 <button
                                     class="btn btn-primary w-100 d-flex align-items-center gap-2"
-                                    title="Add Alias"
-                                    aria-label="Add Alias"
+                                    data-i18n-title="aliases.add"
+                                    data-i18n-aria-label="aliases.add"
                                     data-bs-toggle="modal" data-bs-target="#addAliasModal">
                                     <i class="bi bi-plus-lg"></i>
-                                    <span>Add Alias</span>
+                                    <span data-i18n="aliases.add">Add Alias</span>
                                 </button>
                             </li>
 
                             <li class="px-2 py-1">
                                 <button
                                     class="btn btn-primary w-100 d-flex align-items-center gap-2"
-                                    title="Reload DNS (BIND)"
-                                    aria-label="Reload DNS"
+                                    data-i18n-title="dns.reload.description"
+                                    data-i18n-aria-label="dns.reload.description"
                                     data-action="reloadDns">
                                     <i class="bi bi-arrow-repeat"></i>
-                                    <span>Reload DNS</span>
+                                    <span data-i18n="dns.reload">Reload DNS</span>
                                 </button>
                             </li>
 
                             <li class="px-2 py-1">
                                 <button
                                     class="btn btn-primary w-100 d-flex align-items-center gap-2"
-                                    title="Reload DHCP (Kea)"
-                                    aria-label="Reload DHCP"
+                                    data-i18n-title="dhcp.reload.description"
+                                    data-i18n-aria-label="dhcp.reload.description"
                                     data-action="reloadDhcp">
                                     <i class="bi bi-arrow-repeat"></i>
-                                    <span>Reload DHCP</span>
+                                    <span data-i18n="dhcp.reload">Reload DHCP</span>
                                 </button>
                             </li>
                         </ul>
                 <input
                     type="text"
                     id="searchInputMobile"
-                    placeholder="Search..."
                     class="form-control form-control-sm placeholder-italic"
-                    aria-label="Search alias">
+                    data-i18n-placeholder="common.search.placeholder"
+                    data-i18n-aria-label="aliases.search">
             </div>
         </div>
     </section>
             <thead class="table-light">
                 <tr>
                     <th data-type="string" data-sortable="true" data-sort="0">
-                        Alias <span class="sort-arrow" aria-hidden="true"></span>
+                        <span data-i18n="aliases.name">Alias</span>
+                        <span class="sort-arrow" aria-hidden="true"></span>
                     </th>
 
                     <th data-type="string" data-sortable="true" data-sort="1">
-                        Target <span class="sort-arrow" aria-hidden="true"></span>
+                        <span data-i18n="aliases.target">Target</span>
+                        <span class="sort-arrow" aria-hidden="true"></span>
                     </th>
 
                     <th data-type="string" data-sortable="true" data-sort="2">
-                        Description <span class="sort-arrow" aria-hidden="true"></span>
+                        <span data-i18n="common.description">Description</span>
+                        <span class="sort-arrow" aria-hidden="true"></span>
                     </th>
 
                     <th data-type="string" data-sortable="true" data-sort="3" class="text-center text-nowrap">
-                        Options <span class="sort-arrow" aria-hidden="true"></span>
+                        <span data-i18n="common.options">Options</span>
+                        <span class="sort-arrow" aria-hidden="true"></span>
                     </th>
 
                     <th data-type="string" data-sortable="false" class="text-center text-nowrap">
-                        Actions
+                        <span data-i18n="common.actions">Actions</span>
                     </th>
                 </tr>
             </thead>
     <!-- Loader -->
     <div id="loader" class="text-center my-3" style="display: none;">
         <div class="spinner-border text-primary" role="status">
-            <span class="visually-hidden">Loading...</span>
+            <span class="visually-hidden" data-i18n="common.loading">Loading...</span>
         </div>
     </div>
-    <div id="devices-container"></div>
 
     <!-- Modals -->
     <div id="modals-container"></div>
index d729c9762866ad2b79fcffb32c170bac7c2c4da9..cb61317022b631954b1329ef9aaf93e6cd331338 100644 (file)
@@ -728,7 +728,7 @@ td.actions {
 
     .table-responsive {
         margin: 10px;
-        max-height: calc(100dvh - 9rem);
+        max-height: calc(100dvh - 8.5rem);
     }
 
     .table {
index 8c686588869a421ac6e1f08df09db5c71a70bd0a..bbe6273c07257444bb295b9f57b2a3e4cd9af1a8 100644 (file)
@@ -20,7 +20,7 @@
     <link rel="stylesheet" href="css/layout.css">
 </head>
 
-<body>
+<body data-page-title="dashboard.title">
     <!-- Topbar -->
     <header class="topbar">
         <div class="topbar-inner">
@@ -33,7 +33,7 @@
                     <line x1="12" y1="6" x2="12" y2="18" stroke="var(--accent)" stroke-width="2"></line>
                     <line x1="6" y1="12" x2="18" y2="12" stroke="var(--accent)" stroke-width="2"></line>
                 </svg>
-                <span>Network Manager</span>
+                <span data-i18n="app.name">Network Manager</span>
             </a>
 
             <nav class="navbar">
 
                 <!-- Desktop nav -->
                 <div class="d-none d-md-flex gap-2 ms-auto">
-                        <a href="/hosts"    class="btn btn-primary">       Hosts</a>
-                        <a href="/aliases"  class="btn btn-primary">       Alias</a>
-                        <a href="/leases"   class="btn btn-primary">       DHCP Leases</a>
-                        <a href="/devices"  class="btn btn-primary active">Devices</a>
-                        <a href="/logs"     class="btn btn-primary">       Logs</a>
-                        <a href="/settings" class="btn btn-primary">       Settings</a>
-                        <button class="btn btn-primary btn-logout">Logout</button>
+                        <a href="/hosts"    class="btn btn-primary"        data-i18n="hosts.title">Hosts</a>
+                        <a href="/aliases"  class="btn btn-primary"        data-i18n="aliases.title">Alias</a>
+                        <a href="/leases"   class="btn btn-primary"        data-i18n="dhcp.title">DHCP Leases</a>
+                        <a href="/devices"  class="btn btn-primary active" data-i18n="devices.title">Devices</a>
+                        <a href="/logs"     class="btn btn-primary"        data-i18n="logs.title">Logs</a>
+                        <a href="/settings" class="btn btn-primary"        data-i18n="settings.title">Settings</a>
+                        <button class="btn btn-primary btn-logout"         data-i18n="auth.logout">Logout</button>
                 </div>
             </nav>
 
             <div class="offcanvas offcanvas-end d-md-none" tabindex="-1" id="mobileMenu">
 
                 <div class="offcanvas-header">
-                    <h5 class="offcanvas-title">Network Manager</h5>
+                    <h5 class="offcanvas-title" data-i18n="app.name">Network Manager</h5>
 
                     <button
                         type="button"
                         class="btn-close"
+                        data-i18n-title="common.close"
+                        data-i18n-aria-label="common.close"
                         data-bs-dismiss="offcanvas">
                     </button>
                 </div>
 
                 <div class="offcanvas-body">
                     <div class="d-grid gap-2">
-                        <a href="/hosts"    class="btn btn-primary">       Hosts</a>
-                        <a href="/aliases"  class="btn btn-primary">       Alias</a>
-                        <a href="/leases"   class="btn btn-primary">       DHCP Leases</a>
-                        <a href="/devices"  class="btn btn-primary active">Devices</a>
-                        <a href="/logs"     class="btn btn-primary">       Logs</a>
-                        <a href="/settings" class="btn btn-primary">       Settings</a>
-                        <button class="btn btn-primary btn-logout">Logout</button>
+                        <a href="/hosts"    class="btn btn-primary"        data-i18n="hosts.title">Hosts</a>
+                        <a href="/aliases"  class="btn btn-primary"        data-i18n="aliases.title">Alias</a>
+                        <a href="/leases"   class="btn btn-primary"        data-i18n="dhcp.title">DHCP Leases</a>
+                        <a href="/devices"  class="btn btn-primary active" data-i18n="devices.title">Devices</a>
+                        <a href="/logs"     class="btn btn-primary"        data-i18n="logs.title">Logs</a>
+                        <a href="/settings" class="btn btn-primary"        data-i18n="settings.title">Settings</a>
+                        <button class="btn btn-primary btn-logout"         data-i18n="auth.logout">Logout</button>
                     </div>
                 </div>
             </div>
                 <div class="col-auto col-md-auto">
                     <h2 class="mb-0 d-flex align-items-center gap-2 lh-1">
                         <span class="title-icon"><i class="bi bi-diagram-3-fill"></i></span>
-                        <span class="section-title">Devices List</span>
+                        <span class="section-title" data-i18n="devices.page.title">Devices List</span>
                     </h2>
                 </div>
 
                         <input
                             type="text"
                             id="searchInput"
-                            placeholder="Search..."
                             class="form-control form-control-sm placeholder-italic"
-                            aria-label="Search device">
+                            data-i18n-placeholder="common.search.placeholder"
+                            data-i18n-aria-label="devices.search">
                     </div>
                 </div>
 
                     <!-- Add host Desktop -->
                     <button
                         class="btn btn-primary d-none d-md-flex align-items-center gap-2 px-3"
-                        title="Add Host"
-                        aria-label="Add Host"
+                        data-i18n-title="hosts.add"
+                        data-i18n-aria-label="hosts.add"
                         data-bs-toggle="modal" data-bs-target="#addHostModal">
                         <i class="bi bi-plus-lg"></i>
-                        <span class="label">Add Host</span>
+                        <span class="label" data-i18n="hosts.add">Add Host</span>
                     </button>
 
                     <!-- Separator -->
                     <!-- Reload DNS Desktop -->
                     <button
                         class="btn btn-primary d-none d-md-flex align-items-center gap-2 px-3"
-                        title="Reload DNS (BIND)"
-                        aria-label="Reload DNS"
+                        data-i18n-title="dns.reload.description"
+                        data-i18n-aria-label="dns.reload.description"
                         data-action="reloadDns">
                         <i class="bi bi-arrow-repeat"></i>
-                        <span class="label">Reload DNS</span>
+                        <span class="label" data-i18n="dns.reload">Reload DNS</span>
                     </button>
 
                     <!-- Reload DHCP Desktop -->
                     <button
                         class="btn btn-primary d-none d-md-flex align-items-center gap-2 px-3"
-                        title="Reload DHCP (Kea)"
-                        aria-label="Reload DHCP"
+                        data-i18n-title="dhcp.reload.description"
+                        data-i18n-aria-label="dhcp.reload.description"
                         data-action="reloadDhcp">
                         <i class="bi bi-arrow-repeat"></i>
-                        <span class="label">Reload DHCP</span>
+                        <span class="label" data-i18n="dhcp.reload">Reload DHCP</span>
                     </button>
 
                     <!-- Mobile Dropdown -->
                             <li class="px-2 py-1">
                                 <button
                                     class="btn btn-primary w-100 d-flex align-items-center gap-2"
-                                    title="Add Host"
-                                    aria-label="Add Host"
+                                    data-i18n-title="hosts.add"
+                                    data-i18n-aria-label="hosts.add"
                                     data-bs-toggle="modal" data-bs-target="#addHostModal">
                                     <i class="bi bi-plus-lg"></i>
-                                    <span>Add Host</span>
+                                    <span data-i18n="hosts.add">Add Host</span>
                                 </button>
                             </li>
 
                             <li class="px-2 py-1">
                                 <button
                                     class="btn btn-primary w-100 d-flex align-items-center gap-2"
-                                    title="Reload DNS (BIND)"
-                                    aria-label="Reload DNS"
+                                    data-i18n-title="dns.reload.description"
+                                    data-i18n-aria-label="dns.reload.description"
                                     data-action="reloadDns">
                                     <i class="bi bi-arrow-repeat"></i>
-                                    <span>Reload DNS</span>
+                                    <span data-i18n="dns.reload">Reload DNS</span>
                                 </button>
                             </li>
 
                             <li class="px-2 py-1">
                                 <button
                                     class="btn btn-primary w-100 d-flex align-items-center gap-2"
-                                    title="Reload DHCP (Kea)"
-                                    aria-label="Reload DHCP"
+                                    data-i18n-title="dhcp.reload.description"
+                                    data-i18n-aria-label="dhcp.reload.description"
                                     data-action="reloadDhcp">
                                     <i class="bi bi-arrow-repeat"></i>
-                                    <span>Reload DHCP</span>
+                                    <span data-i18n="dhcp.reload">Reload DHCP</span>
                                 </button>
                             </li>
                         </ul>
                 <input
                     type="text"
                     id="searchInputMobile"
-                    placeholder="Search..."
                     class="form-control form-control-sm placeholder-italic"
-                    aria-label="Search device">
+                    data-i18n-placeholder="common.search.placeholder"
+                    data-i18n-aria-label="devices.search">
             </div>
         </div>
     </section>
             <thead class="table-light">
                 <tr>
                     <th data-type="ipv4" data-sortable="true" data-sort="0">
-                        IP Address <span class="sort-arrow" aria-hidden="true"></span>
+                        <span data-i18n="hosts.ipv4">IP Address</span>
+                        <span class="sort-arrow" aria-hidden="true"></span>
                     </th>
 
                     <th data-type="mac" data-sortable="true" data-sort="1">
-                        MAC Address <span class="sort-arrow" aria-hidden="true"></span>
+                        <span data-i18n="hosts.mac">MAC Address</span>
+                        <span class="sort-arrow" aria-hidden="true"></span>
                     </th>
 
                     <th data-type="string" data-sortable="true" data-sort="2">
-                        Hostname <span class="sort-arrow" aria-hidden="true"></span>
+                        <span data-i18n="hosts.hostname">Hostname</span>
+                        <span class="sort-arrow" aria-hidden="true"></span>
                     </th>
 
                     <th data-type="string" data-sortable="true" data-sort="3">
-                        Description <span class="sort-arrow" aria-hidden="true"></span>
+                        <span data-i18n="common.description">Description</span>
+                        <span class="sort-arrow" aria-hidden="true"></span>
                     </th>
 
                     <th data-type="string" data-sortable="true" data-sort="4" class="text-center text-nowrap">
-                        State <span class="sort-arrow" aria-hidden="true"></span>
+                        <span data-i18n="common.state">State</span>
+                        <span class="sort-arrow" aria-hidden="true"></span>
                     </th>
 
                     <th data-type="string" data-sortable="true" data-sort="5" class="text-center text-nowrap">
-                        Active <span class="sort-arrow" aria-hidden="true"></span>
+                        <span data-i18n="devices.active">Active</span>
+                        <span class="sort-arrow" aria-hidden="true"></span>
                     </th>
 
                     <th data-type="string" data-sortable="false" class="text-center text-nowrap">
-                        Actions
+                        <span data-i18n="common.actions">Actions</span>
                     </th>
                 </tr>
             </thead>
     <!-- Loader -->
     <div id="loader" class="text-center my-3" style="display: none;">
         <div class="spinner-border text-primary" role="status">
-            <span class="visually-hidden">Loading...</span>
+            <span class="visually-hidden" data-i18n="common.loading">Loading...</span>
         </div>
     </div>
-    <div id="devices-container"></div>
 
     <!-- Modals -->
     <div id="modals-container"></div>
index ddfd8944988cae2c74a2028f9bf26370232f18a6..7e5696e2af531f9ffe31a3b69ec21d98d4fcce4d 100644 (file)
@@ -20,7 +20,7 @@
     <link rel="stylesheet" href="css/layout.css">
 </head>
 
-<body>
+<body data-page-title="dashboard.title">
     <!-- Topbar -->
     <header class="topbar">
         <div class="topbar-inner">
@@ -33,7 +33,7 @@
                     <line x1="12" y1="6" x2="12" y2="18" stroke="var(--accent)" stroke-width="2"></line>
                     <line x1="6" y1="12" x2="18" y2="12" stroke="var(--accent)" stroke-width="2"></line>
                 </svg>
-                <span>Network Manager</span>
+                <span data-i18n="app.name">Network Manager</span>
             </a>
 
             <nav class="navbar">
 
                 <!-- Desktop nav -->
                 <div class="d-none d-md-flex gap-2 ms-auto">
-                        <a href="/hosts"    class="btn btn-primary active">Hosts</a>
-                        <a href="/aliases"  class="btn btn-primary">       Alias</a>
-                        <a href="/leases"   class="btn btn-primary">       DHCP Leases</a>
-                        <a href="/devices"  class="btn btn-primary">       Devices</a>
-                        <a href="/logs"     class="btn btn-primary">       Logs</a>
-                        <a href="/settings" class="btn btn-primary">       Settings</a>
-                        <button class="btn btn-primary btn-logout">Logout</button>
+                        <a href="/hosts"    class="btn btn-primary active" data-i18n="hosts.title">Hosts</a>
+                        <a href="/aliases"  class="btn btn-primary"        data-i18n="aliases.title">Alias</a>
+                        <a href="/leases"   class="btn btn-primary"        data-i18n="dhcp.title">DHCP Leases</a>
+                        <a href="/devices"  class="btn btn-primary"        data-i18n="devices.title">Devices</a>
+                        <a href="/logs"     class="btn btn-primary"        data-i18n="logs.title">Logs</a>
+                        <a href="/settings" class="btn btn-primary"        data-i18n="settings.title">Settings</a>
+                        <button class="btn btn-primary btn-logout"         data-i18n="auth.logout">Logout</button>
                 </div>
             </nav>
 
             <div class="offcanvas offcanvas-end d-md-none" tabindex="-1" id="mobileMenu">
 
                 <div class="offcanvas-header">
-                    <h5 class="offcanvas-title">Network Manager</h5>
+                    <h5 class="offcanvas-title" data-i18n="app.name">Network Manager</h5>
 
                     <button
                         type="button"
                         class="btn-close"
+                        data-i18n-title="common.close"
+                        data-i18n-aria-label="common.close"
                         data-bs-dismiss="offcanvas">
                     </button>
                 </div>
 
                 <div class="offcanvas-body">
                     <div class="d-grid gap-2">
-                        <a href="/hosts"    class="btn btn-primary active">Hosts</a>
-                        <a href="/aliases"  class="btn btn-primary">       Alias</a>
-                        <a href="/leases"   class="btn btn-primary">       DHCP Leases</a>
-                        <a href="/devices"  class="btn btn-primary">       Devices</a>
-                        <a href="/logs"     class="btn btn-primary">       Logs</a>
-                        <a href="/settings" class="btn btn-primary">       Settings</a>
-                        <button class="btn btn-primary btn-logout">Logout</button>
+                        <a href="/hosts"    class="btn btn-primary active" data-i18n="hosts.title">Hosts</a>
+                        <a href="/aliases"  class="btn btn-primary"        data-i18n="aliases.title">Alias</a>
+                        <a href="/leases"   class="btn btn-primary"        data-i18n="dhcp.title">DHCP Leases</a>
+                        <a href="/devices"  class="btn btn-primary"        data-i18n="devices.title">Devices</a>
+                        <a href="/logs"     class="btn btn-primary"        data-i18n="logs.title">Logs</a>
+                        <a href="/settings" class="btn btn-primary"        data-i18n="settings.title">Settings</a>
+                        <button class="btn btn-primary btn-logout"         data-i18n="auth.logout">Logout</button>
                     </div>
                 </div>
             </div>
                 <div class="col-auto col-md-auto">
                     <h2 class="mb-0 d-flex align-items-center gap-2 lh-1">
                         <span class="title-icon"><i class="bi bi-hdd-network"></i></span>
-                        <span class="section-title">Hosts List</span>
+                        <span class="section-title" data-i18n="hosts.page.title">Hosts List</span>
                     </h2>
                 </div>
 
                         <input
                             type="text"
                             id="searchInput"
-                            placeholder="Search..."
                             class="form-control form-control-sm placeholder-italic"
-                            aria-label="Search hosts">
+                            data-i18n-placeholder="common.search.placeholder"
+                            data-i18n-aria-label="hosts.search">
                     </div>
                 </div>
 
                     <!-- Add host Desktop -->
                     <button
                         class="btn btn-primary d-none d-md-flex align-items-center gap-2 px-3"
-                        title="Add Host"
-                        aria-label="Add Host"
+                        data-i18n-title="hosts.add"
+                        data-i18n-aria-label="hosts.add"
                         data-bs-toggle="modal" data-bs-target="#addHostModal">
                         <i class="bi bi-plus-lg"></i>
-                        <span class="label">Add Host</span>
+                        <span class="label" data-i18n="hosts.add">Add Host</span>
                     </button>
 
                     <!-- Separator -->
                     <!-- Reload DNS Desktop -->
                     <button
                         class="btn btn-primary d-none d-md-flex align-items-center gap-2 px-3"
-                        title="Reload DNS (BIND)"
-                        aria-label="Reload DNS"
+                        data-i18n-title="dns.reload.description"
+                        data-i18n-aria-label="dns.reload.description"
                         data-action="reloadDns">
                         <i class="bi bi-arrow-repeat"></i>
-                        <span class="label">Reload DNS</span>
+                        <span class="label" data-i18n="dns.reload">Reload DNS</span>
                     </button>
 
                     <!-- Reload DHCP Desktop -->
                     <button
                         class="btn btn-primary d-none d-md-flex align-items-center gap-2 px-3"
-                        title="Reload DHCP (Kea)"
-                        aria-label="Reload DHCP"
+                        data-i18n-title="dhcp.reload.description"
+                        data-i18n-aria-label="dhcp.reload.description"
                         data-action="reloadDhcp">
                         <i class="bi bi-arrow-repeat"></i>
-                        <span class="label">Reload DHCP</span>
+                        <span class="label" data-i18n="dhcp.reload">Reload DHCP</span>
                     </button>
 
                     <!-- Mobile Dropdown -->
                             <li class="px-2 py-1">
                                 <button
                                     class="btn btn-primary w-100 d-flex align-items-center gap-2"
-                                    title="Add Host"
-                                    aria-label="Add Host"
+                                    data-i18n-title="hosts.add"
+                                    data-i18n-aria-label="hosts.add"
                                     data-bs-toggle="modal" data-bs-target="#addHostModal">
                                     <i class="bi bi-plus-lg"></i>
-                                    <span>Add Host</span>
+                                    <span data-i18n="hosts.add">Add Host</span>
                                 </button>
                             </li>
 
                             <li class="px-2 py-1">
                                 <button
                                     class="btn btn-primary w-100 d-flex align-items-center gap-2"
-                                    title="Reload DNS (BIND)"
-                                    aria-label="Reload DNS"
+                                    data-i18n-title="dns.reload.description"
+                                    data-i18n-aria-label="dns.reload.description"
                                     data-action="reloadDns">
                                     <i class="bi bi-arrow-repeat"></i>
-                                    <span>Reload DNS</span>
+                                    <span data-i18n="dns.reload">Reload DNS</span>
                                 </button>
                             </li>
 
                             <li class="px-2 py-1">
                                 <button
                                     class="btn btn-primary w-100 d-flex align-items-center gap-2"
-                                    title="Reload DHCP (Kea)"
-                                    aria-label="Reload DHCP"
+                                    data-i18n-title="dhcp.reload.description"
+                                    data-i18n-aria-label="dhcp.reload.description"
                                     data-action="reloadDhcp">
                                     <i class="bi bi-arrow-repeat"></i>
-                                    <span>Reload DHCP</span>
+                                    <span data-i18n="dhcp.reload">Reload DHCP</span>
                                 </button>
                             </li>
                         </ul>
                 <input
                     type="text"
                     id="searchInputMobile"
-                    placeholder="Search..."
                     class="form-control form-control-sm placeholder-italic"
-                    aria-label="Search hosts">
+                    data-i18n-placeholder="common.search.placeholder"
+                    data-i18n-aria-label="hosts.search">
             </div>
         </div>
     </section>
             <thead class="table-light">
                 <tr>
                     <th data-type="string" data-sortable="true" data-sort="0">
-                        Hostname <span class="sort-arrow" aria-hidden="true"></span>
+                        <span data-i18n="hosts.hostname">Hostname</span>
+                        <span class="sort-arrow" aria-hidden="true"></span>
                     </th>
 
                     <th data-type="ipv4" data-sortable="true" data-sort="1">
-                        IP Address <span class="sort-arrow" aria-hidden="true"></span>
+                        <span data-i18n="hosts.ipv4">IP Address</span>
+                        <span class="sort-arrow" aria-hidden="true"></span>
                     </th>
 
                     <th data-type="mac" data-sortable="true" data-sort="2">
-                        MAC Address <span class="sort-arrow" aria-hidden="true"></span>
+                        <span data-i18n="hosts.mac">MAC Address</span>
+                        <span class="sort-arrow" aria-hidden="true"></span>
                     </th>
 
                     <th data-type="string" data-sortable="true" data-sort="3">
-                        Description <span class="sort-arrow" aria-hidden="true"></span>
+                        <span data-i18n="common.description">Description</span>
+                        <span class="sort-arrow" aria-hidden="true"></span>
                     </th>
 
                     <th data-type="string" data-sortable="true" data-sort="4" class="text-center text-nowrap">
-                        Options <span class="sort-arrow" aria-hidden="true"></span>
+                        <span data-i18n="common.options">Options</span>
+                        <span class="sort-arrow" aria-hidden="true"></span>
                     </th>
 
                     <th data-type="string" data-sortable="false" class="text-center text-nowrap">
-                        Actions
+                        <span data-i18n="common.actions">Actions</span>
                     </th>
                 </tr>
             </thead>
     <!-- Loader -->
     <div id="loader" class="text-center my-3" style="display: none;">
         <div class="spinner-border text-primary" role="status">
-            <span class="visually-hidden">Loading...</span>
+            <span class="visually-hidden" data-i18n="common.loading">Loading...</span>
         </div>
     </div>
-    <div id="devices-container"></div>
 
     <!-- Modals -->
     <div id="modals-container"></div>
index 276285fbdfa6a897afa40f78a1b99aed0e73dedc..a0ae91d4bb5dbbb50cdf755d03783a3b642a4014 100644 (file)
@@ -20,7 +20,7 @@
     <link rel="stylesheet" href="css/layout.css">
 </head>
 
-<body>
+<body data-page-title="dashboard.title">
     <!-- Topbar -->
     <header class="topbar">
         <div class="topbar-inner">
@@ -33,7 +33,7 @@
                     <line x1="12" y1="6" x2="12" y2="18" stroke="var(--accent)" stroke-width="2"></line>
                     <line x1="6" y1="12" x2="18" y2="12" stroke="var(--accent)" stroke-width="2"></line>
                 </svg>
-                <span>Network Manager</span>
+                <span data-i18n="app.name">Network Manager</span>
             </a>
 
             <nav class="navbar">
@@ -48,8 +48,8 @@
 
                 <!-- Desktop nav -->
                 <div class="d-none d-md-flex gap-2 ms-auto">
-                        <button class="btn btn-outline-primary btn-api" title="API Status" aria-label="API Status" data-action="apiCheck">API Status</button>
-                        <button class="btn btn-primary btn-logout">Logout</button>
+                        <button class="btn btn-outline-primary btn-api" data-i18n-title="header.api_status" data-i18n-aria-label="header.api_status" data-action="apiCheck" data-i18n="header.api_status">API Status</button>
+                        <button class="btn btn-primary btn-logout" data-i18n="auth.logout">Logout</button>
                 </div>
             </nav>
 
             <div class="offcanvas offcanvas-end d-md-none" tabindex="-1" id="mobileMenu">
 
                 <div class="offcanvas-header">
-                    <h5 class="offcanvas-title">Network Manager</h5>
+                    <h5 class="offcanvas-title" data-i18n="app.name">Network Manager</h5>
 
                     <button
                         type="button"
                         class="btn-close"
+                        data-i18n-title="common.close"
+                        data-i18n-aria-label="common.close"
                         data-bs-dismiss="offcanvas">
                     </button>
                 </div>
 
                 <div class="offcanvas-body">
                     <div class="d-grid gap-2">
-                        <button class="btn btn-outline-primary btn-api" title="API Status" aria-label="API Status" data-action="apiCheck">API Status</button>
-                        <button class="btn btn-primary btn-logout">Logout</button>
+                        <button class="btn btn-outline-primary btn-api" data-i18n-title="header.api_status" data-i18n-aria-label="header.api_status" data-action="apiCheck" data-i18n="header.api_status">API Status</button>
+                        <button class="btn btn-primary btn-logout" data-i18n="auth.logout">Logout</button>
                     </div>
                 </div>
             </div>
@@ -88,7 +90,7 @@
                 <div class="col-auto col-md-auto">
                     <h2 class="mb-0 d-flex align-items-center gap-2 lh-1">
                         <span class="title-icon"><i class="bi bi-grid me-2"></i></span>
-                        <span class="section-title">Dashboard</span>
+                        <span class="section-title" data-i18n="dashboard.title">Dashboard</span>
                     </h2>
                 </div>
             </div>
             <!-- DNS -->
             <div class="tile">
                 <div class="tile-icon"><i class="bi bi-diagram-3"></i></div>
-                <h3>DNS (BIND)</h3>
-                <p>Zone, record e configurazioni.</p>
+                <h3 data-i18n="dns.card.title">DNS (BIND)</h3>
+                <p data-i18n="dns.card.description">Zones, records and configuration.</p>
                 <div class="mt-2 d-flex gap-2 flex-wrap">
-                    <a href="/dns-config" class="btn btn-primary btn-sm" title="DNS Configuration" aria-label="DNS Configuration">
-                        <i class="bi bi-list-ul me-1"></i><span class="label">DNS Configuration</span>
+                    <a href="/dns-config" class="btn btn-primary btn-sm" data-i18n-title="dns.config" data-i18n-aria-label="dns.config">
+                        <i class="bi bi-list-ul me-1"></i><span class="label" data-i18n="dns.config">DNS Configuration</span>
                     </a>
-                    <button class="btn btn-primary btn-sm" title="Reload DNS (BIND)" aria-label="Reload DNS" data-action="reloadDns">
-                        <i class="bi bi-arrow-repeat me-1"></i><span class="label">Reload DNS</span>
+                    <button class="btn btn-primary btn-sm" data-i18n-title="dns.reload" data-i18n-aria-label="dns.reload" data-action="reloadDns">
+                        <i class="bi bi-arrow-repeat me-1"></i><span class="label" data-i18n="dns.reload">Reload DNS</span>
                     </button>
                 </div>
             </div>
             <!-- DHCP -->
             <div class="tile">
                 <div class="tile-icon"><i class="bi bi-broadcast"></i></div>
-                <h3>DHCP (Kea)</h3>
-                <p>Pools, leases, reservations.</p>
+                <h3 data-i18n="dhcp.card.title">DHCP (Kea)</h3>
+                <p data-i18n="dhcp.card.description">Pools, leases, reservations.</p>
                 <div class="mt-2 d-flex gap-2 flex-wrap">
-                    <a href="/leases" class="btn btn-primary btn-sm" title="DHCP Leases (Kea)" aria-label="DHCP Leases">
-                        <i class="bi bi-list-ul me-1"></i><span class="label">DHCP Leases</span>
+                    <a href="/leases" class="btn btn-primary btn-sm" data-i18n-title="dhcp.leases" data-i18n-aria-label="dhcp.leases">
+                        <i class="bi bi-list-ul me-1"></i><span class="label" data-i18n="dhcp.leases">DHCP Leases</span>
                     </a>
-                    <button class="btn btn-primary btn-sm" title="Reload DHCP (Kea)" aria-label="Reload DHCP" data-action="reloadDhcp">
-                        <i class="bi bi-arrow-repeat me-1"></i><span class="label">Reload DHCP</span>
+                    <button class="btn btn-primary btn-sm" data-i18n-title="dns.reload" data-i18n-aria-label="dhcp.reload" data-action="reloadDhcp">
+                        <i class="bi bi-arrow-repeat me-1"></i><span class="label" data-i18n="dhcp.reload">Reload DHCP</span>
                     </button>
                 </div>
             </div>
             <!-- Hosts -->
             <a href="/hosts" class="tile text-decoration-none">
                 <div class="tile-icon"><i class="bi bi-hdd-network"></i></div>
-                <h3>Hosts</h3>
-                <p>Inventario IP/MAC.</p>
+                <h3 data-i18n="hosts.card.title">Hosts</h3>
+                <p data-i18n="hosts.card.description">IP/MAC inventory.</p>
             </a>
 
             <!-- Aliases -->
             <a href="/aliases" class="tile text-decoration-none">
                 <div class="tile-icon"><i class="bi bi-diagram-2"></i></div>
-                <h3>Aliases</h3>
-                <p>Inventario alias DNS.</p>
+                <h3 data-i18n="aliases.card.title">Aliases</h3>
+                <p data-i18n="aliases.card.description">DNS alias inventory.</p>
             </a>
 
             <!-- Devices Status -->
             <a href="/devices" class="tile text-decoration-none">
                 <div class="tile-icon"><i class="bi bi-diagram-3-fill"></i></div>
-                <h3>Devices</h3>
-                <p>Device status (Host + DHCP).</p>
+                <h3 data-i18n="devices.card.title">Devices</h3>
+                <p data-i18n="devices.card.description">Device status (Host + DHCP).</p>
             </a>
 
             <!-- Certificati -->
             <a href="/api/certificates" class="tile text-decoration-none">
                 <div class="tile-icon"><i class="bi bi-shield-lock"></i></div>
-                <h3>Certificati</h3>
-                <p>Let’s Encrypt e rinnovi.</p>
+                <h3 data-i18n="certificates.card.title">Certificates</h3>
+                <p data-i18n="certificates.card.description">Let's Encrypt certificates and renewals.</p>
             </a>
 
             <!-- Backup & Restore -->
             <div class="tile" data-action="openBackupModal" role="button">
                 <div class="tile-icon"><i class="bi bi-arrow-counterclockwise"></i></div>
-                <h3>Backup &amp; Restore</h3>
-                <p>Esecuzione backup e gestione archivi.</p>
+                <h3 data-i18n="backup.card.title">Backup &amp; Restore</h3>
+                <p data-i18n="backup.card.description">Backup execution and archive management.</p>
 
                 <div class="mt-2 d-flex gap-2 flex-wrap">
-                    <button class="btn btn-primary btn-sm" title="Start Backup" aria-label="Start Backup" data-action="startBackup">
-                        <i class="bi bi-cloud-upload me-1"></i><span class="label">Create Backup</span>
+                    <button class="btn btn-primary btn-sm" data-i18n-title="backup.create" data-i18n-aria-label="backup.create" data-action="startBackup">
+                        <i class="bi bi-cloud-upload me-1"></i><span class="label" data-i18n="backup.create">Create Backup</span>
                     </button>
-                    <button class="btn btn-primary btn-sm" title="Backup Management" aria-label="Backup Management" data-action="openBackupModal">
-                        <i class="bi bi-cloud-download me-1"></i><span class="label">Backup Management</span>
+                    <button class="btn btn-primary btn-sm" data-i18n-title="backup.manage" data-i18n-aria-label="backup.manage" data-action="openBackupModal">
+                        <i class="bi bi-cloud-download me-1"></i><span class="label" data-i18n="backup.manage">Backup Management</span>
                     </button>
                 </div>
             </div>
             <!-- Logs -->
             <a href="/logs" class="tile text-decoration-none">
                 <div class="tile-icon"><i class="bi bi-file-earmark-text"></i></div>
-                <h3>Logs</h3>
-                <p>Eventi e access log.</p>
+                <h3 data-i18n="logs.card.title">Logs</h3>
+                <p data-i18n="logs.card.description">Events and access logs.</p>
             </a>
 
             <!-- Settings -->
             <a href="/settings" class="tile text-decoration-none">
                 <div class="tile-icon"><i class="bi bi-gear"></i></div>
-                <h3>Impostazioni</h3>
-                <p>Configurazione sistema e variabili.</p>
+                <h3 data-i18n="settings.card.title">Settings</h3>
+                <p data-i18n="settings.card.description">System configuration and variables.</p>
             </a>
 
             <!-- Health -->
             <a href="/api/health" id="healthBtn" class="tile text-decoration-none" data-action="openHealthModal" aria-controls="healthModal" role="button">
                 <div class="tile-icon"><i class="bi bi-heart-pulse"></i></div>
-                <h3>Health</h3>
-                <p>Stato servizi e risorse.</p>
+                <h3 data-i18n="health.card.title">Health</h3>
+                <p data-i18n="health.card.description">Service and resource status.</p>
             </a>
 
         </div>
     <div id="modals-container"></div>
 
     <!-- Scripts -->
-    <script type=module src="js/index.js"></script>
+    <script type="module" src="js/index.js"></script>
     <script type="module" src="js/session.js"></script>
 
     <!-- Bootstrap JS -->
index f3eb2e182f183bf61cb786c5664777096468fc68..ee1bcc557b8601f835ed84ec94aca11c0429159b 100644 (file)
@@ -1,7 +1,10 @@
-// Import common js
+// -------------------------------------------------------
+// IMPORT
+// -------------------------------------------------------
 import { loadModals, showToast, sortTable, initSortableTable, resetSorting, handleSearch, filterTable, clearSearch, handleReload } from './common.js';
-// Import services
 import { serviceReloadDNS, serviceReloadDHCP, serviceGetAliases, serviceGetAlias, serviceCreateAlias, serviceUpdateAlias, serviceDeleteAlias } from './services.js';
+import { loadLanguage, translatePage, t } from "./i18n.js";
+import { getBackendMessage } from "./backendMessages.js";
 
 // -----------------------------
 // State variables
@@ -30,8 +33,8 @@ async function fetchAliases () {
         viewAliases = [...allAliases];
 
     } catch (err) {
-        console.error(err?.message || "Error loading aliases");
-        showToast(err?.message || "Error loading aliase", false);
+        console.error(err?.message || t("aliases.list.error"));
+        showToast(err?.message || t("aliases.list.error"), false);
         allAliases = [];
         viewAliases = [];
         // hide loader and show table
@@ -64,7 +67,7 @@ function updateTable () {
         const trEmpty = document.createElement("tr");
         const tdEmpty = document.createElement("td");
         tdEmpty.colSpan = 5;
-        tdEmpty.textContent = "No alias available.";
+        tdEmpty.textContent = t("aliases.empty");
         tdEmpty.style.textAlign = "center";
         trEmpty.appendChild(tdEmpty);
         tbody.appendChild(trEmpty);
@@ -77,15 +80,15 @@ function updateTable () {
     // fragment per performance
     const frag = document.createDocumentFragment();
 
-    viewAliases.forEach(h => {
+    viewAliases.forEach(a => {
 
-        const id = Number(h.id);
+        const id = Number(a.id);
         const tr = document.createElement("tr");
 
         // Name
         {
             const td = document.createElement("td");
-            const val = (h.name ?? "").toString();
+            const val = (a.name ?? "").toString();
             td.textContent = val;
             if (val) td.setAttribute("data-value", val.toLowerCase());
             tr.appendChild(td);
@@ -94,7 +97,7 @@ function updateTable () {
         // Target
         {
             const td = document.createElement("td");
-            const val = (h.target ?? "").toString();
+            const val = (a.target ?? "").toString();
             td.textContent = val;
             if (val) td.setAttribute("data-value", val.toLowerCase());
             tr.appendChild(td);
@@ -103,7 +106,7 @@ function updateTable () {
         // Description
         {
             const td = document.createElement("td");
-            const val = (h.description ?? "").toString();
+            const val = (a.description ?? "").toString();
             td.textContent = val;
             if (val) td.setAttribute("data-value", val.toLowerCase());
             tr.appendChild(td);
@@ -118,53 +121,56 @@ function updateTable () {
             //
             // SSL icon
             //
-            const sslEnabled = !!h.ssl_enabled;
-            td.setAttribute("data-value", sslEnabled ? "true" : "false");
-            td.setAttribute("aria-label", sslEnabled ? "SSL attivo" : "SSL non attivo");
-            const icon = document.createElement("i");
+            const sslEnabled = !!a.ssl_enabled;
+            const wrapperSslIcon = document.createElement("span");
+            const sslIcon = document.createElement("i");
+            let description = "";
             if (sslEnabled) {
-                icon.className = "bi bi-shield-lock-fill icon icon-static";
-                icon.setAttribute("aria-hidden", "true");
-                icon.setAttribute("title", "SSL certificate enabled");
+                sslIcon.className = "bi bi-shield-lock-fill icon icon-static";
+                description = t("aliases.ssl.enabled");
+                wrapperSslIcon.setAttribute("title", description);
+                wrapperSslIcon.setAttribute("aria-label", description);
             } else {
-                icon.className = "bi bi-shield-lock-fill icon icon-static icon-placeholder";
-                icon.setAttribute("aria-hidden", "true");
+                sslIcon.className = "bi bi-shield-lock-fill icon icon-static icon-placeholder";
+                sslIcon.setAttribute("aria-hidden", "true");
             }
-            td.appendChild(icon);
+            wrapperSslIcon.appendChild(sslIcon);
+            td.appendChild(wrapperSslIcon);
 
             //
             // visibility icon
             //
-            const ext = (h.visibility ?? "").toString();
-            let aria = "";
+            const ext = (a.visibility ?? "").toString();
+            const wrapperVisibilityIcon = document.createElement("span");
+            const visibilityIcon = document.createElement("i");
             let iconClass = "";
             switch (ext) {
                 case "0":
                     // Only local (CNAME record internally resolved)
-                    aria = "Only local (CNAME record internally resolved)";
+                    description = t("aliases.visibility.local.description");
                     iconClass = "bi bi-hdd-network";
                     break;
 
                 case "1":
                     // Local and external (CNAME record internally resolved, CNAME externally)
-                    aria = "Internal and external are identical";
+                    description = t("aliases.visibility.global.description");
                     iconClass = "bi bi-globe2";
                     break;
 
                 case "2":
                     // CNAME -> DDNS / external_name
-                    aria = "External is a CNAME to external_name";
+                    description = t("aliases.visibility.alias.description");
                     iconClass = "bi bi-link-45deg";
                     break;
             }
             if (iconClass) {
-                const icon = document.createElement("i");
-                icon.className = iconClass + " icon icon-static";
-                icon.setAttribute("aria-hidden", "true");
-                icon.setAttribute("title", aria);
-                td.appendChild(icon);
+                visibilityIcon.className = iconClass + " icon icon-static";
+                wrapperVisibilityIcon.setAttribute("title", description);
+                wrapperVisibilityIcon.setAttribute("aria-label", description);
+                wrapperVisibilityIcon.appendChild(visibilityIcon);
+                td.appendChild(wrapperVisibilityIcon);
             }
-
+            if (ext) td.setAttribute("data-value", ext);
             tr.appendChild(td);
         }
 
@@ -177,11 +183,12 @@ function updateTable () {
 
             // Edit Button
             const editSpan = document.createElement("span");
+            const editText = t("aliases.edit");
             editSpan.className = "action-icon";
             editSpan.setAttribute("role", "button");
             editSpan.tabIndex = 0;
-            editSpan.title = "Edit alias";
-            editSpan.setAttribute("aria-label", "Edit alias");
+            editSpan.title = editText;
+            editSpan.setAttribute("aria-label", editText);
             editSpan.setAttribute("data-bs-toggle", "modal");
             editSpan.setAttribute("data-bs-target", "#addAliasModal");
             editSpan.setAttribute("data-action", "edit");
@@ -195,11 +202,12 @@ function updateTable () {
 
             // Delete Button
             const delSpan = document.createElement("span");
+            const deleteText = t("aliases.delete");
             delSpan.className = "action-icon";
             delSpan.setAttribute("role", "button");
             delSpan.tabIndex = 0;
-            delSpan.title = "Delete alias";
-            delSpan.setAttribute("aria-label", "Delete alias");
+            delSpan.title = deleteText;
+            delSpan.setAttribute("aria-label", deleteText);
             delSpan.setAttribute("data-action", "delete");
             delSpan.setAttribute("data-alias-id", String(id));
             {
@@ -237,7 +245,7 @@ function updateTable () {
         document.getElementById('searchInput')?.value ||
         document.getElementById('searchInputMobile')?.value;
     if (term?.trim()) {
-        handleSearch(term);
+        handleSearch(term, filterTable);
     }
 }
 
@@ -267,8 +275,8 @@ async function editAlias(id) {
             document.getElementById("aliasVisibilityLocal").checked = true;
         }
     } catch (err) {
-        console.error(err?.message || "Error loading alias");
-        showToast(err?.message || "Error loading alias", false);
+        console.error(err?.message || t("aliases.loaded.error"));
+        showToast(err?.message || t("aliases.loaded.error"), false);
     }
 }
 
@@ -278,12 +286,12 @@ async function editAlias(id) {
 async function saveAlias(aliasData) {
     // Validate alias
     if (!aliasData.name.trim()) {
-        showToast("Alias is required", false);
+        showToast(t("validation.alias.required"), false);
         return false;
     }
     // Validate Target
     if (!aliasData.target.trim()) {
-        showToast("Target is required", false);
+        showToast(t("validation.target.required"), false);
         return false;
     }
 
@@ -298,19 +306,27 @@ async function saveAlias(aliasData) {
             result = await serviceCreateAlias(aliasData);
         }
 
-        const msg = (typeof result === 'object' && result?.message)
-            ? result.message
-            : editingAliasId !== null
-                ? 'Alias updated successfully'
-                : 'Alias created successfully';
+        const msg = getBackendMessage(
+            result,
+            editingAliasId !== null
+                ? "aliases.updated.ok"
+                : "aliases.created.ok"
+        );
 
         showToast(msg, true);
 
         return true;
 
     } catch (err) {
-        console.error(err?.message || "Error saving alias");
-        showToast(err?.message || "Error saving alias", false);
+
+        const msg = getBackendMessage(
+            err,
+            editingAliasId !== null
+                ? "aliases.updated.error"
+                : "aliases.created.error"
+        );
+
+        showToast(msg, false);
     }
 
     return false;
@@ -366,8 +382,20 @@ async function handleAddAliasSubmit(e) {
         }
 
     } catch (err) {
-        console.error(err?.message || "Error saving alias");
-        showToast(err?.message || "Error saving alias", false);
+        console.error(
+            err?.message ||
+            (editingAliasId !== null
+                ? t("aliases.updated.error")
+                : t("aliases.created.error"))
+        );
+
+        showToast(
+            err?.message ||
+            (editingAliasId !== null
+                ? t("aliases.updated.error")
+                : t("aliases.created.error")),
+            false
+        );
     }
 
     return false;
@@ -383,17 +411,13 @@ async function handleDeleteAlias(e, el) {
     // Get alias ID
     const id = Number(el.dataset.aliasId);
     if (!Number.isFinite(id)) {
-        showToast('Alias id not valid for delete', false);
+        showToast(t("aliases.delete.invalid_id"), false);
         return;
     }
 
     try {
         const result = await serviceDeleteAlias(id);
-
-        const msg = (typeof result === 'object' && result?.message)
-            ? result.message
-            : 'Alias deleted successfully';
-
+        const msg = getBackendMessage(result, "aliases.deleted.ok");
         showToast(msg, true);
 
         // Reload aliases
@@ -403,8 +427,8 @@ async function handleDeleteAlias(e, el) {
         return true;
 
     } catch (err) {
-        console.error(err?.message || "Error deleting alias");
-        showToast(err?.message || "Error deleting alias", false);
+        console.error(err?.message || t("aliases.deleted.error"));
+        showToast(err?.message || t("aliases.deleted.error"), false);
     }
 
     return false;
@@ -427,9 +451,9 @@ const actionHandlers = {
         await handleReload(
             el,
             serviceReloadDNS,
-            "DNS reload successfully",
-            "Error reloading DNS",
-            "Reloading DNS..."
+            t("dns.reload.ok"),
+            t("dns.reload.error"),
+            t("dns.reload.progress")
         );
     },
     // Reload DHCP
@@ -437,9 +461,9 @@ const actionHandlers = {
         await handleReload(
             el,
             serviceReloadDHCP,
-            "DHCP reload successfully",
-            "Error reloading DHCP",
-            "Reloading DHCP..."
+            t("dhcp.reload.ok"),
+            t("dhcp.reload.error"),
+            t("dhcp.reload.progress")
         );
     },
 };
@@ -456,21 +480,32 @@ document.addEventListener("DOMContentLoaded", async () => {
 // -----------------------------
 async function initApp() {
 
+    // Loading translation
+    try {
+        await loadLanguage();
+    } catch (err) {
+        console.error(err?.message || t("app.translation.error"));
+        showToast(t("app.translation.error"), false);
+    }
+
     // Load modals (Bootstrap 5 requires JS initialization for dynamic content)
     try {
         await loadModals();
     } catch (err) {
-        console.error(err?.message || "Error loading modals");
-        showToast(err?.message || "Error loading modals", false);
+        console.error(err?.message || t("app.modals.error"));
+        showToast(t("app.modals.error"), false);
     }
 
+    // Translate page
+    translatePage();
+
     // Load data (aliases)
     try {
         await fetchAliases();
         updateTable();
     } catch (err) {
-        console.error(err?.message || "Error loading aliases");
-        showToast(err?.message || "Error loading aliases:", false);
+        console.error(err?.message || t("aliases.list.error"));
+        showToast(err?.message || t("aliases.list.error"), false);
     }
 
     initUI();
@@ -503,7 +538,7 @@ function initSearch() {
 
         // live filter
         input.addEventListener("input", (e) => {
-            handleSearch(e.target.value);
+            handleSearch(e.target.value, filterTable);
         });
     });
 }
@@ -532,7 +567,7 @@ function initModalLifecycle() {
             try {
                 await editAlias(id);
             } catch (err) {
-                showToast(err?.message || "Error loading alias", false);
+                showToast(err?.message || t("aliases.loaded.error"), false);
                 // Close modal
                 modalEl.addEventListener('shown.bs.modal', () => {
                     closeAddAliasModal();
@@ -590,8 +625,8 @@ async function handleActionClick(e) {
     try {
         await handler(e, el);
     } catch (err) {
-        console.error(err?.message || 'Action error');
-        showToast(err?.message || 'Action error', false);
+        console.error(err?.message || t("app.action.error"));
+        showToast(err?.message || t("app.action.error"), false);
     }
 }
 
index 4bd35b236105e1069dba2b2a8132d2450a912521..faf1c156752363cc4cc0da932949569d8d21e6f0 100644 (file)
@@ -53,17 +53,17 @@ export async function apiRequest(
 
     // Handle HTTP error
     if (!res.ok) {
-        const serverMsg =
-            data?.detail?.message?.trim()
-            || (typeof data?.detail === 'string' ? data.detail.trim() : '')
-            || data?.message?.trim()
-            || data?.error?.message?.trim()
-            || (typeof data?.error === 'string' ? data.error.trim() : '');
+
+        const detail = data?.detail ?? data;
 
         const err = new Error(
-            `${errorPrefix}${serverMsg ? `: ${serverMsg}` : ''}`
+            detail?.message || errorPrefix
         );
+
         err.status = res.status;
+        err.code = detail?.code;
+        err.details = detail?.details;
+
         throw err;
     }
 
diff --git a/frontend/js/backendMessages.js b/frontend/js/backendMessages.js
new file mode 100644 (file)
index 0000000..de1e873
--- /dev/null
@@ -0,0 +1,80 @@
+// -------------------------------------------------------
+// IMPORT
+// -------------------------------------------------------
+import { t } from "./i18n.js";
+
+const CODE_MAP = {
+    // APP
+    APP_RESTARTING: "app.restart.progress",
+
+    // Hosts
+       HOSTS_GET_ERROR: "hosts.list.error",
+       HOST_GET_ERROR: "hosts.loaded.error",
+    HOST_ADDED: "hosts.created.ok",
+    HOST_ADD_ERROR: "hosts.created.error",
+    HOST_UPDATED: "hosts.updated.ok",
+    HOST_UPDATE_ERROR: "hosts.updated.error",
+    HOST_DELETED: "hosts.deleted.ok",
+    HOST_DELETE_ERROR: "hosts.deleted.error",
+    HOST_NOT_FOUND: "hosts.not.found",
+    HOST_ALREADY_PRESENT: "hosts.already.present",
+
+    // Aliases
+       ALIASES_GET_ERROR: "aliases.list.error",
+       ALIAS_GET_ERROR: "aliases.loaded.error",
+    ALIAS_ADDED: "aliases.created.ok",
+    ALIAS_ADD_ERROR: "aliases.created.error",
+    ALIAS_UPDATED: "aliases.updated.ok",
+    ALIAS_UPDATE_ERROR: "aliases.updated.error",
+    ALIAS_DELETED: "aliases.deleted.ok",
+    ALIAS_DELETE_ERROR: "aliases.deleted.error",
+    ALIAS_NOT_FOUND: "aliases.not.found",
+    ALIAS_ALREADY_PRESENT: "aliases.already.present",
+
+    // DNS
+    DNS_RELOAD_OK: "dns.reload.ok",
+    DNS_RELOAD_ERROR: "dns.reload.error",
+
+    // DHCP
+    DHCP_RELOAD_OK: "dhcp.reload.ok",
+    DHCP_RELOAD_ERROR: "dhcp.reload.error",
+       DHCP_LEASES_ERROR: "dhcp.leases.list.error",
+       DHCP_LEASE_ERROR: "dhcp.leases.loaded.error",
+       DHCP_LEASE_DELETED: "dhcp.leases.deleted.ok",
+       DHCP_LEASE_DELETE_ERROR: "dhcp.leases.deleted.error",
+       DHCP_LEASE_NOT_FOUND: "dhcp.leases.not.found",
+
+    BACKUP_CREATED: "backup.create_ok",
+    BACKUP_CREATE_PARTIAL: "backup.create_partial",
+    BACKUP_CREATE_ERROR: "backup.create_error",
+    BACKUP_RESTORED: "backup.restore_ok",
+    BACKUP_RESTORE_PARTIAL: "backup.restore_partial",
+    BACKUP_RESTORE_ERROR: "backup.restore_error",
+    BACKUP_DELETED: "backup.delete_ok",
+    BACKUP_DELETE_ERROR: "backup.delete_error",
+    BACKUP_NOT_FOUND: "backup.not.found",
+
+    // Settings
+    CONFIGS_GET_ERROR: "settings.list.error",
+    CONFIG_GET_ERROR: "settings.loaded.error",
+    CONFIG_UPDATED: "settings.updated.ok",
+    CONFIG_UPDATE_ERROR: "settings.updated.error",
+    CONFIG_RESTORED: "settings.restored.ok",
+    CONFIG_RESTORED_ERROR: "settings.restored.error",
+    CONFIG_NOT_FOUND: "settings.not.found",
+};
+
+export function getBackendMessage(result, fallbackKey = null) {
+
+    const i18nKey = CODE_MAP[result?.code];
+
+    if (i18nKey) {
+        return t(i18nKey);
+    }
+
+    if (result?.message) {
+        return result.message;
+    }
+
+    return fallbackKey ? t(fallbackKey) : "";
+}
index f77e2785944f6116ac5704649e572d258a8db811..97d5d34819ad303a8578452ab2f9bc15c9c3f26d 100644 (file)
@@ -1,3 +1,8 @@
+// -------------------------------------------------------
+// IMPORT
+// -------------------------------------------------------
+import { getBackendMessage } from "./backendMessages.js";
+
 // -----------------------------
 // Configuration parameters
 // -----------------------------
@@ -8,23 +13,20 @@ const stringCollator = new Intl.Collator(undefined, { numeric: true, sensitivity
 // Load modals HTML and initialize them
 // -----------------------------
 export async function loadModals() {
-    try {
-        const r = await fetch("/modals.html");
-        if (!r.ok) throw new Error("Error loading modals");
-
-        const html = await r.text();
+    const r = await fetch("/modals.html");
 
-        const container = document.getElementById("modals-container");
-        if (!container) {
-            console.warn("modals-container not found");
-            return;
-        }
+    if (!r.ok) {
+        throw new Error("Error loading modals");
+    }
 
-        container.innerHTML = html;
+    const html = await r.text();
 
-    } catch (err) {
-        console.error("Modals load error:", err);
+    const container = document.getElementById("modals-container");
+    if (!container) {
+        throw new Error("modals-container not found");
     }
+
+    container.innerHTML = html;
 }
 
 // -----------------------------
@@ -440,7 +442,6 @@ export async function handleReload(
 
     const originalHTML = button.innerHTML;
 
-    // spinner + testo
     button.innerHTML = `
         <i class="bi bi-arrow-repeat spin"></i>
         <span>${workingText}</span>
@@ -448,16 +449,18 @@ export async function handleReload(
 
     try {
         const result = await serviceFn();
-
-        const msg =
-            (result && typeof result === "object" && result.message)
-                ? result.message
-                : defaultSuccessMsg;
-
+        const msg = getBackendMessage(
+            result,
+            defaultSuccessMsg
+        );
         showToast(msg, true);
 
     } catch (err) {
-        showToast(err?.message || defaultErrorMsg, false);
+        const msg = getBackendMessage(
+            err,
+            defaultErrorMsg
+        );
+        showToast(msg, false);
 
     } finally {
         if (!keepDisabled) {
index 97485987cc75d19ec98fa9ccaff9bda7f00cc594..c36c984bf63253c46d9e2c5954783917403f35c9 100644 (file)
@@ -1,7 +1,10 @@
-// Import common js
+// -------------------------------------------------------
+// IMPORT
+// -------------------------------------------------------
 import { loadModals, isValidIPv4, isValidIPv6, isValidMAC, showToast, sortTable, initSortableTable, resetSorting, handleSearch, filterTable, clearSearch, handleReload } from './common.js';
-// Import services
-import { serviceReloadDNS, serviceReloadDHCP, serviceGetDHCPLeases, serviceGetDHCPLease, serviceDeleteDHCPLease, serviceGetDevices, serviceGetHost, serviceCreateHost, serviceUpdateHost, serviceDeleteHost } from './services.js';
+import { serviceReloadDNS, serviceReloadDHCP, serviceGetDHCPLease, serviceDeleteDHCPLease, serviceGetDevices, serviceGetHost, serviceCreateHost, serviceUpdateHost, serviceDeleteHost } from './services.js';
+import { loadLanguage, translatePage, t } from "./i18n.js";
+import { getBackendMessage } from "./backendMessages.js";
 
 // -----------------------------
 // State variables
@@ -30,8 +33,8 @@ async function fetchDevices () {
         viewDevices = [...allDevices];
 
     } catch (err) {
-        console.error(err?.message || "Error loading devices");
-        showToast(err?.message || "Error loading devices", false);
+        console.error(err?.message || t("devices.list.error"));
+        showToast(err?.message || t("devices.list.error"), false);
         allDevices = [];
         viewDevices = [];
         // hide loader and show table
@@ -64,7 +67,7 @@ function updateTable () {
         const trEmpty = document.createElement("tr");
         const tdEmpty = document.createElement("td");
         tdEmpty.colSpan = 7;
-        tdEmpty.textContent = "No devices available.";
+        tdEmpty.textContent = t("devices.empty");
         tdEmpty.style.textAlign = "center";
         trEmpty.appendChild(tdEmpty);
         tbody.appendChild(trEmpty);
@@ -89,8 +92,8 @@ function updateTable () {
         } else if (id.startsWith("d-")) {
             type = 2;
         } else {
-            console.error("updateTable: unknown device type:", id);
-            showToast("updateTable: unknown device type:", false);
+            console.error(t("devices.unknown.type"), id);
+            showToast(t("devices.unknown.type"), false);
         }
 
         const tr = document.createElement("tr");
@@ -139,47 +142,47 @@ function updateTable () {
             td.style.verticalAlign = "middle";
 
             const val = (d.dhcp_state ?? "").toString();
-            let aria = "";
+            let description = "";
             let iconClass = "";
             switch (val) {
                 case "static":
-                    // Static device
-                    aria = "Device is static";
+                    // Static lease
+                    description = t("dhcp.leases.static.description");
                     iconClass = "bi bi-gear-fill";
                     break;
 
                 case "active":
                     // DHCP active lease
-                    aria = "DHCP lease is active";
+                    description = t("dhcp.leases.active.description");
                     iconClass = "bi bi-check-circle-fill";
                     break;
 
                 case "expired":
                     // DHCP expired lease
-                    aria = "DHCP lease is expired";
+                    description = t("dhcp.leases.expired.description");
                     iconClass = "bi bi-clock-history";
                     break;
 
                 case "released":
                     // DHCP released lease
-                    aria = "DHCP lease is released";
+                    description = t("dhcp.leases.released.description");
                     iconClass = "bi bi-box-arrow-in-right";
                     break;
 
                 case "declined":
                     // DHCP declined lease
-                    aria = "DHCP lease is declined";
+                    description = t("dhcp.leases.declined.description");
                     iconClass = "bi bi-x-octagon-fill";
                     break;
             }
             if (iconClass) {
                 const icon = document.createElement("i");
                 icon.className = iconClass + " icon icon-static";
-                icon.setAttribute("aria-hidden", "true");
-                icon.setAttribute("title", aria);
+                td.setAttribute("title", description);
+                td.setAttribute("aria-label", description);
                 td.appendChild(icon);
             }
-
+            if (val) td.setAttribute("data-value", val);
             tr.appendChild(td);
         }
 
@@ -188,20 +191,20 @@ function updateTable () {
             const td = document.createElement("td");
             td.style.textAlign = "center";
             td.style.verticalAlign = "middle";
+            let description = "";
 
             const active = !!d.active;
             td.setAttribute("data-value", active ? "true" : "false");
-            td.setAttribute("aria-label", active ? "device active" : "device not active");
             const icon = document.createElement("i");
             if (active) {
+                description = t("devices.active.description");
                 icon.className = "bi bi-circle-fill text-success icon icon-static";
-                icon.setAttribute("aria-hidden", "true");
-                icon.setAttribute("title", "Device is active");
             } else {
+                description = t("devices.not.active.description");
                 icon.className = "bi bi-circle-fill text-danger icon icon-static";
-                icon.setAttribute("aria-hidden", "true");
-                icon.setAttribute("title", "Device is not active");
             }
+            td.setAttribute("title", description);
+            td.setAttribute("aria-label", description);
             td.appendChild(icon);
             tr.appendChild(td);
         }
@@ -215,11 +218,12 @@ function updateTable () {
 
             // Edit Button
             const editSpan = document.createElement("span");
+            const editText = t("devices.edit");
             editSpan.className = "action-icon";
             editSpan.setAttribute("role", "button");
             editSpan.tabIndex = 0;
-            editSpan.title = "Edit host";
-            editSpan.setAttribute("aria-label", "Edit host");
+            editSpan.title = editText;
+            editSpan.setAttribute("aria-label", editText);
             editSpan.setAttribute("data-bs-toggle", "modal");
             editSpan.setAttribute("data-bs-target", "#addHostModal");
             editSpan.setAttribute("data-action", "edit");
@@ -233,11 +237,12 @@ function updateTable () {
 
             // Add Button
             const addSpan = document.createElement("span");
+            const addText = t("dhcp.leases.add");
             addSpan.className = "action-icon";
             addSpan.setAttribute("role", "button");
             addSpan.tabIndex = 0;
-            addSpan.title = "Add static lease";
-            addSpan.setAttribute("aria-label", "Add static lease");
+            addSpan.title = addText;
+            addSpan.setAttribute("aria-label", addText);
             addSpan.setAttribute("data-bs-toggle", "modal");
             addSpan.setAttribute("data-bs-target", "#addHostModal");
             addSpan.setAttribute("data-action", "add");
@@ -251,11 +256,12 @@ function updateTable () {
 
             // Delete Button
             const delSpan = document.createElement("span");
+            const deleteText = t("devices.delete");
             delSpan.className = "action-icon";
             delSpan.setAttribute("role", "button");
             delSpan.tabIndex = 0;
-            delSpan.title = "Delete device";
-            delSpan.setAttribute("aria-label", "Delete device");
+            delSpan.title = deleteText;
+            delSpan.setAttribute("aria-label", deleteText);
             delSpan.setAttribute("data-action", "delete");
             delSpan.setAttribute("data-device-id", String(id));
             {
@@ -322,11 +328,11 @@ async function editHost(id) {
             // dynamic
             host = false;
         } else {
-            throw new Error("Invalid Device ID format for edit");
+            throw new Error(t("devices.edit.invalid_id"));
         }
         id = Number(id.slice(2));
     } else {
-        throw new Error("Invalid Device ID for edit");
+        throw new Error(t("devices.edit.invalid_id"));
     }
 
     try {
@@ -355,10 +361,9 @@ async function editHost(id) {
         } else {
             document.getElementById("hostVisibilityLocal").checked = true;
         }
-
     } catch (err) {
-        console.error(err?.message || "Error loading device");
-        showToast(err?.message || "Error loading device", false);
+        console.error(err?.message || t("devices.loaded.error"));
+        showToast(err?.message || t("devices.loaded.error"), false);
     }
 }
 
@@ -368,22 +373,22 @@ async function editHost(id) {
 async function saveHost(hostData) {
     // Validate hostname
     if (!hostData.name.trim()) {
-        showToast("Hostname is required", false);
+        showToast(t("validation.name.required"), false);
         return false;
     }
     // Validate IPv4 format
     if (!isValidIPv4(hostData.ipv4)) {
-        showToast("Invalid IPv4 format", false);
+        showToast(t("validation.ipv4.invalid"), false);
         return false;
     }
     // Validate IPv6 format
     if (!isValidIPv6(hostData.ipv6)) {
-        showToast("Invalid IPv6 format", false);
+        showToast(t("validation.ipv6.invalid"), false);
         return false;
     }
     // Validate MAC format
     if (!isValidMAC(hostData.mac)) {
-        showToast("Invalid MAC format", false);
+        showToast(t("validation.mac.invalid"), false);
         return false;
     }
 
@@ -398,19 +403,27 @@ async function saveHost(hostData) {
             result = await serviceCreateHost(hostData);
         }
 
-        const msg = (typeof result === 'object' && result?.message)
-            ? result.message
-            : editingHostId !== null
-                ? 'Host updated successfully'
-                : 'Host created successfully';
+        const msg = getBackendMessage(
+            result,
+            editingHostId !== null
+                ? "devices.updated.ok"
+                : "devices.created.ok"
+        );
 
         showToast(msg, true);
 
         return true;
 
     } catch (err) {
-        console.error(err?.message || "Error saving host");
-        showToast(err?.message || "Error saving host", false);
+
+        const msg = getBackendMessage(
+            err,
+            editingHostId !== null
+                ? "devices.updated.error"
+                : "devices.created.error"
+        );
+
+        showToast(msg, false);
     }
 
     return false;
@@ -468,8 +481,20 @@ async function handleAddHostSubmit(e) {
         }
 
     } catch (err) {
-        console.error(err?.message || "Error saving host");
-        showToast(err?.message || "Error saving host", false);
+        console.error(
+            err?.message ||
+            (editingHostId !== null
+                ? t("devices.updated.error")
+                : t("devices.created.error"))
+        );
+
+        showToast(
+            err?.message ||
+            (editingHostId !== null
+                ? t("devices.updated.error")
+                : t("devices.created.error")),
+            false
+        );
     }
 
     return false;
@@ -489,8 +514,7 @@ async function handleDeleteDevice(e, el) {
     let id = el.dataset.deviceId;
 
     if (!id) {
-        console.warn('Delete: device id not valid for delete:', id);
-        showToast('Device id not valid for delete', false);
+        showToast(t("devices.delete.invalid_id"), false);
         return;
     }
 
@@ -504,25 +528,23 @@ async function handleDeleteDevice(e, el) {
             // dynamic
             host = false;
         } else {
-            throw new Error("Invalid Device ID format for edit");
+            throw new Error(t("devices.edit.invalid_id"));
         }
         id = Number(id.slice(2));
     } else {
-        throw new Error("Invalid Device ID for edit");
+        throw new Error(t("devices.edit.invalid_id"));
     }
 
     try {
-       if(host){
-            const result = await serviceDeleteHost(id);
+       let result;
 
+       if(host){
+            result = await serviceDeleteHost(id);
         } else {
-            const result = await serviceDeleteDHCPLease(id);
+            result = await serviceDeleteDHCPLease(id);
         }
 
-        const msg = (typeof result === 'object' && result?.message)
-            ? result.message
-            : 'Host deleted successfully';
-
+        const msg = getBackendMessage(result, "devices.deleted.ok");
         showToast(msg, true);
 
         // Reload devices
@@ -532,8 +554,8 @@ async function handleDeleteDevice(e, el) {
         return true;
 
     } catch (err) {
-        console.error(err?.message || "Error deleting device");
-        showToast(err?.message || "Error deleting device", false);
+        console.error(err?.message || t("devices.deleted.error"));
+        showToast(err?.message || t("devices.deleted.error"), false);
     }
 
     return false;
@@ -547,7 +569,11 @@ const actionHandlers = {
     delete: (e, el) => {
         handleDeleteDevice(e, el);
     },
-    // Edit host
+    // Add device
+    add: () => {
+        // handled by bootstrap modal show event
+    },
+    // Edit device
     edit: () => {
         // handled by bootstrap modal show event
     },
@@ -556,9 +582,9 @@ const actionHandlers = {
         await handleReload(
             el,
             serviceReloadDNS,
-            "DNS reload successfully",
-            "Error reloading DNS",
-            "Reloading DNS..."
+            t("dns.reload.ok"),
+            t("dns.reload.error"),
+            t("dns.reload.progress")
         );
     },
     // Reload DHCP
@@ -566,9 +592,9 @@ const actionHandlers = {
         await handleReload(
             el,
             serviceReloadDHCP,
-            "DHCP reload successfully",
-            "Error reloading DHCP",
-            "Reloading DHCP..."
+            t("dhcp.reload.ok"),
+            t("dhcp.reload.error"),
+            t("dhcp.reload.progress")
         );
     },
 };
@@ -585,21 +611,32 @@ document.addEventListener("DOMContentLoaded", async () => {
 // -----------------------------
 async function initApp() {
 
+    // Loading translation
+    try {
+        await loadLanguage();
+    } catch (err) {
+        console.error(err?.message || t("app.translation.error"));
+        showToast(t("app.translation.error"), false);
+    }
+
     // Load modals (Bootstrap 5 requires JS initialization for dynamic content)
     try {
         await loadModals();
     } catch (err) {
-        console.error(err?.message || "Error loading modals");
-        showToast(err?.message || "Error loading modals", false);
+        console.error(err?.message || t("app.modals.error"));
+        showToast(t("app.modals.error"), false);
     }
 
+    // Translate page
+    translatePage();
+
     // Load data (devices)
     try {
         await fetchDevices();
         updateTable();
     } catch (err) {
-        console.error(err?.message || "Error loading devices");
-        showToast(err?.message || "Error loading devices", false);
+        console.error(err?.message || t("devices.list.error"));
+        showToast(err?.message || t("devices.list.error"), false);
     }
 
     initUI();
@@ -661,7 +698,7 @@ function initModalLifecycle() {
             try {
                 await editHost(id);
             } catch (err) {
-                showToast(err?.message || "Error loading host", false);
+                showToast(err?.message || t("devices.loaded.error"), false);
                 // Close modal
                 modalEl.addEventListener('shown.bs.modal', () => {
                     closeAddHostModal();
@@ -719,8 +756,8 @@ async function handleActionClick(e) {
     try {
         await handler(e, el);
     } catch (err) {
-        console.error(err?.message || 'Action error');
-        showToast(err?.message || 'Action error', false);
+        console.error(err?.message || t("app.action.error"));
+        showToast(err?.message || t("app.action.error"), false);
     }
 }
 
index 788350aeb2452ac42d8e6348dfce28b3df4d8714..5dda214d47ef47480accd6ebe50e077ab28d89a0 100644 (file)
@@ -1,7 +1,10 @@
-// Import common js
+// -------------------------------------------------------
+// IMPORT
+// -------------------------------------------------------
 import { loadModals, isValidIPv4, isValidIPv6, isValidMAC, showToast, sortTable, initSortableTable, resetSorting, handleSearch, filterTable, clearSearch, handleReload } from './common.js';
-// Import services
 import { serviceReloadDNS, serviceReloadDHCP, serviceGetHosts, serviceGetHost, serviceCreateHost, serviceUpdateHost, serviceDeleteHost } from './services.js';
+import { loadLanguage, translatePage, t } from "./i18n.js";
+import { getBackendMessage } from "./backendMessages.js";
 
 // -----------------------------
 // State variables
@@ -30,8 +33,8 @@ async function fetchHosts () {
         viewHosts = [...allHosts];
 
     } catch (err) {
-        console.error(err?.message || "Error loading hosts");
-        showToast(err?.message || "Error loading hosts", false);
+        console.error(err?.message || t("hosts.list.error"));
+        showToast(err?.message || t("hosts.list.error"), false);
         allHosts = [];
         viewHosts = [];
         // hide loader and show table
@@ -64,7 +67,7 @@ function updateTable () {
         const trEmpty = document.createElement("tr");
         const tdEmpty = document.createElement("td");
         tdEmpty.colSpan = 6;
-        tdEmpty.textContent = "No hosts available.";
+        tdEmpty.textContent = t("hosts.empty");
         tdEmpty.style.textAlign = "center";
         trEmpty.appendChild(tdEmpty);
         tbody.appendChild(trEmpty);
@@ -129,52 +132,55 @@ function updateTable () {
             // SSL icon
             //
             const sslEnabled = !!h.ssl_enabled;
-            td.setAttribute("data-value", sslEnabled ? "true" : "false");
-            td.setAttribute("aria-label", sslEnabled ? "SSL attivo" : "SSL non attivo");
-            const icon = document.createElement("i");
+            const wrapperSslIcon = document.createElement("span");
+            const sslIcon = document.createElement("i");
+            let description = "";
             if (sslEnabled) {
-                icon.className = "bi bi-shield-lock-fill icon icon-static";
-                icon.setAttribute("aria-hidden", "true");
-                icon.setAttribute("title", "SSL certificate enabled");
+                sslIcon.className = "bi bi-shield-lock-fill icon icon-static";
+                description = t("hosts.ssl.enabled");
+                wrapperSslIcon.setAttribute("title", description);
+                wrapperSslIcon.setAttribute("aria-label", description);
             } else {
-                icon.className = "bi bi-shield-lock-fill icon icon-static icon-placeholder";
-                icon.setAttribute("aria-hidden", "true");
+                sslIcon.className = "bi bi-shield-lock-fill icon icon-static icon-placeholder";
+                sslIcon.setAttribute("aria-hidden", "true");
             }
-            td.appendChild(icon);
+            wrapperSslIcon.appendChild(sslIcon);
+            td.appendChild(wrapperSslIcon);
 
             //
             // visibility icon
             //
             const ext = (h.visibility ?? "").toString();
-            let aria = "";
+            const wrapperVisibilityIcon = document.createElement("span");
+            const visibilityIcon = document.createElement("i");
             let iconClass = "";
             switch (ext) {
                 case "0":
                     // Only local (A record internally resolved)
-                    aria = "Only local (A record internally resolved)";
+                    description = t("hosts.visibility.local.description");
                     iconClass = "bi bi-hdd-network";
                     break;
 
                 case "1":
                     // Local and external (A record internally resolved, A externally)
-                    aria = "Internal and external are identical";
+                    description = t("hosts.visibility.global.description");
                     iconClass = "bi bi-globe2";
                     break;
 
                 case "2":
                     // CNAME -> DDNS / external_name
-                    aria = "External is a CNAME to external_name";
+                    description = t("hosts.visibility.alias.description");
                     iconClass = "bi bi-link-45deg";
                     break;
             }
             if (iconClass) {
-                const icon = document.createElement("i");
-                icon.className = iconClass + " icon icon-static";
-                icon.setAttribute("aria-hidden", "true");
-                icon.setAttribute("title", aria);
-                td.appendChild(icon);
+                visibilityIcon.className = iconClass + " icon icon-static";
+                wrapperVisibilityIcon.setAttribute("title", description);
+                wrapperVisibilityIcon.setAttribute("aria-label", description);
+                wrapperVisibilityIcon.appendChild(visibilityIcon);
+                td.appendChild(wrapperVisibilityIcon);
             }
-
+            if (ext) td.setAttribute("data-value", ext);
             tr.appendChild(td);
         }
 
@@ -187,11 +193,12 @@ function updateTable () {
 
             // Edit Button
             const editSpan = document.createElement("span");
+            const editText = t("hosts.edit");
             editSpan.className = "action-icon";
             editSpan.setAttribute("role", "button");
             editSpan.tabIndex = 0;
-            editSpan.title = "Edit host";
-            editSpan.setAttribute("aria-label", "Edit host");
+            editSpan.title = editText;
+            editSpan.setAttribute("aria-label", editText);
             editSpan.setAttribute("data-bs-toggle", "modal");
             editSpan.setAttribute("data-bs-target", "#addHostModal");
             editSpan.setAttribute("data-action", "edit");
@@ -205,11 +212,12 @@ function updateTable () {
 
             // Delete Button
             const delSpan = document.createElement("span");
+            const deleteText = t("hosts.delete");
             delSpan.className = "action-icon";
             delSpan.setAttribute("role", "button");
             delSpan.tabIndex = 0;
-            delSpan.title = "Delete host";
-            delSpan.setAttribute("aria-label", "Delete host");
+            delSpan.title = deleteText;
+            delSpan.setAttribute("aria-label", deleteText);
             delSpan.setAttribute("data-action", "delete");
             delSpan.setAttribute("data-host-id", String(id));
             {
@@ -278,10 +286,9 @@ async function editHost(id) {
         } else {
             document.getElementById("hostVisibilityLocal").checked = true;
         }
-
     } catch (err) {
-        console.error(err?.message || "Error loading host");
-        showToast(err?.message || "Error loading host", false);
+        console.error(err?.message || t("hosts.loaded.error"));
+        showToast(err?.message || t("hosts.loaded.error"), false);
     }
 }
 
@@ -291,22 +298,22 @@ async function editHost(id) {
 async function saveHost(hostData) {
     // Validate hostname
     if (!hostData.name.trim()) {
-        showToast("Hostname is required", false);
+        showToast(t("validation.name.required"), false);
         return false;
     }
     // Validate IPv4 format
     if (!isValidIPv4(hostData.ipv4)) {
-        showToast("Invalid IPv4 format", false);
+        showToast(t("validation.ipv4.invalid"), false);
         return false;
     }
     // Validate IPv6 format
     if (!isValidIPv6(hostData.ipv6)) {
-        showToast("Invalid IPv6 format", false);
+        showToast(t("validation.ipv6.invalid"), false);
         return false;
     }
     // Validate MAC format
     if (!isValidMAC(hostData.mac)) {
-        showToast("Invalid MAC format", false);
+        showToast(t("validation.mac.invalid"), false);
         return false;
     }
 
@@ -321,19 +328,27 @@ async function saveHost(hostData) {
             result = await serviceCreateHost(hostData);
         }
 
-        const msg = (typeof result === 'object' && result?.message)
-            ? result.message
-            : editingHostId !== null
-                ? 'Host updated successfully'
-                : 'Host created successfully';
+        const msg = getBackendMessage(
+            result,
+            editingHostId !== null
+                ? "hosts.updated.ok"
+                : "hosts.created.ok"
+        );
 
         showToast(msg, true);
 
         return true;
 
     } catch (err) {
-        console.error(err?.message || "Error saving host");
-        showToast(err?.message || "Error saving host", false);
+
+        const msg = getBackendMessage(
+            err,
+            editingHostId !== null
+                ? "hosts.updated.error"
+                : "hosts.created.error"
+        );
+
+        showToast(msg, false);
     }
 
     return false;
@@ -391,8 +406,20 @@ async function handleAddHostSubmit(e) {
         }
 
     } catch (err) {
-        console.error(err?.message || "Error saving host");
-        showToast(err?.message || "Error saving host", false);
+        console.error(
+            err?.message ||
+            (editingHostId !== null
+                ? t("hosts.updated.error")
+                : t("hosts.created.error"))
+        );
+
+        showToast(
+            err?.message ||
+            (editingHostId !== null
+                ? t("hosts.updated.error")
+                : t("hosts.created.error")),
+            false
+        );
     }
 
     return false;
@@ -408,17 +435,13 @@ async function handleDeleteHost(e, el) {
     // Get host ID
     const id = Number(el.dataset.hostId);
     if (!Number.isFinite(id)) {
-        showToast('Host id not valid for delete', false);
+        showToast(t("hosts.delete.invalid_id"), false);
         return;
     }
 
     try {
         const result = await serviceDeleteHost(id);
-
-        const msg = (typeof result === 'object' && result?.message)
-            ? result.message
-            : 'Host deleted successfully';
-
+        const msg = getBackendMessage(result, "hosts.deleted.ok");
         showToast(msg, true);
 
         // Reload hosts
@@ -428,8 +451,8 @@ async function handleDeleteHost(e, el) {
         return true;
 
     } catch (err) {
-        console.error(err?.message || "Error deleting host");
-        showToast(err?.message || "Error deleting host", false);
+        console.error(err?.message || t("hosts.deleted.error"));
+        showToast(err?.message || t("hosts.deleted.error"), false);
     }
 
     return false;
@@ -452,9 +475,9 @@ const actionHandlers = {
         await handleReload(
             el,
             serviceReloadDNS,
-            "DNS reload successfully",
-            "Error reloading DNS",
-            "Reloading DNS..."
+            t("dns.reload.ok"),
+            t("dns.reload.error"),
+            t("dns.reload.progress")
         );
     },
     // Reload DHCP
@@ -462,9 +485,9 @@ const actionHandlers = {
         await handleReload(
             el,
             serviceReloadDHCP,
-            "DHCP reload successfully",
-            "Error reloading DHCP",
-            "Reloading DHCP..."
+            t("dhcp.reload.ok"),
+            t("dhcp.reload.error"),
+            t("dhcp.reload.progress")
         );
     },
 };
@@ -481,21 +504,32 @@ document.addEventListener("DOMContentLoaded", async () => {
 // -----------------------------
 async function initApp() {
 
+    // Loading translation
+    try {
+        await loadLanguage();
+    } catch (err) {
+        console.error(err?.message || t("app.translation.error"));
+        showToast(t("app.translation.error"), false);
+    }
+
     // Load modals (Bootstrap 5 requires JS initialization for dynamic content)
     try {
         await loadModals();
     } catch (err) {
-        console.error(err?.message || "Error loading modals");
-        showToast(err?.message || "Error loading modals", false);
+        console.error(err?.message || t("app.modals.error"));
+        showToast(t("app.modals.error"), false);
     }
 
+    // Translate page
+    translatePage();
+
     // Load data (hosts)
     try {
         await fetchHosts();
         updateTable();
     } catch (err) {
-        console.error(err?.message || "Error loading hosts");
-        showToast(err?.message || "Error loading hosts", false);
+        console.error(err?.message || t("hosts.list.error"));
+        showToast(err?.message || t("hosts.list.error"), false);
     }
 
     initUI();
@@ -557,7 +591,7 @@ function initModalLifecycle() {
             try {
                 await editHost(id);
             } catch (err) {
-                showToast(err?.message || "Error loading host", false);
+                showToast(err?.message || t("hosts.loaded.error"), false);
                 // Close modal
                 modalEl.addEventListener('shown.bs.modal', () => {
                     closeAddHostModal();
@@ -615,8 +649,8 @@ async function handleActionClick(e) {
     try {
         await handler(e, el);
     } catch (err) {
-        console.error(err?.message || 'Action error');
-        showToast(err?.message || 'Action error', false);
+        console.error(err?.message || t("app.action.error"));
+        showToast(err?.message || t("app.action.error"), false);
     }
 }
 
diff --git a/frontend/js/i18n.js b/frontend/js/i18n.js
new file mode 100644 (file)
index 0000000..c117146
--- /dev/null
@@ -0,0 +1,72 @@
+// -------------------------------------------------------
+// IMPORT
+// -------------------------------------------------------
+import { apiGet } from "./api.js";
+
+let _language = "en";
+let _translations = {};
+
+// -------------------------------------------------------
+// Load translations from backend
+// -------------------------------------------------------
+export async function loadLanguage() {
+    const data = await apiGet(
+        "/api/i18n",
+        "Unable to load translations"
+    );
+
+    _language = data.language;
+    _translations = data.translations;
+}
+
+// -------------------------------------------------------
+// Get current language
+// -------------------------------------------------------
+export function getLanguage() {
+    return _language;
+}
+
+// -------------------------------------------------------
+// Translate key
+// -------------------------------------------------------
+export function t(key) {
+    return _translations[key] || key;
+}
+
+// -------------------------------------------------------
+// Translate DOM elements
+// -------------------------------------------------------
+export function translatePage() {
+
+    // Page Title
+    const pageTitleKey = document.body.dataset.pageTitle;
+    if (pageTitleKey) {
+        document.title = `${t(pageTitleKey)} - ${t("app.name")}`;
+    }
+
+    // Generic Content
+    document.querySelectorAll("[data-i18n]").forEach(element => {
+        const key = element.dataset.i18n;
+        element.textContent = t(key);
+    });
+
+    // title
+    document.querySelectorAll("[data-i18n-title]").forEach(element => {
+        element.title = t(element.dataset.i18nTitle);
+    });
+
+    // aria-label
+    document.querySelectorAll("[data-i18n-aria-label]").forEach(element => {
+        element.setAttribute(
+            "aria-label",
+            t(element.dataset.i18nAriaLabel)
+        );
+    });
+
+    // placeholder
+    document.querySelectorAll("[data-i18n-placeholder]").forEach(element => {
+        element.placeholder = t(
+            element.dataset.i18nPlaceholder
+        );
+    });
+}
index 99037807a23ff87dd90f06cf8d639afa8a9b21b5..ed619c72e0d507efee81871941e227ec79513d74 100644 (file)
@@ -3,6 +3,29 @@
 // -------------------------------------------------------
 import { loadModals, showToast, showConfirmModal, handleReload } from './common.js';
 import { serviceIsAlive, serviceCheckHealth , serviceReloadDNS, serviceReloadDHCP, serviceBackupCreate, serviceBackupList, serviceBackupRestore, serviceDeleteBackup, serviceDownloadBackup, serviceUploadBackup } from './services.js';
+import { loadLanguage, translatePage, t } from "./i18n.js";
+import { getBackendMessage } from "./backendMessages.js";
+
+// -------------------------------------------------------
+// Localize health status for display in the UI
+// -------------------------------------------------------
+function localizeHealthStatus(status) {
+    const norm = String(status || "").toLowerCase();
+
+    switch (norm) {
+        case "healthy":
+            return t("health.status.healthy");
+
+        case "degraded":
+            return t("health.status.degraded");
+
+        case "unhealthy":
+            return t("health.status.unhealthy");
+
+        default:
+            return t("health.status.unknown");
+    }
+}
 
 // -------------------------------------------------------
 // BACKUP MODAL OPEN/CLOSE
@@ -14,22 +37,25 @@ async function openBackupModal() {
     modal.style.display = 'flex';
 
     const tbody = document.getElementById("backupList");
-    if (tbody) {
-        tbody.innerHTML = `
-            <tr>
-                <td colspan="5" class="text-center text-muted">
-                    Loading backups...
-                </td>
-            </tr>
-        `;
-    }
+    if (!tbody) return;
+    tbody.innerHTML = `
+        <tr>
+            <td colspan="5" class="text-center text-muted">
+                ${t("backup.loading")}
+            </td>
+        </tr>
+    `;
 
     // Refresh backup list
     try {
         const result = await serviceBackupList();
         renderBackupList(result);
     } catch (err) {
-        showToast(err?.message || "Error refreshing backup list", false);
+        const msg = getBackendMessage(
+            err,
+            "backup.refresh_error"
+        );
+        showToast(msg, false);
     }
 }
 
@@ -44,19 +70,19 @@ function closeBackupModal() {
 export async function serviceCheckAbout() {
     const pills = document.querySelectorAll('.btn-api');
 
-    if (!pills.length) return;
+    if (!pills.length) return false;
 
     const ok = await serviceIsAlive();
 
     pills.forEach(pill => {
 
         if (ok) {
-            pill.textContent = 'API OK';
+            pill.textContent = t("header.api_status_online");
             pill.classList.remove('btn-outline-primary');
             pill.classList.add('btn-primary');
 
         } else {
-            pill.textContent = 'API OFFLINE';
+            pill.textContent = t("header.api_status_offline");
         }
     });
 
@@ -68,13 +94,14 @@ export async function serviceCheckAbout() {
 // -------------------------------------------------------
 function renderBackupList(data) {
     const tbody = document.getElementById("backupList");
+    if (!tbody) return;
     tbody.innerHTML = "";
 
     if (!data?.backups || !Array.isArray(data.backups) || data.backups.length === 0) {
         tbody.innerHTML = `
             <tr>
                 <td colspan="5" class="text-center text-muted">
-                    No backups available
+                    ${t("backup.empty")}
                 </td>
             </tr>
         `;
@@ -123,17 +150,21 @@ function renderBackupList(data) {
         tdActions.classList.add("text-end");
         // download button
         const downloadBtn = document.createElement("button");
+        const downloadText = t("backup.download.file");
         downloadBtn.className = "btn btn-sm btn-outline-primary me-2";
-        downloadBtn.title = "Download backup";
+        downloadBtn.title = downloadText;
         downloadBtn.innerHTML = `<i class="bi bi-download"></i>`;
+        downloadBtn.setAttribute("aria-label", downloadText);
         downloadBtn.setAttribute("data-action", "downloadBackup");
         downloadBtn.setAttribute("data-id", b.name);
         tdActions.appendChild(downloadBtn);
         // delete button
         const deleteBtn = document.createElement("button");
+        const deleteText = t("backup.delete.file");
         deleteBtn.className = "btn btn-sm btn-outline-danger";
-        deleteBtn.title = "Delete backup";
+        deleteBtn.title = deleteText;
         deleteBtn.innerHTML = `<i class="bi bi-trash-fill"></i>`;
+        deleteBtn.setAttribute("aria-label", deleteText);
         deleteBtn.setAttribute("data-action", "deleteBackup");
         deleteBtn.setAttribute("data-id", b.name);
         tdActions.appendChild(deleteBtn);
@@ -198,13 +229,13 @@ function openHealthModal() {
     Promise.resolve()
       .then(() => serviceCheckHealth())
       .then((data) => {
-          // Se serviceCheckHealth ritorna true o {message}, non abbiamo i dettagli: mostra un messaggio
+          // Verify that the response contains health details
           const isDetailed =
               data && typeof data === 'object' &&
               ('status' in data || 'latency_ms' in data || 'database' in data);
 
           if (!isDetailed) {
-              throw new Error('Health details not available');
+              throw new Error(t("health.details_unavailable"));
           }
 
           renderHealth(data);
@@ -214,7 +245,7 @@ function openHealthModal() {
       .catch((err) => {
           loadingEl?.classList?.add('d-none');
           errorEl?.classList?.remove('d-none');
-          showToast(err?.message || 'Error while fetching health status', false);
+          showToast(err?.message || t("health.error"), false);
           console.error(err);
       });
 }
@@ -229,13 +260,19 @@ function setHealthBadge(status) {
     if (!badgeEl) return;
 
     const norm = String(status || '').toLowerCase();
+
     let cls = 'bg-secondary';
-    if (norm === 'ok' || norm === 'healthy' || norm === 'up') cls = 'bg-success';
-    if (norm === 'warn' || norm === 'warning' || norm === 'degraded') cls = 'bg-warning text-dark';
-    if (norm === 'down' || norm === 'error' || norm === 'fail' || norm === 'critical') cls = 'bg-danger';
+
+    if (norm === 'healthy') {
+        cls = 'bg-success';
+    } else if (norm === 'degraded') {
+        cls = 'bg-warning text-dark';
+    } else if (norm === 'unhealthy') {
+        cls = 'bg-danger';
+    }
 
     badgeEl.className = `badge rounded-pill ${cls}`;
-    badgeEl.textContent = norm || 'unknown';
+    badgeEl.textContent = localizeHealthStatus(norm);
 }
 
 function renderHealth(data) {
@@ -254,16 +291,16 @@ function renderHealth(data) {
     setHealthBadge(status);
     if (updatedAtEl) {
         const now = new Date();
-        updatedAtEl.textContent = `Updated at ${now.toLocaleTimeString()}`;
+        updatedAtEl.textContent = `${t("health.updated_at")} ${now.toLocaleTimeString()}`;
     }
 
     const rows = [
-        { label: 'Status', value: status },
-        { label: 'Latency', value: (typeof latency === 'number') ? `${latency} ms` : '—' },
-        { label: 'DB Status', value: dbStatus },
-        { label: 'DB Version', value: dbVersion },
-        { label: 'DB Tables', value: dbTables },
-        { label: 'DB Size', value: dbSize },
+        { label: t("health.status"), value: localizeHealthStatus(status) },
+        { label: t("health.latency"), value: (typeof latency === "number") ? `${latency} ms` : "—" },
+        { label: t("health.db_status"), value: localizeHealthStatus(dbStatus) },
+        { label: t("health.db_version"), value: dbVersion },
+        { label: t("health.db_tables"), value: dbTables },
+        { label: t("health.db_size"), value: dbSize },
     ];
 
     if (summaryEl) {
@@ -290,7 +327,6 @@ const actionHandlers = {
     // Create Backup
     startBackup: async (e, el) => {
         const btn = el;
-        const modal = document.getElementById('backupModal');
 
         if (!btn) return;
 
@@ -298,16 +334,24 @@ const actionHandlers = {
         const originalLabel = label?.textContent ?? '';
 
         btn.disabled = true;
-        label.textContent = ' Exporting…';
+        label.textContent = t("backup.create.progress");
 
         try {
             const result = await serviceBackupCreate();
-            const msg = (typeof result === 'object' && result?.message)
-                        ? result.message
-                        : 'Backup completed successfully';
-            showToast(msg, !result?.partial);
+            const success = result?.status !== 'partial';
+            const msg = getBackendMessage(
+                        result,
+                        success
+                            ? "backup.create_ok"
+                            : "backup.create_partial"
+                    );
+            showToast(msg, success);
         } catch (err) {
-            showToast(err?.message || "Error performing backup", false);
+            const msg = getBackendMessage(
+                err,
+                "backup.create_error"
+            );
+            showToast(msg, false);
         } finally {
             label.textContent = originalLabel;
             btn.disabled = false;
@@ -317,17 +361,21 @@ const actionHandlers = {
             const result = await serviceBackupList();
             renderBackupList(result);
         } catch (err) {
-            showToast(err?.message || "Error refreshing backup list", false);
+            const msg = getBackendMessage(
+                err,
+                "backup.refresh_error"
+            );
+            showToast(msg, false);
         }
     },
     // Restore Backup
     startRestore: async (e, el) => {
         const btn = el;
-        const modal = document.getElementById('backupModal');
+        //const modal = document.getElementById('backupModal');
 
         const id = getSelectedBackup();
         if (!id) {
-            showToast('Select a backup', false);
+            showToast(t("backup.select"), false);
             return;
         }
 
@@ -335,18 +383,26 @@ const actionHandlers = {
         const originalLabel = label?.textContent ?? '';
 
         btn.disabled = true;
-        label.textContent = ' Restoring…';
+        label.textContent = t("backup.restore.progress");
 
         try {
             const result = await serviceBackupRestore(id);
-            const msg = (typeof result === 'object' && result?.message)
-                        ? result.message
-                        : 'Restore completed successfully';
-            showToast(msg, !result?.partial);
+            const success = result?.status !== 'partial';
+            const msg = getBackendMessage(
+                        result,
+                        success
+                            ? "backup.restore_ok"
+                            : "backup.restore_partial"
+                    );
+            showToast(msg, success);
             // Close modal
             //if (modal) modal.style.display = 'none';
         } catch (err) {
-            showToast(err?.message || "Error performing restore", false);
+            const msg = getBackendMessage(
+                err,
+                "backup.restore_error"
+            );
+            showToast(msg, false);
         } finally {
             label.textContent = originalLabel;
             btn.disabled = false;
@@ -360,39 +416,50 @@ const actionHandlers = {
         const id = el.dataset.id;
         if (!id) return;
 
-        const confirmed = await showConfirmModal(`Delete backup "${id}" ?`);
+        const confirmed = await showConfirmModal(t("backup.delete_confirm").replace("{id}", id));
         if (!confirmed) return;
 
         try {
             const result = await serviceDeleteBackup(id);
-
-            const msg = (typeof result === 'object' && result?.message)
-                ? result.message
-                : 'Backup deleted successfully';
-
+            const msg = getBackendMessage(
+                        result,
+                        "backup.delete_ok"
+                    );
             showToast(msg, true);
         } catch (err) {
-            console.error(err);
-            showToast(err?.message || "Error deleting backup", false);
+            const msg = getBackendMessage(
+                err,
+                "backup.delete_error"
+            );
+            showToast(msg, false);
         }
         // Refresh backup list
         try {
             const result = await serviceBackupList();
             renderBackupList(result);
         } catch (err) {
-            showToast(err?.message || "Error refreshing backup list", false);
+            const msg = getBackendMessage(
+                err,
+                "backup.refresh_error"
+            );
+            showToast(msg, false);
         }
     },
     refreshBackupList: async () => {
         try {
             const result = await serviceBackupList();
-            const msg = (typeof result === 'object' && result?.message)
-                        ? result.message
-                        : 'Backup list refreshed successfully';
+            const msg = getBackendMessage(
+                result,
+                "backup.refresh_ok"
+            );
             showToast(msg, true);
             renderBackupList(result);
         } catch (err) {
-            showToast(err?.message || "Error refreshing backup list", false);
+            const msg = getBackendMessage(
+                err,
+                "backup.refresh_error"
+            );
+            showToast(msg, false);
         }
     },
     // Download Backup
@@ -407,20 +474,20 @@ const actionHandlers = {
 
             const msg = (typeof result === 'object' && result?.message)
                 ? result.message
-                : 'Backup downloaded successfully';
+                : t("backup.download_ok");
 
             showToast(msg, true);
 
         } catch (err) {
             console.error(err);
-            showToast(err?.message || "Error downloading backup", false);
+            showToast(err?.message || t("backup.download_error"), false);
         }
     },
     // Upload Backup
     uploadBackup: async (e, el) => {
         const input = document.getElementById('backupUploadInput');
         if (!input?.files?.length) {
-            showToast("Select a file first", false);
+            showToast(t("backup.select_file"), false);
             return;
         }
 
@@ -439,15 +506,12 @@ const actionHandlers = {
 
             const msg = (result?.message)
                 ? result.message
-                : 'Backup uploaded successfully';
+                : t("backup.update_ok");
 
             showToast(msg, true);
-
-            console.log("Uploaded backup ID:", result?.backup_id);
-
             input.value = '';
         } catch (err) {
-            showToast(err?.message || "Error uploading backup", false);
+            showToast(err?.message || t("backup.update_error"), false);
         } finally {
             if (icon && originalClass) {
                 icon.className = originalClass;
@@ -459,7 +523,11 @@ const actionHandlers = {
             const result = await serviceBackupList();
             renderBackupList(result);
         } catch (err) {
-            showToast(err?.message || "Error refreshing backup list", false);
+            const msg = getBackendMessage(
+                err,
+                "backup.refresh_error"
+            );
+            showToast(msg, false);
         }
     },
     openBackupModal,       // managed by boostrap
@@ -469,9 +537,9 @@ const actionHandlers = {
         await handleReload(
             el,
             serviceReloadDNS,
-            "DNS reload successfully",
-            "Error reloading DNS",
-            "Reloading DNS..."
+            t("dns.reload.ok"),
+            t("dns.reload.error"),
+            t("dns.reload.progress")
         );
     },
     // Reload DHCP
@@ -479,18 +547,18 @@ const actionHandlers = {
         await handleReload(
             el,
             serviceReloadDHCP,
-            "DHCP reload successfully",
-            "Error reloading DHCP",
-            "Reloading DHCP..."
+            t("dhcp.reload.ok"),
+            t("dhcp.reload.error"),
+            t("dhcp.reload.progress")
         );
     },
     // Check API status
     apiCheck: async () => {
         const result = await serviceCheckAbout();
         if(result) {
-            showToast('API status updated succesfully', true);
+            showToast(t("health.update.ok"), true);
         } else {
-            showToast('Error updating API status', false);
+            showToast(t("health.update.error"), false);
         }
     },
     // Health
@@ -512,10 +580,13 @@ document.addEventListener("DOMContentLoaded", async () => {
     try {
         await loadModals();
     } catch (err) {
-        console.error(err?.message || "Error loading modals");
-        showToast(err?.message || "Error loading modals", false);
+        console.error(err?.message || t("app.modals.error"));
+        showToast(t("app.modals.error"), false);
     }
 
+    // translate modals
+    translatePage();
+
     // Init Backup Modal (backdrop click to close)
     initBackupModal();
 });
@@ -531,11 +602,12 @@ document.addEventListener('click', async (e) => {
     const handler = actionHandlers[action];
     if (!handler) return;
 
+    // Execute handler
     try {
         await handler(e, el);
     } catch (err) {
-        console.error(err?.message || 'Action error');
-        showToast(err?.message || 'Action error', false);
+        console.error(err?.message || t("app.action.error"));
+        showToast(err?.message || t("app.action.error"), false);
     }
 });
 
@@ -569,4 +641,16 @@ async function periodicTest() {
     setTimeout(periodicTest, 10000);
 }
 
+// Loading translation
+try {
+    await loadLanguage();
+} catch (err) {
+    console.error(err?.message || t("app.translation.error"));
+    showToast(t("app.translation.error"), false);
+}
+
+// Translate page
+translatePage();
+
+// Periodic Test
 periodicTest();
index 3e139e6f27387b0b208922304c0b6d74647cdec7..8d3e5bc1616d217d928adb6af509d0daf95c6ec6 100644 (file)
@@ -1,7 +1,10 @@
-// Import common js
+// -------------------------------------------------------
+// IMPORT
+// -------------------------------------------------------
 import { loadModals, isValidIPv4, isValidIPv6, isValidMAC, showToast, sortTable, initSortableTable, resetSorting, handleSearch, filterTable, clearSearch, handleReload } from './common.js';
-// Import services
 import { serviceReloadDNS, serviceReloadDHCP, serviceGetDHCPLeases, serviceDeleteDHCPLease, serviceGetDHCPLease, serviceCreateHost} from './services.js';
+import { loadLanguage, translatePage, t } from "./i18n.js";
+import { getBackendMessage } from "./backendMessages.js";
 
 // -----------------------------
 // State variables
@@ -29,8 +32,8 @@ async function fetchLeases () {
         viewLeases = [...allLeases];
 
     } catch (err) {
-        console.error(err?.message || "Error loading leases");
-        showToast(err?.message || "Error loading leases", false);
+        console.error(err?.message || t("dhcp.leases.list.error"));
+        showToast(err?.message || t("dhcp.leases.list.error"), false);
         allLeases = [];
         viewLeases = [];
         // hide loader and show table
@@ -42,7 +45,7 @@ async function fetchLeases () {
 }
 
 // -----------------------------
-// Update table with current hosts
+// Update table with current leases
 // -----------------------------
 function updateTable () {
     const loader = document.getElementById("loader");
@@ -63,7 +66,7 @@ function updateTable () {
         const trEmpty = document.createElement("tr");
         const tdEmpty = document.createElement("td");
         tdEmpty.colSpan = 7;
-        tdEmpty.textContent = "No leases available.";
+        tdEmpty.textContent = t("dhcp.leases.empty");
         tdEmpty.style.textAlign = "center";
         trEmpty.appendChild(tdEmpty);
         tbody.appendChild(trEmpty);
@@ -155,41 +158,41 @@ function updateTable () {
             td.style.verticalAlign = "middle";
 
             const val = (l.dhcp_state ?? "").toString();
-            let aria = "";
+            let description = "";
             let iconClass = "";
             switch (val) {
                 case "active":
                     // DHCP active lease
-                    aria = "DHCP lease is active";
+                    description = t("dhcp.leases.active.description");
                     iconClass = "bi bi-check-circle-fill";
                     break;
 
                 case "expired":
                     // DHCP expired lease
-                    aria = "DHCP lease is expired";
+                    description = t("dhcp.leases.expired.description");
                     iconClass = "bi bi-clock-history";
                     break;
 
                 case "released":
                     // DHCP released lease
-                    aria = "DHCP lease is released";
+                    description = t("dhcp.leases.released.description");
                     iconClass = "bi bi-box-arrow-in-right";
                     break;
 
                 case "declined":
                     // DHCP declined lease
-                    aria = "DHCP lease is declined";
+                    description = t("dhcp.leases.declined.description");
                     iconClass = "bi bi-x-octagon-fill";
                     break;
             }
             if (iconClass) {
                 const icon = document.createElement("i");
                 icon.className = iconClass + " icon icon-static";
-                icon.setAttribute("aria-hidden", "true");
-                icon.setAttribute("title", aria);
+                td.setAttribute("title", description);
+                td.setAttribute("aria-label", description);
                 td.appendChild(icon);
             }
-
+            if (val) td.setAttribute("data-value", val);
             tr.appendChild(td);
         }
 
@@ -202,11 +205,12 @@ function updateTable () {
 
             // Add Button
             const addSpan = document.createElement("span");
+            const addText = t("dhcp.leases.add");
             addSpan.className = "action-icon";
             addSpan.setAttribute("role", "button");
             addSpan.tabIndex = 0;
-            addSpan.title = "Add static lease";
-            addSpan.setAttribute("aria-label", "Add static lease");
+            addSpan.title = addText;
+            addSpan.setAttribute("aria-label", addText);
             addSpan.setAttribute("data-bs-toggle", "modal");
             addSpan.setAttribute("data-bs-target", "#addHostModal");
             addSpan.setAttribute("data-action", "add");
@@ -220,11 +224,12 @@ function updateTable () {
 
             // Delete Button
             const delSpan = document.createElement("span");
+            const deleteText = t("dhcp.leases.delete");
             delSpan.className = "action-icon";
             delSpan.setAttribute("role", "button");
             delSpan.tabIndex = 0;
-            delSpan.title = "Delete lease";
-            delSpan.setAttribute("aria-label", "Delete lease");
+            delSpan.title = deleteText;
+            delSpan.setAttribute("aria-label", deleteText);
             delSpan.setAttribute("data-action", "delete");
             delSpan.setAttribute("data-lease-id", String(id));
             {
@@ -292,8 +297,8 @@ async function addHost(id) {
         }
 
     } catch (err) {
-        console.error(err?.message || "Error loading lease");
-        showToast(err?.message || "Error loading lease", false);
+        console.error(err?.message || t("dhcp.leases.loaded.error"));
+        showToast(err?.message || t("dhcp.leases.loaded.error"), false);
     }
 }
 
@@ -303,39 +308,45 @@ async function addHost(id) {
 async function saveHost(hostData) {
     // Validate hostname
     if (!hostData.name.trim()) {
-        showToast("Hostname is required", false);
+        showToast(t("validation.name.required"), false);
         return false;
     }
     // Validate IPv4 format
     if (!isValidIPv4(hostData.ipv4)) {
-        showToast("Invalid IPv4 format", false);
+        showToast(t("validation.ipv4.invalid"), false);
         return false;
     }
     // Validate IPv6 format
     if (!isValidIPv6(hostData.ipv6)) {
-        showToast("Invalid IPv6 format", false);
+        showToast(t("validation.ipv6.invalid"), false);
         return false;
     }
     // Validate MAC format
     if (!isValidMAC(hostData.mac)) {
-        showToast("Invalid MAC format", false);
+        showToast(t("validation.mac.invalid"), false);
         return false;
     }
 
     try {
         const result = await serviceCreateHost(hostData);
 
-        const msg = (typeof result === 'object' && result?.message)
-            ? result.message
-            : 'Host created successfully';
+        const msg = getBackendMessage(
+            result,
+            "hosts.created.ok"
+        );
 
         showToast(msg, true);
 
         return true;
 
     } catch (err) {
-        console.error(err?.message || "Error saving host");
-        showToast(err?.message || "Error saving host", false);
+
+        const msg = getBackendMessage(
+            err,
+            "hosts.created.error"
+        );
+
+        showToast(msg, false);
     }
 
     return false;
@@ -383,7 +394,7 @@ async function handleAddHostSubmit(e) {
 
         const ok = await saveHost(data);
         if (ok !== false) {
-            // close modal and reload hosts
+            // close modal and reload leases
             closeAddHostModal();
             await fetchLeases();
             updateTable();
@@ -391,8 +402,8 @@ async function handleAddHostSubmit(e) {
         }
 
     } catch (err) {
-        console.error(err?.message || "Error saving host");
-        showToast(err?.message || "Error saving host", false);
+        console.error(err?.message || t("hosts.created.error"));
+        showToast(err?.message || t("hosts.created.error"), false);
     }
 
     return false;
@@ -408,17 +419,14 @@ async function handleDeleteLease(e, el) {
     // Get lease ID
     const id = Number(el.dataset.leaseId);
     if (!Number.isFinite(id)) {
-        showToast('Lease id not valid for delete', false);
+        showToast(t("dhcp.leases.delete.invalid_id"), false);
         return;
     }
 
     try {
         const result = await serviceDeleteDHCPLease(id);
 
-        const msg = (typeof result === 'object' && result?.message)
-            ? result.message
-            : 'Lease deleted successfully';
-
+        const msg = getBackendMessage(result, "dhcp.leases.deleted.ok");
         showToast(msg, true);
 
         // Reload leases
@@ -428,8 +436,8 @@ async function handleDeleteLease(e, el) {
         return true;
 
     } catch (err) {
-        console.error(err?.message || "Error deleting lease");
-        showToast(err?.message || "Error deleting lease", false);
+        console.error(err?.message || t("dhcp.leases.deleted.error"));
+        showToast(err?.message || t("dhcp.leases.deleted.error"), false);
     }
 
     return false;
@@ -452,9 +460,9 @@ const actionHandlers = {
         await handleReload(
             el,
             serviceReloadDNS,
-            "DNS reload successfully",
-            "Error reloading DNS",
-            "Reloading DNS..."
+            t("dns.reload.ok"),
+            t("dns.reload.error"),
+            t("dns.reload.progress")
         );
     },
     // Reload DHCP
@@ -462,9 +470,9 @@ const actionHandlers = {
         await handleReload(
             el,
             serviceReloadDHCP,
-            "DHCP reload successfully",
-            "Error reloading DHCP",
-            "Reloading DHCP..."
+            t("dhcp.reload.ok"),
+            t("dhcp.reload.error"),
+            t("dhcp.reload.progress")
         );
     },
 };
@@ -481,21 +489,32 @@ document.addEventListener("DOMContentLoaded", async () => {
 // -----------------------------
 async function initApp() {
 
+    // Loading translation
+    try {
+        await loadLanguage();
+    } catch (err) {
+        console.error(err?.message || t("app.translation.error"));
+        showToast(t("app.translation.error"), false);
+    }
+
     // Load modals (Bootstrap 5 requires JS initialization for dynamic content)
     try {
         await loadModals();
     } catch (err) {
-        console.error(err?.message || "Error loading modals");
-        showToast(err?.message || "Error loading modals", false);
+        console.error(err?.message || t("app.modals.error"));
+        showToast(t("app.modals.error"), false);
     }
 
+    // Translate page
+    translatePage();
+
     // Load data (leases)
     try {
         await fetchLeases();
         updateTable();
     } catch (err) {
-        console.error(err?.message || "Error loading dhcp leases");
-        showToast(err?.message || "Error loading dhcp leases", false);
+        console.error(err?.message || t("dhcp.leases.list.error"));
+        showToast(err?.message || t("dhcp.leases.list.error"), false);
     }
 
     initUI();
@@ -556,7 +575,7 @@ function initModalLifecycle() {
             try {
                 await addHost(id);
             } catch (err) {
-                showToast(err?.message || "Error loading host", false);
+                showToast(err?.message || t("dhcp.leases.loaded.error"), false);
                 // Close modal
                 modalEl.addEventListener('shown.bs.modal', () => {
                     closeAddHostModal();
@@ -607,8 +626,8 @@ async function handleActionClick(e) {
     try {
         await handler(e, el);
     } catch (err) {
-        console.error(err?.message || 'Action error');
-        showToast(err?.message || 'Action error', false);
+        console.error(err?.message || t("app.action.error"));
+        showToast(err?.message || t("app.action.error"), false);
     }
 }
 
index 31d0422a35a3ec4c657808712cc28b32c631fd9c..f3c043ddc6627f86b7bae61f74e519369682852c 100644 (file)
@@ -1,3 +1,8 @@
+// -------------------------------------------------------
+// IMPORT
+// -------------------------------------------------------
+import { loadLanguage, translatePage, t } from "./i18n.js";
+
 // -----------------------------
 // DOMContentLoaded: initialize everything
 // -----------------------------
index 92325a7115c97f6e4fbddcdce2f83814473fc7a3..0cad8e1eabcb89c4c8a468c653d18901dadf6458 100644 (file)
@@ -1,7 +1,10 @@
-// Import common js
+// -------------------------------------------------------
+// IMPORT
+// -------------------------------------------------------
 import { loadModals, showToast, showConfirmModal, handleReload } from './common.js';
-// Import services
 import { serviceReloadDNS, serviceReloadDHCP, serviceRestartApp, serviceIsAlive, serviceGetLogs } from './services.js';
+import { loadLanguage, translatePage, t } from "./i18n.js";
+import { getBackendMessage } from "./backendMessages.js";
 
 // -----------------------------
 // State variables
@@ -107,7 +110,8 @@ function clearFilters() {
     updateFilterButton();
 
     requestAnimationFrame(() => {
-        logViewer.scrollTop = savedScrollPosition;
+        if (savedScrollPosition !== null)
+            logViewer.scrollTop = savedScrollPosition;
     });
 }
 
@@ -376,7 +380,7 @@ function startReconnectPolling(button, originalHtmlButton) {
         attempts++;
         if (attempts > maxAttempts) {
             clearInterval(interval);
-            showToast("Server did not come back online", false);
+            showToast(t("app.reconnect.timeout"), false);
             button.innerHTML = originalHtmlButton;
             button.disabled = false;
             return;
@@ -390,13 +394,13 @@ function startReconnectPolling(button, originalHtmlButton) {
 
                 clearInterval(interval);
 
-                showToast("Application is back online", true);
+                showToast(t("app.reconnect.success"), true);
 
                 setTimeout(() => location.reload(), 500);
 
             }
         } catch (err) {
-            console.log("Waiting for server...");
+            console.log(t("app.reconnect.waiting"));
         }
 
     }, 2000); // check every 2 seconds
@@ -406,7 +410,7 @@ function startReconnectPolling(button, originalHtmlButton) {
 // Restart application
 // -----------------------------
 async function handleRestartApp(button) {
-    const confirmed = await showConfirmModal("Restart the application?");
+    const confirmed = await showConfirmModal(t("app.restart.confirm"));
     if (!confirmed) return;
 
     const originalHtmlButton = button.innerHTML;
@@ -414,9 +418,9 @@ async function handleRestartApp(button) {
     const ok = await handleReload(
         button,
         serviceRestartApp,
-        "Application is restarting...",
-        "Error restarting application",
-        "Restarting...",
+        t("app.restart.progress"),
+        t("app.restart.error"),
+        t("app.restart.progress"),
         true
     );
 
@@ -443,24 +447,22 @@ const actionHandlers = {
     //},
     // Reload DNS
     reloadDns: async (e, el) => {
-        showToast("DNS is reloading...", true);
         await handleReload(
             el,
             serviceReloadDNS,
-            "DNS reload successfully",
-            "Error reloading DNS",
-            "Reloading DNS..."
+            t("dns.reload.ok"),
+            t("dns.reload.error"),
+            t("dns.reload.progress")
         );
     },
     // Reload DHCP
     reloadDhcp: async (e, el) => {
-        showToast("DHCP is reloading...", true);
         await handleReload(
             el,
             serviceReloadDHCP,
-            "DHCP reload successfully",
-            "Error reloading DHCP",
-            "Reloading DHCP..."
+            t("dhcp.reload.ok"),
+            t("dhcp.reload.error"),
+            t("dhcp.reload.progress")
         );
     },
     // Reload App
@@ -473,8 +475,8 @@ const actionHandlers = {
 // DOMContentLoaded: bootstrap app
 // -----------------------------
 document.addEventListener("DOMContentLoaded", async () => {
-    initApp();
-    loadLogs();
+    await initApp();
+    await loadLogs();
 });
 
 // -----------------------------
@@ -482,14 +484,25 @@ document.addEventListener("DOMContentLoaded", async () => {
 // -----------------------------
 async function initApp() {
 
+    // Loading translation
+    try {
+        await loadLanguage();
+    } catch (err) {
+        console.error(err?.message || t("app.translation.error"));
+        showToast(t("app.translation.error"), false);
+    }
+
     // Load modals (Bootstrap 5 requires JS initialization for dynamic content)
     try {
         await loadModals();
     } catch (err) {
-        console.error(err?.message || "Error loading modals");
-        showToast(err?.message || "Error loading modals", false);
+        console.error(err?.message || t("app.modals.error"));
+        showToast(t("app.modals.error"), false);
     }
 
+    // Translate page
+    translatePage();
+
     initEvents();
     initDropdown();
     initFilters();
@@ -530,8 +543,8 @@ async function handleActionClick(e) {
     try {
         await handler(e, el);
     } catch (err) {
-        console.error(err?.message || 'Action error');
-        showToast(err?.message || 'Action error', false);
+        console.error(err?.message || t("app.action.error"));
+        showToast(err?.message || t("app.action.error"), false);
     }
 }
 
index f8fefabfeeed47294c99b6aa189e46b571e1f0ea..5ae247b15873915218bcd0b7f2848070376dc263 100644 (file)
@@ -24,26 +24,22 @@ export async function serviceCheckHealth() {
 // Reload DNS
 // -----------------------------
 export async function serviceReloadDNS() {
-    const data = await apiPost(
+    return await apiPost(
         "/api/dns/reload",
         null,
         "Error reloading DNS"
     );
-
-    return data?.message ? { message: data.message } : true;
 }
 
 // -----------------------------
 // Reload DHCP action
 // -----------------------------
 export async function serviceReloadDHCP() {
-    const data = await apiPost(
+    return await apiPost(
         "/api/dhcp/reload",
         null,
         "Error reloading DHCP"
     );
-
-    return data?.message ? { message: data.message } : true;
 }
 
 // -----------------------------
@@ -68,13 +64,11 @@ export async function serviceGetDHCPLease(id) {
 // Delete DHCP Lease
 // -----------------------------
 export async function serviceDeleteDHCPLease(id) {
-    const data = await apiRequest(
+    return await apiRequest(
         `/api/dhcp/leases/${id}`,
         { method: "DELETE" },
         "Error deleting host"
     );
-
-    return data?.message ? { message: data.message } : true;
 }
 
 // -----------------------------
@@ -99,7 +93,7 @@ export async function serviceGetHost(id) {
 // Create a new host
 // -----------------------------
 export async function serviceCreateHost(hostData) {
-    const data = await apiRequest(
+    return await apiRequest(
         "/api/hosts",
         {
             method: "POST",
@@ -108,15 +102,13 @@ export async function serviceCreateHost(hostData) {
         },
         "Error creating host"
     );
-
-    return data?.message ? { message: data.message } : true;
 }
 
 // -----------------------------
 // Update an host
 // -----------------------------
 export async function serviceUpdateHost(id, hostData) {
-    const data = await apiRequest(
+    return await apiRequest(
         `/api/hosts/${id}`,
         {
             method: "PUT",
@@ -125,21 +117,17 @@ export async function serviceUpdateHost(id, hostData) {
         },
         "Error updating host"
     );
-
-    return data?.message ? { message: data.message } : true;
 }
 
 // -----------------------------
 // Delete Hosts
 // -----------------------------
 export async function serviceDeleteHost(id) {
-    const data = await apiRequest(
+    return await apiRequest(
         `/api/hosts/${id}`,
         { method: "DELETE" },
         "Error deleting host"
     );
-
-    return data?.message ? { message: data.message } : true;
 }
 
 // -----------------------------
@@ -164,7 +152,7 @@ export async function serviceGetAlias(id) {
 // Create a new alias
 // -----------------------------
 export async function serviceCreateAlias(aliasData) {
-    const data = await apiRequest(
+    return await apiRequest(
         "/api/aliases",
         {
             method: "POST",
@@ -173,15 +161,13 @@ export async function serviceCreateAlias(aliasData) {
         },
         "Error creating alias"
     );
-
-    return data?.message ? { message: data.message } : true;
 }
 
 // -----------------------------
 // Update an alias
 // -----------------------------
 export async function serviceUpdateAlias(id, aliasData) {
-    const data = await apiRequest(
+    return await apiRequest(
         `/api/aliases/${id}`,
         {
             method: "PUT",
@@ -190,21 +176,17 @@ export async function serviceUpdateAlias(id, aliasData) {
         },
         "Error updating alias"
     );
-
-    return data?.message ? { message: data.message } : true;
 }
 
 // -----------------------------
 // Delete Alias
 // -----------------------------
 export async function serviceDeleteAlias(id) {
-    const data = await apiRequest(
+    return await apiRequest(
         `/api/aliases/${id}`,
         { method: "DELETE" },
         "Error deleting alias"
     );
-
-    return data?.message ? { message: data.message } : true;
 }
 
 // -----------------------------
@@ -218,23 +200,12 @@ export async function serviceGetDevices() {
 // Create a Backup
 // -------------------------------------------------------
 export async function serviceBackupCreate() {
-    const data = await apiPost(
+    return await apiPost(
         "/api/backup/create",
         null,
         "Error performing backup"
     );
 
-    if (data.status === 'success') {
-        return data?.message ? { message: data.message } : true;
-    }
-
-    if (data.status === 'partial') {
-        return data?.message
-            ? { message: data.message, partial: true }
-            : { partial: true };
-    }
-
-    return false;
 }
 
 // -------------------------------------------------------
@@ -248,36 +219,22 @@ export async function serviceBackupList() {
 // Restore a Backup
 // -------------------------------------------------------
 export async function serviceBackupRestore(id) {
-    const data = await apiPost(
+    return await apiPost(
         "/api/backup/restore",
         { backup_id: id },
         "Error performing restore"
     );
-
-    if (data.status === 'success') {
-        return data?.message ? { message: data.message } : true;
-    }
-
-    if (data.status === 'partial') {
-        return data?.message
-            ? { message: data.message, partial: true }
-            : { partial: true };
-    }
-
-    return false;
 }
 
 // -------------------------------------------------------
 // Delete a Backup
 // -------------------------------------------------------
 export async function serviceDeleteBackup(id) {
-    const data = await apiPost(
+    return await apiPost(
         "/api/backup/delete",
         { backup_id: id },
         "Error performing delete"
     );
-
-    return data?.message ? { message: data.message } : true;
 }
 
 // -------------------------------------------------------
@@ -344,7 +301,7 @@ export async function serviceGetConfig(key) {
 // Update a configuration parameter
 // -----------------------------
 export async function serviceUpdateConfig(key, configData) {
-    const data = await apiRequest(
+    return await apiRequest(
         `/api/settings/${key}`,
         {
             method: "PUT",
@@ -353,34 +310,28 @@ export async function serviceUpdateConfig(key, configData) {
         },
         "Error updating configuration parameter"
     );
-
-    return data?.message ? { message: data.message } : true;
 }
 
 // -----------------------------
 // Reset a configuration parameter to its default value
 // -----------------------------
 export async function serviceResetConfig(key) {
-    const data = await apiPost(
+    return await apiPost(
         `/api/settings/${key}/reset`,
         null,
         "Error restoring default value"
     );
-
-    return data?.message ? { message: data.message } : true;
 }
 
 // -----------------------------
 // Reset a configuration parameter to its default value
 // -----------------------------
 export async function serviceRestartApp(key) {
-    const data = await apiPost(
+    return await apiPost(
         "/api/restart",
         null,
         "Error restarting application"
     );
-
-    return data?.message ? { message: data.message } : true;
 }
 
 // -----------------------------
index 3f4a17de8149694d4d3780b1c1aea1a1755cbdfc..0fa8c44654c4b38d7466ae18735fff41873c92fb 100644 (file)
@@ -1,7 +1,10 @@
-// Import common js
+// -------------------------------------------------------
+// IMPORT
+// -------------------------------------------------------
 import { loadModals, showToast, handleSearch, clearSearch, showConfirmModal, handleReload } from './common.js';
-// Import services
 import { serviceGetConfigs, serviceGetConfig, serviceUpdateConfig, serviceResetConfig, serviceReloadDNS, serviceReloadDHCP, serviceRestartApp, serviceIsAlive } from './services.js';
+import { loadLanguage, translatePage, t } from "./i18n.js";
+import { getBackendMessage } from "./backendMessages.js";
 
 // -----------------------------
 // State variables
@@ -71,8 +74,8 @@ async function fetchConfigs () {
         viewConfigs = [...allConfigs];
 
     } catch (err) {
-        console.error(err?.message || "Error loading configs");
-        showToast(err?.message || "Error loading configs", false);
+        console.error(err?.message || t("settings.list.error"));
+        showToast(err?.message || t("settings.list.error"), false);
         allConfigs = [];
         viewConfigs = [];
         // hide loader and show table
@@ -164,7 +167,7 @@ function startReconnectPolling(button, originalHtmlButton) {
         attempts++;
         if (attempts > maxAttempts) {
             clearInterval(interval);
-            showToast("Server did not come back online", false);
+            showToast(t("app.reconnect.timeout"), false);
             button.innerHTML = originalHtmlButton;
             button.disabled = false;
             return;
@@ -178,13 +181,13 @@ function startReconnectPolling(button, originalHtmlButton) {
 
                 clearInterval(interval);
 
-                showToast("Application is back online", true);
+                showToast(t("app.reconnect.success"), true);
 
                 setTimeout(() => location.reload(), 500);
 
             }
         } catch (err) {
-            console.log("Waiting for server...");
+            console.log(t("app.reconnect.waiting"));
         }
 
     }, 2000); // check every 2 seconds
@@ -194,7 +197,7 @@ function startReconnectPolling(button, originalHtmlButton) {
 // Restart application
 // -----------------------------
 async function handleRestartApp(button) {
-    const confirmed = await showConfirmModal("Restart the application?");
+    const confirmed = await showConfirmModal(t("app.restart.confirm"));
     if (!confirmed) return;
 
     const originalHtmlButton = button.innerHTML;
@@ -202,9 +205,9 @@ async function handleRestartApp(button) {
     const ok = await handleReload(
         button,
         serviceRestartApp,
-        "Application is restarting...",
-        "Error restarting application",
-        "Restarting...",
+        t("app.restart.progress"),
+        t("app.restart.error"),
+        t("app.restart.progress"),
         true
     );
 
@@ -226,11 +229,11 @@ function updateTable (filter = null) {
     expandBtn?.toggleAttribute("disabled", hasSearch);
     collapseBtn?.toggleAttribute("disabled", hasSearch);
     if (hasSearch) {
-        expandBtn?.setAttribute("title", "Disabled during search");
-        collapseBtn?.setAttribute("title", "Disabled during search");
+        expandBtn?.setAttribute("title", t("settings.expand.disabled"));
+        collapseBtn?.setAttribute("title", t("settings.expand.disabled"));
     } else {
-        expandBtn?.setAttribute("title", "Expand all groups");
-        collapseBtn?.setAttribute("title", "Collapse all groups");
+        expandBtn?.setAttribute("title", t("settings.expand.all"));
+        collapseBtn?.setAttribute("title", t("settings.collapse.all"));
     }
 
     // DOM Reference
@@ -248,7 +251,7 @@ function updateTable (filter = null) {
         const trEmpty = document.createElement("tr");
         const tdEmpty = document.createElement("td");
         tdEmpty.colSpan = 4;
-        tdEmpty.textContent = "No configs available.";
+        tdEmpty.textContent = t("settings.empty");
         tdEmpty.style.textAlign = "center";
         trEmpty.appendChild(tdEmpty);
         tbody.appendChild(trEmpty);
@@ -358,8 +361,9 @@ function updateTable (filter = null) {
                 editSpan.className = "action-icon";
                 editSpan.setAttribute("role", "button");
                 editSpan.tabIndex = 0;
-                editSpan.title = "Edit config";
-                editSpan.setAttribute("aria-label", "Edit config");
+                const editText = t("settings.edit");
+                editSpan.title = editText;
+                editSpan.setAttribute("aria-label", editText);
                 editSpan.setAttribute("data-bs-toggle", "modal");
                 editSpan.setAttribute("data-bs-target", "#editConfigModal");
                 editSpan.setAttribute("data-action", "edit");
@@ -373,11 +377,12 @@ function updateTable (filter = null) {
 
                 // Reset Button
                 const resetSpan = document.createElement("span");
+                const resetText = t("settings.reset");
                 resetSpan.className = "action-icon";
                 resetSpan.setAttribute("role", "button");
                 resetSpan.tabIndex = 0;
-                resetSpan.title = "Reset to default value";
-                resetSpan.setAttribute("aria-label", "Reset to default value");
+                resetSpan.title = resetText;
+                resetSpan.setAttribute("aria-label", resetText);
                 resetSpan.setAttribute("data-action", "reset");
                 resetSpan.setAttribute("data-config-key", String(c.key));
                 {
@@ -534,8 +539,8 @@ async function editConfig(key) {
         }
 
     } catch (err) {
-        console.error(err?.message || "Error loading config");
-        showToast(err?.message || "Error loading config", false);
+        console.error(err?.message || t("settings.loaded.error"));
+        showToast(err?.message || t("settings.loaded.error"), false);
     }
 }
 
@@ -547,7 +552,7 @@ async function saveConfig(configData) {
     switch (typeof configData.value) {
         case "string":
             if (!configData.value.trim()) {
-                showToast("Configuration value is required", false);
+                showToast(t("validation.config.required"), false);
                 return false;
             }
             break;
@@ -557,13 +562,13 @@ async function saveConfig(configData) {
 
         case "number":
             if (isNaN(configData.value)) {
-                showToast("Invalid numeric value", false);
+                showToast(t("validation.config.invalid_number"), false);
                 return false;
             }
             break;
 
         default:
-            showToast("Invalid configuration value", false);
+            showToast(t("validation.config.invalid"), false);
             return false;
     }
 
@@ -572,17 +577,23 @@ async function saveConfig(configData) {
 
         // Update
         result = await serviceUpdateConfig(editingConfigKey, configData);
-        const msg = (typeof result === 'object' && result?.message)
-            ? result.message
-            : 'Config updated successfully';
+        const msg = getBackendMessage(
+            result,
+            "settings.updated.ok"
+        );
 
         showToast(msg, true);
 
         return true;
 
     } catch (err) {
-        console.error(err?.message || "Error updating config");
-        showToast(err?.message || "Error updating config", false);
+
+        const msg = getBackendMessage(
+            err,
+            "settings.updated.error"
+        );
+
+        showToast(msg, false);
     }
 
     return false;
@@ -629,12 +640,20 @@ async function handleEditConfigSubmit(e) {
             closeEditConfigModal();
             await fetchConfigs();
             updateTable();
-            return true
+            return true;
         }
 
     } catch (err) {
-        console.error(err?.message || "Error saving config");
-        showToast(err?.message || "Error saving config", false);
+        console.error(
+            err?.message ||
+            t("settings.updated.error")
+        );
+
+        showToast(
+            err?.message ||
+            t("settings.updated.error"),
+            false
+        );
     }
 
     return false;
@@ -650,21 +669,18 @@ async function handleResetConfig(e, el) {
     // Get config ID
     const key = el.dataset.configKey;
     if (typeof key !== "string" || key.length === 0) {
-        showToast('Configuration key not valid for reset', false);
+        showToast(t("settings.restored.invalid_id"), false);
         return;
     }
 
     // Confirm requested
-    const confirmed = await showConfirmModal("Reset this configuration?");
+    const confirmed = await showConfirmModal(t("settings.reset.confirm"));
     if (!confirmed) return;
 
     try {
         const result = await serviceResetConfig(key);
 
-        const msg = (typeof result === 'object' && result?.message)
-            ? result.message
-            : 'Config reset to default successfully';
-
+        const msg = getBackendMessage(result, "settings.restored.ok");
         showToast(msg, true);
 
         // Reload configs
@@ -674,8 +690,8 @@ async function handleResetConfig(e, el) {
         return true;
 
     } catch (err) {
-        console.error(err?.message || "Error resetting config to default");
-        showToast(err?.message || "Error resetting config to default", false);
+        console.error(err?.message || t("settings.restored.error"));
+        showToast(err?.message || t("settings.restored.error"), false);
     }
 
     return false;
@@ -694,24 +710,22 @@ const actionHandlers = {
         // handled by bootstrap modal show event
     },
     reloadDns: async (e, el) => {
-        showToast("DNS is reloading...", true);
         await handleReload(
             el,
             serviceReloadDNS,
-            "DNS reload successfully",
-            "Error reloading DNS",
-            "Reloading DNS..."
+            t("dns.reload.ok"),
+            t("dns.reload.error"),
+            t("dns.reload.progress")
         );
     },
     // Reload DHCP
     reloadDhcp: async (e, el) => {
-        showToast("DHCP is reloading...", true);
         await handleReload(
             el,
             serviceReloadDHCP,
-            "DHCP reload successfully",
-            "Error reloading DHCP",
-            "Reloading DHCP..."
+            t("dhcp.reload.ok"),
+            t("dhcp.reload.error"),
+            t("dhcp.reload.progress")
         );
     },
     // Reload App
@@ -732,21 +746,32 @@ document.addEventListener("DOMContentLoaded", async () => {
 // -----------------------------
 async function initApp() {
 
+    // Loading translation
+    try {
+        await loadLanguage();
+    } catch (err) {
+        console.error(err?.message || t("app.translation.error"));
+        showToast(t("app.translation.error"), false);
+    }
+
     // Load modals (Bootstrap 5 requires JS initialization for dynamic content)
     try {
         await loadModals();
     } catch (err) {
-        console.error(err?.message || "Error loading modals");
-        showToast(err?.message || "Error loading modals", false);
+        console.error(err?.message || t("app.modals.error"));
+        showToast(t("app.modals.error"), false);
     }
 
+    // Translate page
+    translatePage();
+
     // Load data (configs)
     try {
         await fetchConfigs();
         updateTable();
     } catch (err) {
-        console.error(err?.message || "Error loading configs");
-        showToast(err?.message || "Error loading configs", false);
+        console.error(err?.message || t("settings.list.error"));
+        showToast(err?.message || t("settings.list.error"), false);
     }
 
     initUI();
@@ -806,14 +831,13 @@ function initModalLifecycle() {
             try {
                 await editConfig(key);
             } catch (err) {
-                showToast(err?.message || "Error loading config", false);
+                showToast(err?.message || t("settings.loaded.error"), false);
                 // Close modal
                 modalEl.addEventListener('shown.bs.modal', () => {
                     closeEditConfigModal();
                 }, { once: true });
             }
         } else {
-            console.warn("Invalid Configuration Key for edit");
             closeEditConfigModal();
             return;
         }
@@ -850,10 +874,10 @@ function initEvents() {
         const groupKey = groupRow.dataset.group;
         toggleGroup(groupKey);
     });
-    document.getElementById("expandAllBtn")
-        ?.addEventListener("click", expandAllGroups);
-    document.getElementById("collapseAllBtn")
-        ?.addEventListener("click", collapseAllGroups);
+    document.querySelectorAll(".expand-all-btn")
+        .forEach(btn => btn.addEventListener("click", expandAllGroups));
+    document.querySelectorAll(".collapse-all-btn")
+        .forEach(btn => btn.addEventListener("click", collapseAllGroups));
 }
 
 // -----------------------------
@@ -871,8 +895,8 @@ async function handleActionClick(e) {
     try {
         await handler(e, el);
     } catch (err) {
-        console.error(err?.message || 'Action error');
-        showToast(err?.message || 'Action error', false);
+        console.error(err?.message || t("app.action.error"));
+        showToast(err?.message || t("app.action.error"), false);
     }
 }
 
index 065cd3f01bcd5ee2d09aea5833664de892f9fc39..db2b2afb768fa9ef7636ca96c2f249be34d8f1ba 100644 (file)
@@ -20,7 +20,7 @@
     <link rel="stylesheet" href="css/layout.css">
 </head>
 
-<body>
+<body data-page-title="dashboard.title">
     <!-- Topbar -->
     <header class="topbar">
         <div class="topbar-inner">
@@ -33,7 +33,7 @@
                     <line x1="12" y1="6" x2="12" y2="18" stroke="var(--accent)" stroke-width="2"></line>
                     <line x1="6" y1="12" x2="18" y2="12" stroke="var(--accent)" stroke-width="2"></line>
                 </svg>
-                <span>Network Manager</span>
+                <span data-i18n="app.name">Network Manager</span>
             </a>
 
             <nav class="navbar">
 
                 <!-- Desktop nav -->
                 <div class="d-none d-md-flex gap-2 ms-auto">
-                        <a href="/hosts"    class="btn btn-primary">       Hosts</a>
-                        <a href="/aliases"  class="btn btn-primary">       Alias</a>
-                        <a href="/leases"   class="btn btn-primary active">DHCP Leases</a>
-                        <a href="/devices"  class="btn btn-primary">       Devices</a>
-                        <a href="/logs"     class="btn btn-primary">       Logs</a>
-                        <a href="/settings" class="btn btn-primary">       Settings</a>
-                        <button class="btn btn-primary btn-logout">Logout</button>
+                        <a href="/hosts"    class="btn btn-primary"        data-i18n="hosts.title">Hosts</a>
+                        <a href="/aliases"  class="btn btn-primary"        data-i18n="aliases.title">Alias</a>
+                        <a href="/leases"   class="btn btn-primary active" data-i18n="dhcp.title">DHCP Leases</a>
+                        <a href="/devices"  class="btn btn-primary"        data-i18n="devices.title">Devices</a>
+                        <a href="/logs"     class="btn btn-primary"        data-i18n="logs.title">Logs</a>
+                        <a href="/settings" class="btn btn-primary"        data-i18n="settings.title">Settings</a>
+                        <button class="btn btn-primary btn-logout"         data-i18n="auth.logout">Logout</button>
                 </div>
             </nav>
 
             <div class="offcanvas offcanvas-end d-md-none" tabindex="-1" id="mobileMenu">
 
                 <div class="offcanvas-header">
-                    <h5 class="offcanvas-title">Network Manager</h5>
+                    <h5 class="offcanvas-title" data-i18n="app.name">Network Manager</h5>
 
                     <button
                         type="button"
                         class="btn-close"
+                        data-i18n-title="common.close"
+                        data-i18n-aria-label="common.close"
                         data-bs-dismiss="offcanvas">
                     </button>
                 </div>
 
                 <div class="offcanvas-body">
                     <div class="d-grid gap-2">
-                        <a href="/hosts"    class="btn btn-primary">       Hosts</a>
-                        <a href="/aliases"  class="btn btn-primary">       Alias</a>
-                        <a href="/leases"   class="btn btn-primary active">DHCP Leases</a>
-                        <a href="/devices"  class="btn btn-primary">       Devices</a>
-                        <a href="/logs"     class="btn btn-primary">       Logs</a>
-                        <a href="/settings" class="btn btn-primary">       Settings</a>
-                        <button class="btn btn-primary btn-logout">Logout</button>
+                        <a href="/hosts"    class="btn btn-primary"        data-i18n="hosts.title">Hosts</a>
+                        <a href="/aliases"  class="btn btn-primary"        data-i18n="aliases.title">Alias</a>
+                        <a href="/leases"   class="btn btn-primary active" data-i18n="dhcp.title">DHCP Leases</a>
+                        <a href="/devices"  class="btn btn-primary"        data-i18n="devices.title">Devices</a>
+                        <a href="/logs"     class="btn btn-primary"        data-i18n="logs.title">Logs</a>
+                        <a href="/settings" class="btn btn-primary"        data-i18n="settings.title">Settings</a>
+                        <button class="btn btn-primary btn-logout"         data-i18n="auth.logout">Logout</button>
                     </div>
                 </div>
             </div>
                 <div class="col-auto col-md-auto">
                     <h2 class="mb-0 d-flex align-items-center gap-2 lh-1">
                         <span class="title-icon"><i class="bi bi-broadcast"></i></span>
-                        <span class="section-title">DHCP Leases</span>
+                        <span class="section-title" data-i18n="dhcp.page.title">DHCP Leases</span>
                     </h2>
                 </div>
 
                         <input
                             type="text"
                             id="searchInput"
-                            placeholder="Search..."
                             class="form-control form-control-sm placeholder-italic"
-                            aria-label="Search lease">
+                            data-i18n-placeholder="common.search.placeholder"
+                            data-i18n-aria-label="dhcp.leases.search">
                     </div>
                 </div>
 
                     <!-- Add host Desktop -->
                     <button
                         class="btn btn-primary d-none d-md-flex align-items-center gap-2 px-3"
-                        title="Add Host"
-                        aria-label="Add Host"
+                        data-i18n-title="hosts.add"
+                        data-i18n-aria-label="hosts.add"
                         data-bs-toggle="modal" data-bs-target="#addHostModal">
                         <i class="bi bi-plus-lg"></i>
-                        <span class="label">Add Host</span>
+                        <span class="label" data-i18n="hosts.add">Add Host</span>
                     </button>
 
                     <!-- Separator -->
                     <!-- Reload DNS Desktop -->
                     <button
                         class="btn btn-primary d-none d-md-flex align-items-center gap-2 px-3"
-                        title="Reload DNS (BIND)"
-                        aria-label="Reload DNS"
+                        data-i18n-title="dns.reload.description"
+                        data-i18n-aria-label="dns.reload.description"
                         data-action="reloadDns">
                         <i class="bi bi-arrow-repeat"></i>
-                        <span class="label">Reload DNS</span>
+                        <span class="label" data-i18n="dns.reload">Reload DNS</span>
                     </button>
 
                     <!-- Reload DHCP Desktop -->
                     <button
                         class="btn btn-primary d-none d-md-flex align-items-center gap-2 px-3"
-                        title="Reload DHCP (Kea)"
-                        aria-label="Reload DHCP"
+                        data-i18n-title="dhcp.reload.description"
+                        data-i18n-aria-label="dhcp.reload.description"
                         data-action="reloadDhcp">
                         <i class="bi bi-arrow-repeat"></i>
-                        <span class="label">Reload DHCP</span>
+                        <span class="label" data-i18n="dhcp.reload">Reload DHCP</span>
                     </button>
 
                     <!-- Mobile Dropdown -->
                             <li class="px-2 py-1">
                                 <button
                                     class="btn btn-primary w-100 d-flex align-items-center gap-2"
-                                    title="Add Host"
-                                    aria-label="Add Host"
+                                    data-i18n-title="hosts.add"
+                                    data-i18n-aria-label="hosts.add"
                                     data-bs-toggle="modal" data-bs-target="#addHostModal">
                                     <i class="bi bi-plus-lg"></i>
-                                    <span>Add Host</span>
+                                    <span data-i18n="hosts.add">Add Host</span>
                                 </button>
                             </li>
 
                             <li class="px-2 py-1">
                                 <button
                                     class="btn btn-primary w-100 d-flex align-items-center gap-2"
-                                    title="Reload DNS (BIND)"
-                                    aria-label="Reload DNS"
+                                    data-i18n-title="dns.reload.description"
+                                    data-i18n-aria-label="dns.reload.description"
                                     data-action="reloadDns">
                                     <i class="bi bi-arrow-repeat"></i>
-                                    <span>Reload DNS</span>
+                                    <span data-i18n="dns.reload">Reload DNS</span>
                                 </button>
                             </li>
 
                             <li class="px-2 py-1">
                                 <button
                                     class="btn btn-primary w-100 d-flex align-items-center gap-2"
-                                    title="Reload DHCP (Kea)"
-                                    aria-label="Reload DHCP"
+                                    data-i18n-title="dhcp.reload.description"
+                                    data-i18n-aria-label="dhcp.reload.description"
                                     data-action="reloadDhcp">
                                     <i class="bi bi-arrow-repeat"></i>
-                                    <span>Reload DHCP</span>
+                                    <span data-i18n="dhcp.reload">Reload DHCP</span>
                                 </button>
                             </li>
                         </ul>
                 <input
                     type="text"
                     id="searchInputMobile"
-                    placeholder="Search..."
                     class="form-control form-control-sm placeholder-italic"
-                    aria-label="Search lease">
+                    data-i18n-placeholder="common.search.placeholder"
+                    data-i18n-aria-label="dhcp.lease.search">
             </div>
         </div>
     </section>
             <thead class="table-light">
                 <tr>
                     <th data-type="ipv4" data-sortable="true" data-sort="0">
-                        IP Address <span class="sort-arrow" aria-hidden="true"></span>
+                        <span data-i18n="hosts.ipv4">IP Address</span>
+                        <span class="sort-arrow" aria-hidden="true"></span>
                     </th>
 
                     <th data-type="mac" data-sortable="true" data-sort="1">
-                        MAC <span class="sort-arrow" aria-hidden="true"></span>
+                        <span data-i18n="hosts.mac">MAC Address</span>
+                        <span class="sort-arrow" aria-hidden="true"></span>
                     </th>
 
                     <th data-type="string" data-sortable="true" data-sort="2">
-                        Hostname <span class="sort-arrow" aria-hidden="true"></span>
+                        <span data-i18n="hosts.hostname">Hostname</span>
+                        <span class="sort-arrow" aria-hidden="true"></span>
                     </th>
 
                     <th data-type="string" data-sortable="true" data-sort="3">
-                        Start <span class="sort-arrow" aria-hidden="true"></span>
+                        <span data-i18n="dhcp.lease.start">Start</span>
+                        <span class="sort-arrow" aria-hidden="true"></span>
                     </th>
 
                     <th data-type="string" data-sortable="true" data-sort="4">
-                        End <span class="sort-arrow" aria-hidden="true"></span>
+                        <span data-i18n="dhcp.lease.end">End</span>
+                        <span class="sort-arrow" aria-hidden="true"></span>
                     </th>
 
-                    <th data-type="string" data-sortable="true" data-sort="5">
-                        State <span class="sort-arrow" aria-hidden="true"></span>
+                    <th data-type="string" data-sortable="true" data-sort="5" class="text-center text-nowrap">
+                        <span data-i18n="common.state">State</span>
+                        <span class="sort-arrow" aria-hidden="true"></span>
                     </th>
 
                     <th data-type="string" data-sortable="false" class="text-center text-nowrap">
-                        Actions
+                        <span data-i18n="common.actions">Actions</span>
                     </th>
                 </tr>
             </thead>
     <!-- Loader -->
     <div id="loader" class="text-center my-3" style="display: none;">
         <div class="spinner-border text-primary" role="status">
-            <span class="visually-hidden">Loading...</span>
+            <span class="visually-hidden" data-i18n="common.loading">Loading...</span>
         </div>
     </div>
-    <div id="devices-container"></div>
 
     <!-- Modals -->
     <div id="modals-container"></div>
diff --git a/frontend/locales/en.json b/frontend/locales/en.json
new file mode 100644 (file)
index 0000000..87f3105
--- /dev/null
@@ -0,0 +1,307 @@
+{
+  "language": "en",
+  "translations": {
+
+    "app.name": "Network Manager",
+    "app.translation.error": "Error loading translation",
+    "app.modals.error": "Error loading modals",
+    "app.action.error": "Action error",
+    "app.restart": "Restart",
+    "app.restart.description": "Restart application",
+    "app.restart.confirm": "Restart the application?",
+    "app.restart.progress": "Application restart in progress...",
+    "app.restart.error": "Error restarting application",
+    "app.reconnect.waiting": "Waiting for the server to come back online...",
+    "app.reconnect.success": "Application is back online",
+    "app.reconnect.timeout": "Server did not come back online",
+    "app.restart.button": "Restarting...",
+
+    "auth.login": "Login",
+    "auth.logout": "Logout",
+    "auth.username": "Username",
+    "auth.password": "Password",
+    "auth.sign_in": "Sign In",
+    "auth.session_expired": "Session expired",
+    "auth.invalid_credentials": "Invalid username or password",
+
+    "header.api_status": "API Status",
+    "header.api_status_online": "API OK",
+    "header.api_status_offline": "API OFFLINE",
+
+    "dashboard.title": "Dashboard",
+
+    "dns.title": "DNS",
+    "dns.card.title": "DNS (BIND)",
+    "dns.card.description": "Zones, records and configuration.",
+    "dns.config": "DNS Configuration",
+    "dns.reload": "Reload DNS",
+    "dns.reload.description": "Reload DNS (BIND)",
+    "dns.reload.ok": "DNS reloaded successfully",
+    "dns.reload.error":"Error reloading DNS",
+    "dns.reload.progress":"Restarting DNS...",
+
+    "dhcp.title": "DHCP Leases",
+    "dhcp.card.title": "DHCP (Kea)",
+    "dhcp.card.description": "Pools, leases and reservations.",
+    "dhcp.page.title": "DHCP Leases",
+    "dhcp.leases": "DHCP Leases",
+    "dhcp.leases.search": "Search DHCP Leases",
+    "dhcp.reload": "Reload DHCP",
+    "dhcp.reload.description": "Reload DHCP (Kea)",
+    "dhcp.lease.start": "Start",
+    "dhcp.lease.end": "End",
+    "dhcp.leases.empty": "No leases available",
+    "dhcp.leases.static": "static",
+    "dhcp.leases.static.description": "Static lease",
+    "dhcp.leases.active": "active",
+    "dhcp.leases.active.description": "Active lease",
+    "dhcp.leases.expired": "expired",
+    "dhcp.leases.expired.description": "Expired lease",
+    "dhcp.leases.released": "released",
+    "dhcp.leases.released.description": "Released lease",
+    "dhcp.leases.declined": "declined",
+    "dhcp.leases.declined.description": "Declined lease",
+    "dhcp.leases.add": "Add static lease",
+    "dhcp.leases.delete": "Delete lease",
+    "dhcp.reload.ok": "DHCP reloaded successfully",
+    "dhcp.reload.error":"Error reloading DHCP",
+    "dhcp.reload.progress":"Restarting DHCP...",
+    "dhcp.leases.deleted.ok": "Lease deleted successfully",
+    "dhcp.leases.deleted.error": "Failed to delete lease",
+    "dhcp.leases.list.error": "Failed to load leases",
+    "dhcp.leases.loaded.error": "Failed to load lease",
+    "dhcp.leases.delete.invalid_id": "Invalid lease ID for deletion",
+    "dhcp.leases.not.found": "Lease not found",
+
+    "hosts.title": "Hosts",
+    "hosts.card.title": "Hosts",
+    "hosts.card.description": "IP/MAC inventory.",
+    "hosts.page.title": "Host List",
+    "hosts.add": "Add Host",
+    "hosts.edit": "Edit Host",
+    "hosts.delete": "Delete Host",
+    "hosts.search": "Search Host",
+    "hosts.hostname": "Hostname",
+    "hosts.ipv4": "IPv4 Address",
+    "hosts.placeholder.ipv4": "e.g. 192.168.1.10",
+    "hosts.ipv6": "IPv6 Address",
+    "hosts.placeholder.ipv6": "e.g. fe80::1",
+    "hosts.mac": "MAC Address",
+    "hosts.placeholder.mac": "e.g. AA:BB:CC:DD:EE:FF",
+    "hosts.ssl": "SSL Certificate",
+    "hosts.ssl.enabled": "SSL Certificate enabled",
+    "hosts.visibility": "Visibility",
+    "hosts.visibility.local": "Local",
+    "hosts.visibility.local.description": "Only local (A record internally resolved)",
+    "hosts.visibility.global": "Global",
+    "hosts.visibility.global.description": "Internal and external are identical",
+    "hosts.visibility.alias": "Alias",
+    "hosts.visibility.alias.description": "External name is a CNAME to external_name",
+    "hosts.empty": "No hosts available.",
+    "hosts.created.ok": "Host created successfully",
+    "hosts.created.error": "Failed to create host",
+    "hosts.updated.ok": "Host updated successfully",
+    "hosts.updated.error": "Failed to update host",
+    "hosts.deleted.ok": "Host deleted successfully",
+    "hosts.deleted.error": "Failed to delete host",
+    "hosts.delete.invalid_id": "Invalid host ID for deletion",
+    "hosts.list.error": "Failed to load hosts",
+    "hosts.loaded.error": "Failed to load host",
+    "hosts.not.found": "Host not found",
+    "hosts.already.present": "Host already present",
+
+    "aliases.title": "Aliases",
+    "aliases.card.title": "Aliases",
+    "aliases.card.description": "DNS alias inventory.",
+    "aliases.page.title": "Alias List",
+    "aliases.add": "Add Alias",
+    "aliases.edit": "Edit Alias",
+    "aliases.delete": "Delete Alias",
+    "aliases.search": "Search Aliases",
+    "aliases.name": "Alias",
+    "aliases.target": "Target",
+    "aliases.ssl": "SSL Certificate",
+    "aliases.ssl.enabled": "SSL Certificate enabled",
+    "aliases.visibility": "Visibility",
+    "aliases.visibility.local": "Local",
+    "aliases.visibility.local.description": "Only local (CNAME record internally resolved)",
+    "aliases.visibility.global": "Global",
+    "aliases.visibility.global.description": "Internal and external are identical",
+    "aliases.visibility.alias": "Alias",
+    "aliases.visibility.alias.description": "External name is a CNAME to external_name",
+    "aliases.empty": "No aliases available.",
+    "aliases.created.ok": "Alias created successfully",
+    "aliases.created.error": "Failed to create alias",
+    "aliases.updated.ok": "Alias updated successfully",
+    "aliases.updated.error": "Failed to update alias",
+    "aliases.deleted.ok": "Alias deleted successfully",
+    "aliases.deleted.error": "Failed to delete alias",
+    "aliases.delete.invalid_id": "Invalid alias ID for deletion",
+    "aliases.list.error": "Failed to load aliases",
+    "aliases.loaded.error": "Failed to load alias",
+    "aliases.not.found": "Alias not found",
+    "aliases.already.present": "Alias already present",
+
+    "devices.title": "Devices",
+    "devices.card.title": "Devices",
+    "devices.card.description": "Device status (Host + DHCP).",
+    "devices.page.title": "Device List",
+    "devices.edit": "Edit Device",
+    "devices.delete": "Delete Device",
+    "devices.search": "Search Devices",
+    "devices.active": "Active",
+    "devices.active.description": "Device active",
+    "devices.not.active.description": "Device not active",
+    "devices.empty": "No devices available.",
+    "devices.created.ok": "Device created successfully",
+    "devices.created.error": "Failed to create device",
+    "devices.updated.ok": "Device updated successfully",
+    "devices.updated.error": "Failed to update device",
+    "devices.deleted.ok": "Device deleted successfully",
+    "devices.deleted.error": "Failed to delete device",
+    "devices.edit.invalid_id": "Invalid device ID for modification",
+    "devices.delete.invalid_id": "Invalid device ID for deletion",
+    "devices.list.error": "Failed to load devices",
+    "devices.loaded.error": "Failed to load device",
+    "devices.not.found": "Device not found",
+    "devices.already.present": "Device already present",
+    "devices.unknown.type": "Unknown device type",
+
+    "certificates.title": "Certificates",
+    "certificates.card.title": "Certificates",
+    "certificates.card.description": "Let's Encrypt certificates and renewals.",
+    "certificates.page.title": "Certificate List",
+    "certificates.empty": "No certificates available.",
+
+    "backup.title": "Backup",
+    "backup.card.title": "Backup & Restore",
+    "backup.card.description": "Backup execution and archive management.",
+    "backup.modal.title": "Backup Management",
+    "backup.create": "Create Backup",
+    "backup.create.progress": "Create\85",
+    "backup.manage": "Backup Management",
+    "backup.available": "Available backups",
+    "backup.name": "Name",
+    "backup.date": "Date",
+    "backup.size": "Size",
+    "backup.actions": "Actions",
+    "backup.loading": "Loading backups...",
+    "backup.restore": "Restore Backup",
+    "backup.restore.select": "Select one backup to restore.",
+    "backup.restore.progress": "Restore...",
+    "backup.upload": "Upload Backup",
+    "backup.upload.select": "Select a backup file to upload.",
+    "backup.upload.file": "Upload Backup File",
+    "backup.download.file": "Download Backup File",
+    "backup.delete.file": "Delete Backup File",
+    "backup.refresh": "Refresh List",
+    "backup.empty": "No backups available.",
+    "backup.create_ok": "Backup completed successfully",
+    "backup.create_partial": "Backup completed partially",
+    "backup.create_error": "Error performing backup",
+    "backup.restore_ok": "Restore completed successfully",
+    "backup.restore_partial": "Restore completed partially",
+    "backup.restore_error": "Error performing restore",
+    "backup.delete_ok": "Backup deleted successfully",
+    "backup.delete_error": "Error deleting backup",
+    "backup.download_ok": "Backup downloaded successfully",
+    "backup.download_error": "Error downloading backup",
+    "backup.upload_ok": "Backup uploaded successfully",
+    "backup.upload_error": "Error uploading backup",
+    "backup.refresh_ok": "Backup list refreshed successfully",
+    "backup.refresh_error": "Error refreshing backup list",
+    "backup.select": "Select a backup",
+    "backup.select_file": "Select a file first",
+    "backup.delete_confirm": "Delete backup \"{id}\"?",
+    "backup.not.found": "Backup not found",
+
+    "logs.title": "Logs",
+    "logs.card.title": "Logs",
+    "logs.card.description": "Events and access logs.",
+    "logs.page.title": "System Logs",
+    "logs.view": "View",
+    "logs.live": "Live",
+    "logs.live.description": "Live Stream On/Off",
+    "logs.refresh": "Refresh",
+    "logs.refresh.description": "Refresh logs",
+    "logs.app": "App",
+    "logs.search": "Search logs",
+    "logs.filter": "Filters",
+
+    "settings.title": "Settings",
+    "settings.card.title": "Settings",
+    "settings.card.description": "System configuration and variables.",
+    "settings.page.title": "Settings",
+    "settings.edit": "Edit setting",
+    "settings.reset": "Restore to default value",
+    "settings.reset.confirm": "Restore this setting to its default value?",
+    "settings.search": "Search Configuration Parameter",
+    "settings.parameter": "Parameter",
+    "settings.modal.parameter": "Configuration Parameter",
+    "settings.value": "Value",
+    "settings.enabled": "Enabled",
+    "settings.current": "Current",
+    "settings.admin": "Admin",
+    "settings.admin.description": "Admin menu",
+    "settings.empty": "No settings available",
+    "settings.updated.ok": "Setting updated successfully",
+    "settings.updated.error": "Failed to update setting",
+    "settings.restored.ok": "Setting restored to default values successfully",
+    "settings.restored.error": "Failed to restore default values",
+    "settings.restored.invalid_id": "Invalid setting key",
+    "settings.list.error": "Failed to load settings",
+    "settings.loaded.error": "Failed to load setting",
+    "settings.not.found": "Setting not found",
+    "settings.expand.disabled": "Disabled during search",
+    "settings.expand.all": "Expand all groups",
+    "settings.collapse.all": "Collapse all groups",
+
+    "health.title": "Health",
+    "health.card.title": "Health",
+    "health.card.description": "Service and resource status.",
+    "health.modal.title": "Health Status",
+    "health.loading": "Loading status...",
+    "health.error": "Error fetching health status.",
+    "health.update.ok": "API status updated succesfully",
+    "health.update.error": "Error updating API status",
+    "health.details_unavailable": "Health details not available",
+    "health.updated_at": "Updated at",
+    "health.status": "Status",
+    "health.status.healthy": "Healthy",
+    "health.status.degraded": "Degraded",
+    "health.status.unhealthy": "Unhealthy",
+    "health.status.unknown": "Unknown",
+    "health.latency": "Latency",
+    "health.db_status": "DB Status",
+    "health.db_version": "DB Version",
+    "health.db_tables": "DB Tables",
+    "health.db_size": "DB Size",
+
+    "common.close": "Close",
+    "common.cancel": "Cancel",
+    "common.confirm": "Confirm",
+    "common.confirmation": "Confirmation",
+    "common.are_you_sure": "Are you sure?",
+    "common.description": "Description",
+    "common.options": "Options",
+    "common.state": "State",
+    "common.actions": "Actions",
+    "common.loading": "Loading...",
+    "common.search": "Search",
+    "common.search.placeholder": "Search...",
+    "common.expand": "Expand",
+    "common.expand.description": "Expand all groups",
+    "common.collapse": "Collapse",
+    "common.collapse.description": "Collapse all groups",
+
+    "validation.name.required": "Hostname is required",
+    "validation.ipv4.invalid": "Invalid IPv4 format",
+    "validation.ipv6.invalid": "Invalid IPv6 format",
+    "validation.mac.invalid": "Invalid MAC format",
+    "validation.target.required": "Target is required",
+    "validation.alias.required": "Alias is required",
+    "validation.config.required": "Configuration value is required",
+    "validation.config.invalid_number": "Invalid numeric value",
+    "validation.config.invalid": "Invalid configuration value"
+  }
+}
\ No newline at end of file
diff --git a/frontend/locales/it.json b/frontend/locales/it.json
new file mode 100644 (file)
index 0000000..27d4766
--- /dev/null
@@ -0,0 +1,307 @@
+{
+  "language": "it",
+  "translations": {
+
+    "app.name": "Network Manager",
+    "app.translation.error": "Errore durante il caricamento della traduzione",
+    "app.modals.error": "Errore nel caricamento delle finestre modali",
+    "app.action.error": "Errore nell'esecuzione di un'azione",
+    "app.restart": "Riavvia App",
+    "app.restart.description": "Riavvia applicazione",
+    "app.restart.confirm": "Riavviare l'applicazione?",
+    "app.restart.progress": "Riavvio applicazione in corso...",
+    "app.restart.error": "Errore durante il riavvio dell'applicazione",
+    "app.reconnect.waiting": "In attesa che il server torni disponibile...",
+    "app.reconnect.success": "Applicazione nuovamente disponibile",
+    "app.reconnect.timeout": "Il server non è tornato disponibile",
+    "app.restart.button": "Riavvio...",
+
+    "auth.login": "Accesso",
+    "auth.logout": "Logout",
+    "auth.username": "Nome utente",
+    "auth.password": "Password",
+    "auth.sign_in": "Accedi",
+    "auth.session_expired": "Sessione scaduta",
+    "auth.invalid_credentials": "Nome utente o password non validi",
+
+    "header.api_status": "Stato API",
+    "header.api_status_online": "API OK",
+    "header.api_status_offline": "API OFFLINE",
+
+    "dashboard.title": "Dashboard",
+
+    "dns.title": "DNS",
+    "dns.card.title": "DNS (BIND)",
+    "dns.card.description": "Zone, record e configurazioni.",
+    "dns.config": "Configurazione DNS",
+    "dns.reload": "Ricarica DNS",
+    "dns.reload.description": "Ricarica DNS (BIND)",
+    "dns.reload.ok": "DNS ricaricato con successo",
+    "dns.reload.error":"Errore nel ricaricare DNS",
+    "dns.reload.progress":"Riavvio DNS...",
+
+    "dhcp.title": "DHCP Leases",
+    "dhcp.card.title": "DHCP (Kea)",
+    "dhcp.card.description": "Pool, lease e prenotazioni.",
+    "dhcp.page.title": "Lease DHCP",
+    "dhcp.leases": "Lease DHCP",
+    "dhcp.leases.search": "Cerca Lease DHCP",
+    "dhcp.reload": "Ricarica DHCP",
+    "dhcp.reload.description": "Ricarica DHCP (Kea)",
+    "dhcp.lease.start": "Inizio",
+    "dhcp.lease.end": "Fine",
+    "dhcp.leases.empty": "Nessuna lease disponibile",
+    "dhcp.leases.static": "statico",
+    "dhcp.leases.static.description": "Lease statico",
+    "dhcp.leases.active": "attivo",
+    "dhcp.leases.active.description": "Lease attivo",
+    "dhcp.leases.expired": "scaduto",
+    "dhcp.leases.expired.description": "Lease scaduto",
+    "dhcp.leases.released": "rilasciato",
+    "dhcp.leases.released.description": "Lease rilasciato",
+    "dhcp.leases.declined": "rifiutato",
+    "dhcp.leases.declined.description": "Lease rifiutato",
+    "dhcp.leases.add": "Aggiungi lease statica",
+    "dhcp.leases.delete": "Cancella lease",
+    "dhcp.reload.ok": "DHCP ricaricato con successo",
+    "dhcp.reload.error":"Errore nel ricaricare DHCP",
+    "dhcp.reload.progress":"Riavvio DHCP...",
+    "dhcp.leases.deleted.ok": "Lease eliminata con successo",
+    "dhcp.leases.deleted.error": "Errore nell'eliminazione della lease",
+    "dhcp.leases.list.error": "Errore nel caricamento delle lease",
+    "dhcp.leases.loaded.error": "Errore nel caricamento della lease",
+    "dhcp.leases.delete.invalid_id": "ID lease non valido per la rimozione",
+    "dhcp.leases.not.found": "Lease non trovata",
+
+    "hosts.title": "Host",
+    "hosts.card.title": "Host",
+    "hosts.card.description": "Inventario IP/MAC.",
+    "hosts.page.title": "Lista Host",
+    "hosts.add": "Aggiungi Host",
+    "hosts.edit": "Modifica Host",
+    "hosts.delete": "Cancella Host",
+    "hosts.search": "Cerca Host",
+    "hosts.hostname": "Nome Host",
+    "hosts.ipv4": "Indirizzo IPv4",
+    "hosts.placeholder.ipv4": "es. 192.168.1.10",
+    "hosts.ipv6": "Indirizzo IPv6",
+    "hosts.placeholder.ipv6": "es. fe80::1",
+    "hosts.mac": "Indirizzo MAC",
+    "hosts.placeholder.mac": "es. AA:BB:CC:DD:EE:FF",
+    "hosts.ssl": "Certificato SSL",
+    "hosts.ssl.enabled": "Certificato SSL abilitato",
+    "hosts.visibility": "Visibilità",
+    "hosts.visibility.local": "Locale",
+    "hosts.visibility.local.description": "Solo locale (record A risolto internamente)",
+    "hosts.visibility.global": "Globale",
+    "hosts.visibility.global.description": "Il nome interno e quello esterno coincidono",
+    "hosts.visibility.alias": "Alias",
+    "hosts.visibility.alias.description": "Il nome esterno è un CNAME di external_name",
+    "hosts.empty": "Nessun host disponibile.",
+    "hosts.created.ok": "Host creato con successo",
+    "hosts.created.error": "Errore nel creare l'host",
+    "hosts.updated.ok": "Host modificato con successo",
+    "hosts.updated.error": "Errore nel modificare l'host",
+    "hosts.deleted.ok": "Host eliminato con successo",
+    "hosts.deleted.error": "Errore nell'eliminare l'host",
+    "hosts.delete.invalid_id": "ID host non valido per la rimozione",
+    "hosts.list.error": "Errore nel caricamento degli host",
+    "hosts.loaded.error": "Errore nel caricamento dell'host",
+    "hosts.not.found": "Host non trovato",
+    "hosts.already.present": "Host già presente",
+
+    "aliases.title": "Alias",
+    "aliases.card.title": "Alias",
+    "aliases.card.description": "Inventario alias DNS.",
+    "aliases.page.title": "Lista Alias",
+    "aliases.add": "Aggiungi Alias",
+    "aliases.edit": "Modifica Alias",
+    "aliases.delete": "Cancella Alias",
+    "aliases.search": "Cerca Alias",
+    "aliases.name": "Alias",
+    "aliases.target": "Target",
+    "aliases.ssl": "Certificato SSL",
+    "aliases.ssl.enabled": "Certificato SSL abilitato",
+    "aliases.visibility": "Visibilità",
+    "aliases.visibility.local": "Locale",
+    "aliases.visibility.local.description": "Solo locale (record CNAME risolto internamente)",
+    "aliases.visibility.global": "Globale",
+    "aliases.visibility.global.description": "Il nome interno e quello esterno coincidono",
+    "aliases.visibility.alias": "Alias",
+    "aliases.visibility.alias.description": "Il nome esterno è un CNAME di external_name",
+    "aliases.empty": "Nessun alias disponibile.",
+    "aliases.created.ok": "Alias creato con successo",
+    "aliases.created.error": "Errore nel creare l'alias",
+    "aliases.updated.ok": "Alias modificato con successo",
+    "aliases.updated.error": "Errore nel modificare l'alias",
+    "aliases.deleted.ok": "Alias eliminato con successo",
+    "aliases.deleted.error": "Errore nell'eliminare l'alias",
+    "aliases.delete.invalid_id": "ID alias non valido per la rimozione",
+    "aliases.list.error": "Errore nel caricamento degli alias",
+    "aliases.loaded.error": "Errore nel caricamento dell'alias",
+    "aliases.not.found": "Alias non trovato",
+    "aliases.already.present": "Alias già presente",
+
+    "devices.title": "Dispositivi",
+    "devices.card.title": "Dispositivi",
+    "devices.card.description": "Stato dispositivi (Host + DHCP).",
+    "devices.page.title": "Lista Dispositivi",
+    "devices.edit": "Modifica Dispositivo",
+    "devices.delete": "Cancella Dispositivo",
+    "devices.search": "Cerca Dispositivo",
+    "devices.active": "Attivo",
+    "devices.active.description": "Dispositivo attivo",
+    "devices.not.active.description": "Dispositivo non attivo",
+    "devices.empty": "Nessun dispositivo disponibile.",
+    "devices.created.ok": "Dispositivo creato con successo",
+    "devices.created.error": "Errore nel creare il dispositivo",
+    "devices.updated.ok": "Dispositivo modificato con successo",
+    "devices.updated.error": "Errore nel modificare il dispositivo",
+    "devices.deleted.ok": "Dispositivo eliminato con successo",
+    "devices.deleted.error": "Errore nell'eliminare il dispositivo",
+    "devices.edit.invalid_id": "ID dispositivo non valido per la modifica",
+    "devices.delete.invalid_id": "ID dispositivo non valido per la rimozione",
+    "devices.list.error": "Errore nel caricamento dei dispositivi",
+    "devices.loaded.error": "Errore nel caricamento del dispositivo",
+    "devices.not.found": "Dispositivo non trovato",
+    "devices.already.present": "Dispositivo già presente",
+    "devices.unknown.type": "Tipologia di dispositivo sconosciuta",
+
+    "certificates.title": "Certificati",
+    "certificates.card.title": "Certificati",
+    "certificates.card.description": "Certificati Let's Encrypt e rinnovi.",
+    "certificates.page.title": "Lista Certificati",
+    "certificates.empty": "Nessun certificato disponibile.",
+
+    "backup.title": "Backup",
+    "backup.card.title": "Backup e Ripristino",
+    "backup.card.description": "Esecuzione backup e gestione archivi.",
+    "backup.modal.title": "Gestione Backup",
+    "backup.create": "Crea Backup",
+    "backup.create.progress": "Creazione…",
+    "backup.manage": "Gestione Backup",
+    "backup.available": "Backup disponibili",
+    "backup.name": "Nome",
+    "backup.date": "Data",
+    "backup.size": "Dimensione",
+    "backup.actions": "Azioni",
+    "backup.loading": "Caricamento backup...",
+    "backup.restore": "Ripristina Backup",
+    "backup.restore.select": "Seleziona un backup da ripristinare.",
+    "backup.restore.progress": "Ripristino...",
+    "backup.upload": "Carica backup",
+    "backup.upload.select": "Seleziona un file di backup da caricare.",
+    "backup.upload.file": "Carica file backup",
+    "backup.download.file": "Scarica file backup",
+    "backup.delete.file": "Elimina file backup",
+    "backup.refresh": "Aggiorna elenco",
+    "backup.empty": "Nessun backup disponibile.",
+    "backup.create_ok": "Backup completato con successo",
+    "backup.create_partial": "Backup completato parzialmente",
+    "backup.create_error": "Errore durante il backup",
+    "backup.restore_ok": "Ripristino completato con successo",
+    "backup.restore_partial": "Ripristino completato parzialmente",
+    "backup.restore_error": "Errore durante il ripristino",
+    "backup.delete_ok": "Backup eliminato con successo",
+    "backup.delete_error": "Errore durante l'eliminazione del backup",
+    "backup.download_ok": "Backup scaricato con successo",
+    "backup.download_error": "Errore durante il download del backup",
+    "backup.upload_ok": "Backup caricato con successo",
+    "backup.upload_error": "Errore durante il caricamento del backup",
+    "backup.refresh_ok": "Lista backup aggiornata con successo",
+    "backup.refresh_error": "Errore durante l'aggiornamento della lista backup",
+    "backup.select": "Seleziona un backup",
+    "backup.select_file": "Seleziona prima un file",
+    "backup.delete_confirm": "Eliminare il backup \"{id}\"?",
+    "backup.not.found": "Backup non trovato",
+
+    "logs.title": "Log",
+    "logs.card.title": "Log",
+    "logs.card.description": "Eventi e log di accesso.",
+    "logs.page.title": "Log di sistema",
+    "logs.view": "Vista",
+    "logs.live": "Live",
+    "logs.live.description": "Attiva/Disattiva Live Stream",
+    "logs.refresh": "Ricarica",
+    "logs.refresh.description": "Ricarica log",
+    "logs.app": "App",
+    "logs.search": "Cerca log",
+    "logs.filter": "Filtri",
+
+    "settings.title": "Impostazioni",
+    "settings.card.title": "Impostazioni",
+    "settings.card.description": "Configurazione sistema e variabili.",
+    "settings.page.title": "Impostazioni",
+    "settings.edit": "Modifica parametro",
+    "settings.reset": "Ripristina al valore predefinito",
+    "settings.reset.confirm": "Ripristinare questo parametro ai valori predefiniti?",
+    "settings.search": "Cerca parametro",
+    "settings.parameter": "Parametro",
+    "settings.modal.parameter": "Parametro di configurazione",
+    "settings.value": "Valore",
+    "settings.enabled": "Abilitato",
+    "settings.current": "Corrente",
+    "settings.admin": "Admin",
+    "settings.admin.description": "Menu amministrazione",
+    "settings.empty": "Nessun parametro disponibile",
+    "settings.updated.ok": "Parametro modificato con successo",
+    "settings.updated.error": "Errore nel modificare il parametro",
+    "settings.restored.ok": "Parametro ripristinato ai valori predefiniti",
+    "settings.restored.error": "Errore durante il ripristino dei valori predefiniti",
+    "settings.restored.invalid_id": "Chiave parametro non valida",
+    "settings.list.error": "Errore nel caricamento dei parametri",
+    "settings.loaded.error": "Errore nel caricamento del parametro",
+    "settings.not.found": "Parametro non trovato",
+    "settings.expand.disabled": "Disabilitato durante la ricerca",
+    "settings.expand.all": "Espandi tutti i gruppi",
+    "settings.collapse.all": "Comprimi tutti i gruppi",
+
+    "health.title": "Stato del sistema",
+    "health.card.title": "Stato del sistema",
+    "health.card.description": "Stato servizi e risorse.",
+    "health.modal.title": "Stato del sistema",
+    "health.loading": "Caricamento stato...",
+    "health.error": "Errore durante il recupero dello stato del sistema.",
+    "health.update.ok": "Stato API aggiornato con successo",
+    "health.update.error": "Errore nell'aggiornamento dello stato API",
+    "health.details_unavailable": "Informazioni sullo stato del sistema non disponibili",
+    "health.updated_at": "Aggiornato alle",
+    "health.status": "Stato",
+    "health.status.healthy": "Normale",
+    "health.status.degraded": "Degradato",
+    "health.status.unhealthy": "Critico",
+    "health.status.unknown": "Sconosciuto",
+    "health.latency": "Latenza",
+    "health.db_status": "Stato DB",
+    "health.db_version": "Versione DB",
+    "health.db_tables": "Tabelle DB",
+    "health.db_size": "Dimensione DB",
+
+    "common.close": "Chiudi",
+    "common.cancel": "Annulla",
+    "common.confirm": "Conferma",
+    "common.confirmation": "Conferma",
+    "common.are_you_sure": "Sei sicuro?",
+    "common.description": "Descrizione",
+    "common.options": "Opzioni",
+    "common.state": "Stato",
+    "common.actions": "Azioni",
+    "common.loading": "Caricamento...",
+    "common.search": "Cerca",
+    "common.search.placeholder": "Cerca...",
+    "common.expand": "Espandi",
+    "common.expand.description": "Espandi tutti i gruppi",
+    "common.collapse": "Comprimi",
+    "common.collapse.description": "Comprimi tutti i gruppi",
+
+    "validation.name.required": "Nome host richiesto",
+    "validation.ipv4.invalid": "Formato IPv4 non valido",
+    "validation.ipv6.invalid": "Formato IPv6 non valido",
+    "validation.mac.invalid": "Formato MAC non valido",
+    "validation.target.required": "Target richiesto",
+    "validation.alias.required": "Alias richiesto",
+    "validation.config.required": "Valore del parametro obbligatorio",
+    "validation.config.invalid_number": "Valore numerico non valido",
+    "validation.config.invalid": "Valore del parametro non valido"
+  }
+}
\ No newline at end of file
index 7f0bd3eee11cde95eb6d77b13175a2a70c748bcf..83c186b41a4ca3794d438b025e5ec3b0d96f1360 100644 (file)
@@ -20,7 +20,7 @@
     <link rel="stylesheet" href="css/layout.css">
 </head>
 
-<body>
+<body data-page-title="dashboard.title">
     <!-- Topbar -->
     <header class="topbar">
         <div class="topbar-inner">
@@ -33,7 +33,7 @@
                     <line x1="12" y1="6" x2="12" y2="18" stroke="var(--accent)" stroke-width="2"></line>
                     <line x1="6" y1="12" x2="18" y2="12" stroke="var(--accent)" stroke-width="2"></line>
                 </svg>
-                <span>Network Manager</span>
+                <span data-i18n="app.name">Network Manager</span>
             </a>
 
             <nav class="navbar">
 
                 <!-- Desktop nav -->
                 <div class="d-none d-md-flex gap-2 ms-auto">
-                        <a href="/hosts"    class="btn btn-primary">       Hosts</a>
-                        <a href="/aliases"  class="btn btn-primary">       Alias</a>
-                        <a href="/leases"   class="btn btn-primary">       DHCP Leases</a>
-                        <a href="/devices"  class="btn btn-primary">       Devices</a>
-                        <a href="/logs"     class="btn btn-primary active">Logs</a>
-                        <a href="/settings" class="btn btn-primary">       Settings</a>
-                        <button class="btn btn-primary btn-logout">Logout</button>
+                        <a href="/hosts"    class="btn btn-primary"        data-i18n="hosts.title">Hosts</a>
+                        <a href="/aliases"  class="btn btn-primary"        data-i18n="aliases.title">Alias</a>
+                        <a href="/leases"   class="btn btn-primary"        data-i18n="dhcp.title">DHCP Leases</a>
+                        <a href="/devices"  class="btn btn-primary"        data-i18n="devices.title">Devices</a>
+                        <a href="/logs"     class="btn btn-primary active" data-i18n="logs.title">Logs</a>
+                        <a href="/settings" class="btn btn-primary"        data-i18n="settings.title">Settings</a>
+                        <button class="btn btn-primary btn-logout"         data-i18n="auth.logout">Logout</button>
                 </div>
             </nav>
 
             <div class="offcanvas offcanvas-end d-md-none" tabindex="-1" id="mobileMenu">
 
                 <div class="offcanvas-header">
-                    <h5 class="offcanvas-title">Network Manager</h5>
+                    <h5 class="offcanvas-title" data-i18n="app.name">Network Manager</h5>
 
                     <button
                         type="button"
                         class="btn-close"
+                        data-i18n-title="common.close"
+                        data-i18n-aria-label="common.close"
                         data-bs-dismiss="offcanvas">
                     </button>
                 </div>
 
                 <div class="offcanvas-body">
                     <div class="d-grid gap-2">
-                        <a href="/hosts"    class="btn btn-primary">       Hosts</a>
-                        <a href="/aliases"  class="btn btn-primary">       Alias</a>
-                        <a href="/leases"   class="btn btn-primary">       DHCP Leases</a>
-                        <a href="/devices"  class="btn btn-primary">       Devices</a>
-                        <a href="/logs"     class="btn btn-primary active">Logs</a>
-                        <a href="/settings" class="btn btn-primary">       Settings</a>
-                        <button class="btn btn-primary btn-logout">Logout</button>
+                        <a href="/hosts"    class="btn btn-primary"        data-i18n="hosts.title">Hosts</a>
+                        <a href="/aliases"  class="btn btn-primary"        data-i18n="aliases.title">Alias</a>
+                        <a href="/leases"   class="btn btn-primary"        data-i18n="dhcp.title">DHCP Leases</a>
+                        <a href="/devices"  class="btn btn-primary"        data-i18n="devices.title">Devices</a>
+                        <a href="/logs"     class="btn btn-primary active" data-i18n="logs.title">Logs</a>
+                        <a href="/settings" class="btn btn-primary"        data-i18n="settings.title">Settings</a>
+                        <button class="btn btn-primary btn-logout"         data-i18n="auth.logout">Logout</button>
                     </div>
                 </div>
             </div>
                 <div class="col-auto col-md-auto">
                     <h2 class="mb-0 d-flex align-items-center gap-2 lh-1">
                         <span class="title-icon"><i class="bi bi-hdd-network"></i></span>
-                        <span class="section-title">System Logs</span>
+                        <span class="section-title" data-i18n="logs.page.title">System Logs</span>
                     </h2>
                 </div>
 
                             class="btn btn-primary dropdown-toggle d-flex align-items-center gap-2"
                             data-bs-toggle="dropdown">
                             <i class="bi bi-eye"></i>
-                            <span class="label d-none d-md-inline">View</span>
+                            <span class="label d-none d-md-inline" data-i18n="logs.view">View</span>
                         </button>
 
                         <ul class="dropdown-menu dropdown-menu-end shadow">
                             <li class="px-2 py-1">
                                 <button
                                     class="btn btn-success w-100 d-flex align-items-center gap-2"
-                                    title="Live Stream On"
-                                    aria-label="Live Stream On"
+                                    data-i18n-title="logs.live.description"
+                                    data-i18n-aria-label="logs.live.description"
                                     data-action="liveToggle">
                                     <i class="bi bi-broadcast"></i>
-                                    <span class="label d-md-inline">Live</span>
+                                    <span class="label d-md-inline" data-i18n="logs.live">Live</span>
                                 </button>
                             </li>
 
                             <li class="px-2 py-1">
                                 <button
                                     class="btn btn-primary w-100 d-flex align-items-center gap-2"
-                                    title="Refresh"
-                                    aria-label="Refresh"
+                                    data-i18n-title="logs.refresh"
+                                    data-i18n-aria-label="logs.refresh"
                                     data-action="refreshLogs">
                                     <i class="bi bi-arrow-repeat"></i>
-                                    <span class="label d-md-inline">Refresh</span>
+                                    <span class="label d-md-inline" data-i18n="logs.refresh">Refresh</span>
                                 </button>
                             </li>
                         </ul>
                             class="btn btn-primary dropdown-toggle d-flex align-items-center gap-2 px-3"
                             data-bs-toggle="dropdown">
                             <i class="bi bi-file-text"></i>
-                            <span class="label d-none d-md-inline">App</span>
+                            <span class="label d-none d-md-inline" data-i18n="logs.app">App</span>
                         </button>
                         <ul class="dropdown-menu dropdown-menu-end shadow">
                             <li><a class="dropdown-item active" data-value="app" href="#">App</a></li>
                             class="btn btn-primary dropdown-toggle d-flex align-items-center gap-2 px-3"
                             data-bs-toggle="dropdown">
                             <i class="bi bi-sliders"></i>
-                            <span class="label d-none d-md-inline">Filter</span>
+                            <span class="label d-none d-md-inline" data-i18n="logs.filter">Filter</span>
                         </button>
 
                         <div class="dropdown-menu dropdown-menu-end shadow p-3" id="filterDropdown">
                             <!-- Search Bar -->
                             <div class="mb-2">
-                                <label class="form-label small">Search</label>
+                                <label class="form-label small" data-i18n="common.search">Search</label>
                                 <input
                                     type="text"
                                     id="logSearch"
                                     class="form-control mb-2"
-                                    placeholder="Search logs">
+                                    data-i18n-placeholder="common.search.placeholder"
+                                    data-i18n-aria-label="logs.search">
                             </div>
 
                             <!-- Separator -->
                             <li class="px-2 py-1">
                                 <button
                                     class="btn btn-primary w-100 d-flex align-items-center gap-2 text-nowrap"
-                                    title="Reload DNS (BIND)"
-                                    aria-label="Reload DNS"
+                                    data-i18n-title="dns.reload.description"
+                                    data-i18n-aria-label="dns.reload.description"
                                     data-action="reloadDns">
                                     <i class="bi bi-arrow-clockwise"></i>
-                                    <span class="label d-md-inline">Reload DNS</span>
+                                    <span class="label d-md-inline" data-i18n="dns.reload">Reload DNS</span>
                                 </button>
                             </li>
 
                             <li class="px-2 py-1">
                                 <button
                                     class="btn btn-primary w-100 d-flex align-items-center gap-2 text-nowrap"
-                                    title="Reload DHCP (Kea)"
-                                    aria-label="Reload DHCP"
+                                    data-i18n-title="dhcp.reload.description"
+                                    data-i18n-aria-label="dhcp.reload.description"
                                     data-action="reloadDhcp">
                                     <i class="bi bi-arrow-clockwise"></i>
-                                    <span class="label d-md-inline">Reload DHCP</span>
+                                    <span class="label d-md-inline" data-i18n="dhcp.reload">Reload DHCP</span>
                                 </button>
                             </li>
 
                             <li class="px-2 py-1">
                                 <button
                                     class="btn btn-danger w-100 d-flex align-items-center gap-2 text-nowrap"
-                                    title="Restart system"
-                                    aria-label="Restart system"
+                                    data-i18n-title="app.restart.description"
+                                    data-i18n-aria-label="app.restart.description"
                                     data-action="restartApp">
                                     <i class="bi bi-power"></i>
-                                    <span class="label d-md-inline">Restart</span>
+                                    <span class="label d-md-inline" data-i18n="app.restart">Restart</span>
                                 </button>
                             </li>
                         </ul>
     <!-- Loader -->
     <div id="loader" class="text-center my-3" style="display: none;">
         <div class="spinner-border text-primary" role="status">
-            <span class="visually-hidden">Loading...</span>
+            <span class="visually-hidden" data-i18n="common.loading">Loading...</span>
         </div>
     </div>
-    <div id="devices-container"></div>
 
     <!-- Modals -->
     <div id="modals-container"></div>
index b468c619adf18bc69e7af3c5f9724533fb48647d..6730470abfc1772fe5f03b75d3daecda828f3fa8 100644 (file)
@@ -1,4 +1,4 @@
-<!-- AddHost -->
+<!-- Modal hosts -->
 <div class="modal fade" id="addHostModal" tabindex="-1" aria-labelledby="addHostTitle" aria-hidden="true">
     <div class="modal-dialog modal-dialog-centered"><!-- modal-sm|md|lg se vuoi cambiare -->
         <div class="modal-content modal-content-sm addhost-modal">
@@ -7,73 +7,84 @@
                 <div class="d-flex align-items-center gap-2">
                     <!-- Emoji o icona -->
                     <span class="title-icon" aria-hidden="true"><i class="bi bi-hdd-network"></i></span>
-                    <h5 class="modal-title mb-0" id="addHostTitle">Aggiungi Host</h5>
+                    <h5 class="modal-title mb-0" id="addHostTitle" data-i18n="hosts.add">Add host</h5>
                 </div>
-                <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Chiudi"></button>
+                <button type="button"
+                    class="btn-close btn-close-white"
+                    data-bs-dismiss="modal"
+                    data-i18n-aria-label="common.close"></button>
             </div>
 
             <div class="modal-body">
                 <form id="addHostForm">
                     <div class="mb-2">
-                        <label for="hostName" class="form-label">Hostname</label>
+                        <label for="hostName" class="form-label" data-i18n="hosts.hostname">Hostname</label>
                         <input type="text" id="hostName" class="form-control" required>
                     </div>
 
                     <div class="mb-2">
-                        <label for="hostIPv4" class="form-label">IPv4</label>
-                        <input type="text" id="hostIPv4" class="form-control" inputmode="decimal" placeholder="es. 192.168.1.10">
+                        <label for="hostIPv4" class="form-label" data-i18n="hosts.ipv4">IPv4</label>
+                        <input type="text" id="hostIPv4" class="form-control" inputmode="decimal" data-i18n-placeholder="hosts.placeholder.ipv4">
                     </div>
 
                     <div class="mb-2">
-                        <label for="hostIPv6" class="form-label">IPv6</label>
-                        <input type="text" id="hostIPv6" class="form-control" placeholder="es. fe80::1">
+                        <label for="hostIPv6" class="form-label" data-i18n="hosts.ipv6">IPv6</label>
+                        <input type="text" id="hostIPv6" class="form-control" data-i18n-placeholder="hosts.placeholder.ipv6">
                     </div>
 
                     <div class="mb-2">
-                        <label for="hostMAC" class="form-label">MAC Address</label>
-                        <input type="text" id="hostMAC" class="form-control" placeholder="es. AA:BB:CC:DD:EE:FF">
+                        <label for="hostMAC" class="form-label" data-i18n="hosts.mac">MAC Address</label>
+                        <input type="text" id="hostMAC" class="form-control" data-i18n-placeholder="hosts.placeholder.mac">
                     </div>
 
                     <div class="mb-2">
-                        <label for="hostDescription" class="form-label">Description</label>
+                        <label for="hostDescription" class="form-label" data-i18n="common.description">Description</label>
                         <input type="text" id="hostDescription" class="form-control">
                     </div>
 
                     <div class="form-check my-2">
                         <input class="form-check-input" type="checkbox" id="hostSSL">
-                        <label class="form-check-label" for="hostSSL">SSL?</label>
+                        <label class="form-check-label" for="hostSSL" data-i18n="hosts.ssl">SSL?</label>
                     </div>
 
                     <div class="mb-2">
-                        <label class="form-label d-block">Visibility</label>
+                        <label class="form-label d-block" data-i18n="hosts.visibility">Visibility</label>
                         <div class="btn-group" role="group">
                             <!-- Local -->
                             <input type="radio" class="btn-check" id="hostVisibilityLocal" name="hostVisibility" value="0" checked>
-                            <label class="btn btn-outline-primary" for="hostVisibilityLocal">Local</label>
+                            <label class="btn btn-outline-primary" for="hostVisibilityLocal" data-i18n="hosts.visibility.local">Local</label>
                             <!-- Global -->
                             <input type="radio" class="btn-check" id="hostVisibilityGlobal" name="hostVisibility" value="1">
-                            <label class="btn btn-outline-primary" for="hostVisibilityGlobal">Global</label>
+                            <label class="btn btn-outline-primary" for="hostVisibilityGlobal" data-i18n="hosts.visibility.global">Global</label>
                             <!-- Alias -->
                             <input type="radio" class="btn-check" id="hostVisibilityAlias" name="hostVisibility" value="2">
-                            <label class="btn btn-outline-primary" for="hostVisibilityAlias">Alias</label>
+                            <label class="btn btn-outline-primary" for="hostVisibilityAlias" data-i18n="hosts.visibility.alias">Alias</label>
                         </div>
                     </div>
                 </form>
             </div>
 
             <div class="modal-footer">
-                <button type="submit" form="addHostForm" class="btn btn-primary">
-                    <i class="bi bi-check2"></i>
+                <button type="submit"
+                    form="addHostForm"
+                    class="btn btn-primary"
+                    data-i18n-title="common.confirm"
+                    data-i18n-aria-label="common.confirm">
+                        <i class="bi bi-check2"></i>
                 </button>
-                <button type="button" class="btn btn-primary" data-bs-dismiss="modal">
-                    <i class="bi bi-x"></i>
+                <button type="button"
+                    class="btn btn-primary"
+                    data-bs-dismiss="modal"
+                    data-i18n-title="common.cancel"
+                    data-i18n-aria-label="common.cancel">
+                        <i class="bi bi-x"></i>
                 </button>
             </div>
         </div>
     </div>
 </div>
 
-<!-- AddAlias -->
+<!-- Modal aliases -->
 <div class="modal fade" id="addAliasModal" tabindex="-1" aria-labelledby="addAliasTitle" aria-hidden="true">
     <div class="modal-dialog modal-dialog-centered"><!-- modal-sm|md|lg se vuoi cambiare -->
         <div class="modal-content modal-content-sm addhost-modal">
                 <div class="d-flex align-items-center gap-2">
                     <!-- Emoji o icona -->
                     <span class="title-icon" aria-hidden="true"><i class="bi bi-diagram-2"></i></span>
-                    <h5 class="modal-title mb-0" id="addAliasTitle">Aggiungi Alias</h5>
+                    <h5 class="modal-title mb-0" id="addAliasTitle" data-i18n="aliases.add">Add alias</h5>
                 </div>
-                <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Chiudi"></button>
+                <button type="button"
+                    class="btn-close btn-close-white"
+                    data-bs-dismiss="modal"
+                    data-i18n-aria-label="common.close">
+                </button>
             </div>
 
             <div class="modal-body">
                 <form id="addAliasForm">
                     <div class="mb-2">
-                        <label for="aliasName" class="form-label">Alias</label>
+                        <label for="aliasName" class="form-label" data-i18n="aliases.name">Alias</label>
                         <input type="text" id="aliasName" class="form-control" required>
                     </div>
 
                     <div class="mb-2">
-                        <label for="aliasTarget" class="form-label">Target</label>
+                        <label for="aliasTarget" class="form-label" data-i18n="aliases.target">Target</label>
                         <input type="text" id="aliasTarget" class="form-control" required>
                     </div>
 
                     <div class="mb-2">
-                        <label for="aliasDescription" class="form-label">Description</label>
+                        <label for="aliasDescription" class="form-label" data-i18n="common.description">Description</label>
                         <input type="text" id="aliasDescription" class="form-control">
                     </div>
 
                     <div class="form-check my-2">
                         <input class="form-check-input" type="checkbox" id="aliasSSL">
-                        <label class="form-check-label" for="aliasSSL">SSL?</label>
+                        <label class="form-check-label" for="aliasSSL" data-i18n="aliases.ssl">SSL?</label>
                     </div>
 
                     <div class="mb-2">
-                        <label class="form-label d-block">Visibility</label>
+                        <label class="form-label d-block" data-i18n="aliases.visibility">Visibility</label>
                         <div class="btn-group" role="group">
                             <!-- Local -->
                             <input type="radio" class="btn-check" id="aliasVisibilityLocal" name="aliasVisibility" value="0" checked>
-                            <label class="btn btn-outline-primary" for="aliasVisibilityLocal">Local</label>
+                            <label class="btn btn-outline-primary" for="aliasVisibilityLocal" data-i18n="aliases.visibility.local">Local</label>
                             <!-- Global -->
                             <input type="radio" class="btn-check" id="aliasVisibilityGlobal" name="aliasVisibility" value="1">
-                            <label class="btn btn-outline-primary" for="aliasVisibilityGlobal">Global</label>
+                            <label class="btn btn-outline-primary" for="aliasVisibilityGlobal" data-i18n="aliases.visibility.global">Global</label>
                             <!-- Alias -->
                             <input type="radio" class="btn-check" id="aliasVisibilityAlias" name="aliasVisibility" value="2">
-                            <label class="btn btn-outline-primary" for="aliasVisibilityAlias">Alias</label>
+                            <label class="btn btn-outline-primary" for="aliasVisibilityAlias" data-i18n="aliases.visibility.alias">Alias</label>
                         </div>
                     </div>
                 </form>
             </div>
 
             <div class="modal-footer">
-                <button type="submit" form="addAliasForm" class="btn btn-primary">
-                    <i class="bi bi-check2"></i>
+                <button type="submit"
+                    form="addAliasForm"
+                    class="btn btn-primary"
+                    data-i18n-title="common.confirm"
+                    data-i18n-aria-label="common.confirm">
+                        <i class="bi bi-check2"></i>
                 </button>
-                <button type="button" class="btn btn-primary" data-bs-dismiss="modal">
-                    <i class="bi bi-x"></i>
+                <button type="button"
+                    class="btn btn-primary"
+                    data-bs-dismiss="modal"
+                    data-i18n-title="common.cancel"
+                    data-i18n-aria-label="common.cancel">
+                        <i class="bi bi-x"></i>
                 </button>
             </div>
         </div>
         <div class="addhost-header d-flex justify-content-between align-items-center">
             <div class="d-flex align-items-center gap-2">
                 <span class="title-icon"><i class="bi bi-arrow-counterclockwise"></i></span>
-                <span id="backupTitle" class="modal-title">Backup Management</span>
+                <span id="backupTitle" class="modal-title" data-i18n="backup.modal.title">Backup Management</span>
             </div>
-            <button type="button" class="btn-close" title="Close" aria-label="Close" data-action="closeBackupModal"></button>
+            <button type="button"
+                class="btn-close"
+                data-i18n-title="common.close"
+                data-i18n-aria-label="common.close"
+                data-action="closeBackupModal">
+            </button>
         </div>
 
         <div class="modal-body">
-            <label class="form-label">Available backups</label>
+            <label class="form-label" data-i18n="backup.available">Available backups</label>
 
             <div class="table-responsive" style="max-height: 250px; overflow-y: auto;">
                 <table class="table table-hover table-sm align-middle">
                     <thead class="table-light sticky-top">
                         <tr>
                             <th style="width: 40px;"></th>
-                            <th>Name</th>
-                            <th>Date</th>
-                            <th>Size</th>
-                            <th style="width: 130px;" class="text-center">Actions</th>
+                            <th data-i18n="backup.name">Name</th>
+                            <th data-i18n="backup.date">Date</th>
+                            <th data-i18n="backup.size">Size</th>
+                            <th style="width: 130px;"
+                                class="text-center"
+                                data-i18n="backup.actions">
+                                Actions
+                            </th>
                         </tr>
                     </thead>
                     <tbody id="backupList">
             <!-- Hidden input opzionale -->
             <input type="hidden" id="restoreBackupId" />
 
-            <small class="text-muted">Select one backup to restore.</small>
+            <small class="text-muted" data-i18n="backup.restore.select">Select one backup to restore.</small>
         </div>
 
         <div class="modal-body">
-            <label class="form-label">Upload backup</label>
+            <label class="form-label" data-i18n="backup.upload">Upload backup</label>
             <div class="d-flex gap-2">
                 <input type="file" id="backupUploadInput" class="form-control" accept=".zip,.bak,.tar,.gz">
-                <button type="button" class="btn btn-primary" title="Upload backup file" aria-label="Upload backup file" data-action="uploadBackup">
-                    <i class="bi bi-upload icon"></i><span class="label"></span>
+                <button type="button"
+                    class="btn btn-primary"
+                    data-i18n-title="backup.upload.file"
+                    data-i18n-aria-label="backup.upload.file"
+                    data-action="uploadBackup">
+                        <i class="bi bi-upload icon"></i><span class="label"></span>
                 </button>
             </div>
-            <small class="text-muted">Select a backup file to upload.</small>
+            <small class="text-muted" data-i18n="backup.upload.select">Select a backup file to upload.</small>
         </div>
 
         <div class="modal-footer modal-buttons">
-            <button type="button" class="btn btn-primary" data-action="startBackup">
-                <i class="bi bi-cloud-upload me-1"></i><span class="label">Create Backup</span>
+            <button type="button"
+                class="btn btn-primary"
+                data-action="startBackup">
+                    <i class="bi bi-cloud-upload me-1"></i>
+                    <span class="label"
+                        data-i18n-title="backup.create"
+                        data-i18n-aria-label="backup.create"
+                        data-i18n="backup.create">
+                            Create Backup
+                        </span>
             </button>
-            <button type="button" class="btn btn-primary" data-action="startRestore">
-                <i class="bi bi-cloud-download me-1"></i><span class="label">Restore Restore</span>
+            <button type="button"
+                class="btn btn-primary"
+                data-action="startRestore">
+                    <i class="bi bi-cloud-download me-1"></i>
+                    <span class="label"
+                        data-i18n-title="backup.restore"
+                        data-i18n-aria-label="backup.restore"
+                        data-i18n="backup.restore">
+                        Restore Backup
+                    </span>
             </button>
-            <button type="button" class="btn btn-primary" data-action="refreshBackupList">
-                <i class="bi bi-arrow-clockwise me-1"></i><span class="label">Refresh List</span>
+            <button type="button"
+                class="btn btn-primary"
+                data-action="refreshBackupList">
+                    <i class="bi bi-arrow-clockwise me-1"></i>
+                    <span class="label"
+                        data-i18n-title="backup.refresh"
+                        data-i18n-aria-label="backup.refresh"
+                        data-i18n="backup.refresh">
+                        Refresh List
+                    </span>
             </button>
-            <button type="button" class="btn btn-outline-primary" data-action="closeBackupModal">
-                <i class="bi bi-x-circle me-1"></i><span class="label">Cancel</span>
+            <button type="button"
+                class="btn btn-outline-primary"
+                data-action="closeBackupModal">
+                    <i class="bi bi-x-circle me-1"></i>
+                    <span class="label"
+                        data-i18n-title="common.close"
+                        data-i18n-aria-label="common.close"
+                        data-i18n="common.close">
+                        Cancel
+                    </span>
             </button>
         </div>
     </div>
 </div>
 
-<!-- Modal Health -->
+<!-- Modal health -->
 <div id="healthModal" class="modal" role="dialog" aria-modal="true" aria-labelledby="healthTitle">
     <div class="modal-dialog modal-dialog-centered">
         <div class="modal-content modal-content-md addhost-modal">
             <div class="addhost-header d-flex justify-content-between align-items-center">
                 <div class="d-flex align-items-center gap-2">
                     <span class="title-icon"><i class="bi bi-heart-pulse"></i></span>
-                    <span id="healthTitle" class="modal-title">Health Status</span>
+                    <span id="healthTitle" class="modal-title" data-i18n="health.modal.title">Health Status</span>
                 </div>
-                <button type="button" class="btn-close" title="Close" aria-label="Close" data-action="closeHealthModal"></button>
+                <button type="button"
+                    class="btn-close"
+                    data-i18n-title="common.close"
+                    data-i18n-aria-label="common.close"
+                    data-action="closeHealthModal">
+                </button>
             </div>
 
             <div class="modal-body">
                 <!-- Loading -->
                 <div id="healthLoading" class="d-flex align-items-center gap-2">
-                    <div class="spinner-border text-primary spinner-border-sm" role="status" aria-label="Loading"></div>
-                        <span class="text-muted">Loading status…</span>
+                    <div class="spinner-border text-primary spinner-border-sm" role="status" data-i18n-aria-label="health.loading"></div>
+                        <span class="text-muted" data-i18n="health.loading">Loading status…</span>
                     </div>
 
                     <!-- Content -->
                     </div>
 
                 <!-- Error -->
-                <div id="healthError" class="alert alert-danger d-none" role="alert" aria-live="assertive">
+                <div id="healthError" class="alert alert-danger d-none" role="alert" aria-live="assertive" data-i18n="health.error">
                     Error fetching health status. Please try again later.
                 </div>
             </div>
 
             <div class="modal-footer modal-buttons">
-                <button type="button" class="btn btn-outline-primary" data-action="closeHealthModal">
-                    <i class="bi bi-x-circle me-1"></i><span class="label">Close</span>
+                <button type="button"
+                    class="btn btn-outline-primary"
+                    data-i18n-title="common.close"
+                    data-i18n-aria-label="common.close"
+                    data-action="closeHealthModal">
+                        <i class="bi bi-x-circle me-1"></i><span class="label" data-i18n="common.close">Close</span>
                 </button>
             </div>
         </div>
     </div>
 </div>
 
-<!-- EditConfig -->
+<!-- Modal settings -->
 <div class="modal fade" id="editConfigModal" tabindex="-1" aria-labelledby="editConfigTitle" aria-hidden="true">
-    <div class="modal-dialog modal-dialog-centered"><!-- modal-sm|md|lg se vuoi cambiare -->
+    <div class="modal-dialog modal-dialog-centered">
         <div class="modal-content modal-content-sm addhost-modal">
-            <!-- Header scuro con logo/brand -->
+            <!-- Dark Header with logo -->
             <div class="modal-header addhost-header">
                 <div class="d-flex align-items-center gap-2">
                     <!-- Emoji o icona -->
                     <span class="title-icon" aria-hidden="true"><i class="bi bi-diagram-2"></i></span>
-                    <h5 class="modal-title mb-0" id="editConfigTitle">Modifica Parametro</h5>
+                    <h5 class="modal-title mb-0" id="editConfigTitle" data-i18n="settings.edit.title">Edit Configuration</h5>
                 </div>
-                <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Chiudi"></button>
+                <button type="button"
+                    class="btn-close btn-close-white"
+                    data-bs-dismiss="modal"
+                    data-i18n-title="common.close"
+                    data-i18n-aria-label="common.close">
+                </button>
             </div>
 
             <div class="modal-body">
                 <form id="editConfigForm">
                     <div class="mb-2">
-                        <label for="configKey" class="form-label">Configuration Parameter</label>
+                        <label for="configKey" class="form-label" data-i18n="settings.modal.parameter">Configuration Parameter</label>
                         <input type="text" id="configKey" class="form-control" readonly>
                     </div>
 
                     <div class="mb-2">
-                        <label for="configDescription" class="form-label">Description</label>
+                        <label for="configDescription" class="form-label" data-i18n="common.description">Description</label>
                         <input type="text" id="configDescription" class="form-control" readonly>
                     </div>
 
                     <!-- Boolean Input -->
                     <div class="mb-2 d-none" id="configValueBoolGroup">
-                        <label class="form-label">Value</label>
+                        <label class="form-label" data-i18n="settings.value">Value</label>
                         <div class="form-check form-switch">
                             <input type="checkbox" id="configValueBool" class="form-check-input">
-                            <label class="form-check-label" for="configValueBool">Enabled</label>
+                            <label class="form-check-label" for="configValueBool" data-i18n="settings.enabled">Enabled</label>
                         </div>
                     </div>
 
                     <!-- Range Input -->
                     <div class="mb-2 d-none" id="configValueRangeGroup">
-                        <label for="configValueRange" class="form-label">Value</label>
+                        <label for="configValueRange" class="form-label" data-i18n="settings.value">Value</label>
                         <input type="range" id="configValueRange" class="form-range">
                         <div class="small text-muted">
-                            Current: <span id="configValueRangeDisplay"></span>
+                            <span data-i18n="settings.current">Current</span>:
+                            <span id="configValueRangeDisplay"></span>
                         </div>
                     </div>
 
                     <!-- Select Input -->
                     <div class="mb-2 d-none" id="configValueSelectGroup">
-                        <label for="configValueSelect" class="form-label">Value</label>
+                        <label for="configValueSelect" class="form-label" data-i18n="settings.value">Value</label>
                         <select id="configValueSelect" class="form-select"></select>
                     </div>
 
                     <!-- Text Input -->
                     <div class="mb-2 d-none" id="configValueGroup">
-                        <label for="configValue" class="form-label">Value</label>
+                        <label for="configValue" class="form-label" data-i18n="settings.value">Value</label>
                         <input type="text" id="configValue" class="form-control">
                     </div>
                 </form>
             </div>
 
             <div class="modal-footer">
-                <button type="submit" form="editConfigForm" class="btn btn-primary">
-                    <i class="bi bi-check2"></i>
+                <button type="submit"
+                    form="editConfigForm"
+                    class="btn btn-primary"
+                    data-i18n-title="common.confirm"
+                    data-i18n-aria-label="common.confirm">
+                        <i class="bi bi-check2"></i>
                 </button>
-                <button type="button" class="btn btn-primary" data-bs-dismiss="modal">
-                    <i class="bi bi-x"></i>
+                <button type="button"
+                    class="btn btn-primary"
+                    data-bs-dismiss="modal"
+                    data-i18n-title="common.cancel"
+                    data-i18n-aria-label="common.cancel">
+                        <i class="bi bi-x"></i>
                 </button>
             </div>
         </div>
     <div class="modal-content">
 
       <div class="modal-header">
-        <h5 class="modal-title">Confirmation</h5>
-        <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
+        <h5 class="modal-title" data-i18n="common.confirmation">Confirmation</h5>
+        <button type="button"
+            class="btn-close"
+            data-i18n-title="common.close"
+            data-i18n-aria-label="common.close"
+            data-bs-dismiss="modal">
+        </button>
       </div>
 
-      <div class="modal-body" id="confirmModalBody">
+      <div class="modal-body" id="confirmModalBody" data-i18n="common.are_you_sure">
         Are you sure?
       </div>
 
       <div class="modal-footer">
-        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
-          Cancel
+        <button type="button"
+            class="btn btn-secondary"
+            data-i18n-title="common.cancel"
+            data-i18n-aria-label="common.cancel"
+            data-bs-dismiss="modal"
+            data-i18n="common.cancel">
+                Cancel
         </button>
-        <button type="button" class="btn btn-danger" id="confirmModalOk">
-          Confirm
+        <button type="button"
+            class="btn btn-danger"
+            data-i18n-title="common.confirm"
+            data-i18n-aria-label="common.confirm"
+            id="confirmModalOk"
+            data-i18n="common.confirm">
+                Confirm
         </button>
       </div>
 
index 66cc3b3106ff833eb281ac9652107f9cf449b857..9c6d133b3cf32aaffb70a50549452524e08e822b 100644 (file)
@@ -20,7 +20,7 @@
     <link rel="stylesheet" href="css/layout.css">
 </head>
 
-<body>
+<body data-page-title="dashboard.title">
     <!-- Topbar -->
     <header class="topbar">
         <div class="topbar-inner">
@@ -33,7 +33,7 @@
                     <line x1="12" y1="6" x2="12" y2="18" stroke="var(--accent)" stroke-width="2"></line>
                     <line x1="6" y1="12" x2="18" y2="12" stroke="var(--accent)" stroke-width="2"></line>
                 </svg>
-                <span>Network Manager</span>
+                <span data-i18n="app.name">Network Manager</span>
             </a>
 
             <nav class="navbar">
 
                 <!-- Desktop nav -->
                 <div class="d-none d-md-flex gap-2 ms-auto">
-                        <a href="/hosts"    class="btn btn-primary">       Hosts</a>
-                        <a href="/aliases"  class="btn btn-primary">       Alias</a>
-                        <a href="/leases"   class="btn btn-primary">       DHCP Leases</a>
-                        <a href="/devices"  class="btn btn-primary">       Devices</a>
-                        <a href="/logs"     class="btn btn-primary">       Logs</a>
-                        <a href="/settings" class="btn btn-primary active">Settings</a>
-                        <button class="btn btn-primary btn-logout">Logout</button>
+                        <a href="/hosts"    class="btn btn-primary"        data-i18n="hosts.title">Hosts</a>
+                        <a href="/aliases"  class="btn btn-primary"        data-i18n="aliases.title">Alias</a>
+                        <a href="/leases"   class="btn btn-primary"        data-i18n="dhcp.title">DHCP Leases</a>
+                        <a href="/devices"  class="btn btn-primary"        data-i18n="devices.title">Devices</a>
+                        <a href="/logs"     class="btn btn-primary"        data-i18n="logs.title">Logs</a>
+                        <a href="/settings" class="btn btn-primary active" data-i18n="settings.title">Settings</a>
+                        <button class="btn btn-primary btn-logout"         data-i18n="auth.logout">Logout</button>
                 </div>
             </nav>
 
             <div class="offcanvas offcanvas-end d-md-none" tabindex="-1" id="mobileMenu">
 
                 <div class="offcanvas-header">
-                    <h5 class="offcanvas-title">Network Manager</h5>
+                    <h5 class="offcanvas-title" data-i18n="app.name">Network Manager</h5>
 
                     <button
                         type="button"
                         class="btn-close"
+                        data-i18n-title="common.close"
+                        data-i18n-aria-label="common.close"
                         data-bs-dismiss="offcanvas">
                     </button>
                 </div>
 
                 <div class="offcanvas-body">
                     <div class="d-grid gap-2">
-                        <a href="/hosts"    class="btn btn-primary">       Hosts</a>
-                        <a href="/aliases"  class="btn btn-primary">       Alias</a>
-                        <a href="/leases"   class="btn btn-primary">       DHCP Leases</a>
-                        <a href="/devices"  class="btn btn-primary">       Devices</a>
-                        <a href="/logs"     class="btn btn-primary">       Logs</a>
-                        <a href="/settings" class="btn btn-primary active">Settings</a>
-                        <button class="btn btn-primary btn-logout">Logout</button>
+                        <a href="/hosts"    class="btn btn-primary"        data-i18n="hosts.title">Hosts</a>
+                        <a href="/aliases"  class="btn btn-primary"        data-i18n="aliases.title">Alias</a>
+                        <a href="/leases"   class="btn btn-primary"        data-i18n="dhcp.title">DHCP Leases</a>
+                        <a href="/devices"  class="btn btn-primary"        data-i18n="devices.title">Devices</a>
+                        <a href="/logs"     class="btn btn-primary"        data-i18n="logs.title">Logs</a>
+                        <a href="/settings" class="btn btn-primary active" data-i18n="settings.title">Settings</a>
+                        <button class="btn btn-primary btn-logout"         data-i18n="auth.logout">Logout</button>
                     </div>
                 </div>
             </div>
                 <div class="col-auto col-md-auto">
                     <h2 class="mb-0 d-flex align-items-center gap-2 lh-1">
                         <span class="title-icon"><i class="bi bi-gear"></i></span>
-                        <span class="section-title">Settings</span>
+                        <span class="section-title" data-i18n="settings.page.title">Settings</span>
                     </h2>
                 </div>
 
                         <input
                             type="text"
                             id="searchInput"
-                            placeholder="Search..."
                             class="form-control form-control-sm placeholder-italic"
-                            aria-label="Search config">
+                            data-i18n-placeholder="common.search.placeholder"
+                            data-i18n-aria-label="settings.search">
                     </div>
                 </div>
 
                     <!-- Separator -->
                     <div class="vr mx-2 d-none d-md-block"></div>
 
-                    <!-- Expand -->
+                    <!-- Expand Desktop -->
                     <button
-                        class="btn btn-primary d-none d-md-flex align-items-center gap-2 px-3"
-                        title="Expand all groups"
-                        aria-label="Expand all groups"
-                        id="expandAllBtn">
+                        class="btn btn-primary expand-all-btn d-none d-md-flex align-items-center gap-2 px-3"
+                        data-i18n-title="common.expand.description"
+                        data-i18n-aria-label="common.expand.description">
                         <i class="bi bi-arrows-expand"></i>
-                        <span class="label">Expand</span>
+                        <span class="label" data-i18n="common.expand">Expand</span>
                     </button>
 
-                    <!-- Collapse -->
+                    <!-- Collapse Desktop -->
                     <button
-                        class="btn btn-primary d-none d-md-flex align-items-center gap-2 px-3"
-                        title="Collapse all groups"
-                        aria-label="Collapse all groups"
-                        id="collapseAllBtn">
+                        class="btn btn-primary collapse-all-btn d-none d-md-flex align-items-center gap-2 px-3"
+                        data-i18n-title="common.collapse.description"
+                        data-i18n-aria-label="common.collapse.description">
                         <i class="bi bi-arrows-collapse"></i>
-                        <span class="label">Collapse</span>
+                        <span class="label" data-i18n="common.collapse">Collapse</span>
                     </button>
 
                     <!-- Separator -->
                     <!-- Admin Panel -->
                     <div id="adminDropdown" class="dropdown">
                         <button
-                            id="adminBtn"
                             class="btn btn-primary dropdown-toggle d-none d-md-flex align-items-center gap-2 px-3"
-                            data-bs-toggle="dropdown">
+                            data-i18n-title="settings.admin.description"
+                            data-i18n-aria-label="settings.admin.description"
+                            data-bs-toggle="dropdown"
+                            id="adminBtn">
                             <i class="bi bi-gear"></i>
-                            <span class="label d-none d-md-inline">Admin</span>
+                            <span class="label d-none d-md-inline" data-i18n="settings.admin">Admin</span>
                         </button>
 
                         <ul class="dropdown-menu dropdown-menu-end shadow">
 
-                            <!-- Reload DNS -->
+                            <!-- Reload DNS Desktop -->
                             <li class="px-2 py-1">
                                 <button
                                     class="btn btn-primary w-100 d-flex align-items-center gap-2 text-nowrap"
-                                    title="Reload DNS (BIND)"
-                                    aria-label="Reload DNS"
+                                    data-i18n-title="dns.reload.description"
+                                    data-i18n-aria-label="dns.reload.description"
                                     data-action="reloadDns">
                                     <i class="bi bi-arrow-clockwise"></i>
-                                    <span class="label d-md-inline">Reload DNS</span>
+                                    <span class="label d-md-inline" data-i18n="dns.reload">Reload DNS</span>
                                 </button>
                             </li>
 
-                            <!-- Reload DHCP -->
+                            <!-- Reload DHCP Desktop -->
                             <li class="px-2 py-1">
                                 <button
                                     class="btn btn-primary w-100 d-flex align-items-center gap-2 text-nowrap"
-                                    title="Reload DHCP (Kea)"
-                                    aria-label="Reload DHCP"
+                                    data-i18n-title="dhcp.reload.description"
+                                    data-i18n-aria-label="dhcp.reload.description"
                                     data-action="reloadDhcp">
                                     <i class="bi bi-arrow-clockwise"></i>
-                                    <span class="label d-md-inline">Reload DHCP</span>
+                                    <span class="label d-md-inline" data-i18n="dhcp.reload">Reload DHCP</span>
                                 </button>
                             </li>
 
                             <li class="px-2 py-1">
                                 <button
                                     class="btn btn-danger w-100 d-flex align-items-center gap-2 text-nowrap"
-                                    title="Restart system"
-                                    aria-label="Restart system"
+                                    data-i18n-title="app.restart.description"
+                                    data-i18n-aria-label="app.restart.description"
                                     data-action="restartApp">
                                     <i class="bi bi-power"></i>
-                                    <span class="label d-md-inline">Restart</span>
+                                    <span class="label d-md-inline" data-i18n="app.restart">Restart</span>
                                 </button>
                             </li>
                         </ul>
                             <!-- Expand Mobile -->
                             <li class="px-2 py-1">
                                 <button
-                                    class="btn btn-primary w-100 d-flex align-items-center gap-2"
-                                    title="Expand all groups"
-                                    aria-label="Expand all groups"
-                                    id="expandAllBtn">
+                                    class="btn btn-primary expand-all-btn w-100 d-flex align-items-center gap-2"
+                                    data-i18n-title="common.expand.description"
+                                    data-i18n-aria-label="common.expand.description">
                                     <i class="bi bi-arrows-expand"></i>
-                                    <span class="label">Expand</span>
+                                    <span class="label" data-i18n="common.expand">Expand</span>
                                 </button>
                             </li>
 
                             <!-- Collapse Mobile -->
                             <li class="px-2 py-1">
                                 <button
-                                    class="btn btn-primary w-100 d-flex align-items-center gap-2"
-                                    title="Collapse all groups"
-                                    aria-label="Collapse all groups"
-                                    id="collapseAllBtn">
+                                    class="btn btn-primary collapse-all-btn w-100 d-flex align-items-center gap-2"
+                                    data-i18n-title="common.collapse.description"
+                                    data-i18n-aria-label="common.collapse.description">
                                     <i class="bi bi-arrows-collapse"></i>
-                                    <span class="label">Collapse</span>
+                                    <span class="label" data-i18n="common.collapse">Collapse</span>
                                 </button>
                             </li>
 
                             <li class="px-2 py-1">
                                 <button
                                     class="btn btn-primary w-100 d-flex align-items-center gap-2"
-                                    title="Reload DNS (BIND)"
-                                    aria-label="Reload DNS"
+                                    data-i18n-title="dns.reload.description"
+                                    data-i18n-aria-label="dns.reload.description"
                                     data-action="reloadDns">
-                                    <i class="bi bi-arrow-clockwise"></i>
-                                    <span class="label d-md-inline">Reload DNS</span>
+                                    <i class="bi bi-arrow-repeat"></i>
+                                    <span data-i18n="dns.reload">Reload DNS</span>
                                 </button>
                             </li>
 
                             <li class="px-2 py-1">
                                 <button
                                     class="btn btn-primary w-100 d-flex align-items-center gap-2"
-                                    title="Reload DHCP (Kea)"
-                                    aria-label="Reload DHCP"
+                                    data-i18n-title="dhcp.reload.description"
+                                    data-i18n-aria-label="dhcp.reload.description"
                                     data-action="reloadDhcp">
                                     <i class="bi bi-arrow-clockwise"></i>
-                                    <span class="label d-md-inline">Reload DHCP</span>
+                                    <span class="label d-md-inline" data-i18n="dhcp.reload">Reload DHCP</span>
                                 </button>
                             </li>
 
                             <li class="px-2 py-1">
                                 <button
                                     class="btn btn-danger w-100 d-flex align-items-center gap-2"
-                                    title="Restart system"
-                                    aria-label="Restart system"
+                                    data-i18n-title="app.reload.description"
+                                    data-i18n-aria-label="app.reload.description"
                                     data-action="restartApp">
                                     <i class="bi bi-arrow-repeat"></i>
-                                    <span class="label">Restart</span>
+                                    <span class="label" data-i18n="app.reload">Restart</span>
                                 </button>
                             </li>
                         </ul>
                 <input
                     type="text"
                     id="searchInputMobile"
-                    placeholder="Search..."
                     class="form-control form-control-sm placeholder-italic"
-                    aria-label="Search config">
+                    data-i18n-placeholder="common.search.placeholder"
+                    data-i18n-aria-label="settings.search">
             </div>
         </div>
     </section>
             <thead class="table-light">
                 <tr>
                     <th data-type="string" data-sortable="false">
-                        Parameter <span class="sort-arrow" aria-hidden="true"></span>
+                        <span data-i18n="settings.parameter">Parameter</span>
                     </th>
 
                     <th data-type="string" data-sortable="false">
-                        Value <span class="sort-arrow" aria-hidden="true"></span>
+                        <span data-i18n="settings.value">Value</span>
                     </th>
 
                     <th data-type="string" data-sortable="false">
-                        Description <span class="sort-arrow" aria-hidden="true"></span>
+                        <span data-i18n="common.description">Description</span>
                     </th>
 
                     <th data-type="string" data-sortable="false" class="text-center text-nowrap">
-                        Actions
+                        <span data-i18n="common.actions">Actions</span>
                     </th>
                 </tr>
             </thead>
     <!-- Loader -->
     <div id="loader" class="text-center my-3" style="display: none;">
         <div class="spinner-border text-primary" role="status">
-            <span class="visually-hidden">Loading...</span>
+            <span class="visually-hidden" data-i18n="common.loading">Loading...</span>
         </div>
     </div>
-    <div id="devices-container"></div>
 
     <!-- Modals -->
     <div id="modals-container"></div>