i18next and React application localization in 3 steps

i18next is one of the most widely used internationalization libraries in the JavaScript ecosystem. Combined with react-i18next, it gives React applications a clean API for loading translations, handling pluralization, and switching languages at runtime.
This guide is a practical quick-start for plain React (Vite) projects. If you are building with Next.js App Router and need server components, middleware-based locale detection, and hreflang tags, see how to localize a React app using i18next — that guide covers the full App Router setup.
For the conceptual background before diving into library specifics, the complete technical guide to internationalization and software localization is a good foundation.
Create a sample project
npm create vite@latest simplelocalize-i18next-example -- --template react-ts
cd simplelocalize-i18next-example
npm install
Step 1: Install dependencies
npm install react-i18next i18next i18next-http-backend i18next-browser-languagedetector
- i18next — core library
- react-i18next — React bindings (hooks and components)
- i18next-http-backend — fetches translation JSON files over HTTP at runtime
- i18next-browser-languagedetector — detects the user's preferred language from the browser
Step 2: Configure i18next
Create src/i18n.ts and import it once at the app entry point:
// src/i18n.ts
import i18n from 'i18next';
import Backend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'en',
debug: import.meta.env.DEV,
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
interpolation: {
escapeValue: false, // React handles XSS
},
});
export default i18n;
Because this walkthrough uses a Vite template, import.meta.env.DEV is the correct development flag for the debug option.
Import it before any component renders:
// src/main.tsx
import React, { Suspense } from 'react';
import ReactDOM from 'react-dom/client';
import './i18n';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<Suspense fallback={<div>Loading…</div>}>
<App />
</Suspense>
</React.StrictMode>
);
The Suspense boundary is required: i18next-http-backend fetches translation files asynchronously, so the first render needs a fallback while the files load.
Create your source translation file at public/locales/en/translation.json:
{
"home": "Home",
"rooms": "Rooms",
"contact": "Contact",
"welcome": "Welcome to Pillow Hostel",
"availableRooms_one": "Only {{count}} room left",
"availableRooms_other": "{{count}} rooms available"
}
Step 3: Use translations in components
// src/App.tsx
import { useTranslation } from 'react-i18next';
function App() {
const { t, i18n } = useTranslation();
return (
<div>
<h1>{t('welcome')}</h1>
<nav>
<a href="/">{t('home')}</a>
<a href="/rooms">{t('rooms')}</a>
<a href="/contact">{t('contact')}</a>
</nav>
<p>{t('availableRooms', { count: 1 })}</p>
<p>{t('availableRooms', { count: 5 })}</p>
<div>
<button onClick={() => i18n.changeLanguage('en')}>English</button>
<button onClick={() => i18n.changeLanguage('es')}>Español</button>
<button onClick={() => i18n.changeLanguage('pl')}>Polski</button>
</div>
</div>
);
}
export default App;
i18n.changeLanguage triggers a fetch for the new language's JSON file if it has not been loaded yet, then re-renders all components using useTranslation.
That is the working app. Everything below is about scaling the workflow.
Managing translations with SimpleLocalize
Editing one JSON file per language by hand breaks down quickly. SimpleLocalize gives your team — developers, translators, reviewers — a shared workspace for managing translation keys and keeping files in sync.
Install the CLI
# macOS / Linux / Windows (WSL)
curl -s https://get.simplelocalize.io/2.10/install | bash
# npm
npm install @simplelocalize/cli
Configure simplelocalize.yml
# simplelocalize.yml
apiKey: YOUR_PROJECT_API_KEY
uploadFormat: single-language-json
uploadLanguageKey: en
uploadPath: ./public/locales/en/translation.json
uploadOptions:
- REPLACE_TRANSLATION_IF_FOUND
downloadFormat: single-language-json
downloadLanguageKeys: ['es', 'pl', 'de']
downloadPath: ./public/locales/{lang}/translation.json
Daily workflow
# Push English source strings to SimpleLocalize
simplelocalize upload
# Translate in the editor or trigger auto-translation
simplelocalize auto-translate
# Pull completed translations back into the project
simplelocalize download
This keeps your API key out of client-side code and gives you explicit control over when new keys enter the translation platform. To run this automatically on every deploy, see localization workflow for developers: from CLI setup to CI/CD automation.
If you want translators to push updates to production without a code deployment, point i18next-http-backend at the SimpleLocalize Translation Hosting CDN instead of your local public/ directory.
Troubleshooting common issues
Even with a perfect setup, i18n can be tricky. Here are the most common issues developers face when using i18next with React.
1. Translations are not loading (404 Not Found)
If you see errors in the console saying GET /locales/en/translation.json 404, check your public folder structure.
- Vite specific: Ensure your files are in
public/locales/...and notsrc/public/.... - Path check: If your app is hosted on a subpath (e.g.,
company.com/app/), you may need to adjust theloadPathini18n.tsto include the base path.
2. "Keys" are showing instead of "Translations"
If your UI displays welcome instead of "Welcome to Pillow Hostel", i18next cannot find the key.
- Namespace mismatch: By default, i18next looks for a file named
translation.json. If you named your filecommon.json, you must specify it:useTranslation('common'). - Case sensitivity: i18next keys are case-sensitive.
Welcomeandwelcomeare different keys.
3. Suspense errors or "flickering" text
If your app crashes with a "component suspended" error, it's because i18next-http-backend is still fetching the JSON files.
- Solution: Ensure your
<App />is wrapped in a<Suspense>block inmain.tsx. - Alternative: Set
useSuspense: falsein thereactsection of your i18n configuration, but be prepared to handle thereadystate manually in your components.
4. CLI "Unauthorized" errors
If the SimpleLocalize CLI fails to upload or download:
- API Key: Double-check your
apiKeyinsimplelocalize.yml. It should be your Project API Key, not your personal profile token. - IP Whitelisting: If you are behind a corporate VPN, ensure your network allows outgoing traffic to
api.simplelocalize.io.
5. Pluralization not working
If you pass a count but always see the same string:
- Suffix check: Ensure your keys use the correct underscore suffix (e.g.,
_oneand_other). - Language Rules: Remember that different languages have different plural rules. For example, Arabic has six different plural forms. Use the SimpleLocalize editor to see exactly which suffixes are required for your target languages.
What comes next
Once you have the basics working, you can explore more advanced features of i18next and react-i18next:
- Namespaces: split translations by feature area so each page loads only what it needs. Pass a namespace string to
useTranslation('rooms')and create a matchingpublic/locales/en/rooms.json. - Pluralization: i18next selects
_one/_other(and more categories for languages like Polish or Arabic) based on thecountvalue you pass tot(). Full breakdown in how to handle pluralization across languages. - Performance: loading all translations upfront adds unnecessary bytes. Lazy loading translations with code-splitting explains how to load namespaces on demand.
- Alternatives: best i18n libraries for React, React Native & Next.js compares i18next, FormatJS, next-intl, and others side by side.
The live demo and complete source code are available on GitHub.





