DEV Community

Digital dev
Digital dev

Posted on

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

The Architectural Shift: From CSR to SSR i18n

Internationalization (i18n) is one of those features that feels straightforward in a Client-Side Rendered (CSR) Vite environment but becomes significantly more complex when moving to a framework like Next.js. In Vite, you likely used react-i18next with a simple backend plugin to fetch JSON files. In Next.js, you have to decide between Client-side transitions and Server-Side Rendering (SSR) for your localized content.

This guide explores how to migrate your translation logic without losing your SEO rankings or breaking the user experience.

1. The Strategy: Path-based vs. Browser-based Detection

In a standard Vite app, you might detect the user's language via navigator.language and store it in localStorage. In Next.js, search engines need to see localized content at specific URLs (e.g., /en/about vs /fr/about).

Before you move a single file, decide on your URL structure. The standard App Router approach uses dynamic segments: app/[lng]/layout.tsx. This ensures that every page request is aware of the locale before the first byte is sent to the browser.

2. Handling the Migration of Translation Files

In Vite, your structure probably looks like this:

public/locales/en/common.json
src/i18n.ts
Enter fullscreen mode Exit fullscreen mode

When moving to Next.js, you should ideally move these into a folder structure that isn't purely public if you plan on using Server Components. If you are handling a massive codebase, tools like ViteToNext.AI can help automate the structural conversion of your React components to Next.js standards, allowing you to focus purely on the i18n logic.

3. Implementing the Middleware

Next.js uses middleware to handle redirects based on the user's preferred language. This replaces the useEffect hooks you likely used in Vite to redirect users.

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

const languages = ['en', 'de']
const cookieName = 'i18next'

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

  // Redirect if lng in path is not supported
  if (
    !languages.some(loc => req.nextUrl.pathname.startsWith(`/${loc}`)) &&
    !req.nextUrl.pathname.startsWith('/_next')
  ) {
    return NextResponse.redirect(new URL(`/${lng}${req.nextUrl.pathname}`, req.url))
  }

  return NextResponse.next()
}
Enter fullscreen mode Exit fullscreen mode

4. Server Components vs. Client Components

This is where most migrations break. In Vite, all components are "Client Components." In Next.js:

  • Server Components: Use a simple asynchronous function to load JSON files directly from the filesystem. You don't need useTranslation hooks here.
  • Client Components: You still need a provider. You can wrap your [lng] layout in a context provider, or use the i18next client-side instance specifically for interactive elements.

Example: Server-side Translation

Instead of:

const { t } = useTranslation(); // Vite way
Enter fullscreen mode Exit fullscreen mode

In Next.js App Router, you'd use a pattern like this:

import { useTranslation } from './i18n/server'

export default async function Page({ params: { lng } }) {
  const { t } = await useTranslation(lng)
  return <h1>{t('title')}</h1>
}
Enter fullscreen mode Exit fullscreen mode

5. Bridging the Gap with react-i18next

If you want to keep as much of your Vite code as possible, you can use the i18next "resources" approach where you pass the translations as props from the Server Component to a Client Component. This avoids the "flash of unlocalized text" (FOUT).

  1. Fetch translations on the server.
  2. Pass them to a specialized I18nextProvider on the client side.
  3. Initialize the client instance with the pre-fetched resources.

6. Common Pitfalls to Avoid

  1. Hydration Mismatches: If the server renders English but the client detects French via localStorage, React will throw an error. Always prioritize the URL locale over localStorage during the initial render.
  2. Static Exporting: If you are using output: 'export', you cannot use the Middleware. You'll need to generate all paths at build time using generateStaticParams.
  3. Asset Paths: Remember that public/ assets in Vite are referenced relative to the root, but in Next.js, ensure your i18n configuration correctly points to the new directory structure.

Conclusion

Migrating i18n from Vite to Next.js is less about rewriting your translations and more about changing when those translations are loaded. By moving detection to the middleware and fetching resources on the server, you gain significant SEO advantages and faster First Contentful Paint (FCP).

Focus on getting your folder structure right first, then tackle the middleware, and finally refactor your hooks into async server functions where possible.

Further reading: Automate your React migration at ViteToNext.AI

Top comments (0)