How to Translate a Next.js Website (App Router and No-Code)

Two ways to translate a Next.js website: App Router internationalization with [lang] routes, proxy.js, dictionaries and hreflang metadata, or a no-code script with SEO-friendly translated URLs.
No card details No commitment

· Updated · Alex B · Blog

Summarize this post with: 9 min read

There are two ways to translate a Next.js website. The code route: put every route under an app/[lang] segment, detect the visitor’s language in proxy.js, load a translation dictionary per locale, and output hreflang tags with the alternates metadata field. The no-code route: add a translation service’s script to your root layout and let it translate the rendered pages, with translated URLs served from a sub-directory or sub-domain for SEO.

The code route gives you full control and works best when the text lives in your codebase and a developer maintains it. The no-code route is faster when the text comes from a CMS, when non-developers need to edit translations, or when you need many languages at once. This guide shows both, based on the Next.js 16 documentation, and ends with a checklist for multilingual SEO either way.

Key takeaways

  • App Router: nest pages under app/[lang], redirect in proxy.js (called middleware.js before Next.js 16), load JSON dictionaries in Server Components, and prerender locales with generateStaticParams.
  • Pages Router: use the built-in i18n config in next.config.js. It doesn’t work with output: 'export'.
  • Next.js doesn’t add hreflang for you. In the App Router, use alternates.languages in generateMetadata.
  • The no-code option is one <Script> tag in app/layout.tsx. For indexable translated pages, use a sub-directory or sub-domain setup rather than translation in the browser only.
  • Whichever route you pick, set <html lang>, translate titles and descriptions, and link every language version with hreflang.

Option 1: Internationalize Next.js with the App Router

This follows the official Next.js internationalization guide (version 16.3 at the time of writing).

Step 1: Put your routes under a language segment

Move your pages and layouts into app/[lang]/. Every page then receives the locale as a route parameter:

// app/[lang]/page.tsx
export default async function Page({ params }: PageProps<'/[lang]'>) {
  const { lang } = await params;
  return <h1>{lang}</h1>;
}

Routing can use a sub-path (/fr/products) or a domain (my-site.fr/products). The sub-path is simpler to host and keeps all languages on one domain.

Step 2: Detect the language and redirect in proxy.js

In Next.js 16 the middleware file convention was renamed to proxy (there is a codemod: npx @next/codemod@canary middleware-to-proxy .). The proxy checks whether the URL already has a locale and, if not, redirects:

// proxy.js
import { NextResponse } from 'next/server';

const locales = ['en', 'fr', 'de'];

function getLocale(request) {
  // Read Accept-Language, e.g. with @formatjs/intl-localematcher and negotiator
  return 'en';
}

export function proxy(request) {
  const { pathname } = request.nextUrl;
  const hasLocale = locales.some((l) => pathname.startsWith(`/${l}/`) || pathname === `/${l}`);
  if (hasLocale) return;

  request.nextUrl.pathname = `/${getLocale(request)}${pathname}`;
  return NextResponse.redirect(request.nextUrl);
}

export const config = {
  matcher: ['/((?!_next).*)'],
};

Be careful with automatic redirects. Google’s guidance for multilingual sites recommends letting users choose: “Consider adding hyperlinks to other language versions of a page.” Redirect only when the URL has no locale, never away from a locale the visitor (or Googlebot) asked for, and always show a language switcher.

Step 3: Load a dictionary per language

Keep one JSON file per language and load it on the server:

// app/[lang]/dictionaries.ts
import 'server-only';

const dictionaries = {
  en: () => import('./dictionaries/en.json').then((m) => m.default),
  fr: () => import('./dictionaries/fr.json').then((m) => m.default),
};

export type Locale = keyof typeof dictionaries;
export const hasLocale = (l: string): l is Locale => l in dictionaries;
export const getDictionary = async (l: Locale) => dictionaries[l]();
// app/[lang]/page.tsx
import { notFound } from 'next/navigation';
import { getDictionary, hasLocale } from './dictionaries';

export default async function Page({ params }: PageProps<'/[lang]'>) {
  const { lang } = await params;
  if (!hasLocale(lang)) notFound();
  const dict = await getDictionary(lang);
  return <button>{dict.products.cart}</button>;
}

Because App Router pages are Server Components by default, the dictionaries aren’t shipped to the browser. The docs also describe next/root-params, which lets any server component read lang without passing it down through props.

Step 4: Prerender each language and set the html lang attribute

// app/[lang]/layout.tsx
export async function generateStaticParams() {
  return [{ lang: 'en' }, { lang: 'fr' }, { lang: 'de' }];
}

export default async function RootLayout({ children, params }: LayoutProps<'/[lang]'>) {
  return (
    <html lang={(await params).lang}>
      <body>{children}</body>
    </html>
  );
}

Step 5: Add hreflang and canonical tags

Next.js doesn’t know which pages are translations of each other, so you declare it. With the metadata API, alternates outputs both the canonical and the hreflang links:

// app/[lang]/pricing/page.tsx
export async function generateMetadata({ params }) {
  const { lang } = await params;
  return {
    alternates: {
      canonical: `https://example.com/${lang}/pricing`,
      languages: {
        en: 'https://example.com/en/pricing',
        fr: 'https://example.com/fr/pricing',
        de: 'https://example.com/de/pricing',
        'x-default': 'https://example.com/en/pricing',
      },
    },
  };
}

Each language version should list all versions, including itself. Our article on self-referencing hreflang tags explains why that matters.

Libraries that do the heavy lifting

The Next.js docs list several libraries for routing and translation, including next-intl, next-international, next-i18n-router, paraglide-next, lingui and tolgee. They add pluralization, number and date formatting, and type-safe message keys. For anything beyond a small site, pick one rather than writing your own.

Option 1b: The Pages Router

If your project still uses the pages/ directory, Next.js has had built-in i18n routing since version 10. Add the locales to next.config.js:

module.exports = {
  i18n: {
    locales: ['en-US', 'fr', 'de'],
    defaultLocale: 'en-US',
  },
};

This gives you /fr/blog and /de/blog automatically and sets <html lang>. According to the Pages Router guide, two limits matter: you still add hreflang yourself (with next/head), and “Internationalized Routing does not integrate with output: 'export'”. Static exports need the App Router approach or a different setup.

What the code route costs you

The code route is the right choice for a product UI whose strings live in components. It gets expensive for everything else:

  • Content outside your code (a headless CMS, product feeds, user-generated content) needs its own translation pipeline.
  • Every new string needs a key and a translation in every language before release.
  • Non-developers can’t fix a translation without a pull request, unless you add a translation management tool.
  • More languages mean more files to keep in sync.

Option 2: Translate a Next.js site without changing your code

A website translation service like ConveyThis translates the HTML your Next.js app renders, so you don’t create dictionaries or change routes.

Step 1: Create an account and add your domain

Create a ConveyThis account, add your domain, and choose your source and target languages.

Step 2: Add the script to your root layout

ConveyThis gives you a script URL with your API key. In the App Router, add it once in app/layout.tsx with next/script:

// app/layout.tsx
import Script from 'next/script';

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Script
          src="https://cdn.conveythis.com/javascript/conveythis.js?api_key=YOUR_API_KEY"
          strategy="afterInteractive"
        />
      </body>
    </html>
  );
}

The widget watches for page changes and client-side navigation, so content that React renders after the first load and routes that change without a full reload are translated too. It also sets the page’s lang attribute to the language being shown. The general steps are the same as in our React translation help article and the JavaScript integration guide.

Step 3: Choose an SEO-friendly URL structure

A script that swaps text in the browser is enough for visitors, but search engines need a separate URL for each language to index it. In the ConveyThis dashboard, choose Sub-domain (fr.example.com) or Sub-directory (example.com/fr/) under URL structure. Translated pages are then served on their own URLs with hreflang tags added. For a custom-built site, sub-domain is usually the simpler choice: your Next.js host keeps serving the main domain and you add one CNAME record per language, while sub-directory means routing the whole domain through ConveyThis. Both options are on the Business plan and above; the sub-domain vs. sub-directory help article explains the DNS records each one needs. If your domain is already on Cloudflare, the Cloudflare Workers (O2O) setup keeps your zone in place.

Step 4: Review and refine the translations

Machine translation gets you a complete first version in minutes. Then use the visual editor to adjust wording in context, add brand terms to the glossary, and invite a translator or colleague with team roles. See the full list on the features page.

Code route or no-code route?

App Router i18n (code)ConveyThis (no code)
SetupRestructure routes, add proxy, dictionaries, metadataOne script tag plus DNS for SEO URLs
Where text comes fromYour dictionariesWhatever the page renders, including CMS content
Who edits translationsDevelopers (or a TMS)Anyone with dashboard access
hreflangYou add it with alternatesAdded with sub-directory or sub-domain URLs
Best forApp UI strings, full controlMarketing sites, CMS content, many languages
CostDeveloper timePlan based on words and languages (pricing)

Many teams combine them: code-based i18n for the logged-in app, a translation service for the marketing site and help center.

Multilingual SEO checklist for Next.js

  • One URL per language (/fr/… or fr.example.com), never the same URL for every language.
  • <html lang> matches the page’s language.
  • Title and meta description translated for each language.
  • Canonical on each translated page points to itself, not to the English page.
  • Hreflang lists every language version, including the page itself, plus x-default.
  • The sitemap includes all language URLs.
  • A visible language switcher with real links, not only a dropdown that changes state.
  • No forced redirects away from the language a visitor requested.

For the bigger picture, read our multilingual SEO overview.

FAQ

Does Next.js have built-in translation? It has built-in internationalized routing in the Pages Router and documented i18n patterns for the App Router. The translated text itself comes from your dictionaries, a library, or a translation service.

What happened to middleware.js? In Next.js 16, the middleware file convention was deprecated and renamed to proxy. The logic for locale detection is the same.

Does Next.js add hreflang tags automatically? No. Use alternates.languages in the App Router metadata, or next/head in the Pages Router.

Can I translate a statically exported Next.js site? Pages Router i18n routing doesn’t work with output: 'export', and proxy isn’t supported for static exports. Use pre-generated [lang] routes, or a translation service that serves translated pages from its own sub-directory or sub-domain.


Want your Next.js site in several languages this week instead of next quarter? Create a ConveyThis account, add the script to your root layout, and pick the URL structure that fits your SEO plan.

Share:
G2 High Performer Spring 2023
G2 Easiest Setup Fall 2024
G2 Best Support Spring 2025