Adding internationalization to a React app is straightforward with the right tools. This guide covers the most popular approaches: react-i18next, react-intl (FormatJS), and Next.js’s built-in i18n—plus how to manage translations as your app scales.
Choosing Your i18n Library
react-i18next
The most popular choice. It’s flexible, well-documented, and works with any React setup.
Best for: Most React apps, especially those not using Next.js
react-intl (FormatJS)
Backed by the FormatJS project. Strong ICU message format support and good TypeScript integration.
Best for: Apps with complex pluralization/formatting needs
Next.js built-in i18n
Native routing-based internationalization for Next.js apps.
Best for: Next.js apps where you want locale-based routing
For this guide, we’ll focus on react-i18next as it’s the most widely used.
Setting Up react-i18next
Installation
npm install react-i18next i18next i18next-http-backend i18next-browser-languagedetector
Configuration
Create src/i18n/index.ts:
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import HttpBackend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
i18n
.use(HttpBackend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'en',
supportedLngs: ['en', 'es', 'de', 'fr'],
debug: process.env.NODE_ENV === 'development',
interpolation: {
escapeValue: false, // React already escapes
},
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
detection: {
order: ['localStorage', 'navigator'],
caches: ['localStorage'],
},
});
export default i18n;
Import in your entry point (src/index.tsx or src/main.tsx):
import './i18n';
Create Translation Files
Create your locale files at public/locales/en/translation.json:
{
"nav": {
"home": "Home",
"about": "About",
"contact": "Contact"
},
"home": {
"title": "Welcome to Our App",
"subtitle": "Build something amazing",
"cta": "Get Started"
},
"common": {
"loading": "Loading...",
"error": "Something went wrong",
"retry": "Try Again"
}
}
And public/locales/es/translation.json:
{
"nav": {
"home": "Inicio",
"about": "Acerca de",
"contact": "Contacto"
},
"home": {
"title": "Bienvenido a Nuestra App",
"subtitle": "Construye algo increíble",
"cta": "Comenzar"
},
"common": {
"loading": "Cargando...",
"error": "Algo salió mal",
"retry": "Intentar de Nuevo"
}
}
Using Translations
The useTranslation Hook
import { useTranslation } from 'react-i18next';
function HomePage() {
const { t } = useTranslation();
return (
<div>
<h1>{t('home.title')}</h1>
<p>{t('home.subtitle')}</p>
<button>{t('home.cta')}</button>
</div>
);
}
With Parameters
{
"greeting": "Hello, {{name}}!",
"items": "You have {{count}} items"
}
function Greeting({ name, itemCount }) {
const { t } = useTranslation();
return (
<div>
<p>{t('greeting', { name })}</p>
<p>{t('items', { count: itemCount })}</p>
</div>
);
}
Pluralization
{
"messages": "You have {{count}} message",
"messages_plural": "You have {{count}} messages",
"messages_zero": "You have no messages"
}
// Automatically picks the right form based on count
{t('messages', { count: messageCount })}
For complex pluralization (ICU format):
{
"items": "{count, plural, =0 {No items} one {# item} other {# items}}"
}
The Trans Component
For translations with embedded JSX:
{
"welcome": "Welcome to <strong>{{appName}}</strong>. Click <link>here</link> to continue."
}
import { Trans } from 'react-i18next';
function Welcome() {
return (
<Trans
i18nKey="welcome"
values={{ appName: 'MyApp' }}
components={{
strong: <strong />,
link: <a href="/start" />
}}
/>
);
}
Language Switching
Create a language switcher component:
import { useTranslation } from 'react-i18next';
const languages = [
{ code: 'en', name: 'English', flag: '🇺🇸' },
{ code: 'es', name: 'Español', flag: '🇪🇸' },
{ code: 'de', name: 'Deutsch', flag: '🇩🇪' },
{ code: 'fr', name: 'Français', flag: '🇫🇷' },
];
function LanguageSwitcher() {
const { i18n } = useTranslation();
return (
<select
value={i18n.language}
onChange={(e) => i18n.changeLanguage(e.target.value)}
>
{languages.map((lang) => (
<option key={lang.code} value={lang.code}>
{lang.flag} {lang.name}
</option>
))}
</select>
);
}
Namespaces for Code Splitting
Large apps benefit from splitting translations by feature:
public/locales/en/
├── common.json # Shared translations
├── auth.json # Login/signup
├── dashboard.json # Dashboard feature
└── settings.json # Settings feature
Load specific namespaces:
function Dashboard() {
const { t } = useTranslation(['dashboard', 'common']);
return (
<div>
{/* Uses dashboard namespace */}
<h1>{t('dashboard:title')}</h1>
{/* Uses common namespace */}
<button>{t('common:save')}</button>
</div>
);
}
With React Suspense for loading states:
import { Suspense } from 'react';
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Dashboard />
</Suspense>
);
}
Next.js Integration
Next.js has built-in i18n routing. Combine it with react-i18next:
next.config.js
module.exports = {
i18n: {
locales: ['en', 'es', 'de', 'fr'],
defaultLocale: 'en',
},
};
Server-Side Translation Loading
For App Router (Next.js 13+):
// src/i18n/settings.ts
export const fallbackLng = 'en';
export const languages = ['en', 'es', 'de', 'fr'];
export function getOptions(lng = fallbackLng, ns = 'translation') {
return {
supportedLngs: languages,
fallbackLng,
lng,
fallbackNS: 'translation',
defaultNS: 'translation',
ns,
};
}
// src/i18n/server.ts
import { createInstance } from 'i18next';
import { initReactI18next } from 'react-i18next/initReactI18next';
import { getOptions } from './settings';
export async function getServerTranslation(lng: string, ns?: string) {
const i18nInstance = createInstance();
await i18nInstance
.use(initReactI18next)
.init({
...getOptions(lng, ns),
resources: {
[lng]: {
[ns || 'translation']: await import(`../../public/locales/${lng}/${ns || 'translation'}.json`)
}
}
});
return {
t: i18nInstance.t,
i18n: i18nInstance,
};
}
TypeScript Support
Create type-safe translations:
// src/types/i18n.d.ts
import 'react-i18next';
import translation from '../../public/locales/en/translation.json';
declare module 'react-i18next' {
interface CustomTypeOptions {
defaultNS: 'translation';
resources: {
translation: typeof translation;
};
}
}
Now you get autocomplete for translation keys!
Managing Translations at Scale
Once you have more than a handful of keys, managing JSON files becomes tedious. Here’s where tooling helps.
The Problem with Manual Management
- Keys get out of sync between languages
- Unused keys accumulate
- New keys aren’t flagged for translation
- No easy way to track translation progress
Using LangCtl with React
LangCtl integrates with react-i18next projects:
# Initialize
langctl init
# Scan for translation keys in your codebase
langctl scan ./src --format react-i18next
# See what needs translation
langctl status
# Push new keys to the dashboard
langctl push
# Pull translations back
langctl pull
The CLI detects:
t('key')calls<Trans i18nKey="key">components- Namespace usage
CI/CD Integration
Add to your pipeline:
# .github/workflows/i18n.yml
name: i18n Check
on: [pull_request]
jobs:
check-translations:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Check for missing translations
run: |
npx langctl scan ./src
npx langctl status --fail-missing
This fails the build if new keys haven’t been translated.
Best Practices
1. Use Meaningful Key Names
// ❌ Bad
{
"text1": "Welcome",
"btn": "Submit"
}
// ✅ Good
{
"home.hero.title": "Welcome",
"forms.submit_button": "Submit"
}
2. Keep Keys Flat-ish
Two to three levels of nesting is ideal:
{
"feature.component.element": "Translation"
}
3. Provide Context for Translators
Use description comments in your code:
// i18n: Button shown after successful form submission
{t('forms.success_message')}
4. Handle Loading States
function App() {
const { ready } = useTranslation();
if (!ready) {
return <LoadingSpinner />;
}
return <MainContent />;
}
5. Test with Pseudo-Localization
Add a “pseudo” locale that transforms English to catch hardcoded strings:
"Welcome" → "[Ŵéļçöɱé]"
If you see un-transformed text, it’s not going through i18n.
Common Mistakes
Hardcoding text “just for now”: It’s never just for now. Use keys from day one.
Forgetting about text expansion: German is often 30% longer than English. Design for it.
Ignoring RTL: If you might support Arabic or Hebrew, set up RTL CSS early.
Not handling missing translations: Show the key or fallback, don’t show blank strings.
Over-engineering namespaces: Start simple. Split namespaces when you actually need to.
Conclusion
React i18n is straightforward:
- Install react-i18next
- Set up translation files
- Use the
useTranslationhook - Add a language switcher
- Use a tool like LangCtl when you have multiple languages
The key is starting early. Adding i18n to an existing app is much harder than building with it from the start.
Have questions? Get in touch—we’ve built dozens of multilingual React apps and are happy to help.