The Challenge of Framework-Level Localization
When your React application grows, you often hit a ceiling with Client-Side Rendering (CSR). You might start craving the SEO benefits of Server-Side Rendering (SSR) or the performance gains of Incremental Static Regeneration (ISR). However, if you’ve built a robust internationalization (i18n) system in Vite using react-i18next, moving to Next.js isn't just a copy-paste job.
In Vite, i18n usually lives entirely in the browser. In Next.js, it needs to be aware of the server, the middleware, and the routing structure. Here is how to handle the transition without losing your mind—or your translations.
1. The Strategy: Client-Side vs. Server-Side i18n
In a standard Vite app, you likely initialize i18next in your main.tsx. It detects the browser language and loads JSON files from a public/locales folder.
Next.js changes the game because the server needs to know the locale before the page is even rendered to prevent the "flash of unlocalized content" (FOUT). You have two main paths:
- The App Router approach: Using
next-intlori18nextwith a middleware-based strategy. - The Pages Router approach: Using the built-in
i18nconfig innext.config.js.
For most modern migrations, the App Router is the preferred destination, leveraging Dynamic Routes like /[locale]/page.tsx.
2. Setting Up the Directory Structure
In Vite, your routes are managed by react-router-dom. In Next.js, your file system defines the routes. To maintain i18n, you should wrap your entire app directory content inside a [locale] folder.
// Vite Structure
src/
components/
locales/
main.tsx
// Next.js Structure
app/
[locale]/
layout.tsx
page.tsx
api/
middleware.ts
This structure ensures that every URL contains the language code (e.g., /en/dashboard, /fr/dashboard), which is vital for SEO and consistent server-side rendering.
3. Handling the Translation Files
One of the biggest pain points in migration is the boilerplate. If you have a massive Vite project with dozens of routes and complex hooks, manually rewriting every component to fit Next.js paradigms is tedious. For those looking to skip the manual setup, ViteToNext.AI can automate the heavy lifting of converting Vite-specific structures into Next.js compatible code, including initial project scaffolding.
Once your files are moved, you should transition from loading translations via http-backend (common in Vite) to importing them directly or fetching them from the filesystem in your server components.
4. Middleware: The Secret Sauce
In Vite, you might use a useEffect to redirect users based on navigator.language. In Next.js, this happens at the edge via middleware.ts. This prevents the server from ever serving a page without a defined locale.
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const locales = ['en', 'de', 'es'];
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const pathnameHasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (pathnameHasLocale) return;
// Redirect if there is no locale
const locale = 'en';
request.nextUrl.pathname = `/${locale}${pathname}`;
return NextResponse.redirect(request.nextUrl);
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};
5. Bridging Client and Server Components
This is where most migrations break. In Vite, all components are client components. In Next.js, components are Server Components by default.
If you use the useTranslation hook from react-i18next, you must add the 'use client' directive at the top of your files. However, to truly benefit from Next.js, you should pass translations as props from a Server Component or use a library like next-intl that supports Server Component translations natively.
Example: Server Component Translation
// app/[locale]/page.tsx
import { getMessages } from 'next-intl/server';
export default async function Page({ params: { locale } }) {
const t = await getMessages(locale);
return <h1>{t('welcome_message')}</h1>;
}
6. Managing State and Switches
Your language switcher component needs to change. Instead of just calling i18n.changeLanguage('fr'), you now need to perform a router navigation to the new locale path.
'use client';
import { useRouter, usePathname } from 'next/navigation';
export function LanguageSwitcher() {
const router = useRouter();
const pathname = usePathname();
const switchLanguage = (newLocale: string) => {
const segments = pathname.split('/');
segments[1] = newLocale;
router.push(segments.join('/'));
};
return <button onClick={() => switchLanguage('fr')}>Français</button>;
}
Conclusion
Migrating an i18n-heavy app from Vite to Next.js is a significant architectural shift. You move from a "browser-first" mindset to a "server-first" mindset. By using a [locale] routing strategy, implementing robust middleware, and carefully separating Client and Server components, you can enjoy the benefits of Next.js without losing your existing translation logic.
Further reading: ViteToNext.AI Migration Guide
Top comments (0)