diff --git a/web/static/i18n.js b/web/static/i18n.js
new file mode 100644
index 0000000..bf035ba
--- /dev/null
+++ b/web/static/i18n.js
@@ -0,0 +1,199 @@
+(function () {
+ var config = window.__TC_I18N_CONFIG || {};
+ var DEFAULT_LANG = config.defaultLang || 'it';
+ var STORAGE_KEY = config.storageKey || 'tc_lang';
+ var SERVER_LANG = config.serverLang || '';
+ var IS_AUTHENTICATED = !!config.isAuthenticated;
+ var dictionaries = {
+ it: {
+ 'nav.open_main_menu': 'Apri menu principale', 'nav.open_user_menu': 'Apri menu utente', 'nav.dashboard': 'Dashboard', 'nav.users': 'Users', 'nav.admin': 'Admin', 'nav.login': 'Login', 'nav.signup': 'Signup', 'nav.logout': 'Logout',
+ 'home.subtitle': 'Accedi o registrati per continuare.', 'home.login': 'Accedi', 'home.signup': 'Registrati',
+ 'login.title': 'Login', 'login.subtitle': 'Accedi al tuo account.', 'form.email': 'Email', 'form.password': 'Password', 'login.submit': 'Entra', 'login.forgot': 'Password dimenticata?', 'login.create_account': 'Crea account',
+ 'signup.title': 'Registrazione', 'signup.subtitle': 'Crea il tuo account.', 'signup.submit': 'Registrati', 'signup.has_account': 'Hai già un account?', 'signup.login': 'Accedi',
+ 'forgot.title': 'Password dimenticata', 'forgot.subtitle': 'Inserisci la tua email per ricevere il link di reset.', 'forgot.submit': 'Invia link reset', 'forgot.back_login': 'Torna al login',
+ 'reset.title': 'Reset password', 'reset.subtitle': 'Imposta una nuova password.', 'reset.new_password': 'Nuova password', 'reset.submit': 'Aggiorna password', 'reset.invalid_token': 'Token mancante o non valido.',
+ 'verify.title': 'Verifica email', 'verify.p1': 'Controlla la casella di posta e apri il link di verifica ricevuto.', 'verify.p2': 'Se il link è scaduto, ripeti la registrazione o contatta supporto.', 'verify.go_login': 'Vai al login',
+ 'private.title': 'Dashboard', 'private.back_prefix': 'Bentornato', 'private.generic': 'Benvenuto.', 'private.quick_links': 'Link rapidi',
+ 'admin.dashboard.title': 'Admin Dashboard', 'admin.dashboard.area': 'Area amministrazione.', 'admin.users_count': 'Utenti', 'admin.current_role': 'Ruolo corrente', 'admin.navigation': 'Navigazione', 'admin.manage_users': 'Gestione utenti',
+ 'users.title': 'Users', 'users.subtitle': 'Ricerca, ordinamento e paging server-side.', 'users.new_user': 'Nuovo Utente', 'users.search': 'Search', 'users.search_placeholder': 'Cerca nome o email', 'users.page_size': 'Page size', 'users.search_button': 'Cerca', 'users.user_detail': 'Dettaglio utente', 'users.actions': 'Azioni', 'users.open': 'Apri', 'users.none': 'Nessun utente trovato.', 'users.total': 'Totale', 'users.users_label': 'utenti', 'users.page': 'Pagina', 'users.prev': 'Prev', 'users.next': 'Next', 'users.close': 'Chiudi',
+ 'users.new_user_modal_title': 'Nuovo utente', 'users.new_user_modal_placeholder': 'Placeholder UI. La creazione utente può essere collegata a una route backend quando disponibile.',
+ 'table.id': 'ID', 'table.name': 'Name', 'table.email': 'Email', 'table.role': 'Role', 'user.role_admin': 'admin', 'user.role_user': 'user', 'user.verified': 'Verificato', 'user.created': 'Creato', 'user.yes': 'sì', 'user.no': 'no',
+ 'audit.title': 'Audit Logs', 'audit.activity': 'Attività', 'audit.security': 'Sicurezza', 'audit.timestamp': 'Timestamp', 'audit.actor': 'Attore', 'audit.action': 'Azione', 'audit.placeholder': 'Placeholder log di sicurezza.'
+ },
+ en: {
+ 'nav.open_main_menu': 'Open main menu', 'nav.open_user_menu': 'Open user menu', 'nav.dashboard': 'Dashboard', 'nav.users': 'Users', 'nav.admin': 'Admin', 'nav.login': 'Login', 'nav.signup': 'Signup', 'nav.logout': 'Logout',
+ 'home.subtitle': 'Login or sign up to continue.', 'home.login': 'Login', 'home.signup': 'Sign up',
+ 'login.title': 'Login', 'login.subtitle': 'Access your account.', 'form.email': 'Email', 'form.password': 'Password', 'login.submit': 'Sign in', 'login.forgot': 'Forgot password?', 'login.create_account': 'Create account',
+ 'signup.title': 'Signup', 'signup.subtitle': 'Create your account.', 'signup.submit': 'Sign up', 'signup.has_account': 'Already have an account?', 'signup.login': 'Login',
+ 'forgot.title': 'Forgot Password', 'forgot.subtitle': 'Enter your email to receive a reset link.', 'forgot.submit': 'Send reset link', 'forgot.back_login': 'Back to login',
+ 'reset.title': 'Reset Password', 'reset.subtitle': 'Set a new password.', 'reset.new_password': 'New password', 'reset.submit': 'Update password', 'reset.invalid_token': 'Missing or invalid token.',
+ 'verify.title': 'Verify email', 'verify.p1': 'Check your inbox and open the verification link.', 'verify.p2': 'If the link expired, sign up again or contact support.', 'verify.go_login': 'Go to login',
+ 'private.title': 'Dashboard', 'private.back_prefix': 'Signed in as', 'private.generic': 'Signed in.', 'private.quick_links': 'Quick links',
+ 'admin.dashboard.title': 'Admin Dashboard', 'admin.dashboard.area': 'Administration area.', 'admin.users_count': 'Users', 'admin.current_role': 'Current role', 'admin.navigation': 'Navigation', 'admin.manage_users': 'Manage users',
+ 'users.title': 'Users', 'users.subtitle': 'Search, sorting and server-side paging.', 'users.new_user': 'New user', 'users.search': 'Search', 'users.search_placeholder': 'Search by name or email', 'users.page_size': 'Page size', 'users.search_button': 'Search', 'users.user_detail': 'User details', 'users.actions': 'Actions', 'users.open': 'Open', 'users.none': 'No users found.', 'users.total': 'Total', 'users.users_label': 'users', 'users.page': 'Page', 'users.prev': 'Prev', 'users.next': 'Next', 'users.close': 'Close',
+ 'users.new_user_modal_title': 'New user', 'users.new_user_modal_placeholder': 'UI placeholder. Connect creation to backend route when available.',
+ 'table.id': 'ID', 'table.name': 'Name', 'table.email': 'Email', 'table.role': 'Role', 'user.role_admin': 'admin', 'user.role_user': 'user', 'user.verified': 'Verified', 'user.created': 'Created', 'user.yes': 'yes', 'user.no': 'no',
+ 'audit.title': 'Audit Logs', 'audit.activity': 'Activity', 'audit.security': 'Security', 'audit.timestamp': 'Timestamp', 'audit.actor': 'Actor', 'audit.action': 'Action', 'audit.placeholder': 'Security logs placeholder.'
+ },
+ en_us: {},
+ de: {
+ 'nav.open_main_menu': 'Hauptmenü öffnen', 'nav.open_user_menu': 'Benutzermenü öffnen', 'nav.dashboard': 'Dashboard', 'nav.users': 'Benutzer', 'nav.admin': 'Admin', 'nav.login': 'Login', 'nav.signup': 'Registrieren', 'nav.logout': 'Abmelden',
+ 'home.subtitle': 'Melden Sie sich an oder registrieren Sie sich, um fortzufahren.', 'home.login': 'Anmelden', 'home.signup': 'Registrieren', 'login.title': 'Login', 'login.subtitle': 'Melden Sie sich in Ihrem Konto an.', 'form.email': 'E-Mail', 'form.password': 'Passwort', 'login.submit': 'Anmelden', 'login.forgot': 'Passwort vergessen?', 'login.create_account': 'Konto erstellen',
+ 'signup.title': 'Registrierung', 'signup.subtitle': 'Erstellen Sie Ihr Konto.', 'signup.submit': 'Registrieren', 'signup.has_account': 'Haben Sie bereits ein Konto?', 'signup.login': 'Anmelden',
+ 'forgot.title': 'Passwort vergessen', 'forgot.subtitle': 'Geben Sie Ihre E-Mail ein, um einen Reset-Link zu erhalten.', 'forgot.submit': 'Reset-Link senden', 'forgot.back_login': 'Zurück zum Login',
+ 'reset.title': 'Passwort zurücksetzen', 'reset.subtitle': 'Legen Sie ein neues Passwort fest.', 'reset.new_password': 'Neues Passwort', 'reset.submit': 'Passwort aktualisieren', 'reset.invalid_token': 'Token fehlt oder ist ungültig.',
+ 'verify.title': 'E-Mail verifizieren', 'verify.p1': 'Öffnen Sie die Verifizierungs-E-Mail in Ihrem Posteingang.', 'verify.p2': 'Wenn der Link abgelaufen ist, registrieren Sie sich erneut oder kontaktieren Sie den Support.', 'verify.go_login': 'Zum Login',
+ 'private.title': 'Dashboard', 'private.back_prefix': 'Willkommen zurück', 'private.generic': 'Willkommen.', 'private.quick_links': 'Schnelllinks',
+ 'admin.dashboard.title': 'Admin-Dashboard', 'admin.dashboard.area': 'Administrationsbereich.', 'admin.users_count': 'Benutzer', 'admin.current_role': 'Aktuelle Rolle', 'admin.navigation': 'Navigation', 'admin.manage_users': 'Benutzer verwalten',
+ 'users.title': 'Benutzer', 'users.subtitle': 'Suche, Sortierung und serverseitiges Paging.', 'users.new_user': 'Neuer Benutzer', 'users.search': 'Suche', 'users.search_placeholder': 'Nach Name oder E-Mail suchen', 'users.page_size': 'Seitengröße', 'users.search_button': 'Suchen', 'users.user_detail': 'Benutzerdetails', 'users.actions': 'Aktionen', 'users.open': 'Öffnen', 'users.none': 'Keine Benutzer gefunden.', 'users.total': 'Gesamt', 'users.users_label': 'Benutzer', 'users.page': 'Seite', 'users.prev': 'Zurück', 'users.next': 'Weiter', 'users.close': 'Schließen',
+ 'users.new_user_modal_title': 'Neuer Benutzer', 'users.new_user_modal_placeholder': 'Placeholder-UI. Bei Bedarf mit Backend-Route verbinden.',
+ 'table.id': 'ID', 'table.name': 'Name', 'table.email': 'E-Mail', 'table.role': 'Rolle', 'user.role_admin': 'admin', 'user.role_user': 'user', 'user.verified': 'Verifiziert', 'user.created': 'Erstellt', 'user.yes': 'ja', 'user.no': 'nein',
+ 'audit.title': 'Audit-Logs', 'audit.activity': 'Aktivität', 'audit.security': 'Sicherheit', 'audit.timestamp': 'Zeitstempel', 'audit.actor': 'Akteur', 'audit.action': 'Aktion', 'audit.placeholder': 'Platzhalter für Sicherheitsprotokolle.'
+ },
+ fr: {
+ 'nav.open_main_menu': 'Ouvrir le menu principal', 'nav.open_user_menu': 'Ouvrir le menu utilisateur', 'nav.dashboard': 'Tableau de bord', 'nav.users': 'Utilisateurs', 'nav.admin': 'Admin', 'nav.login': 'Connexion', 'nav.signup': 'Inscription', 'nav.logout': 'Déconnexion',
+ 'home.subtitle': 'Connectez-vous ou inscrivez-vous pour continuer.', 'home.login': 'Connexion', 'home.signup': 'Inscription', 'login.title': 'Connexion', 'login.subtitle': 'Accédez à votre compte.', 'form.email': 'Email', 'form.password': 'Mot de passe', 'login.submit': 'Se connecter', 'login.forgot': 'Mot de passe oublié ?', 'login.create_account': 'Créer un compte',
+ 'signup.title': 'Inscription', 'signup.subtitle': 'Créez votre compte.', 'signup.submit': 'S’inscrire', 'signup.has_account': 'Vous avez déjà un compte ?', 'signup.login': 'Connexion',
+ 'forgot.title': 'Mot de passe oublié', 'forgot.subtitle': 'Entrez votre email pour recevoir un lien de réinitialisation.', 'forgot.submit': 'Envoyer le lien', 'forgot.back_login': 'Retour à la connexion',
+ 'reset.title': 'Réinitialiser le mot de passe', 'reset.subtitle': 'Définissez un nouveau mot de passe.', 'reset.new_password': 'Nouveau mot de passe', 'reset.submit': 'Mettre à jour', 'reset.invalid_token': 'Jeton manquant ou invalide.',
+ 'verify.title': 'Vérifier l’email', 'verify.p1': 'Vérifiez votre boîte mail et ouvrez le lien de vérification.', 'verify.p2': 'Si le lien a expiré, réinscrivez-vous ou contactez le support.', 'verify.go_login': 'Aller à la connexion',
+ 'private.title': 'Tableau de bord', 'private.back_prefix': 'Bon retour', 'private.generic': 'Bienvenue.', 'private.quick_links': 'Liens rapides',
+ 'admin.dashboard.title': 'Tableau de bord admin', 'admin.dashboard.area': 'Zone d’administration.', 'admin.users_count': 'Utilisateurs', 'admin.current_role': 'Rôle actuel', 'admin.navigation': 'Navigation', 'admin.manage_users': 'Gérer les utilisateurs',
+ 'users.title': 'Utilisateurs', 'users.subtitle': 'Recherche, tri et pagination côté serveur.', 'users.new_user': 'Nouvel utilisateur', 'users.search': 'Recherche', 'users.search_placeholder': 'Rechercher par nom ou email', 'users.page_size': 'Taille de page', 'users.search_button': 'Rechercher', 'users.user_detail': 'Détails utilisateur', 'users.actions': 'Actions', 'users.open': 'Ouvrir', 'users.none': 'Aucun utilisateur trouvé.', 'users.total': 'Total', 'users.users_label': 'utilisateurs', 'users.page': 'Page', 'users.prev': 'Préc.', 'users.next': 'Suiv.', 'users.close': 'Fermer',
+ 'users.new_user_modal_title': 'Nouvel utilisateur', 'users.new_user_modal_placeholder': 'UI placeholder. Connecter à une route backend si nécessaire.',
+ 'table.id': 'ID', 'table.name': 'Nom', 'table.email': 'Email', 'table.role': 'Rôle', 'user.role_admin': 'admin', 'user.role_user': 'user', 'user.verified': 'Vérifié', 'user.created': 'Créé', 'user.yes': 'oui', 'user.no': 'non',
+ 'audit.title': 'Journaux d’audit', 'audit.activity': 'Activité', 'audit.security': 'Sécurité', 'audit.timestamp': 'Horodatage', 'audit.actor': 'Acteur', 'audit.action': 'Action', 'audit.placeholder': 'Espace réservé des journaux de sécurité.'
+ }
+ };
+
+ dictionaries.en_us = Object.assign({}, dictionaries.en);
+ dictionaries.de_ch = Object.assign({}, dictionaries.de);
+ dictionaries.fr_ch = Object.assign({}, dictionaries.fr);
+
+ function normalizeLang(lang) {
+ if (!lang) return '';
+ return String(lang).trim().toLowerCase().replace('-', '_');
+ }
+
+ function isSupportedLang(lang) {
+ return !!dictionaries[normalizeLang(lang)];
+ }
+
+ function getLang() {
+ var serverLang = normalizeLang(SERVER_LANG);
+ if (IS_AUTHENTICATED && isSupportedLang(serverLang)) {
+ localStorage.setItem(STORAGE_KEY, serverLang);
+ return serverLang;
+ }
+ var stored = localStorage.getItem(STORAGE_KEY);
+ return isSupportedLang(stored) ? normalizeLang(stored) : DEFAULT_LANG;
+ }
+
+ function t(key, lang) {
+ if (dictionaries[lang] && dictionaries[lang][key]) return dictionaries[lang][key];
+ if (dictionaries.it[key]) return dictionaries.it[key];
+ return key;
+ }
+
+ function localeFromLang(lang) {
+ var localeMap = {
+ it: 'it-IT',
+ en: 'en-GB',
+ en_us: 'en-US',
+ de: 'de-DE',
+ fr: 'fr-FR',
+ de_ch: 'de-CH',
+ fr_ch: 'fr-CH'
+ };
+ return localeMap[lang] || 'it-IT';
+ }
+
+ function localizeDate(rawValue, lang) {
+ if (!rawValue) return '';
+ var date = new Date(rawValue);
+ if (isNaN(date.getTime())) return rawValue;
+ return new Intl.DateTimeFormat(localeFromLang(lang), {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit'
+ }).format(date);
+ }
+
+ function applyTranslations(root) {
+ var lang = getLang();
+ document.documentElement.setAttribute('lang', lang.replace('_', '-'));
+ var langSelect = document.getElementById('lang-select');
+ var flag = document.getElementById('lang-flag');
+ if (langSelect && langSelect.value !== lang) langSelect.value = lang;
+ if (flag) {
+ var flags = {
+ it: '/static/vendor/flags/it.svg',
+ en: '/static/vendor/flags/en.svg',
+ en_us: '/static/vendor/flags/en_us.svg',
+ de: '/static/vendor/flags/de.svg',
+ fr: '/static/vendor/flags/fr.svg',
+ de_ch: '/static/vendor/flags/ch.svg',
+ fr_ch: '/static/vendor/flags/ch.svg'
+ };
+ var labels = {
+ it: 'Italiano',
+ en: 'English',
+ en_us: 'English USA',
+ de: 'Deutsch',
+ fr: 'Français',
+ de_ch: 'Deutsch CH',
+ fr_ch: 'Français CH'
+ };
+ flag.src = flags[lang] || '/static/vendor/flags/it.svg';
+ flag.alt = labels[lang] || 'Lingua';
+ flag.style.width = '32px';
+ flag.style.height = '22px';
+ }
+
+ (root || document).querySelectorAll('[data-i18n]').forEach(function (el) {
+ el.textContent = t(el.getAttribute('data-i18n'), lang);
+ });
+
+ (root || document).querySelectorAll('[data-i18n-placeholder]').forEach(function (el) {
+ el.setAttribute('placeholder', t(el.getAttribute('data-i18n-placeholder'), lang));
+ });
+
+ (root || document).querySelectorAll('[data-localize-date]').forEach(function (el) {
+ var rawValue = el.getAttribute('data-localize-date');
+ el.textContent = localizeDate(rawValue, lang);
+ });
+ }
+
+ var langSelect = document.getElementById('');
+ if (langSelect) {
+ langSelect.addEventListener('change', function () {
+ var selectedLang = normalizeLang(langSelect.value);
+ localStorage.setItem(STORAGE_KEY, selectedLang);
+ if (langSelect.dataset.authenticated === '1') {
+ fetch('/preferences/lang', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ body: 'lang=' + encodeURIComponent(selectedLang)
+ }).catch(function () {
+ // Keep UI responsive even if persistence fails.
+ });
+ }
+ applyTranslations(document);
+ });
+ }
+
+ function initI18n() {
+ applyTranslations(document);
+ }
+
+ window.applyTranslations = applyTranslations;
+ window.initI18n = initI18n;
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', initI18n);
+ } else {
+ initI18n();
+ }
+})();
diff --git a/web/templates/layout.html b/web/templates/layout.html
index 00c32c2..9c8791f 100644
--- a/web/templates/layout.html
+++ b/web/templates/layout.html
@@ -7,8 +7,15 @@
+
@@ -33,190 +40,6 @@