Building a Dynamic i18n Middleware for Next.js App Router with Prismic
Sunday, December 1, 2024
Introduction
When building multilingual websites, localization is essential for providing a user-friendly experience. Prismic CMS makes it easy to define locales in your repository, but it doesn’t offer middleware to dynamically match a user’s preferred language. This is where Next.js shines with its middleware capabilities.
In this blog post, I’ll show you how to create a middleware for the Next.js App Router that dynamically detects user language preferences and integrates seamlessly with Prismic’s localization features. The result is a smooth, personalized experience for your users.
The Problem
Prismic’s default setup uses the first locale defined in your repository as the fallback. While this works for simple setups, it doesn’t account for the user’s `Accept-Language` preferences. This limitation can frustrate users by not serving content in their preferred language when multiple locales are available.
To solve this, we need a middleware that:
- Fetches available locales dynamically from Prismic.
- Detects the user’s preferred language from their request headers.
- Redirects users to the appropriate localized URL if it’s missing.
The Solution: Dynamic i18n Middleware
Here’s the complete implementation, enhanced with comments to explain the key logic:
```typescript import { NextRequest, NextResponse } from 'next/server'; import Negotiator from 'negotiator'; import { match as matchLocale } from '@formatjs/intl-localematcher'; import { createClient } from '@/prismicio'; // Cache for storing locales fetched from Prismic let cachedLocales: string[] | null = null; // Function to fetch locales from Prismic // This function ensures locales are only fetched once and reused for efficiency async function getPrismicLocales() { if (!cachedLocales) { const client = createClient(); const repository = await client.getRepository(); cachedLocales = repository.languages.map((lang) => lang.id); } return cachedLocales; } // Function to determine the user's preferred locale // Uses the Negotiator library to parse Accept-Language headers async function getLocale(request: NextRequest): Promise<string> { const locales = await getPrismicLocales(); // Convert request headers to a format compatible with Negotiator const negotiatorHeaders: Record<string, string> = {}; request.headers.forEach((value, key) => (negotiatorHeaders[key] = value)); // Extract the user's preferred languages const languages = new Negotiator({ headers: negotiatorHeaders }).languages(); // Match the user's preferences to available locales, defaulting to the first locale return matchLocale(languages, locales, locales[0]); } // Middleware function export async function middleware(request: NextRequest) { const locales = await getPrismicLocales(); const pathname = request.nextUrl.pathname; // Check if the URL is missing a locale prefix const pathnameIsMissingLocale = locales.every( (locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}` ); if (pathnameIsMissingLocale) { // Detect the user's preferred locale const locale = await getLocale(request); // Redirect the user to the URL with the preferred locale return NextResponse.redirect( new URL( `/${locale}${pathname.startsWith('/') ? '' : '/'}${pathname}`, request.url ) ); } } // Configuration to apply middleware selectively export const config = { matcher:['/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)', ], }; ```
How It Works
1. Fetching Locales from Prismic
The `getPrismicLocales` function retrieves the list of supported locales directly from your Prismic repository. To optimize performance, it caches the result, avoiding redundant API calls.
2. Detecting User Preferences
The `getLocale` function uses the `Negotiator` library to extract the `Accept-Language` header from the incoming request. It then matches the user’s preferred languages to the available locales using the `@formatjs/intl-localematcher` library.
3. Redirecting Based on Locale
If the requested URL lacks a locale prefix, the middleware determines the user’s preferred locale and redirects them to the appropriate localized version of the URL.
4. Selective Middleware Application
The `config.matcher` ensures the middleware only runs for specific paths, excluding API routes, static assets, and other special files.
Why This Middleware Stands Out
- Dynamic Locale Matching: No need to hardcode locale logic. The middleware adapts automatically to changes in your Prismic repository.
- User-Centric Design: Detecting and prioritizing user language preferences enhances the overall user experience.
- Optimized Performance: By caching locales and selectively applying the middleware, this solution minimizes unnecessary processing.
Conclusion
With this middleware, you can elevate your Next.js app’s multilingual capabilities by integrating dynamic i18n with Prismic CMS. The result is a seamless, user-friendly localization experience that reflects a deep commitment to accessibility and usability.