Add internationalization (i18n) with next-intl and Italian translations

- Integrated `next-intl` for server-side and client-side i18n support.
- Added English (`en.json`) and Italian (`it.json`) localization files.
- Configured routing with locale-based subdirectories (`/[locale]/path`) using `next-intl`.
- Introduced type-safe i18n utilities and TypeScript definitions for translation keys.
- Updated middleware to handle locale detection and routing.
- Implemented dynamic translation loading to reduce bundle size.
- Enhanced developer experience with auto-complete and compile-time validation for i18n keys.
This commit is contained in:
Felipe Cardoso
2025-11-17 20:27:09 +01:00
parent b7c1191335
commit fe6a98c379
11 changed files with 873 additions and 13 deletions

165
frontend/messages/en.json Normal file
View File

@@ -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"
}
}

165
frontend/messages/it.json Normal file
View File

@@ -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"
}
}

View File

@@ -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);

View File

@@ -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",

View File

@@ -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",

View File

@@ -16,7 +16,6 @@ import {
LogIn,
Settings,
Users,
Lock,
Activity,
UserCog,
BarChart3,

View File

@@ -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(),
};
});

View File

@@ -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];

View File

@@ -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<string, string> = {
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<string, string> = {
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<string, string> = {
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`;
}
}

View File

@@ -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/(.*)',
],
};

25
frontend/src/types/i18n.d.ts vendored Normal file
View File

@@ -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 {}
}