How to Build Multi-Language React Apps with Internationalization (i18n)

Published: (December 8, 2025 at 02:03 AM EST)
4 min read
Source: Dev.to

Source: Dev.to

What is Internationalization (i18n)?

Internationalization (i18n) is the process of designing and developing your application so it can be easily adapted to different languages and regions without requiring engineering changes.

The term i18n comes from the fact that there are 18 letters between the first “i” and the last “n” in “internationalization”.

Key aspects of i18n

  • Translation – Converting text strings to different languages
  • Localization – Adapting content for specific regions (dates, numbers, currencies)
  • Pluralization – Handling singular/plural forms correctly in each language

Why i18n Matters for Your React App

  • Global Reach – Reach users in their native language and expand to new markets
  • Better User Experience – Users prefer apps in their own language (studies show 75 % prefer buying products in their native language)
  • Competitive Advantage – Many apps lack proper internationalization, giving you an edge
  • Legal Requirements – Some countries require apps to be available in local languages
  • SEO Benefits – Multi‑language content improves search rankings in different regions
  • Increased Conversions – Users are more likely to complete actions when content is in their language

Prerequisites

  • Basic knowledge of React and React Hooks
  • Node.js and npm installed on your machine
  • A code editor like VS Code

Project Setup

Create a new React application using Vite:

npm create vite@latest react-i18n-demo -- --template react
cd react-i18n-demo

Install the required i18n packages:

npm install i18next react-i18next i18next-browser-languagedetector

Start the development server:

npm run dev

Your app should now be running on .

Note: This tutorial does not use any styling library. For simplicity, styles are defined inline, but in a real application you should separate CSS into its own files or use a framework like Tailwind CSS.

Replace the default CSS

/* src/index.css */
:root {
  font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
  line-height: 1.5;
  font-weight: 400;
}

The Problem: Hardcoded Strings

Typical React component with hardcoded English strings:

import { useState } from 'react';

function ProductCard({ product }) {
  return (
    <>
      {/* ❌ Hardcoded English strings */}
      <h3>{product.name}</h3>

      <p>Price: ${product.price}</p>

      <p>In stock: {product.stock} items</p>

      {product.stock === 0 && (
        <p>Out of stock</p>
      )}

      {product.stock === 1 && (
        <p>Only 1 item left!</p>
      )}

      {product.stock > 1 && (
        <p>In stock</p>
      )}

      <button>Add to cart</button>
    </>
  );
}

export default ProductCard;

Issues with this approach

  • No translation support – all text is English only
  • Maintenance nightmare – changing text requires editing multiple components
  • No pluralization – “1 item” vs “2 items” is hardcoded
  • No localization – date and number formats are fixed
  • Difficult scaling – adding new languages requires modifying every component

The Solution: react-i18next

react-i18next is the most popular and powerful solution for React internationalization. It provides:

  • Simple API for translations
  • Automatic language detection
  • Pluralization support
  • Nested translations
  • Formatting for dates, numbers, and currencies
  • Lazy loading of translation files
  • TypeScript support

How to Create Translation Files

Organize translations in separate JavaScript files inside a utils/locales folder:

utils/
  locales/
    en/
      translation.js
    es/
      translation.js
    fr/
      translation.js

English (utils/locales/en/translation.js)

export default {
  en: {
    translation: {
      "product.price": "Price",
      "product.inStock": "In stock: {{count}} items",
      "product.outOfStock": "Out of stock",
      "product.onlyOneLeft": "Only 1 item left!",
      "product.addToCart": "Add to cart",

      "profile.welcome": "Welcome, {{name}}!",
      "profile.memberSince": "Member since: {{date}}",
      "profile.loyaltyPoints": "You have {{points}} loyalty points",
      "profile.orders_zero": "You have no orders",
      "profile.orders_one": "You have 1 order",
      "profile.orders_other": "You have {{count}} orders",
      "profile.editProfile": "Edit profile",
    },
  },
};

Spanish (utils/locales/es/translation.js)

export default {
  es: {
    translation: {
      "product.price": "Precio",
      "product.inStock": "En stock: {{count}} artículos",
      "product.outOfStock": "Agotado",
      "product.onlyOneLeft": "¡Solo queda 1 artículo!",
      "product.addToCart": "Añadir al carrito",

      "profile.welcome": "¡Bienvenido, {{name}}!",
      "profile.memberSince": "Miembro desde: {{date}}",
      "profile.loyaltyPoints": "Tienes {{points}} puntos de fidelidad",
      "profile.orders_zero": "No tienes pedidos",
      "profile.orders_one": "Tienes 1 pedido",
      "profile.orders_other": "Tienes {{count}} pedidos",
      "profile.editProfile": "Editar perfil",
    },
  },
};

French (utils/locales/fr/translation.js)

export default {
  fr: {
    translation: {
      "product.price": "Prix",
      "product.inStock": "En stock: {{count}} articles",
      "product.outOfStock": "Rupture de stock",
      "product.onlyOneLeft": "Plus qu'un article !",
      "product.addToCart": "Ajouter au panier",

      "profile.welcome": "Bienvenue, {{name}} !",
      "profile.memberSince": "Membre depuis : {{date}}",
      "profile.loyaltyPoints": "Vous avez {{points}} points de fidélité",
      "profile.orders_zero": "Vous n'avez aucune commande",
      "profile.orders_one": "Vous avez 1 commande",
      "profile.orders_other": "Vous avez {{count}} commandes",
      "profile.editProfile": "Modifier le profil",
    },
  },
};
Back to Blog

Related posts

Read more »