DEV Community

Digital dev
Digital dev

Posted on

Migrating a Vite i18n App to Next.js Without Breaking Everything

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:

  1. The App Router approach: Using next-intl or i18next with a middleware-based strategy.
  2. The Pages Router approach: Using the built-in i18n config in next.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
Enter fullscreen mode Exit fullscreen mode

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).*)'],
};
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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)