diff --git a/frontend/messages/en.json b/frontend/messages/en.json new file mode 100644 index 0000000..179f935 --- /dev/null +++ b/frontend/messages/en.json @@ -0,0 +1,165 @@ +{ + "common": { + "loading": "Loading...", + "error": "Error", + "success": "Success", + "cancel": "Cancel", + "save": "Save", + "delete": "Delete", + "edit": "Edit", + "close": "Close", + "confirm": "Confirm", + "back": "Back", + "next": "Next", + "submit": "Submit", + "search": "Search", + "filter": "Filter", + "clear": "Clear", + "required": "Required", + "optional": "Optional", + "yes": "Yes", + "no": "No" + }, + "navigation": { + "home": "Home", + "dashboard": "Dashboard", + "settings": "Settings", + "profile": "Profile", + "logout": "Logout", + "login": "Login", + "register": "Register", + "demos": "Demos", + "design": "Design System" + }, + "auth": { + "login": { + "title": "Sign in to your account", + "subtitle": "Enter your email below to sign in to your account", + "emailLabel": "Email", + "emailPlaceholder": "name@example.com", + "passwordLabel": "Password", + "passwordPlaceholder": "Enter your password", + "rememberMe": "Remember me", + "forgotPassword": "Forgot password?", + "loginButton": "Sign In", + "noAccount": "Don't have an account?", + "registerLink": "Sign up", + "success": "Successfully logged in", + "error": "Invalid email or password" + }, + "register": { + "title": "Create an account", + "subtitle": "Enter your information to create an account", + "firstNameLabel": "First Name", + "firstNamePlaceholder": "John", + "lastNameLabel": "Last Name", + "lastNamePlaceholder": "Doe", + "emailLabel": "Email", + "emailPlaceholder": "name@example.com", + "passwordLabel": "Password", + "passwordPlaceholder": "Create a strong password", + "confirmPasswordLabel": "Confirm Password", + "confirmPasswordPlaceholder": "Re-enter your password", + "phoneLabel": "Phone Number", + "phonePlaceholder": "+1 (555) 000-0000", + "registerButton": "Create Account", + "hasAccount": "Already have an account?", + "loginLink": "Sign in", + "success": "Account created successfully", + "error": "Failed to create account" + }, + "passwordReset": { + "title": "Reset your password", + "subtitle": "Enter your email address and we'll send you a reset link", + "emailLabel": "Email", + "emailPlaceholder": "name@example.com", + "sendButton": "Send Reset Link", + "backToLogin": "Back to login", + "success": "Password reset link sent to your email", + "error": "Failed to send reset link" + }, + "passwordChange": { + "title": "Change Password", + "currentPasswordLabel": "Current Password", + "newPasswordLabel": "New Password", + "confirmPasswordLabel": "Confirm New Password", + "changeButton": "Change Password", + "success": "Password changed successfully", + "error": "Failed to change password" + } + }, + "settings": { + "title": "Settings", + "profile": { + "title": "Profile", + "subtitle": "Manage your profile information", + "firstNameLabel": "First Name", + "lastNameLabel": "Last Name", + "emailLabel": "Email", + "phoneLabel": "Phone Number", + "updateButton": "Update Profile", + "success": "Profile updated successfully", + "error": "Failed to update profile" + }, + "password": { + "title": "Password", + "subtitle": "Change your account password", + "currentPasswordLabel": "Current Password", + "newPasswordLabel": "New Password", + "confirmPasswordLabel": "Confirm New Password", + "updateButton": "Update Password", + "success": "Password updated successfully", + "error": "Failed to update password" + }, + "sessions": { + "title": "Sessions", + "subtitle": "Manage your active sessions", + "currentSession": "Current Session", + "activeSession": "Active", + "lastActive": "Last active", + "device": "Device", + "location": "Location", + "revokeButton": "Revoke", + "revokeAll": "Revoke All Other Sessions", + "success": "Session revoked successfully", + "error": "Failed to revoke session" + }, + "preferences": { + "title": "Preferences", + "subtitle": "Customize your experience", + "language": "Language", + "languageDescription": "Select your preferred language", + "theme": "Theme", + "themeDescription": "Choose your color scheme", + "themeLight": "Light", + "themeDark": "Dark", + "themeSystem": "System" + } + }, + "errors": { + "notFound": "Page not found", + "notFoundDescription": "The page you're looking for doesn't exist.", + "unauthorized": "Unauthorized", + "unauthorizedDescription": "You don't have permission to access this page.", + "serverError": "Server error", + "serverErrorDescription": "Something went wrong on our end.", + "networkError": "Network error", + "networkErrorDescription": "Please check your internet connection.", + "validation": { + "required": "This field is required", + "email": "Please enter a valid email address", + "minLength": "Must be at least {min} characters", + "maxLength": "Must be at most {max} characters", + "passwordMismatch": "Passwords do not match", + "passwordWeak": "Password is too weak" + } + }, + "validation": { + "required": "This field is required", + "email": "Invalid email address", + "minLength": "Minimum {count} characters required", + "maxLength": "Maximum {count} characters allowed", + "pattern": "Invalid format", + "passwordMismatch": "Passwords do not match" + } +} diff --git a/frontend/messages/it.json b/frontend/messages/it.json new file mode 100644 index 0000000..85a9754 --- /dev/null +++ b/frontend/messages/it.json @@ -0,0 +1,165 @@ +{ + "common": { + "loading": "Caricamento...", + "error": "Errore", + "success": "Successo", + "cancel": "Annulla", + "save": "Salva", + "delete": "Elimina", + "edit": "Modifica", + "close": "Chiudi", + "confirm": "Conferma", + "back": "Indietro", + "next": "Avanti", + "submit": "Invia", + "search": "Cerca", + "filter": "Filtra", + "clear": "Cancella", + "required": "Obbligatorio", + "optional": "Facoltativo", + "yes": "Sì", + "no": "No" + }, + "navigation": { + "home": "Home", + "dashboard": "Dashboard", + "settings": "Impostazioni", + "profile": "Profilo", + "logout": "Disconnetti", + "login": "Accedi", + "register": "Registrati", + "demos": "Demo", + "design": "Design System" + }, + "auth": { + "login": { + "title": "Accedi al tuo account", + "subtitle": "Inserisci la tua email per accedere al tuo account", + "emailLabel": "Email", + "emailPlaceholder": "nome@esempio.com", + "passwordLabel": "Password", + "passwordPlaceholder": "Inserisci la tua password", + "rememberMe": "Ricordami", + "forgotPassword": "Password dimenticata?", + "loginButton": "Accedi", + "noAccount": "Non hai un account?", + "registerLink": "Registrati", + "success": "Accesso effettuato con successo", + "error": "Email o password non validi" + }, + "register": { + "title": "Crea un account", + "subtitle": "Inserisci le tue informazioni per creare un account", + "firstNameLabel": "Nome", + "firstNamePlaceholder": "Mario", + "lastNameLabel": "Cognome", + "lastNamePlaceholder": "Rossi", + "emailLabel": "Email", + "emailPlaceholder": "nome@esempio.com", + "passwordLabel": "Password", + "passwordPlaceholder": "Crea una password sicura", + "confirmPasswordLabel": "Conferma Password", + "confirmPasswordPlaceholder": "Reinserisci la tua password", + "phoneLabel": "Numero di Telefono", + "phonePlaceholder": "+39 123 456 7890", + "registerButton": "Crea Account", + "hasAccount": "Hai già un account?", + "loginLink": "Accedi", + "success": "Account creato con successo", + "error": "Impossibile creare l'account" + }, + "passwordReset": { + "title": "Reimposta la tua password", + "subtitle": "Inserisci il tuo indirizzo email e ti invieremo un link per reimpostare la password", + "emailLabel": "Email", + "emailPlaceholder": "nome@esempio.com", + "sendButton": "Invia Link di Reset", + "backToLogin": "Torna al login", + "success": "Link di reset password inviato alla tua email", + "error": "Impossibile inviare il link di reset" + }, + "passwordChange": { + "title": "Cambia Password", + "currentPasswordLabel": "Password Attuale", + "newPasswordLabel": "Nuova Password", + "confirmPasswordLabel": "Conferma Nuova Password", + "changeButton": "Cambia Password", + "success": "Password cambiata con successo", + "error": "Impossibile cambiare la password" + } + }, + "settings": { + "title": "Impostazioni", + "profile": { + "title": "Profilo", + "subtitle": "Gestisci le informazioni del tuo profilo", + "firstNameLabel": "Nome", + "lastNameLabel": "Cognome", + "emailLabel": "Email", + "phoneLabel": "Numero di Telefono", + "updateButton": "Aggiorna Profilo", + "success": "Profilo aggiornato con successo", + "error": "Impossibile aggiornare il profilo" + }, + "password": { + "title": "Password", + "subtitle": "Cambia la password del tuo account", + "currentPasswordLabel": "Password Attuale", + "newPasswordLabel": "Nuova Password", + "confirmPasswordLabel": "Conferma Nuova Password", + "updateButton": "Aggiorna Password", + "success": "Password aggiornata con successo", + "error": "Impossibile aggiornare la password" + }, + "sessions": { + "title": "Sessioni", + "subtitle": "Gestisci le tue sessioni attive", + "currentSession": "Sessione Corrente", + "activeSession": "Attiva", + "lastActive": "Ultima attività", + "device": "Dispositivo", + "location": "Posizione", + "revokeButton": "Revoca", + "revokeAll": "Revoca Tutte le Altre Sessioni", + "success": "Sessione revocata con successo", + "error": "Impossibile revocare la sessione" + }, + "preferences": { + "title": "Preferenze", + "subtitle": "Personalizza la tua esperienza", + "language": "Lingua", + "languageDescription": "Seleziona la tua lingua preferita", + "theme": "Tema", + "themeDescription": "Scegli la tua combinazione di colori", + "themeLight": "Chiaro", + "themeDark": "Scuro", + "themeSystem": "Sistema" + } + }, + "errors": { + "notFound": "Pagina non trovata", + "notFoundDescription": "La pagina che stai cercando non esiste.", + "unauthorized": "Non autorizzato", + "unauthorizedDescription": "Non hai i permessi per accedere a questa pagina.", + "serverError": "Errore del server", + "serverErrorDescription": "Qualcosa è andato storto dal nostro lato.", + "networkError": "Errore di rete", + "networkErrorDescription": "Controlla la tua connessione internet.", + "validation": { + "required": "Questo campo è obbligatorio", + "email": "Inserisci un indirizzo email valido", + "minLength": "Deve essere di almeno {min} caratteri", + "maxLength": "Deve essere al massimo {max} caratteri", + "passwordMismatch": "Le password non corrispondono", + "passwordWeak": "La password è troppo debole" + } + }, + "validation": { + "required": "Questo campo è obbligatorio", + "email": "Indirizzo email non valido", + "minLength": "Minimo {count} caratteri richiesti", + "maxLength": "Massimo {count} caratteri consentiti", + "pattern": "Formato non valido", + "passwordMismatch": "Le password non corrispondono" + } +} diff --git a/frontend/next.config.ts b/frontend/next.config.ts index e02e7a6..ee1ceac 100755 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -1,4 +1,8 @@ import type { NextConfig } from 'next'; +import createNextIntlPlugin from 'next-intl/plugin'; + +// Initialize next-intl plugin with i18n request config path +const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts'); const nextConfig: NextConfig = { output: 'standalone', @@ -21,4 +25,5 @@ const nextConfig: NextConfig = { // Note: swcMinify is default in Next.js 15 }; -export default nextConfig; +// Wrap config with next-intl plugin +export default withNextIntl(nextConfig); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9c65272..09f3f62 100755 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -31,6 +31,7 @@ "gray-matter": "^4.0.3", "lucide-react": "^0.552.0", "next": "^15.5.6", + "next-intl": "^4.5.3", "next-themes": "^0.4.6", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -1022,7 +1023,6 @@ "version": "2.3.6", "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz", "integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==", - "dev": true, "license": "MIT", "dependencies": { "@formatjs/fast-memoize": "2.2.7", @@ -1035,7 +1035,6 @@ "version": "2.2.7", "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", - "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.8.0" @@ -1045,7 +1044,6 @@ "version": "2.11.4", "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz", "integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==", - "dev": true, "license": "MIT", "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", @@ -1057,7 +1055,6 @@ "version": "1.8.16", "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz", "integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==", - "dev": true, "license": "MIT", "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", @@ -1068,7 +1065,6 @@ "version": "0.6.2", "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz", "integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==", - "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.8.0" @@ -4260,6 +4256,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@schummar/icu-type-parser": { + "version": "1.21.5", + "resolved": "https://registry.npmjs.org/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz", + "integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==", + "license": "MIT" + }, "node_modules/@sentry/core": { "version": "9.46.0", "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.46.0.tgz", @@ -4420,6 +4422,172 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.2.tgz", + "integrity": "sha512-Ghyz4RJv4zyXzrUC1B2MLQBbppIB5c4jMZJybX2ebdEQAvryEKp3gq1kBksCNsatKGmEgXul88SETU19sMWcrw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.2.tgz", + "integrity": "sha512-7n/PGJOcL2QoptzL42L5xFFfXY5rFxLHnuz1foU+4ruUTG8x2IebGhtwVTpaDN8ShEv2UZObBlT1rrXTba15Zw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.2.tgz", + "integrity": "sha512-ZUQVCfRJ9wimuxkStRSlLwqX4TEDmv6/J+E6FicGkQ6ssLMWoKDy0cAo93HiWt/TWEee5vFhFaSQYzCuBEGO6A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.2.tgz", + "integrity": "sha512-GZh3pYBmfnpQ+JIg+TqLuz+pM+Mjsk5VOzi8nwKn/m+GvQBsxD5ectRtxuWUxMGNG8h0lMy4SnHRqdK3/iJl7A==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.2.tgz", + "integrity": "sha512-5av6VYZZeneiYIodwzGMlnyVakpuYZryGzFIbgu1XP8wVylZxduEzup4eP8atiMDFmIm+s4wn8GySJmYqeJC0A==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.2.tgz", + "integrity": "sha512-1nO/UfdCLuT/uE/7oB3EZgTeZDCIa6nL72cFEpdegnqpJVNDI6Qb8U4g/4lfVPkmHq2lvxQ0L+n+JdgaZLhrRA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.2.tgz", + "integrity": "sha512-Ksfrb0Tx310kr+TLiUOvB/I80lyZ3lSOp6cM18zmNRT/92NB4mW8oX2Jo7K4eVEI2JWyaQUAFubDSha2Q+439A==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.2.tgz", + "integrity": "sha512-IzUb5RlMUY0r1A9IuJrQ7Tbts1wWb73/zXVXT8VhewbHGoNlBKE0qUhKMED6Tv4wDF+pmbtUJmKXDthytAvLmg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.2.tgz", + "integrity": "sha512-kCATEzuY2LP9AlbU2uScjcVhgnCAkRdu62vbce17Ro5kxEHxYWcugkveyBRS3AqZGtwAKYbMAuNloer9LS/hpw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.2.tgz", + "integrity": "sha512-iJaHeYCF4jTn7OEKSa3KRiuVFIVYts8jYjNmCdyz1u5g8HRyTDISD76r8+ljEOgm36oviRQvcXaw6LFp1m0yyA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -4429,6 +4597,15 @@ "tslib": "^2.8.0" } }, + "node_modules/@swc/types": { + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", + "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, "node_modules/@tailwindcss/node": { "version": "4.1.16", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.16.tgz", @@ -7461,7 +7638,6 @@ "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, "license": "MIT" }, "node_modules/decimal.js-light": { @@ -9891,7 +10067,6 @@ "version": "10.7.18", "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.18.tgz", "integrity": "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", @@ -13390,6 +13565,15 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -13459,6 +13643,92 @@ } } }, + "node_modules/next-intl": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.5.3.tgz", + "integrity": "sha512-/omQgD0JyewIwJa0F5/HPRe5LYAVBNcGDgZvnv6hul8lI1KMcCcxErMXUiNjyc5kuQqLQeWUa2e4ICx09uL8FA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/intl-localematcher": "^0.5.4", + "@swc/core": "^1.13.19", + "negotiator": "^1.0.0", + "use-intl": "^4.5.3" + }, + "peerDependencies": { + "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0", + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/next-intl/node_modules/@formatjs/intl-localematcher": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz", + "integrity": "sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==", + "license": "MIT", + "dependencies": { + "tslib": "2" + } + }, + "node_modules/next-intl/node_modules/@swc/core": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.2.tgz", + "integrity": "sha512-OQm+yJdXxvSjqGeaWhP6Ia264ogifwAO7Q12uTDVYj/Ks4jBTI4JknlcjDRAXtRhqbWsfbZyK/5RtuIPyptk3w==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.25" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.2", + "@swc/core-darwin-x64": "1.15.2", + "@swc/core-linux-arm-gnueabihf": "1.15.2", + "@swc/core-linux-arm64-gnu": "1.15.2", + "@swc/core-linux-arm64-musl": "1.15.2", + "@swc/core-linux-x64-gnu": "1.15.2", + "@swc/core-linux-x64-musl": "1.15.2", + "@swc/core-win32-arm64-msvc": "1.15.2", + "@swc/core-win32-ia32-msvc": "1.15.2", + "@swc/core-win32-x64-msvc": "1.15.2" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/next-intl/node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/next-themes": { "version": "0.4.6", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", @@ -16536,7 +16806,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -16784,6 +17054,20 @@ } } }, + "node_modules/use-intl": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.5.3.tgz", + "integrity": "sha512-vO2csOEc+xpi5PdvjTKORR4ZZQE6mz2jheefOszLOjppWx8SATC2XkmxUYwSHz1HIrcW6alUsj9qfPa6ZFhTNA==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "^2.2.0", + "@schummar/icu-type-parser": "1.21.5", + "intl-messageformat": "^10.5.14" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" + } + }, "node_modules/use-sidecar": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index dc7ee0c..efbb29a 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -45,6 +45,7 @@ "gray-matter": "^4.0.3", "lucide-react": "^0.552.0", "next": "^15.5.6", + "next-intl": "^4.5.3", "next-themes": "^0.4.6", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/frontend/src/app/demos/page.tsx b/frontend/src/app/demos/page.tsx index 594d0b3..5ce9636 100644 --- a/frontend/src/app/demos/page.tsx +++ b/frontend/src/app/demos/page.tsx @@ -16,7 +16,6 @@ import { LogIn, Settings, Users, - Lock, Activity, UserCog, BarChart3, diff --git a/frontend/src/i18n/request.ts b/frontend/src/i18n/request.ts new file mode 100644 index 0000000..2bb350d --- /dev/null +++ b/frontend/src/i18n/request.ts @@ -0,0 +1,44 @@ +// src/i18n/request.ts +/** + * Server-side i18n request configuration for next-intl. + * + * This file handles: + * - Loading translation messages for the requested locale + * - Server-side locale detection + * - Time zone configuration + * + * Important: + * - This runs on the server only (Next.js App Router) + * - Translation files are NOT sent to the client (zero bundle overhead) + * - Messages are loaded on-demand per request + */ + +import { getRequestConfig } from 'next-intl/server'; +import { routing } from './routing'; + +export default getRequestConfig(async ({ locale }) => { + // Validate that the incoming `locale` parameter is valid + // Type assertion: we know locale will be a string from the URL parameter + const requestedLocale = locale as 'en' | 'it'; + + // Check if the requested locale is supported, otherwise use default + const validLocale = routing.locales.includes(requestedLocale) + ? requestedLocale + : routing.defaultLocale; + + return { + // Return the validated locale + locale: validLocale, + + // Load messages for the requested locale + // Dynamic import ensures only the requested locale is loaded + messages: (await import(`../../messages/${validLocale}.json`)).default, + + // Optional: Configure time zone + // This will be used for date/time formatting + // timeZone: 'Europe/Rome', // Example for Italian users + + // Optional: Configure now (for relative time formatting) + // now: new Date(), + }; +}); diff --git a/frontend/src/i18n/routing.ts b/frontend/src/i18n/routing.ts new file mode 100644 index 0000000..2ca1de4 --- /dev/null +++ b/frontend/src/i18n/routing.ts @@ -0,0 +1,47 @@ +// src/i18n/routing.ts +/** + * Internationalization routing configuration for next-intl. + * + * This file defines: + * - Supported locales (en, it) + * - Default locale (en) + * - Routing strategy (subdirectory pattern: /[locale]/path) + * + * Architecture Decision: + * - Using subdirectory pattern (/en/about, /it/about) for best SEO + * - Only 2 languages (EN, IT) as template showcase + * - Users can extend by adding more locales to this configuration + */ + +import { defineRouting } from 'next-intl/routing'; +import { createNavigation } from 'next-intl/navigation'; + +/** + * Routing configuration for next-intl. + * + * Pattern: /[locale]/[pathname] + * Examples: + * - /en/about + * - /it/about + * - /en/auth/login + * - /it/auth/login + */ +export const routing = defineRouting({ + // A list of all locales that are supported + locales: ['en', 'it'], + + // Used when no locale matches + defaultLocale: 'en', + + // Locale prefix strategy + // - "always": Always show locale in URL (/en/about, /it/about) + // - "as-needed": Only show non-default locales (/about for en, /it/about for it) + // We use "always" for clarity and consistency + localePrefix: 'always', +}); + +// Lightweight wrappers around Next.js' navigation APIs +// that will consider the routing configuration +export const { Link, redirect, usePathname, useRouter } = createNavigation(routing); + +export type Locale = (typeof routing.locales)[number]; diff --git a/frontend/src/lib/i18n/utils.ts b/frontend/src/lib/i18n/utils.ts new file mode 100644 index 0000000..204c306 --- /dev/null +++ b/frontend/src/lib/i18n/utils.ts @@ -0,0 +1,109 @@ +// src/lib/i18n/utils.ts +/** + * Utility functions for internationalization. + * + * This file demonstrates type-safe translation usage. + */ + +import { useTranslations } from 'next-intl'; + +/** + * Get the display name for a locale code. + * + * @param locale - The locale code ('en' or 'it') + * @returns The human-readable locale name + */ +export function getLocaleName(locale: string): string { + const names: Record = { + en: 'English', + it: 'Italiano', + }; + + return names[locale] || names.en; +} + +/** + * Get the native display name for a locale code. + * This shows the language name in its own language. + * + * @param locale - The locale code ('en' or 'it') + * @returns The native language name + */ +export function getLocaleNativeName(locale: string): string { + const names: Record = { + en: 'English', + it: 'Italiano', + }; + + return names[locale] || names.en; +} + +/** + * Get the flag emoji for a locale. + * + * @param locale - The locale code ('en' or 'it') + * @returns The flag emoji + */ +export function getLocaleFlag(locale: string): string { + // Map to country flags (note: 'en' uses US flag, could be GB) + const flags: Record = { + en: '🇺🇸', // or '🇬🇧' for British English + it: '🇮🇹', + }; + + return flags[locale] || flags.en; +} + +/** + * Hook to get common translations. + * This demonstrates type-safe usage of useTranslations. + * + * @returns Object with commonly used translation functions + */ +export function useCommonTranslations() { + const t = useTranslations('common'); + + return { + loading: () => t('loading'), + error: () => t('error'), + success: () => t('success'), + cancel: () => t('cancel'), + save: () => t('save'), + delete: () => t('delete'), + edit: () => t('edit'), + close: () => t('close'), + confirm: () => t('confirm'), + }; +} + +/** + * Format a relative time string (e.g., "2 hours ago"). + * This is a placeholder for future implementation with next-intl's date/time formatting. + * + * @param date - The date to format + * @param locale - The locale to use for formatting + * @returns Formatted relative time string + */ +export function formatRelativeTime(date: Date, locale: string = 'en'): string { + const now = new Date(); + const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000); + + if (diffInSeconds < 60) { + return locale === 'it' ? 'proprio ora' : 'just now'; + } else if (diffInSeconds < 3600) { + const minutes = Math.floor(diffInSeconds / 60); + return locale === 'it' + ? `${minutes} ${minutes === 1 ? 'minuto' : 'minuti'} fa` + : `${minutes} ${minutes === 1 ? 'minute' : 'minutes'} ago`; + } else if (diffInSeconds < 86400) { + const hours = Math.floor(diffInSeconds / 3600); + return locale === 'it' + ? `${hours} ${hours === 1 ? 'ora' : 'ore'} fa` + : `${hours} ${hours === 1 ? 'hour' : 'hours'} ago`; + } else { + const days = Math.floor(diffInSeconds / 86400); + return locale === 'it' + ? `${days} ${days === 1 ? 'giorno' : 'giorni'} fa` + : `${days} ${days === 1 ? 'day' : 'days'} ago`; + } +} diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts index ad18a8c..161054f 100644 --- a/frontend/src/middleware.ts +++ b/frontend/src/middleware.ts @@ -1,10 +1,15 @@ import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; +import createMiddleware from 'next-intl/middleware'; +import { routing } from './i18n/routing'; + +// Create next-intl middleware for locale handling +const intlMiddleware = createMiddleware(routing); export function middleware(request: NextRequest) { const { pathname } = request.nextUrl; - // Block access to /dev routes in production + // Block access to /dev routes in production (before locale handling) if (pathname.startsWith('/dev')) { const isProduction = process.env.NODE_ENV === 'production'; @@ -14,9 +19,20 @@ export function middleware(request: NextRequest) { } } - return NextResponse.next(); + // Handle locale routing with next-intl + return intlMiddleware(request); } export const config = { - matcher: '/dev/:path*', + // Match all pathnames except for: + // - API routes (/api/*) + // - Static files (/_next/*, /favicon.ico, etc.) + // - Files in public folder (images, fonts, etc.) + matcher: [ + // Match all pathnames except for + '/((?!api|_next|_vercel|.*\\..*).*)', + // However, match all pathnames within /api/ + // that don't end with a file extension + '/api/(.*)', + ], }; diff --git a/frontend/src/types/i18n.d.ts b/frontend/src/types/i18n.d.ts new file mode 100644 index 0000000..b193c3f --- /dev/null +++ b/frontend/src/types/i18n.d.ts @@ -0,0 +1,25 @@ +// src/types/i18n.d.ts +/** + * TypeScript type definitions for i18n with next-intl. + * + * This file configures TypeScript autocomplete for translation keys. + * By importing the English messages as the reference type, we get: + * - Full autocomplete for all translation keys + * - Type safety when using t() function + * - Compile-time errors for missing or incorrect keys + * + * Usage: + * ```tsx + * const t = useTranslations('auth.login'); + * t('title'); // ✅ Autocomplete shows available keys + * t('invalid'); // ❌ TypeScript error + * ``` + */ + +type Messages = typeof import('../../messages/en.json'); + +declare global { + // Use type safe message keys with `next-intl` + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + interface IntlMessages extends Messages {} +}