files
function validateLocaleSetup() {
if (process.env.NODE_ENV === 'development') {
SUPPORTED_LOCALES.forEach((locale) => {
try {
// Dynamic import to check if message file exists
require.resolve(../messages/${locale}.json);
} catch (error) {
console.error(Missing message file for locale ${locale}: ../messages/${locale}.json);
throw new Error(i18n validation failed: Missing message file for ${locale});
}
});
}
}
// Initialize next-intl middleware with validated config
const intlMiddleware = createMiddleware({
locales: SUPPORTED_LOCALES,
defaultLocale: DEFAULT_LOCALE,
// Automatically detect locale from Accept-Language header if no path prefix
localeDetection: true,
// Prefix all routes with locale except for default locale (optional, configurable)
localePrefix: 'always',
});
// Main middleware handler with error handling and bypass logic
export function middleware(request: NextRequest) {
try {
// Check if path should bypass i18n routing
const pathname = request.nextUrl.pathname;
if (BYPASS_PATHS.some((path) => pathname.startsWith(path))) {
return NextResponse.next();
}
// Validate locale setup in development
validateLocaleSetup();
// Run next-intl middleware
return intlMiddleware(request);
} catch (error) {
console.error('i18n middleware error:', error);
// Fallback to default locale on error to prevent blank pages
const fallbackUrl = new URL(/${DEFAULT_LOCALE}${request.nextUrl.pathname}, request.url);
return NextResponse.redirect(fallbackUrl);
}
}
// Middleware config: match all paths except static files
export const config: MiddlewareConfig = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};
### Step 2: Configure i18n Messages and Request Config
Create a `messages/` directory with JSON files per locale. The request config centralizes message loading, timezone handling, and fallback logic for RSC and API routes.
```typescript
import { getRequestConfig } from 'next-intl/server';
import type { Messages } from './types/messages';
// Supported locales - must match middleware config
export const locales = ['en', 'fr', 'es', 'de'] as const;
export type Locale = (typeof locales)[number];
export const defaultLocale: Locale = 'en';
// Load message files dynamically based on locale
async function loadMessages(locale: Locale) {
try {
// Validate locale is supported
if (!locales.includes(locale)) {
console.warn(`Unsupported locale ${locale}, falling back to ${defaultLocale}`);
locale = defaultLocale;
}
// Dynamic import of message file - webpack will code split this
const messages = (await import(`../messages/${locale}.json`)).default;
// Validate message structure in development
if (process.env.NODE_ENV === 'development') {
if (!messages?.common?.welcome) {
throw new Error(`Invalid message file for ${locale}: missing common.welcome key`);
}
}
return messages as Messages;
} catch (error) {
console.error(`Failed to load messages for locale ${locale}:`, error);
// Fallback to empty messages to prevent runtime errors
return {} as Messages;
}
}
// next-intl request config - used for RSC, Server Components, and API routes
export const requestConfig = getRequestConfig(async ({ locale }) => {
try {
const messages = await loadMessages(locale as Locale);
return {
messages,
// Set time zone for date/number formatting per locale
timeZone: locale === 'en' ? 'America/New_York' : 'Europe/Paris',
// Fallback locale for missing message keys
fallbackLocale: defaultLocale,
// Enable verbose logging in development
logging: process.env.NODE_ENV === 'development' ? { level: 'verbose' } : undefined,
};
} catch (error) {
console.error('i18n request config error:', error);
return {
messages: {} as Messages,
timeZone: 'UTC',
fallbackLocale: defaultLocale,
};
}
});
// Helper to get locale from pathname (client-side utility)
export function getLocaleFromPathname(pathname: string): Locale {
const segments = pathname.split('/').filter(Boolean);
if (segments.length === 0) return defaultLocale;
const potentialLocale = segments[0];
return locales.includes(potentialLocale as Locale) ? (potentialLocale as Locale) : defaultLocale;
}
Step 3: Implement Localized UI Components
Build reusable components leveraging useTranslations, pluralization, and native Intl formatting. The architecture separates server-safe translations from client-side state to preserve RSC performance.
'use client';
import { useTranslations, useLocale } from 'next-intl';
import { useState, useEffect } from 'react';
import type { Locale } from '@/i18n';
// Props for the Hello component
interface HelloProps {
userName?: string;
showDate?: boolean;
}
// Localized greeting component with pluralization and date formatting
export default function Hello({ userName = 'Guest', showDate = false }: HelloProps) {
const t = useTranslations('common');
const locale = useLocale() as Locale;
const [currentDate, setCurrentDate] = useState(null);
// Load current date on client mount (avoids SSR hydration mismatch)
useEffect(() => {
try {
setCurrentDate(new Date());
} catch (error) {
console.error('Failed to initialize date:', error);
setCurrentDate(new Date(0)); // Fallback to epoch
}
}, []);
// Handle pluralization for user count (example)
const userCount = 5;
const userCountText = t('userCount', { count: userCount });
// Format date based on locale
const formattedDate = currentDate
? new Intl.DateTimeFormat(locale, {
dateStyle: 'full',
timeStyle: 'short',
}).format(currentDate)
: t('loading');
return (
{t('welcome', { name: userName })}
{t('description')}
{userCountText}
{showDate && currentDate && (
{t('currentDate')}: {formattedDate}
)}
);
}
// Locale switcher component (client-side)
function LocaleSwitcher() {
const t = useTranslations('locale');
const locale = useLocale();
const locales = ['en', 'fr', 'es', 'de'];
const handleLocaleChange = (newLocale: string) => {
try {
// Use next-intl's navigation helper to switch locale
const { useRouter } = require('next-intl/navigation');
const router = useRouter();
router.push(`/${newLocale}`);
} catch (error) {
console.error('Failed to switch locale:', error);
window.location.href = `/${newLocale}`;
}
};
return (
{locales.map((loc) => (
handleLocaleChange(loc)}
className={`px-3 py-1 rounded ${
loc === locale ? 'bg-blue-500 text-white' : 'bg-gray-100'
}`}
>
{t(`${loc}.label`)}
))}
);
}
Pitfall Guide
- Static/API Route Interference: Failing to exclude
/_next, /api, and static assets in the middleware matcher causes routing loops, 404s, and broken asset delivery. Always prefix bypass paths explicitly.
- Hydration Mismatches with Client-Side State: Using
new Date(), window, or browser APIs during SSR triggers hydration errors. Defer to useEffect or isolate in 'use client' boundaries.
- Missing Translation Keys in Production: Shipping without
fallbackLocale or dev-time validation results in broken UI. Implement CI checks, verbose logging, and strict message structure validation.
- Overusing Client Components for Translations:
useTranslations forces 'use client', negating RSC benefits. Prefer getTranslations in Server Components to maintain streaming, caching, and zero-client-overhead.
- Incorrect Locale Prefix Strategy: Using
localePrefix: 'as-needed' fragments SEO and complicates canonical URL generation. 'always' ensures predictable routing and consistent search engine indexing.
- Dynamic Import Code-Splitting Pitfalls: Importing unvalidated locale strings causes Webpack build errors or runtime crashes. Always sanitize inputs, verify against
SUPPORTED_LOCALES, and fallback gracefully.
- Ignoring Timezone/Number Formatting Context: Translations alone don't handle regional formatting. Configure
timeZone per request and leverage native Intl APIs to prevent display inconsistencies across locales.
Deliverables
- Blueprint: Complete Next.js 15 i18n architecture diagram covering middleware routing, RSC message loading, client-side switching, and SEO metadata generation.
- Checklist: Pre-deployment validation steps including middleware matcher verification, message file existence checks, fallback locale configuration, CI pipeline integration, and Playwright locale coverage.
- Configuration Templates: Production-ready
middleware.ts, i18n.ts, requestConfig, and component hooks ready for direct integration into App Router projects.