How to Add Translations to React Projects

How to Add Translations to React ProjectsHow to Add Translations to React Projects

Nov 8, 2025 - 9 min

Toni Marković

Toni Marković

Software Engineer


Hardcoded English strings can be a problem when your React app starts getting users from other countries. Instead of "Save", your Croatian users want to see "Spremi." "Speichern" is what your German users want. This is where i18n, or internationalisation, comes in.

We worked for a few weeks to get translations ready for a production app that supports four languages. This guide shows you how to set things up in a way that we use every day.

Why Use i18next?

There are a number of i18n libraries for React, such as LinguiJS, react-i18next, and react-intl. There are a few reasons why we chose i18next.

To start, it has been around since 2011. The documentation is good, the community is active, and most of the edge cases have already been fixed. When we had problems with pluralisation in Slavic languages, we found solutions in minutes.

The second thing is that the React integration feels like it belongs. The use of the translation hook fits in perfectly with how we write modern parts. No wrappers, no HOCs, and no context gymnastics.

Third, namespace support helps keep things in order. We put translations for each feature in its own file. Translations for login stay in auth.json. dashboard.json is where the translations for the dashboard are. We add more namespaces to the app as it grows, but we don't change the ones that are already there.

Installation

npm install i18next react-i18next i18next-browser-languagedetector

Add the type generator for TypeScript projects:

npm install -D i18next-resources-for-ts
  • i18next – takes care of language switching, variable interpolation, and translation logic.
  • react-i18next – lets you use the translation hook to let components get translations.
  • i18next-browser-languagedetector – looks at the browser's settings and localStorage to find the right language.
  • i18next-resources-for-ts – makes TypeScript types from JSON files so that autocomplete and error checking can work.

Project Structure

This is how we set up translations:

src/
├── i18n/
│   └── i18n.ts              # Configuration
└── locales/
    ├── en/
    │   ├── common.json      # Shared translations
    │   ├── auth.json        # Login/signup
    │   └── home.json        # Home module
    ├── hr/                  # Same structure
    └── de/                  # Same structure

There is a separate folder for each language. The same JSON files with the same keys are in each folder. We refer to these JSON files as "namespaces." The common namespace has translations for words that are used all over the place, like "Save," "Cancel", and "Delete". Feature namespaces only hold translations for that feature.

Configuration

In src/i18n/i18n.ts, type in the following:

import { initReactI18next } from 'react-i18next';

import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';

import authEn from '@/locales/en/auth.json';
import commonEn from '@/locales/en/common.json';
import authHr from '@/locales/hr/auth.json';
import commonHr from '@/locales/hr/common.json';

i18n
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    resources: {
      en: {
        common: commonEn,
        auth: authEn,
      },
      hr: {
        common: commonHr,
        auth: authHr,
      },
    },
    fallbackLng: 'en',
    defaultNS: 'common',
    ns: ['common', 'auth'],
    detection: {
      order: ['localStorage', 'navigator'],
      caches: ['localStorage'],
    },
  });

When a translation isn't available, fallbackLng is the language used. If we forget to translate one string and someone switches to German, they will see English instead of a broken key.

The detector knows where to look first because of detection order. We look at localStorage first (so returning users can keep their choice), and then we look at browser settings.

Before rendering anything, import this file into the entry point of your app.

Translation Files

Translation files are just plain JSON:

// en/common.json
{
  "save": "Save",
  "cancel": "Cancel",
  "welcome": "Welcome back, {{name}}!",
  "items": "You have {{count}} item",
  "items_other": "You have {{count}} items",
  "errors": {
    "required": "This field is required"
  }
}
// hr/common.json
{
  "save": "Spremi",
  "cancel": "Odustani",
  "welcome": "Dobrodošli natrag, {{name}}!",
  "items": "Imate {{count}} stavku",
  "items_few": "Imate {{count}} stavke",
  "items_other": "Imate {{count}} stavki",
  "errors": {
    "required": "Ovo polje je obavezno"
  }
}

Placeholders for variables are double curly braces, like {{name}}. The endings _one, _few, and _other are used to make things plural. There are two forms of English: singular and plural. There are three in Croatian: 1, 2–4, and 5+. i18next chooses the right form based on the value of count.

errors.required and other nested objects keep translations that are related together. Use dot notation to get to them.

Getting Translations

import { useTranslation } from 'react-i18next';

function Dashboard({ user, notifications }) {
  const { t } = useTranslation();

  return (
    <div>
      <h1>{t('welcome', { name: user.firstName })}</h1>
      <p>{t('items', { count: notifications.length })}</p>
      <span>{t('errors.required')}</span>
      <button>{t('save')}</button>
    </div>
  );
}

Multiple Namespaces

When a part needs translations from more than one namespace:

const { t } = useTranslation(['dashboard', 'common']);

t('chart-title'); // from 'dashboard' (first = default)
t('common:save'); // from 'common' (prefix is needed)

Language Switcher

function LanguageSwitcher() {
  const { i18n } = useTranslation();

  return (
    <select value={i18n.language} onChange={e => i18n.changeLanguage(e.target.value)}>
      <option value="en">English</option>
      <option value="hr">Hrvatski</option>
    </select>
  );
}

The function changeLanguage() changes the language and saves it to localStorage. The i18n.language property tells you what language code you are currently using.

TypeScript Support

If you don't have types, a typo like t('sav') will fail without a message. You can see the raw key in the UI. The TypeScript compiler finds these errors.

npx i18next-resources-for-ts

It looks at your en/ folder and makes type definitions. Your IDE now fills in translation keys on its own. After adding or renaming keys, run this. As "i18n:types", we put it in our npm scripts.

Adding More Languages

When you add a new feature, make a separate namespace for it:

  1. Make invoices.json in each locale folder with the same keys.
  2. For each language, import the files in i18n.ts.
  3. Add them to the resources object for that language.
  4. Add "invoices" to the ns array.
  5. Run the TypeScript generator.

Adding keys to existing namespaces is easier: just add the key to all the locale JSON files and then regenerate the types.

Common Problems and Solutions

Some languages don't have keys

We added a button, translated it into English, and sent it out. A week later, Croatian users saw "delete-permanently" as plain text. The solution is to update all of the locale files at once. This doesn't happen because of a pre-commit hook that checks for matching keys across locales.

Key names that aren't clear are causing problems

When you have 50 keys, it's hard to keep track of ones like "title". We changed the names to be more descriptive, like "invoice-page-title" and "user-settings-title". It's easier to search for and understand longer keys.

Strings that are hardcoded and never get translated

We lie to ourselves when we say, "We'll add the translation later." Now, every string that users see is wrapped in t() from the start, even when it's still in the prototype stage.

Translations work on your own computer but not on the server

The i18n import was missing from the entry point. Before the app can show up, the configuration file needs to be imported. Check main.tsx or index.tsx again.

People don't notice when there are typos in translation keys

If you don't use TypeScript types, t('sav') will show the raw key instead of "Save". Setting up type generation takes only five minutes, but it saves hours of debugging. After making changes to the translation files, run:

npx i18next-resources-for-ts

Ready to talk?

Send a brief introduction to schedule a discovery call. The call focuses on your challenges and goals and outlines the first steps toward the right digital solution.

How to Add Translations to React Projects | Workspace