DEV Community

Digital dev
Digital dev

Posted on

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

The Architectural Shift: Client-Side vs. Server-Side i18n

Internationalization (i18n) is one of those features that seems simple until you change your rendering strategy. In a standard Vite + React application, i18n usually lives entirely on the client. Libraries like react-i18next load JSON files from a /public folder and swap strings dynamically.

However, when you migrate to Next.js, especially with the App Router, you're moving from a purely client-side environment to a hybrid one. The challenge isn't just moving the files; it's ensuring that your translations work seamlessly in Server Components without causing the dreaded hydration mismatch.

Step 1: Mapping the Translation Files

In a typical Vite project, your structure likely looks like this:

/src
  /locales
    /en
      translation.json
    /es
      translation.json
  i18n.ts
Enter fullscreen mode Exit fullscreen mode

In Next.js, while you can keep this structure, the standard approach is to utilize the filesystem for routing. If you want localized URLs (e.g., /en/dashboard vs /es/dashboard), you'll need to wrap your routes in a dynamic [lng] segment.

Step 2: Choosing Your i18n Strategy

For Next.js App Router, you have two primary choices:

  1. Manual Implementation: Using i18next and react-i18next with custom logic to handle the server-side locale detection.
  2. Community Libraries: Using next-intl or next-i18n-router. These are highly optimized for the App Router's caching mechanisms.

If you are looking to automate the broader architectural changes beyond just i18n, tools like ViteToNext.AI can help refactor your Vite-specific logic and component structures into Next.js-compliant code automatically.

Step 3: Handling Server Components

This is where most migrations break. In Vite, everything is a Client Component. In Next.js, you cannot use the useTranslation hook in a Server Component because hooks require a client context.

Instead, you must create a server-side instance of your i18n configuration.

// Example of a server-side translation fetcher
import { createInstance } from 'i18next';
import resourcesToBackend from 'i18next-resources-to-backend';
import { initReactI18next } from 'react-i18next/initReactI18next';

const initI18next = async (lng, ns) => {
  const i18nInstance = createInstance();
  await i18nInstance
    .use(initReactI18next)
    .use(resourcesToBackend((language, namespace) => import(`./locales/${language}/${namespace}.json`)))
    .init({
      supportedLngs: ['en', 'es'],
      lng,
      fallbackLng: 'en',
      ns,
    });
  return i18nInstance;
}

export async function useTranslation(lng, ns) {
  const i18nextInstance = await initI18next(lng, ns);
  return {
    t: i18nextInstance.getFixedT(lng, ns),
    i18n: i18nextInstance,
  };
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Solving the Hydration Mismatch

When a Client Component renders on the server (SSR), it needs to know the language. If the server thinks the language is English but the client defaults to Spanish before the cookie is read, you get a hydration error.

To prevent this, you should pass the locale as a prop from your layout.tsx down to a Provider that wraps your client-side tree. This ensures the client and server start with the exact same translation state.

Step 5: Middleware for SEO and Redirection

One advantage of Next.js over Vite is the ability to handle redirects at the edge. By using middleware.ts, you can detect a user's browser language and redirect them to the correct locale without a flash of unstyled content (FOUC) or a secondary client-side redirect.

import { NextResponse } from 'next/server';
import acceptLanguage from 'accept-language';

export function middleware(req) {
  let lng = acceptLanguage.get(req.headers.get('Accept-Language'));
  if (!lng) lng = 'en';

  // Redirect logic for root path
  if (req.nextUrl.pathname === '/') {
    return NextResponse.redirect(new URL(`/${lng}`, req.url));
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Migrating i18n from Vite to Next.js is not just a search-and-replace task. It requires rethinking how translations are loaded to leverage Server Components and Edge Middleware. By decoupling your translation logic into server-safe utilities and ensuring your hydration state is synced, you can achieve a localized experience that is faster and more SEO-friendly than a standard SPA.

Further reading: Learn how to automate your Vite to Next.js migration here.

Top comments (0)