forked from cardosofelipe/fast-next-template
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:
165
frontend/messages/en.json
Normal file
165
frontend/messages/en.json
Normal 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
165
frontend/messages/it.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
300
frontend/package-lock.json
generated
300
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
LogIn,
|
||||
Settings,
|
||||
Users,
|
||||
Lock,
|
||||
Activity,
|
||||
UserCog,
|
||||
BarChart3,
|
||||
|
||||
44
frontend/src/i18n/request.ts
Normal file
44
frontend/src/i18n/request.ts
Normal 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(),
|
||||
};
|
||||
});
|
||||
47
frontend/src/i18n/routing.ts
Normal file
47
frontend/src/i18n/routing.ts
Normal 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];
|
||||
109
frontend/src/lib/i18n/utils.ts
Normal file
109
frontend/src/lib/i18n/utils.ts
Normal 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`;
|
||||
}
|
||||
}
|
||||
@@ -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
25
frontend/src/types/i18n.d.ts
vendored
Normal 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 {}
|
||||
}
|
||||
Reference in New Issue
Block a user