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
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()
}
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
useTranslationhooks here. -
Client Components: You still need a provider. You can wrap your
[lng]layout in a context provider, or use thei18nextclient-side instance specifically for interactive elements.
Example: Server-side Translation
Instead of:
const { t } = useTranslation(); // Vite way
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>
}
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).
- Fetch translations on the server.
- Pass them to a specialized
I18nextProvideron the client side. - Initialize the client instance with the pre-fetched resources.
6. Common Pitfalls to Avoid
-
Hydration Mismatches: If the server renders English but the client detects French via
localStorage, React will throw an error. Always prioritize the URL locale overlocalStorageduring the initial render. -
Static Exporting: If you are using
output: 'export', you cannot use the Middleware. You'll need to generate all paths at build time usinggenerateStaticParams. -
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)