Locale Routing
URL-based locale detection
The project uses locale-based routing with automatic locale detection and localized navigation utilities.
URL Structure
All routes are prefixed with the locale:
/en/about
/it/about
/en/dashboard
/it/dashboardThe locale segment is automatically handled by the middleware.
Automatic Locale Detection
The middleware detects the user's locale from multiple sources (in order of priority):
- URL path segment (highest priority)
- Cookie (
NEXT_LOCALE) - Accept-Language header
- Default locale (fallback)
Navigation Utilities
All navigation utilities from @/i18n/navigation automatically handle locale prefixing.
The locale parameter is optional in all navigation functions. The current locale is automatically used from context.
Link Component
import { Link } from "@/i18n/navigation";
import { PageAbout } from "@/routes";
<Link href={PageAbout()}>About</Link>
// Renders: /en/about (or /{locale}/about)
// With declarative route
<PageAbout.Link>About</PageAbout.Link>Client-Side Navigation
"use client";
import { useRouter } from "@/i18n/navigation";
import { PageDashboard, PageUserId } from "@/routes";
function MyComponent() {
const router = useRouter();
const handleNavigate = () => {
// Navigate to a route - locale is automatically applied
router.push(PageDashboard()); // → /en/dashboard
// With params
router.push(PageUserId({ id: "123" }));
// Replace instead of push
router.replace(PageHome());
// Explicit locale (rarely needed)
router.push(PageDashboard({ locale: "it" })); // → /it/dashboard
};
return <Button onClick={handleNavigate}>Go to Dashboard</Button>;
}Server-Side Redirects
import { redirect } from "@/i18n/navigation";
import { PageLogin } from "@/routes";
import { getAuth } from "@/lib/auth/server";
export default async function ProtectedPage() {
const auth = await getAuth();
if (!auth) {
redirect(PageLogin()); // Locale is automatically applied
}
return <div>Protected content</div>;
}Important
The Next.js redirect() function only works in:
- Server Components
- Server Actions
It does NOT work in:
- tRPC procedures (return redirect URL instead)
- Middleware (use
NextResponse.redirect()) - API Routes (use
NextResponse.redirect())
In tRPC Procedures
Return the redirect URL in the response and handle it client-side:
// Server: tRPC procedure
login: publicProcedure
.input(loginSchema)
.mutation(async ({ ctx, input }) => {
const result = await signIn(input);
return {
success: true,
payload: {
redirectUrl: PageDashboard(),
},
};
}),
// Client: Handle redirect
const mutation = api.auth.login.useMutation({
onSuccess: (data) => {
if (data.payload?.redirectUrl) {
router.push(data.payload.redirectUrl);
}
},
});usePathname Hook
import { usePathname } from "@/i18n/navigation";
import { usePathnameWithoutLocale } from "@/lib/utils/navigation-utils";
const pathname = usePathname();
// URL: /en/dashboard → pathname: /en/dashboard
const pathname = usePathnameWithoutLocale();
// URL: /en/dashboard → pathname: /dashboardCheck Active Route
import { navUtils } from "@/lib/utils/navigation-utils";
const pathname = usePathname();
const isActive = navUtils.pathMatches(pathname, PageDashboard);Locale Switching
Locale Switcher Component
import { LocaleSwitcher } from "@/components/locale-switcher";
<LocaleSwitcher />This renders a dropdown with all configured locales.
Programmatic Locale Switch
"use client";
import { useRouter, usePathname } from "@/i18n/navigation";
function SwitchLocale() {
const router = useRouter();
const pathname = usePathname();
const switchToItalian = () => {
router.push(pathname, { locale: "it" });
};
return <Button onClick={switchToItalian}>Switch to Italian</Button>;
}Configuration
Supported Locales
Configure locales in src/i18n/config.tsx:
export const locales = ["en", "it"] as const;
export const defaultLocale = "en";Locale Cookie
The locale is stored in a cookie for persistence:
// src/config/app.ts
Config.Navigation.LOCALE_COOKIE_KEY = "NEXT_LOCALE";Adding a New Locale
Step 1: Update config
Add the locale to src/i18n/config.tsx
Step 2: Create translation files
cp -r src/messages/dictionaries/en src/messages/dictionaries/esStep 3: Translate content
Update all JSON files in the new locale folder
Step 4: Add Zod errors
cp src/messages/zod-errors/en.json src/messages/zod-errors/es.jsonStep 5: Test routing
Navigate to /es/* routes to verify
Middleware Configuration
The middleware handles locale detection and routing in middleware.ts:
import createMiddleware from "next-intl/middleware";
import { routing } from "@/i18n/routing";
export default createMiddleware(routing);
export const config = {
matcher: [
"/((?!api|_next|_vercel|.*\\..*).*)",
"/",
],
};SEO & Metadata
Localized Metadata
import { getLocalizedMetadata } from "@/i18n/seo";
export async function generateMetadata({ params }: Props) {
const { locale } = await params;
return getLocalizedMetadata({
locale,
namespace: "pageAbout",
titleKey: "meta.title",
descriptionKey: "meta.description",
});
}Alternate Links
The middleware automatically adds alternate locale links for SEO:
<link rel="alternate" hreflang="en" href="/en/about" />
<link rel="alternate" hreflang="it" href="/it/about" />Best Practices
- Use declarative routing - Always use route functions like
PageHome()instead of hardcoded paths - Let middleware handle locale - Don't manually add locale prefixes to URLs
- Handle redirects correctly - Use the right redirect method based on context (server component vs tRPC)
- Test all locales - Verify navigation works correctly in all supported locales
Always use the localized navigation utilities from @/i18n/navigation instead of Next.js built-in navigation.