Frontend

Next.js 15 multilingual sites: routing, SEO, and delivery details

A practical walkthrough of multilingual sites with App Router: route structure, message-file management, SEO setup, and the basic delivery choices that matter first.

David Zhang
12/15/2024
8 min read
Next.jsi18nSEOApp Router

Next.js 15 multilingual sites: routing, SEO, and delivery details

The hard part of a multilingual site is usually not translating a few strings. It is whether routing, SEO, content structure, and performance details can still hold together. In Next.js 15, App Router puts those building blocks in clearer places, but the setup still needs deliberate choices.

Project Initialization

First, let's create a new Next.js 15 project:

npx create-next-app@latest my-multilingual-site
cd my-multilingual-site
npm install next-intl

Internationalization Configuration

1. Create Language Configuration File

Create src/lib/i18n.ts:

export const locales = ['zh', 'en', 'fr', 'de'] as const;
export type Locale = typeof locales[number];

export const defaultLocale: Locale = 'en';

export const localeNames: Record<Locale, string> = {
  zh: '中文',
  en: 'English',
  fr: 'Français',
  de: 'Deutsch'
};

2. Configure Middleware

Create middleware.ts:

import { createMiddleware } from 'next-intl/middleware';
import { locales, defaultLocale } from './src/lib/i18n';

export default createMiddleware({
  locales,
  defaultLocale,
  localePrefix: 'always'
});

export const config = {
  matcher: [
    '/((?!api|_next|_vercel|.*\..*).*)',
    '/([\w-]+)?/users/(.+)'
  ]
};

Route Structure Design

Using App Router's dynamic routing features:

src/app/
├── [locale]/
│   ├── layout.tsx
│   ├── page.tsx
│   ├── about/
│   │   └── page.tsx
│   ├── blog/
│   │   ├── page.tsx
│   │   └── [slug]/
│   │       └── page.tsx
│   └── contact/
│       └── page.tsx
├── globals.css
└── layout.tsx

Translation File Management

Create Translation Files

messages/zh.json:

{
  "navigation": {
    "home": "首页",
    "about": "关于我们",
    "blog": "博客",
    "contact": "联系我们"
  },
  "home": {
    "title": "欢迎来到我们的网站",
    "description": "这是一个多语言网站示例"
  }
}

messages/en.json:

{
  "navigation": {
    "home": "Home",
    "about": "About",
    "blog": "Blog",
    "contact": "Contact"
  },
  "home": {
    "title": "Welcome to Our Website",
    "description": "This is a multilingual website example"
  }
}

Component Internationalization

Create Navigation Component

'use client';

import { useTranslations } from 'next-intl';
import { useParams } from 'next/navigation';
import Link from 'next/link';

export default function Navigation() {
  const t = useTranslations('navigation');
  const params = useParams();
  const locale = params.locale as string;

  const navItems = [
    { key: 'home', href: `/${locale}` },
    { key: 'about', href: `/${locale}/about` },
    { key: 'blog', href: `/${locale}/blog` },
    { key: 'contact', href: `/${locale}/contact` }
  ];

  return (
    <nav className="flex space-x-6">
      {navItems.map(item => (
        <Link
          key={item.key}
          href={item.href}
          className="hover:text-blue-600 transition-colors"
        >
          {t(item.key)}
        </Link>
      ))}
    </nav>
  );
}

SEO Optimization

Multilingual SEO Configuration

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

interface Props {
  params: { locale: string };
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const t = await getTranslations({ locale: params.locale, namespace: 'metadata' });

  return {
    title: t('title'),
    description: t('description'),
    alternates: {
      canonical: `https://example.com/${params.locale}`,
      languages: {
        'zh': 'https://example.com/zh',
        'en': 'https://example.com/en',
        'fr': 'https://example.com/fr',
        'de': 'https://example.com/de'
      }
    },
    openGraph: {
      title: t('title'),
      description: t('description'),
      locale: params.locale,
      alternateLocale: ['zh', 'en', 'fr', 'de'].filter(l => l !== params.locale)
    }
  };
}

Performance Optimization

1. Dynamic Import of Translation Files

async function getMessages(locale: string) {
  try {
    return (await import(`../messages/${locale}.json`)).default;
  } catch (error) {
    return (await import('../messages/en.json')).default;
  }
}

2. Static Generation Optimization

export function generateStaticParams() {
  return locales.map((locale) => ({ locale }));
}

Language Switcher

'use client';

import { useParams, usePathname } from 'next/navigation';
import Link from 'next/link';
import { locales, localeNames } from '@/lib/i18n';

export default function LanguageSwitcher() {
  const params = useParams();
  const pathname = usePathname();
  const currentLocale = params.locale as string;

  const getLocalizedPath = (locale: string) => {
    const segments = pathname.split('/');
    segments[1] = locale;
    return segments.join('/');
  };

  return (
    <div className="relative">
      <select
        value={currentLocale}
        onChange={(e) => {
          const newPath = getLocalizedPath(e.target.value);
          window.location.href = newPath;
        }}
        className="bg-white border border-gray-300 rounded px-3 py-1"
      >
        {locales.map(locale => (
          <option key={locale} value={locale}>
            {localeNames[locale]}
          </option>
        ))}
      </select>
    </div>
  );
}

Implementation Notes

1. Translation Key Naming Conventions

  • Use nested structures to organize translation keys
  • Keep key names concise and descriptive
  • Use consistent naming conventions

2. Content Management

  • Consider using CMS for multilingual content management
  • Implement translation pipelines
  • Regularly review and update translations

3. Testing Strategy

// Test multilingual routing
describe('Multilingual Routing', () => {
  test('should redirect to default locale', () => {
    // Test logic
  });

  test('should preserve locale in navigation', () => {
    // Test logic
  });
});

Conclusion

In Next.js 15, the multilingual pieces are easier to reason about once routing, message files, metadata, and static generation each have a clear place in the structure.

Key takeaways:

  • Use middleware for language detection and redirection
  • Properly organize translation files and route structure
  • Focus on SEO optimization and performance improvement
  • Establish comprehensive testing and maintenance processes

If you already know the site needs to go multilingual, settle the routing layer, message files, and metadata structure first. Once those three stay stable, CMS integration and dynamic translation are much less likely to turn into rework.

Discuss an article?

Questions or comments about an article are welcome by email.