Docs / CMS & Website Platforms / Build a Multilingual Website with i18n

Build a Multilingual Website with i18n

By Admin · Mar 15, 2026 · Updated Apr 24, 2026 · 297 views · 4 min read

Building a multilingual website expands your audience across language barriers. Internationalization (i18n) involves structuring your application to support multiple languages and locales, including translated content, date/number formatting, and RTL support. This guide covers implementing i18n in modern web frameworks on your VPS.

i18n Architecture Decisions

URL Strategies

  • Subdirectory: example.com/en/, example.com/fr/ — best for SEO, easiest to implement
  • Subdomain: en.example.com, fr.example.com — separate deployments possible
  • Domain: example.com, example.fr — strongest geo-targeting, most expensive

Next.js i18n Implementation

// next.config.mjs
export default {
  i18n: {
    locales: ['en', 'fr', 'es', 'de', 'ja'],
    defaultLocale: 'en',
    localeDetection: true,
  },
}

// Or with App Router (Next.js 14+):
// Use middleware for locale detection

// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
import { match } from '@formatjs/intl-localematcher'
import Negotiator from 'negotiator'

const locales = ['en', 'fr', 'es', 'de', 'ja']
const defaultLocale = 'en'

function getLocale(request: NextRequest): string {
  const headers = { 'accept-language': request.headers.get('accept-language') || '' }
  const languages = new Negotiator({ headers }).languages()
  return match(languages, locales, defaultLocale)
}

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl
  const pathnameHasLocale = locales.some(
    locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  )

  if (pathnameHasLocale) return

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

Translation Files

// messages/en.json
{
  "common": {
    "nav": {
      "home": "Home",
      "about": "About Us",
      "contact": "Contact",
      "blog": "Blog"
    },
    "footer": {
      "copyright": "All rights reserved.",
      "privacy": "Privacy Policy"
    }
  },
  "home": {
    "hero": {
      "title": "Welcome to Our Platform",
      "subtitle": "Build something amazing today",
      "cta": "Get Started"
    }
  }
}

// messages/fr.json
{
  "common": {
    "nav": {
      "home": "Accueil",
      "about": "À propos",
      "contact": "Contact",
      "blog": "Blog"
    },
    "footer": {
      "copyright": "Tous droits réservés.",
      "privacy": "Politique de confidentialité"
    }
  },
  "home": {
    "hero": {
      "title": "Bienvenue sur notre plateforme",
      "subtitle": "Construisez quelque chose d'incroyable aujourd'hui",
      "cta": "Commencer"
    }
  }
}

Using Translations in Components

// Using next-intl (recommended for App Router)
npm install next-intl

// app/[locale]/page.tsx
import { useTranslations } from 'next-intl'

export default function HomePage() {
  const t = useTranslations('home')

  return (
    <section>
      <h1>{t('hero.title')}</h1>
      <p>{t('hero.subtitle')}</p>
      <button>{t('hero.cta')}</button>
    </section>
  )
}

// Pluralization and interpolation
// messages/en.json:
// "items": "{count, plural, =0 {No items} =1 {One item} other {# items}}"
// Usage: t('items', { count: 5 }) → "5 items"

SEO for Multilingual Sites

// app/[locale]/layout.tsx
export default function LocaleLayout({
  children,
  params: { locale },
}: {
  children: React.ReactNode
  params: { locale: string }
}) {
  return (
    <html lang={locale} dir={locale === 'ar' ? 'rtl' : 'ltr'}>
      <head>
        {/* hreflang tags for SEO */}
        <link rel="alternate" hrefLang="en" href="https://example.com/en" />
        <link rel="alternate" hrefLang="fr" href="https://example.com/fr" />
        <link rel="alternate" hrefLang="es" href="https://example.com/es" />
        <link rel="alternate" hrefLang="x-default" href="https://example.com/en" />
      </head>
      <body>{children}</body>
    </html>
  )
}

Date and Number Formatting

// Using the Intl API (built into JavaScript)
const formatDate = (date: Date, locale: string) => {
  return new Intl.DateTimeFormat(locale, {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
  }).format(date)
}

formatDate(new Date(), 'en-US')  // "March 15, 2026"
formatDate(new Date(), 'fr-FR')  // "15 mars 2026"
formatDate(new Date(), 'ja-JP')  // "2026年3月15日"

const formatCurrency = (amount: number, locale: string, currency: string) => {
  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency,
  }).format(amount)
}

formatCurrency(1234.50, 'en-US', 'USD')  // "$1,234.50"
formatCurrency(1234.50, 'de-DE', 'EUR')  // "1.234,50 €"
formatCurrency(1234.50, 'ja-JP', 'JPY')  // "¥1,235"

Language Switcher Component

// components/LanguageSwitcher.tsx
'use client'
import { usePathname, useRouter } from 'next/navigation'

const languages = [
  { code: 'en', name: 'English', flag: '🇺🇸' },
  { code: 'fr', name: 'Français', flag: '🇫🇷' },
  { code: 'es', name: 'Español', flag: '🇪🇸' },
  { code: 'de', name: 'Deutsch', flag: '🇩🇪' },
  { code: 'ja', name: '日本語', flag: '🇯🇵' },
]

export function LanguageSwitcher({ currentLocale }: { currentLocale: string }) {
  const pathname = usePathname()
  const router = useRouter()

  const switchLocale = (newLocale: string) => {
    const newPath = pathname.replace(`/${currentLocale}`, `/${newLocale}`)
    router.push(newPath)
    document.cookie = `NEXT_LOCALE=${newLocale};path=/;max-age=31536000`
  }

  return (
    <select
      value={currentLocale} => switchLocale(e.target.value)}
    >
      {languages.map(lang => (
        <option key={lang.code} value={lang.code}>
          {lang.flag} {lang.name}
        </option>
      ))}
    </select>
  )
}

Best Practices

  • Use subdirectory routing (/en/, /fr/) for the best SEO results
  • Always include hreflang tags to help search engines serve the right language version
  • Use the Intl API for dates, numbers, and currencies — don't hardcode formats
  • Support RTL layouts with the dir attribute for Arabic, Hebrew, etc.
  • Keep translation keys organized by page/section for maintainability
  • Use professional translators or AI-assisted translation with human review
  • Test thoroughly — text expansion in some languages can break layouts

Was this article helpful?