DEV Community

Digital dev
Digital dev

Posted on

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

The Architectural Shift: From Client-Side to Server-First

When we build internationalized (i18n) applications in Vite, we typically rely on client-side libraries like react-i18next or react-intl. These tools work by loading JSON translation files into the browser's memory and toggling strings dynamically.

However, migrating to Next.js changes the fundamental mental model. In Next.js, i18n isn't just about UI strings; it's about routing, SEO, and leveraging Server Components. If you approach a Next.js migration using only your old Vite patterns, you'll miss out on the performance benefits of Static Site Generation (SSG) and Server-Side Rendering (SSR).

In this guide, we will look at how to port your i18n logic from a Vite environment to Next.js (App Router) without losing your mind or breaking your translation workflows.

1. Mapping the Translation Strategy

In a standard Vite + react-i18next setup, you likely have an i18n.ts file that looks like this:

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';

i18n.use(initReactI18next).init({
  resources: {
    en: { translation: { welcome: "Hello" } },
    fr: { translation: { welcome: "Bonjour" } },
  },
  lng: 'en',
  fallbackLng: 'en',
});
Enter fullscreen mode Exit fullscreen mode

In Next.js, specifically with the App Router, moving to a dynamic segment like /[locale]/page.tsx is the gold standard. This allows the server to know the language before the page is even sent to the user, eliminating that "flicker" of untranslated text often seen in Vite SPAs.

2. Setting up the Directory Structure

To keep things clean, move your existing translation files from src/locales/*.json to the root messages/ or public/locales/ directory.

Next, wrap your application in a locale-based folder structure:

app/
  [locale]/
    layout.tsx
    page.tsx
    components/
middleware.ts
Enter fullscreen mode Exit fullscreen mode

The middleware.ts is crucial here. It detects the user's preferred language (via headers or cookies) and redirects them to the appropriate /[locale] path.

3. Bridging the Hook Gap

One of the biggest pain points in migration is the useTranslation() hook. In Vite, this hook is purely client-side. In Next.js, you have two choices:

  1. Client Components: Use a library like next-intl or next-i18next which provides a similar hook.
  2. Server Components: Fetch translations directly via a function call, avoiding the need for hooks entirely.

For example, using next-intl in a Server Component:

import {getTranslations} from 'next-intl/server';

export default async function ProfilePage() {
  const t = await getTranslations('Profile');
  return <h1>{t('title')}</h1>;
}
Enter fullscreen mode Exit fullscreen mode

4. Automating the Heavy Lifting

Manually rewriting every component from a client-side Vite structure to a Next.js App Router structure can be incredibly time-consuming, especially when dealing with complex providers and localized routing logic. If you're looking to speed up this process, you can use ViteToNext.AI to automatically convert your Vite components and folder structure into a Next.js-ready format. This helps handle the boilerplate of switching from React Router to Next.js routing while maintaining your i18n context.

5. Handling Global State and Context

In a Vite app, your i18n instance is often a singleton. In Next.js, you must be careful not to share state across requests on the server. Always instantiate your i18n configuration per request.

If you use react-i18next, ensure you use the I18nextProvider only in Client Components and pass the initial state (translations) from the Server Component to avoid a re-hydration mismatch error.

6. Metadata and SEO

One significant advantage of migrating is the generateMetadata function. In Vite, you likely used react-helmet. In Next.js, you can generate localized SEO tags effortlessly:

export async function generateMetadata({ params: { locale } }) {
  const t = await getTranslations({ locale, namespace: 'Metadata' });

  return {
    title: t('title'),
    description: t('description')
  };
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Migrating an i18n-heavy Vite app to Next.js is more than a find-and-replace task. It requires shifting your mindset toward server-side data fetching and locale-aware routing. By utilizing middleware for language detection and adopting Server Components for static segments, you can achieve better SEO and faster Page Speed Insights scores compared to a standard SPA.

Take the migration one module at a time, start with the routing middleware, and then gradually move your components to use server-side translation fetchers.

Further reading: How to automate your Vite to Next.js migration

Top comments (0)