Building a website for five or more languages means locking in URL structure, CMS content modeling, and translation workflow before development starts. Subdirectory routing (/fr/, /de/), document-level CMS localization, automated hreflang generation, and per-locale bundle splitting prevent the routing, SEO, and performance problems that break multilingual sites at scale.

Key takeaways

  • Use subdirectory URLs (/fr/, /de/) instead of subdomains or ccTLDs to consolidate SEO authority and simplify infrastructure.
  • Model long-form content at the document level in your CMS, and structured data like product names or metadata at the field level.
  • Automate hreflang tag generation and per-locale sitemaps; manual maintenance breaks down past a handful of languages.
  • Split translation bundles per locale and subset CJK fonts so performance stays consistent across every language.
  • Combine machine translation for first drafts with human review to keep translation workflows sustainable past three languages.

Updated 15 August 2026: sources added, experience claims checked against our project record, summary added.

Why Most Multilingual Implementations Fail at Scale

Same story every time. A team builds a site in English. Somebody asks them to add Spanish. They drop in a translation library, hardcode some locale logic, and ship it. Then French gets requested. Then German. Then Japanese.

By language five, they're drowning in:

  • Routing spaghetti: Locale prefixes that blow up the second you add dynamic routes
  • Content drift: Translations falling weeks or months behind the source language
  • Bundle bloat: Every translation string loaded, no matter which locale the user needs
  • SEO blindspots: Missing or broken hreflang tags, duplicate content penalties tanking rankings
  • Layout breakage: German text running longer than English, Japanese needing different font stacks

The root cause? Teams treat multilingual as a feature. It's not. At 5+ languages, localization touches routing, data modeling, build pipelines, CDN setup, and deployment strategy. You can't npm install something on a Friday afternoon and call it done. It's foundational, or it's a mess.

URL Strategy: Subdomains vs Subdirectories vs TLDs

Your URL structure is the single most important decision for multilingual SEO. It's nearly impossible to change after launch without hurting your rankings.

Three real options on the table:

Strategy Example SEO Authority Implementation Complexity Cost
Subdirectories example.com/fr/about Consolidated (single domain) Low Low
Subdomains fr.example.com/about Split (treated as separate sites) Medium Low
ccTLDs example.fr/about Independent per country High Higher (per-domain registration and certificates)
Query params example.com/about?lang=fr Poor (not recommended) Low Low

Our recommendation for 5+ languages: subdirectories.

Here's why:

  1. Domain authority consolidation: All backlinks help every language version. With 8 languages on subdomains, you're basically building authority for 8 separate sites. That's brutal, and mostly unnecessary.
  2. Simplified infrastructure: One deployment, one SSL cert, one CDN setup. Done.
  3. Easier analytics: A single GA4 property with locale dimensions avoids messy cross-domain tracking.
  4. Lower cost: No domain registration per locale.

The exception: you need genuinely different content per country, not just language. A German site for Germany versus a German site for Switzerland, with different pricing, legal terms, and product availability, is a real distinction. ccTLDs or subdomains with country-specific content make sense there.

## Recommended subdirectory structure
example.com/            → English (default)
example.com/fr/         → French
example.com/de/         → German
example.com/ja/         → Japanese
example.com/ar/         → Arabic
example.com/pt-br/      → Brazilian Portuguese

Note the pt-br instead of just pt. At 5+ languages, you'll run into language-vs-locale distinctions. Brazilian Portuguese and European Portuguese differ enough that users notice right away.

Plan for language-region codes from day one using BCP 47 tags. Adding locale codes after launch is painful. It breaks existing URLs, redirects, and hreflang mappings.

Framework Selection for Multilingual Sites

Not all frameworks handle i18n equally. Here's where the major players stand for 5+ language support in 2026:

Framework Built-in i18n Routing Static + Dynamic Bundle Splitting by Locale RTL Support Best For
Next.js 15 ✅ (App Router) ✅ (with config) Manual Full-stack apps, dynamic content
Astro 5 ✅ (manual + Starlight) ✅ (automatic per-page) Manual Content-heavy, marketing sites
Nuxt 3 ✅ (@nuxtjs/i18n) Manual Vue ecosystem projects
Remix / React Router 7 ❌ (manual) Manual Manual Complex interactive apps
SvelteKit ❌ (manual) Manual Manual Performance-critical apps

Next.js 15 Multilingual Architecture

Next.js has one of the most mature i18n setups available, mostly thanks to the App Router. The [locale] dynamic segment gives you clean routing without middleware hacks:

// app/[locale]/layout.tsx
import { notFound } from 'next/navigation';

const locales = ['en', 'fr', 'de', 'ja', 'ar', 'pt-br', 'es', 'ko'];

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

export default function LocaleLayout({
  children,
  params: { locale },
}: {
  children: React.ReactNode;
  params: { locale: string };
}) {
  if (!locales.includes(locale)) notFound();

  return (
    <html lang={locale} dir={locale === 'ar' ? 'rtl' : 'ltr'}>
      <body>{children}</body>
    </html>
  );
}

For translation strings, next-intl is widely used. It supports ICU MessageFormat, server components, and per-locale bundle splitting. Your Japanese users won't download German translations.

That matters more than most people think.

// i18n/request.ts
import { getRequestConfig } from 'next-intl/server';

export default getRequestConfig(async ({ locale }) => ({
  messages: (await import(`../messages/${locale}.json`)).default,
}));

We cover this architecture in depth in our Next.js development capabilities.

Astro for Content-Heavy Multilingual Sites

Astro's content collections work well for multilingual marketing sites and docs. Each piece of content is organized by locale with zero JavaScript overhead:

src/content/
  blog/
    en/
      getting-started.md
      pricing-guide.md
    fr/
      getting-started.md
      pricing-guide.md
    de/
      getting-started.md

Astro 5's content layer API makes it easy to query content by locale and generate static pages for every language at build time. For a 200-page site in 8 languages, Astro generates 1,600 static HTML pages, each fully optimized with zero runtime JavaScript unless you add interactivity yourself.

More on this in our Astro development practice.

i18n Routing Architecture

Middleware-Based Locale Detection

For the best UX, detect the user's preferred language on first visit and redirect them. In Next.js middleware:

// middleware.ts
import createMiddleware from 'next-intl/middleware';

export default createMiddleware({
  locales: ['en', 'fr', 'de', 'ja', 'ar', 'pt-br', 'es', 'ko'],
  defaultLocale: 'en',
  localeDetection: true, // Uses Accept-Language header
  localePrefix: 'as-needed', // No /en/ prefix for default locale
});

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

Detection priority should go like this:

  1. Explicit URL locale (/fr/about) -- always wins, no exceptions
  2. Cookie (NEXT_LOCALE) -- respects the user's previous choice
  3. Accept-Language header -- browser preference
  4. GeoIP -- use with caution; expats and travelers often browse in a language that doesn't match their location
  5. Default locale -- fallback

Locale Switching Without Full Page Reloads

A common mistake: implementing locale switching as full navigations. When someone switches from English to French on /en/about, they should land on /fr/about, not /fr/.

Nobody wants to get dumped back to the homepage. You need path mapping across locales:

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

export function LocaleSwitcher({ currentLocale, locales }) {
  const pathname = usePathname();
  const router = useRouter();

  const switchLocale = (newLocale: string) => {
    // Replace current locale segment with new one
    const newPath = pathname.replace(`/${currentLocale}`, `/${newLocale}`);
    router.push(newPath);
  };

  return (
    <select
      value={currentLocale}
      onChange={(e) => switchLocale(e.target.value)}
    >
      {locales.map((locale) => (
        <option key={locale} value={locale}>
          {new Intl.DisplayNames([locale], { type: 'language' }).of(locale)}
        </option>
      ))}
    </select>
  );
}

Quick tip: use Intl.DisplayNames to show language names in their own script (Français, Deutsch, 日本語) instead of English. Small detail, but users notice.

Headless CMS Modeling for Multilingual Content

A headless CMS is close to non-negotiable for 5+ languages. WordPress with WPML often gets hard to maintain past three locales, since every added language multiplies the field-level translation work across every content type.

Here's how the major headless platforms stack up:

CMS Localization Model Translation Workflow API Query Pattern Pricing Impact
Contentful Field-level locales Built-in + external integrations ?locale=fr Each locale counts toward entry limits
Sanity Document-level (recommended) Plugin-based (Sanity Translate) GROQ filter by language No per-locale pricing impact
Storyblok Field-level with folder-based Built-in translation UI Dimension API Included in all plans
Hygraph Field-level locales Stage-based workflow locales: [fr] in GraphQL Locales count toward plan limits
Payload CMS Field-level or collection-level Custom workflow Filter by locale field Self-hosted, no per-locale cost

Document-Level vs Field-Level Localization

This is the most important CMS architecture decision for multilingual sites, and it's easy to get wrong.

Field-level localization (Contentful, Storyblok): Each field in a content entry holds values for every locale. One blog post entry contains the English title, French title, German title, and so on, all in one place.

Document-level localization (Sanity's recommended pattern): Each locale gets its own document, linked by a shared reference ID.

For 5+ languages, we recommend document-level localization for long-form content and field-level localization for structured data such as product names, metadata, and UI labels.

Here's why:

  • With field-level localization across 8 languages, editing a blog post means scrolling past 7 other languages' worth of content to find the field you need. Content editors dislike this.
  • Document-level keeps editor UIs clean; your French editors see only French content.
  • Translation status tracking gets simpler per document (draft, in-review, published per locale).
  • Content can diverge by locale when needed, with different hero images or CTAs for different markets.

In Sanity, this looks like:

// schemas/blogPost.ts
export default defineType({
  name: 'blogPost',
  type: 'document',
  fields: [
    defineField({
      name: 'language',
      type: 'string',
      options: {
        list: [
          { title: 'English', value: 'en' },
          { title: 'French', value: 'fr' },
          { title: 'German', value: 'de' },
          // ...
        ],
      },
    }),
    defineField({
      name: 'translationGroup',
      type: 'string', // Shared UUID across all translations of this post
      hidden: true,
    }),
    defineField({ name: 'title', type: 'string' }),
    defineField({ name: 'body', type: 'portableText' }),
  ],
});

Learn more about how we structure headless CMS projects at our CMS development page.

Translation Workflow Automation

Manual translation doesn't scale past 3 languages. Period.

At 8 languages, a single blog post creates 7 translation tasks. If your content team publishes 4 posts a week, that's 28 translations weekly. The math gets ugly fast.

Machine Translation as First Draft

The approach that holds up in 2026: use machine translation for first drafts, then have human translators polish them. DeepL and Google Cloud Translation give strong first-draft quality for European language pairs, though accuracy drops noticeably for CJK languages.

// scripts/auto-translate.ts
import * as deepl from 'deepl-node';

const translator = new deepl.Translator(process.env.DEEPL_API_KEY);

async function translateContent(
  text: string,
  sourceLang: deepl.SourceLanguageCode,
  targetLang: deepl.TargetLanguageCode
): Promise<string> {
  const result = await translator.translateText(text, sourceLang, targetLang, {
    preserveFormatting: true,
    formality: 'more', // Business-appropriate tone
    tagHandling: 'html', // Preserve HTML/markdown structure
  });
  return result.text;
}

Translation Management Systems (TMS)

For enterprise-grade workflows, you'll want a dedicated TMS:

  • Phrase: Integrates with most headless CMSs and supports enterprise localization workflows.
  • Crowdin: Strong developer experience with GitHub and GitLab sync.
  • Lokalise: Strong Figma integration for design-to-translation workflows.
  • Transifex: API-first approach suited to continuous localization.

A workflow that scales well for most teams:

  1. Content author publishes in the source language (usually English)
  2. Webhook triggers translation job creation in the TMS
  3. Machine translation generates a first draft
  4. Human translator reviews and approves
  5. Approved translation gets pushed back to the CMS via API
  6. Webhook triggers rebuild/revalidation of affected pages

That's a lot of moving parts. But once it's wired up, content teams barely notice the machinery underneath. They just write and publish.

SEO for Multilingual Sites

Hreflang Implementation

Hreflang tags tell search engines which language version to serve in which market. Get these wrong and search engines might show your German page to French users instead.

Every page needs hreflang tags pointing to all its language variants:

<!-- On /fr/about -->
<link rel="alternate" hreflang="en" href="https://example.com/about" />
<link rel="alternate" hreflang="fr" href="https://example.com/fr/about" />
<link rel="alternate" hreflang="de" href="https://example.com/de/about" />
<link rel="alternate" hreflang="ja" href="https://example.com/ja/about" />
<link rel="alternate" hreflang="ar" href="https://example.com/ar/about" />
<link rel="alternate" hreflang="x-default" href="https://example.com/about" />

The x-default tag is critical. It tells search engines which version to show when no locale matches. Don't skip it.

Automation is mandatory at scale. With 200 pages × 8 languages, you're managing 1,600 pages, each needing 9 hreflang tags (8 languages plus x-default). That's 14,400 hreflang tags.

You're not doing that by hand. Generate them programmatically:

// lib/generateHreflang.ts
export function generateHreflangTags(
  path: string,
  currentLocale: string,
  locales: string[],
  baseUrl: string
) {
  return locales.map((locale) => ({
    rel: 'alternate',
    hreflang: locale,
    href: `${baseUrl}${locale === 'en' ? '' : `/${locale}`}${path}`,
  })).concat({
    rel: 'alternate',
    hreflang: 'x-default',
    href: `${baseUrl}${path}`,
  });
}

Multilingual Sitemaps

For sites with 5+ languages, use a sitemap index file that points to per-locale sitemaps:

<!-- sitemap-index.xml -->
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap><loc>https://example.com/sitemap-en.xml</loc></sitemap>
  <sitemap><loc>https://example.com/sitemap-fr.xml</loc></sitemap>
  <sitemap><loc>https://example.com/sitemap-de.xml</loc></sitemap>
  <!-- ... -->
</sitemapindex>

Each locale sitemap should include xhtml:link elements for hreflang cross-references. Follow Google's official hreflang guidance for a reliable setup.

Performance Optimization Across Locales

Translation Bundle Splitting

Don't ship all locale strings to every user. A site with 2,000 translation keys per locale across 8 languages can easily produce several hundred kilobytes of uncompressed JSON if every locale loads at once.

Load only what the active locale needs:

// Load translations dynamically
const messages = await import(`@/messages/${locale}.json`);

With Next.js 15 and next-intl, this happens automatically with server components. Translation strings render server-side and never ship as JavaScript to the client.

Font Loading for CJK Languages

Chinese, Japanese, and Korean fonts are large. Full CJK font files can run several megabytes each, which hurts your Core Web Vitals if you're not careful.

Here's what works:

  1. Use unicode-range subsetting: Load only the characters used on each page
  2. Google Fonts with display=swap: Automatic subsetting for CJK
  3. Variable fonts where available: Single file, multiple weights
/* Only load Japanese font for Japanese locale */
@font-face {
  font-family: 'NotoSansJP';
  src: url('/fonts/NotoSansJP-subset.woff2') format('woff2');
  unicode-range: U+3000-9FFF, U+F900-FAFF; /* CJK subset */
  font-display: swap;
}

CDN and Edge Caching

Configure your CDN to cache by locale. On Vercel, this happens automatically with the [locale] segment. On Cloudflare:

Cache-Key: ${URI}-${Accept-Language}
Vary: Accept-Language

Be careful with Vary: Accept-Language. It can fragment your cache in ugly ways. Better to use explicit locale URL paths (subdirectories) so each locale gets its own clean cache entry without header-based variation.

Yet another reason subdirectories win.

Right-to-Left (RTL) Language Support

If any of your 5+ languages include Arabic, Hebrew, Persian, or Urdu, RTL support isn't optional. It touches everything:

  • Document direction: <html dir="rtl">
  • CSS layout: Flexbox and Grid handle direction automatically. margin-left doesn't; use logical properties instead.
  • Icons: Directional icons (arrows, navigation chevrons) need mirroring
/* Use CSS logical properties -- works for both LTR and RTL */
.card {
  margin-inline-start: 1rem;  /* replaces margin-left */
  padding-inline-end: 2rem;   /* replaces padding-right */
  border-inline-start: 3px solid blue; /* replaces border-left */
}

Tailwind CSS supports RTL variants through logical utility classes and directional modifiers:

<div class="ml-4 rtl:mr-4 rtl:ml-0">
  <!-- Or better, use logical utilities -->
<div class="ms-4"> <!-- margin-inline-start -->

Test RTL layouts with pseudo-localization before real Arabic translations arrive. Tools like pseudolocalize can mirror your English text to expose layout issues early, before they show up during client QA.

Testing and QA for Multilingual Sites

Automated Testing Strategy

// e2e/multilingual.spec.ts (Playwright)
import { test, expect } from '@playwright/test';

const locales = ['en', 'fr', 'de', 'ja', 'ar', 'pt-br', 'es', 'ko'];

for (const locale of locales) {
  test(`homepage loads correctly in ${locale}`, async ({ page }) => {
    await page.goto(`/${locale}`);
    
    // Verify HTML lang attribute
    const lang = await page.getAttribute('html', 'lang');
    expect(lang).toBe(locale);
    
    // Verify hreflang tags exist for all locales
    for (const l of locales) {
      const hreflang = page.locator(`link[hreflang="${l}"]`);
      await expect(hreflang).toHaveCount(1);
    }
    
    // Verify x-default exists
    await expect(page.locator('link[hreflang="x-default"]')).toHaveCount(1);
    
    // Verify no untranslated strings (English appearing on non-EN pages)
    if (locale !== 'en') {
      const h1 = await page.textContent('h1');
      expect(h1).not.toBe('Welcome'); // English fallback detection
    }
  });
}

Visual Regression Testing

German text averages 30-40% longer than English. Japanese can be shorter but needs different line-height. Use Percy or Chromatic to catch layout breakage across locales, with snapshots for every supported language at both desktop and mobile breakpoints.

The investment in multilingual testing pays for itself after the second content update that would otherwise quietly break three locales. And there is always a second update.

This is a lot to coordinate. Our own builds span four languages for a HIPAA-safe healthcare platform (SleepDr), up to thirty for a large-scale content platform, plus the eleven languages running on this site. Reach out to discuss your multilingual project, or check our pricing for an estimate.

FAQ

How much does it cost to build a multilingual website with 5+ languages?

Cost depends on page count, chosen CMS, and translation approach. A headless build using Next.js or Astro with a headless CMS for 5+ languages typically starts in the mid five figures, with ongoing spend on translation management tooling and professional human translation billed per word. Machine translation paired with human review can meaningfully cut those per-word costs. Get a tailored estimate based on your specific scope.

Should I use a translation plugin or build custom i18n?

WordPress plugins such as WPML or Polylang work fine for sites with fewer than three languages. Past five languages, a headless CMS paired with a dedicated translation management system scales better, since plugin-based translation on a monolithic CMS gets hard to manage as content volume grows. The architecture separates concerns cleanly: the CMS handles content modeling, the TMS handles workflow, and your frontend framework handles routing and rendering.

What's the best headless CMS for multilingual websites?

There is no single best choice: it depends on what you're optimizing for. Storyblok suits teams that prioritize visual editing, Sanity fits complex content models with document-level localization, and Contentful is often the safest pick for large enterprise teams that need broad third-party integrations. Storyblok has the most polished built-in multilingual editing experience, with its visual editor and field-level localization. Sanity gives you the most flexibility through document-level localization and custom workflows, which helps once your content models get complex. Contentful has strong TMS integrations, but watch the pricing since each locale counts against entry limits.

How do I handle SEO for multilingual websites?

Three things are non-negotiable: correct hreflang tags on every page pointing to all language variants, and per-locale XML sitemaps with cross-references. Add an x-default hreflang pointing to your canonical default language version, and use subdirectory URLs (/fr/, /de/) for consolidated domain authority. Submit locale-specific sitemaps in Google Search Console and Bing Webmaster Tools, and check indexing per locale weekly for the first three months so you catch problems early instead of discovering them when organic traffic drops.

Can I use Google Translate or AI to translate my website?

Not as your production translation without human review. Google Cloud Translation and DeepL do well for European language pairs but noticeably worse for CJK languages. Raw machine output still needs a human pass before publishing, especially for legal, medical, or financial content. A workflow that works well: machine translate for a first draft, have a human translator review and correct it, then publish. This hybrid approach cuts translation costs while keeping quality up, and legal, medical, or financial content should never go live without expert human review.

How do I handle URL slugs in different languages?

Translated URL slugs (/fr/a-propos instead of /fr/about) improve SEO and user experience but add real complexity. For 5+ languages, use translated slugs for top-level pages and key landing pages, but keep blog post slugs in the original language or a transliterated version. You need a slug mapping table in your CMS and bidirectional lookup during routing, since maintaining hundreds of translated URLs across a dozen locales is a burden that compounds fast.

What's the performance impact of supporting many languages?

With the right architecture, the performance impact of supporting many languages is close to zero. Static site generation with Astro or Next.js pre-renders each locale as independent HTML pages, so the server and CDN serve the French page just as fast as the English one. The main performance risks are loading all locale translation bundles at once, unoptimized CJK font loading, and cache fragmentation at the CDN layer. Each of these has a known fix.

How long does it take to add a new language to an existing multilingual site?

With the right architecture already in place, adding a new language is mostly a content-translation problem, not an engineering one. Engineering setup, meaning routing config, CMS locale creation, and TMS configuration, usually takes a few days. Translation and review for a mid-size site typically takes a few weeks, depending on word count and reviewer availability.

The engineering steps are: add the locale to routing config, create the CMS locale or dimension, configure the TMS for the new language, and update hreflang generation.