How to make a website multilingual: A developer's implementation guide

Kinga Pomykała
Kinga Pomykała
Last updated: March 20, 20269 min read
How to make a website multilingual: A developer's implementation guide

Browser auto-translate is not a multilingual website. It's a workaround for when you haven't built one.

This guide covers what it actually takes to make a web application properly multilingual, from restructuring your code for i18n, to choosing translation file formats, wiring up locale detection, handling dates and plurals correctly, and keeping translations in sync as your product ships.

If you're looking for the strategic layer, like market prioritization, ROI, governance, that's in the localization strategy guide for SaaS teams. This guide is for the implementation.

Step 1: Internationalize your codebase first

The single most important thing to understand: you cannot translate a codebase that wasn't built for it without significant rework. Localization starts with internationalization (i18n): restructuring your code so user-facing text lives in external files rather than inside components.

This is not optional scaffolding. It's the architectural decision that determines how expensive every future language will be.

What i18n actually means in practice

Every string a user can see gets replaced with a translation key:

// Before i18n
Welcome back, {name}. You have {count} unread messages.

// After i18n (using i18next as an example)
{t("dashboard.welcome", { name, count })}

The actual content lives in a JSON file:

// en.json
{
  "dashboard": {
    "welcome_one": "Welcome back, {{name}}. You have {{count}} unread message.",
    "welcome_other": "Welcome back, {{name}}. You have {{count}} unread messages."
  }
}

Notice the plural forms already: English has two (one and other). Polish needs four. Arabic needs six. Your i18n library handles this at runtime as long as you've structured the keys correctly, more on plurals below.

The cost of waiting: if you ship a product with hardcoded strings and decide to add internationalization later, you're touching every component in your codebase. In a mature app, that can mean weeks of work, with a high risk of missing strings that only surface in production. Build it in from day one, even if you only ship one language initially.

Step 2: Choose an i18n library for your framework

The right library depends on your stack. The major options:

React and Next.js:

  • react-i18next: the most widely used, flexible plugin ecosystem, good Next.js support
  • next-i18next: wraps react-i18next for Next.js App Router and Pages Router
  • FormatJS / react-intl: strong ICU message format support, popular in enterprise
  • Zero-Intl: lightweight, zero-dependency, CLI for extracting messages

Vue:

Angular:

Other:

  • LinguiJS: framework-agnostic, good developer experience
  • i18next: works across frameworks, large ecosystem

All of these follow the same pattern: define translation keys, store translations in files, load the right file at runtime, expose a t() function or equivalent in your components.

See the full comparison: Best i18n libraries for React, React Native & Next.js

Step 3: Structure your translation files

Translation files are the backbone of the system. How you structure them now determines how maintainable everything is later.

Naming and nesting

Use feature-based namespaces rather than one flat file per language. This keeps files smaller, loads faster, and makes ownership clearer:

translations/
  en/
    common.json      ← buttons, labels, shared UI
    settings.json    ← settings page
    billing.json     ← billing section
    onboarding.json  ← onboarding flow
  de/
    common.json
    settings.json
    ...

Each namespace loads independently. A user on the billing page only loads billing.json and common.json, not your entire translation set.

Key naming conventions

A few conventions that prevent technical debt:

// Consistent, scoped, readable
{
  "settings": {
    "save_button": "Save changes",
    "cancel_button": "Cancel",
    "title": "Account Settings"
  }
}

// Flat, collides easily, hard to organize
{
  "save": "Save changes",
  "settingsTitle": "Account Settings",
  "btn_cancel": "Cancel"
}

Use snake_case or camelCase consistently. Nest by feature. Avoid abbreviations. Keys should be readable without their translation value.

Check out our best practices for creating translation keys.

Choosing a file format

JSON is the default for JavaScript ecosystems. But the choice depends on your stack:

FormatBest for
JSONReact, Vue, Node.js
YAMLRuby, config-heavy systems
PO/POTPHP, Python, WordPress (gettext)
ARBFlutter
.strings / .xcstringsiOS/macOS
XMLAndroid
XLIFFEnterprise, professional translation exchange

Check the comparison: YAML vs JSON for translation files

Step 4: Implement locale detection

Your app needs to determine which locale to serve each user. The main strategies:

The locale is encoded in the URL path or subdomain:

example.com/en/pricing
example.com/de/pricing
de.example.com/pricing

This is the best approach for SEO because search engines index each locale separately, giving you organic visibility in each language. It's also explicit: the user always knows which locale they're viewing, and links are shareable.

In Next.js App Router, you configure this in next.config.js and handle locale resolution in middleware:

// middleware.js
import { match } from '@formatjs/intl-localematcher';
import Negotiator from 'negotiator';

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

export function middleware(request) {
  const headers = { 'accept-language': request.headers.get('accept-language') ?? '' };
  const languages = new Negotiator({ headers }).languages();
  const locale = match(languages, locales, defaultLocale);
  
  // Redirect to locale-prefixed URL if not already there
  const { pathname } = request.nextUrl;
  if (!locales.some(l => pathname.startsWith(`/${l}`))) {
    return Response.redirect(new URL(`/${locale}${pathname}`, request.url));
  }
}

Store the user's locale preference in a cookie after they select it manually. Works well for apps where locale is a user preference rather than a routing concern (authenticated dashboards, for example). Not great for SEO since URLs don't change.

Accept-Language header

Read the Accept-Language header to make an initial guess. Use it as a starting point, not a final decision: the header reflects browser configuration, not necessarily the user's actual preference. Always let users override it.

In practice, combine all three: detect from Accept-Language on first visit, encode in the URL for SEO and cacheability, persist in a cookie or user profile for returning users, and always expose a language selector for manual override.

For more details, check out our guide on URLs in localization.

Step 5: Handle dates, numbers, and currencies correctly

This is where many apps quietly break. Never hardcode format strings.

The JavaScript Intl API handles locale-aware formatting without any library:

// Dates
new Intl.DateTimeFormat('de-DE', {
  dateStyle: 'long'
}).format(new Date());
// → "20. März 2026"  (not "March 20, 2026")

// Numbers
new Intl.NumberFormat('fr-FR').format(1234567.89);
// → "1 234 567,89"  (not "1,234,567.89")

// Currencies
new Intl.NumberFormat('ja-JP', {
  style: 'currency',
  currency: 'JPY'
}).format(12500);
// → "¥12,500"

Always store timestamps in UTC. Convert to the user's timezone only at display time.

Check also: Number formatting in JavaScript

Step 6: Handle pluralization properly

English has two plural forms. Many languages have more. Getting this wrong is one of the most visible localization bugs.

Modern i18n libraries support ICU message format for pluralization:

// English (2 forms: one, other)
{count, plural,
  one {You have # unread message}
  other {You have # unread messages}
}

// Polish (4 forms: one, few, many, other)
{count, plural,
  one {Masz # nieprzeczytaną wiadomość}
  few {Masz # nieprzeczytane wiadomości}
  many {Masz # nieprzeczytanych wiadomości}
  other {Masz # nieprzeczytanej wiadomości}
}

The application code passes count and the library picks the right form. You never write conditional logic for this in your components;that logic lives in the translation file, where the translator can handle it correctly for their language.

Learn more:
How to handle pluralization across languages
ICU message format guide

Step 7: Add a language selector

Users need to be able to choose their language explicitly. A few rules:

  • Don't use country flags to represent languages. Spanish is spoken in 20+ countries. English in dozens more. A US flag for English excludes Australian, British, Canadian, and every other English-speaking user. Use language names or ISO codes instead.
  • Show language names in the language itself: "Deutsch", not "German". "日本語", not "Japanese".
  • Make it findable: navigation header or footer, never buried in settings alone.
  • Persist the choice: store it in a cookie or user profile so it survives page loads.

Related:
Tips for creating a language selector
Top language selector UX examples

Step 8: Manage translations at scale

Once you have more than one language and more than one contributor, you need more than a folder of JSON files.

The problems that emerge quickly:

  • A developer adds a new key. Translators don't know it exists.
  • A string gets updated in English. The German translation is now stale.
  • Three languages are complete. Two are at 60%. You can't easily tell which.
  • A translator changes a key. The developer's local file is now out of sync.
Translation management workflow
Translation management workflow

A translation management system (TMS) like SimpleLocalize solves all of this. It gives translators a proper editing interface, tracks completion per language, integrates with your CI/CD pipeline to sync keys automatically, and supports auto-translation with DeepL, Google Translate, or AI models for a first-pass draft.

The basic workflow with a TMS:

  1. Developer adds a new key to the source code
  2. CI pipeline pushes the key to the TMS on commit
  3. Translator translates it in the editor (or auto-translation does a first pass)
  4. Reviewer approves
  5. CI pipeline pulls the approved translation back into the build

This is called continuous localization: translation runs in parallel with development rather than as a separate batch step. Every release ships fully translated.

Learn more:
Continuous localization: What it is and how to implement it
Localization workflow for developers: From CLI setup to CI/CD automation

Building on top of this

This guide covers the core implementation path. From here, depending on your product's scale, you'll likely need to go deeper on:

  • RTL layouts for Arabic, Hebrew, Persian: more than just flipping text direction
  • Pseudo-localization for testing layouts before real translations exist
  • Performance: lazy-loading translation namespaces, edge delivery via CDN
  • Multi-tenant localization for SaaS products where different customers need different terminology
  • CI/CD automation: pushing keys on commit, pulling translations before deploy

All of these are covered in the complete technical guide to i18n and software localization.

Checklist: Making a website multilingual

Before you ship your first localized release, verify:

  • All user-facing strings use translation keys (no hardcoded text in components)
  • Translation files are structured in namespaces by feature
  • Locale detection is implemented (URL-based for public pages)
  • Users can manually select and override their language
  • Dates, times, numbers, and currencies use Intl formatting
  • Pluralization uses ICU format or equivalent (not hardcoded conditionals)
  • Language selector doesn't use flags
  • Translation files are synced to a TMS for contributor workflow
  • CI/CD pipeline pushes new keys and pulls translations on deploy
  • Missing keys show a fallback (source language), not an empty string
Kinga Pomykała
Kinga Pomykała
Content creator of SimpleLocalize

Get started with SimpleLocalize

  • All-in-one localization platform
  • Web-based translation editor for your team
  • Auto-translation, QA-checks, AI and more
  • See how easily you can start localizing your product.
  • Powerful API, hosting, integrations and developer tools
  • Unmatched customer support
Start for free
No credit card required5-minute setup
"The product
and support
are fantastic."
Laars Buur|CTO
"The support is
blazing fast,
thank you Jakub!"
Stefan|Developer
"Interface that
makes any dev
feel at home!"
Dario De Cianni|CTO
"Excellent app,
saves my time
and money"
Dmitry Melnik|Developer