Implementing i18n in Next.js: The Proxy Approach
Learn how to implement internationalization in Next.js 16 using next-intl with a proxy-based middleware pattern for clean, maintainable bilingual sites.
Implementing i18n in Next.js: The Proxy Approach
Building multilingual websites doesn't have to be complicated. In this post, I'll walk you through the internationalization (i18n) setup I use for this site - a clean, scalable approach using Next.js 16 and next-intl with a proxy-based middleware pattern.
Why This Approach?
When I started adding Spanish support to my site, I wanted a solution that:
- ✅ Keeps routes clean -
/en/aboutand/es/aboutinstead of query parameters - ✅ Type-safe - TypeScript support for locales and translations
- ✅ Server Component friendly - Works seamlessly with Next.js App Router
- ✅ Maintainable - Clear separation of concerns
- ✅ SEO-friendly - Proper URL structure for search engines
The next-intl library with a proxy middleware pattern delivers all of this beautifully.
Architecture Overview
Here's how the pieces fit together:
src/
├── lib/i18n/
│ ├── routing.ts # Locale configuration
│ ├── navigation.ts # Wrapped navigation exports
│ └── i18n.ts # Request-level config & message loading
├── proxy.ts # Middleware for locale handling
└── app/[locale]/ # Internationalized routes
messages/
├── en.json # English translations
└── es.json # Spanish translations
Step 1: Configure Supported Locales
First, define which locales your app supports. This configuration is the foundation for everything else.
// src/lib/i18n/routing.ts
import { defineRouting } from "next-intl/routing";
export const routing = defineRouting({
// A list of all locales that are supported
locales: ["en", "es"],
// Used when no locale matches
defaultLocale: "en",
});Simple and clean! This is your single source of truth for supported languages.
Step 2: Set Up the Proxy Middleware
Here's where the magic happens. The proxy middleware intercepts requests and handles locale detection and routing.
// src/proxy.ts
import createMiddleware from "next-intl/middleware";
import { routing } from "@/lib/i18n/routing";
import { NextRequest } from "next/server";
const intlMiddleware = createMiddleware(routing);
export default async function middleware(request: NextRequest) {
// Handle internationalization
return intlMiddleware(request);
}
export const config = {
// Match all pathnames except for
// - … if they start with `/api`, `/_next` or `/_vercel`
// - … the ones containing a dot (e.g. `favicon.ico`)
matcher: "/((?!api|_next|_vercel|.*\\..*).*)",
};Important Note: In Next.js 16, middleware.ts was deprecated in favor of proxy.ts. This is the new standard for handling middleware in Next.js 16+, not just a naming preference. Next.js will automatically detect and use proxy.ts.
What the Matcher Does
The matcher is crucial - it tells Next.js which routes to process:
- ✅ Includes: All regular routes (
/about,/blog, etc.) - ❌ Excludes: API routes, Next.js internals, static files
This ensures your i18n logic only runs where it's needed, keeping performance optimal.
Step 3: Configure Request-Level Message Loading
Now we need to load the right translations for each request:
// src/lib/i18n/i18n.ts
import { notFound } from "next/navigation";
import { getRequestConfig } from "next-intl/server";
import { routing } from "./routing";
export default getRequestConfig(async ({ requestLocale }) => {
// This typically corresponds to the `[locale]` segment
let locale = await requestLocale;
// Ensure that a valid locale is used
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale;
}
return {
locale,
messages: (await import(`../../../messages/${locale}.json`)).default,
};
});This function:
- Extracts the locale from the request
- Validates it against your supported locales
- Loads the appropriate translation file
- Falls back to default locale if something's wrong
Step 4: Create Navigation Helpers
Here's a game-changer - wrap next-intl's navigation utilities to automatically include the locale:
// src/lib/i18n/navigation.ts
import { createNavigation } from "next-intl/navigation";
import { routing } from "./routing";
export const { Link, redirect, usePathname, useRouter } =
createNavigation(routing);Now you can use these throughout your app:
import { Link } from "@/lib/i18n/navigation";
// This automatically becomes /en/about or /es/about
<Link href="/about">About</Link>Critical Rule: Never import from next/navigation directly in localized pages - always use @/lib/i18n/navigation.
Step 5: Structure Your Routes
With App Router, your routes need the [locale] dynamic segment:
app/
└── [locale]/
├── layout.tsx
├── page.tsx
├── about/
│ └── page.tsx
└── blog/
├── page.tsx
└── [slug]/
└── page.tsx
Every route automatically gets the locale parameter!
Step 6: Create Translation Files
Store your translations as JSON:
// messages/en.json
{
"home": {
"title": "Welcome",
"description": "Build amazing things"
},
"nav": {
"about": "About",
"blog": "Blog",
"projects": "Projects"
}
}// messages/es.json
{
"home": {
"title": "Bienvenido",
"description": "Construye cosas increíbles"
},
"nav": {
"about": "Acerca de",
"blog": "Blog",
"projects": "Proyectos"
}
}Pro tip: Keep the structure identical between languages - it makes maintenance much easier!
Using Translations in Components
Server Components (Default)
import { getTranslations } from "next-intl/server";
export default async function HomePage() {
const t = await getTranslations("home");
return (
<div>
<h1>{t("title")}</h1>
<p>{t("description")}</p>
</div>
);
}Client Components
"use client";
import { useTranslations } from "next-intl";
export function ClientComponent() {
const t = useTranslations("home");
return <h2>{t("title")}</h2>;
}The API is nearly identical - just async vs hook!
Type Safety with TypeScript
Want type-safe translations? Create a type definition:
// src/types/i18n.ts
import en from "@/../messages/en.json";
type Messages = typeof en;
declare global {
interface IntlMessages extends Messages {}
}Now TypeScript will catch typos in translation keys! 🎉
Best Practices
✅ DO:
- Use consistent keys - Keep the same structure across all translation files
- Namespace your translations - Group related translations (
nav.*,home.*) - Import from i18n/navigation - Always use the wrapped navigation helpers
- Validate locales - Handle invalid locales gracefully
- Keep translations focused - One key = one piece of content
❌ DON'T:
- Use middleware.ts - It's deprecated in Next.js 16+, use
proxy.tsinstead - Import from next/navigation directly - You'll lose the automatic locale handling
- Hardcode routes - Always use the Link component or redirect from navigation
- Forget fallbacks - Always have a default locale configured
- Mix translation patterns - Stay consistent with namespacing
Handling Edge Cases
Invalid Locales
The i18n.ts config handles this automatically:
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale;
}API Routes
API routes are automatically excluded by the matcher, so they work normally without locale prefixes.
Static Files
The matcher pattern excludes files with extensions, so images, fonts, and other static assets work unchanged.
Performance Considerations
This setup is highly performant because:
- Translations load per-request - Only the needed locale is loaded
- Middleware is selective - Only runs on necessary routes
- Server Components - Most translations resolve server-side
- Build-time optimization - Next.js optimizes the middleware
Testing Your i18n Setup
Here's a quick checklist:
- [ ]
/en/aboutloads correctly - [ ]
/es/aboutloads correctly - [ ] Invalid locale
/fr/aboutredirects appropriately - [ ] Navigation between pages maintains locale
- [ ] API routes work without locale prefix
- [ ] Static assets load correctly
What's Next?
This foundation opens up possibilities:
- Language switcher component - Let users toggle languages
- Browser locale detection - Auto-select based on user preferences
- SEO optimization - Add hreflang tags for search engines
- Dynamic imports - Load translations on-demand for larger apps
Wrapping Up
Internationalization doesn't have to be daunting. With next-intl and this proxy-based approach, you get:
- Clean, SEO-friendly URLs
- Type-safe translations
- Server Component support
- Easy maintenance
The key is starting simple and building up. Start with two languages, get the patterns right, then expand as needed.
Have questions about implementing i18n in your Next.js app? Feel free to reach out - I'd love to hear about your use case!
Resources: