From b32f2290309e2365ce4d82615504c980f8a9d5a8 Mon Sep 17 00:00:00 2001 From: fabio Date: Sat, 7 Mar 2026 13:20:02 +0100 Subject: [PATCH] add internationalization support and clean up navbar template --- web/static/i18n.js | 199 ++++++++++++++++++++++++++++++ web/templates/layout.html | 192 ++-------------------------- web/templates/public/_navbar.html | 2 - 3 files changed, 206 insertions(+), 187 deletions(-) create mode 100644 web/static/i18n.js 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 @@