We use cookies to enhance your experience on the site
CodeWorlds

React-i18next - Internationalization of React Applications

React-i18next is a powerful library for internationalization (i18n) of React applications, enabling easy addition of multi-language support, formatting of dates, numbers, and currencies. It is a comprehensive solution based on the popular i18next library.

Installation and Basic Configuration

Installation

1npm install react-i18next i18next i18next-browser-languagedetector i18next-http-backend

Basic Configuration

1// i18n.js
2import i18n from 'i18next';
3import { initReactI18next } from 'react-i18next';
4import Backend from 'i18next-http-backend';
5import LanguageDetector from 'i18next-browser-languagedetector';
6
7i18n
8  .use(Backend) // Load translations from server
9  .use(LanguageDetector) // Automatic language detection
10  .use(initReactI18next) // React integration
11  .init({
12    // Fallback configuration
13    fallbackLng: 'en',
14
15    // Debugging in development
16    debug: process.env.NODE_ENV === 'development',
17
18    // Interpolation
19    interpolation: {
20      escapeValue: false // React already protects against XSS
21    },
22
23    // Backend configuration
24    backend: {
25      loadPath: '/locales/{{lng}}/{{ns}}.json'
26    },
27
28    // Language detector configuration
29    detection: {
30      order: ['localStorage', 'navigator', 'htmlTag'],
31      caches: ['localStorage']
32    }
33  });
34
35export default i18n;

Integration with React Application

1// index.js
2import React from 'react';
3import ReactDOM from 'react-dom/client';
4import './i18n'; // Import configuration
5import App from './App';
6
7const root = ReactDOM.createRoot(document.getElementById('root'));
8root.render(<App />);

Basic Usage

The useTranslation Hook

1// App.jsx
2import { useTranslation } from 'react-i18next';
3
4function App() {
5  const { t, i18n } = useTranslation();
6
7  const changeLanguage = (lang) => {
8    i18n.changeLanguage(lang);
9  };
10
11  return (
12    <div>
13      <h1>{t('welcome.title')}</h1>
14      <p>{t('welcome.description')}</p>
15
16      <div>
17        <button onClick={() => changeLanguage('en')}>English</button>
18        <button onClick={() => changeLanguage('pl')}>Polski</button>
19        <button onClick={() => changeLanguage('es')}>Español</button>
20      </div>
21
22      <p>{t('current_language')}: {i18n.language}</p>
23    </div>
24  );
25}
26
27export default App;

Translation Files

1// public/locales/en/translation.json
2{
3  "welcome": {
4    "title": "Welcome to our application",
5    "description": "This is a multilingual React application"
6  },
7  "current_language": "Current language",
8  "navigation": {
9    "home": "Home",
10    "about": "About",
11    "contact": "Contact"
12  }
13}
1// public/locales/pl/translation.json
2{
3  "welcome": {
4    "title": "Witaj w naszej aplikacji",
5    "description": "To jest wielojęzyczna aplikacja React"
6  },
7  "current_language": "Aktualny język",
8  "navigation": {
9    "home": "Strona główna",
10    "about": "O nas",
11    "contact": "Kontakt"
12  }
13}

Advanced Features

1. Interpolation and Pluralization

1function UserProfile({ user, messageCount }) {
2  const { t } = useTranslation();
3
4  return (
5    <div>
6      {/* Interpolation */}
7      <h1>{t('user.greeting', { name: user.name })}</h1>
8
9      {/* Pluralization */}
10      <p>{t('messages.count', { count: messageCount })}</p>
11
12      {/* Date formatting */}
13      <p>{t('user.joined', {
14        date: new Date(user.joinedAt),
15        formatParams: {
16          date: { year: 'numeric', month: 'long', day: 'numeric' }
17        }
18      })}</p>
19    </div>
20  );
21}
1// Translation files with pluralization
2{
3  "user": {
4    "greeting": "Hello, {{name}}!"
5  },
6  "messages": {
7    "count_zero": "No messages",
8    "count_one": "1 message",
9    "count_other": "{{count}} messages"
10  }
11}

2. Namespaces

1// Configuration with namespaces
2i18n.init({
3  //
4  defaultNS: 'common',
5  ns: ['common', 'navigation', 'forms', 'errors']
6});
7
8// Using different namespaces
9function Navigation() {
10  const { t } = useTranslation('navigation');
11
12  return (
13    <nav>
14      <a href="/">{t('home')}</a>
15      <a href="/about">{t('about')}</a>
16      <a href="/contact">{t('contact')}</a>
17    </nav>
18  );
19}
20
21function ContactForm() {
22  const { t } = useTranslation(['forms', 'errors']);
23
24  return (
25    <form>
26      <label>{t('forms:name.label')}</label>
27      <input
28        required
29      />
30
31      <label>{t('forms:email.label')}</label>
32      <input
33        type="email"
34        required
35      />
36
37      <button type="submit">
38        {t('forms:submit')}
39      </button>
40    </form>
41  );
42}

3. Context API Integration

1// LanguageContext.jsx
2import { createContext, useContext, useState } from 'react';
3import { useTranslation } from 'react-i18next';
4
5const LanguageContext = createContext();
6
7export function LanguageProvider({ children }) {
8  const { i18n } = useTranslation();
9  const [language, setLanguage] = useState(i18n.language);
10
11  const changeLanguage = async (newLang) => {
12    await i18n.changeLanguage(newLang);
13    setLanguage(newLang);
14
15    // Save choice in localStorage
16    localStorage.setItem('preferredLanguage', newLang);
17
18    // Change text direction for RTL languages
19    if (['ar', 'he', 'fa'].includes(newLang)) {
20      document.dir = 'rtl';
21    } else {
22      document.dir = 'ltr';
23    }
24  };
25
26  const availableLanguages = [
27    { code: 'en', name: 'English', flag: '🇺🇸' },
28    { code: 'pl', name: 'Polski', flag: '🇵🇱' },
29    { code: 'es', name: 'Español', flag: '🇪🇸' },
30    { code: 'fr', name: 'Français', flag: '🇫🇷' },
31    { code: 'de', name: 'Deutsch', flag: '🇩🇪' }
32  ];
33
34  return (
35    <LanguageContext.Provider value={{
36      language,
37      changeLanguage,
38      availableLanguages,
39      isRTL: ['ar', 'he', 'fa'].includes(language)
40    }}>
41      {children}
42    </LanguageContext.Provider>
43  );
44}
45
46export const useLanguage = () => {
47  const context = useContext(LanguageContext);
48  if (!context) {
49    throw new Error('useLanguage must be used within LanguageProvider');
50  }
51  return context;
52};
53
54// LanguageSelector.jsx
55function LanguageSelector() {
56  const { language, changeLanguage, availableLanguages } = useLanguage();
57  const { t } = useTranslation();
58
59  return (
60    <div className="language-selector">
61      <select
62        value={language}
63        onChange={(e) => changeLanguage(e.target.value)}
64        aria-label={t('common:select_language')}
65      >
66        {availableLanguages.map(lang => (
67          <option key={lang.code} value={lang.code}>
68            {lang.flag} {lang.name}
69          </option>
70        ))}
71      </select>
72    </div>
73  );
74}

Formatting Dates, Numbers, and Currencies

1. Date Formatting

1// DateFormatter.jsx
2import { useTranslation } from 'react-i18next';
3
4function DateFormatter({ date, format = 'short' }) {
5  const { i18n } = useTranslation();
6
7  const formatDate = (date, format) => {
8    const formatOptions = {
9      short: {
10        year: 'numeric',
11        month: 'short',
12        day: 'numeric'
13      },
14      long: {
15        year: 'numeric',
16        month: 'long',
17        day: 'numeric',
18        weekday: 'long'
19      },
20      time: {
21        hour: '2-digit',
22        minute: '2-digit'
23      },
24      datetime: {
25        year: 'numeric',
26        month: 'short',
27        day: 'numeric',
28        hour: '2-digit',
29        minute: '2-digit'
30      }
31    };
32
33    return new Intl.DateTimeFormat(
34      i18n.language,
35      formatOptions[format]
36    ).format(new Date(date));
37  };
38
39  return <span>{formatDate(date, format)}</span>;
40}
41
42// Usage
43function EventCard({ event }) {
44  return (
45    <div className="event-card">
46      <h3>{event.title}</h3>
47      <DateFormatter date={event.startDate} format="long" />
48      <DateFormatter date={event.startTime} format="time" />
49    </div>
50  );
51}

2. Number and Currency Formatting

1// NumberFormatter.jsx
2import { useTranslation } from 'react-i18next';
3
4function NumberFormatter({
5  value,
6  type = 'decimal',
7  currency = 'USD',
8  minimumFractionDigits = 0,
9  maximumFractionDigits = 2
10}) {
11  const { i18n } = useTranslation();
12
13  const formatNumber = (value, type) => {
14    const options = {
15      minimumFractionDigits,
16      maximumFractionDigits
17    };
18
19    if (type === 'currency') {
20      options.style = 'currency';
21      options.currency = currency;
22    } else if (type === 'percent') {
23      options.style = 'percent';
24    }
25
26    return new Intl.NumberFormat(i18n.language, options).format(value);
27  };
28
29  return <span>{formatNumber(value, type)}</span>;
30}
31
32// PriceDisplay.jsx
33function PriceDisplay({ price, currency, originalPrice }) {
34  const { t } = useTranslation();
35
36  return (
37    <div className="price-display">
38      <span className="current-price">
39        <NumberFormatter
40          value={price}
41          type="currency"
42          currency={currency}
43        />
44      </span>
45
46      {originalPrice && originalPrice > price && (
47        <span className="original-price">
48          <NumberFormatter
49            value={originalPrice}
50            type="currency"
51            currency={currency}
52          />
53        </span>
54      )}
55
56      {originalPrice && originalPrice > price && (
57        <span className="discount">
58          {t('product.discount', {
59            percent: ((originalPrice - price) / originalPrice * 100).toFixed(0)
60          })}
61        </span>
62      )}
63    </div>
64  );
65}

Lazy Loading Translations

1. Dynamic Namespace Loading

1// LazyTranslation.jsx
2import { useState, useEffect } from 'react';
3import { useTranslation } from 'react-i18next';
4
5function LazyTranslation({ namespace, children }) {
6  const { t, i18n, ready } = useTranslation(namespace);
7  const [isLoading, setIsLoading] = useState(!ready);
8
9  useEffect(() => {
10    if (!ready) {
11      i18n.loadNamespaces(namespace).then(() => {
12        setIsLoading(false);
13      });
14    } else {
15      setIsLoading(false);
16    }
17  }, [namespace, ready, i18n]);
18
19  if (isLoading) {
20    return <div className="loading">Loading translations...</div>;
21  }
22
23  return children;
24}
25
26// Usage
27function ProductPage() {
28  return (
29    <LazyTranslation namespace="products">
30      <ProductDetails />
31      <ProductReviews />
32    </LazyTranslation>
33  );
34}

2. Suspense Integration

1// TranslationSuspense.jsx
2import { Suspense } from 'react';
3import { useTranslation } from 'react-i18next';
4
5function TranslationLoader({ namespace, children }) {
6  const { ready } = useTranslation(namespace, { useSuspense: true });
7
8  if (!ready) {
9    throw new Promise((resolve) => {
10      // Simulate loading
11      setTimeout(resolve, 100);
12    });
13  }
14
15  return children;
16}
17
18function App() {
19  return (
20    <div>
21      <Suspense fallback={<div>Loading translations...</div>}>
22        <TranslationLoader namespace="common">
23          <Header />
24        </TranslationLoader>
25      </Suspense>
26
27      <Suspense fallback={<div>Loading page content...</div>}>
28        <TranslationLoader namespace="dashboard">
29          <Dashboard />
30        </TranslationLoader>
31      </Suspense>
32    </div>
33  );
34}

SEO and SSR

1. Next.js Integration

1// next-i18next.config.js
2module.exports = {
3  i18n: {
4    defaultLocale: 'en',
5    locales: ['en', 'pl', 'es', 'fr', 'de'],
6    localePath: './public/locales',
7    defaultNS: 'common',
8    fallbackLng: {
9      'en-US': ['en'],
10      'pl-PL': ['pl'],
11      default: ['en']
12    }
13  }
14};
15
16// pages/_app.js
17import { appWithTranslation } from 'next-i18next';
18import { LanguageProvider } from '../components/LanguageProvider';
19
20function MyApp({ Component, pageProps }) {
21  return (
22    <LanguageProvider>
23      <Component {...pageProps} />
24    </LanguageProvider>
25  );
26}
27
28export default appWithTranslation(MyApp);
29
30// pages/index.js
31import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
32import { useTranslation } from 'next-i18next';
33
34function HomePage() {
35  const { t } = useTranslation('common');
36
37  return (
38    <div>
39      <h1>{t('welcome.title')}</h1>
40      <p>{t('welcome.description')}</p>
41    </div>
42  );
43}
44
45export async function getStaticProps({ locale }) {
46  return {
47    props: {
48      ...(await serverSideTranslations(locale, ['common', 'navigation']))
49    }
50  };
51}
52
53export default HomePage;

2. Meta Tags Localization

1// LocalizedHead.jsx
2import Head from 'next/head';
3import { useTranslation } from 'next-i18next';
4import { useRouter } from 'next/router';
5
6function LocalizedHead({
7  title,
8  description,
9  keywords,
10  ogImage,
11  canonical
12}) {
13  const { t, i18n } = useTranslation('meta');
14  const router = useRouter();
15
16  const localizedTitle = title ? t(title) : t('default.title');
17  const localizedDescription = description ? t(description) : t('default.description');
18  const localizedKeywords = keywords ? t(keywords) : t('default.keywords');
19
20  const currentUrl = `https://example.com${router.asPath}`;
21  const canonicalUrl = canonical || currentUrl;
22
23  return (
24    <Head>
25      <title>{localizedTitle}</title>
26      <meta name="description" content={localizedDescription} />
27      <meta name="keywords" content={localizedKeywords} />
28      <meta name="language" content={i18n.language} />
29
30      {/* Open Graph */}
31      <meta property="og:title" content={localizedTitle} />
32      <meta property="og:description" content={localizedDescription} />
33      <meta property="og:url" content={currentUrl} />
34      <meta property="og:locale" content={i18n.language} />
35      <meta property="og:image" content={ogImage} />
36
37      {/* Twitter Card */}
38      <meta name="twitter:title" content={localizedTitle} />
39      <meta name="twitter:description" content={localizedDescription} />
40
41      {/* Canonical URL */}
42      <link rel="canonical" href={canonicalUrl} />
43
44      {/* Alternate language versions */}
45      {i18n.options.locales?.map(locale => (
46        <link
47          key={locale}
48          rel="alternate"
49          hrefLang={locale}
50          href={`https://example.com/${locale}${router.asPath}`}
51        />
52      ))}
53    </Head>
54  );
55}

Testing with react-i18next

1. Test Setup

1// test-utils.js
2import { render } from '@testing-library/react';
3import { I18nextProvider } from 'react-i18next';
4import i18n from './i18n-test-config';
5
6const AllTheProviders = ({ children }) => {
7  return (
8    <I18nextProvider i18n={i18n}>
9      {children}
10    </I18nextProvider>
11  );
12};
13
14const customRender = (ui, options) =>
15  render(ui, { wrapper: AllTheProviders, ...options });
16
17export * from '@testing-library/react';
18export { customRender as render };
19
20// i18n-test-config.js
21import i18n from 'i18next';
22import { initReactI18next } from 'react-i18next';
23
24i18n.use(initReactI18next).init({
25  lng: 'en',
26  fallbackLng: 'en',
27  debug: false,
28  interpolation: {
29    escapeValue: false
30  },
31  resources: {
32    en: {
33      common: {
34        'welcome.title': 'Welcome',
35        'button.submit': 'Submit'
36      }
37    }
38  }
39});
40
41export default i18n;

2. Testing Components

1// Component.test.js
2import { render, screen } from './test-utils';
3import { act } from '@testing-library/react';
4import WelcomeComponent from './WelcomeComponent';
5
6describe('WelcomeComponent', () => {
7  test('renders welcome message', () => {
8    render(<WelcomeComponent />);
9
10    expect(screen.getByText('Welcome')).toBeInTheDocument();
11  });
12
13  test('changes language', async () => {
14    const { rerender } = render(<WelcomeComponent />);
15
16    // Change language
17    await act(async () => {
18      i18n.changeLanguage('pl');
19    });
20
21    rerender(<WelcomeComponent />);
22
23    expect(screen.getByText('Witaj')).toBeInTheDocument();
24  });
25});

Performance Optimization

1. Memoization

1// OptimizedTranslation.jsx
2import { memo, useMemo } from 'react';
3import { useTranslation } from 'react-i18next';
4
5const OptimizedTranslation = memo(function OptimizedTranslation({
6  translationKey,
7  values,
8  namespace = 'common'
9}) {
10  const { t } = useTranslation(namespace);
11
12  const translatedText = useMemo(() => {
13    return t(translationKey, values);
14  }, [t, translationKey, values]);
15
16  return <span>{translatedText}</span>;
17});
18
19// TranslationCache.jsx - Custom hook with cache
20function useTranslationCache() {
21  const { t, i18n } = useTranslation();
22  const cache = useMemo(() => new Map(), [i18n.language]);
23
24  const getCachedTranslation = useCallback((key, options) => {
25    const cacheKey = JSON.stringify({ key, options });
26
27    if (cache.has(cacheKey)) {
28      return cache.get(cacheKey);
29    }
30
31    const translation = t(key, options);
32    cache.set(cacheKey, translation);
33
34    return translation;
35  }, [t, cache]);
36
37  return getCachedTranslation;
38}

2. Bundle Splitting

1// webpack.config.js
2module.exports = {
3  optimization: {
4    splitChunks: {
5      cacheGroups: {
6        i18n: {
7          test: /[\/]locales[\/]/,
8          name: 'i18n',
9          chunks: 'all'
10        }
11      }
12    }
13  }
14};
15
16// Dynamic imports for translations
17const loadTranslations = async (language) => {
18  const translations = await import(`../locales/${language}/common.json`);
19  return translations.default;
20};

Best Practices

1. Translation Key Organization

1// Good structure
2{
3  "pages": {
4    "home": {
5      "title": "Home Page",
6      "meta": {
7        "description": "Welcome to our homepage"
8      }
9    },
10    "about": {
11      "title": "About Us",
12      "sections": {
13        "team": "Our Team",
14        "history": "Company History"
15      }
16    }
17  },
18  "components": {
19    "navigation": {
20      "menu": "Menu",
21      "close": "Close"
22    },
23    "forms": {
24      "validation": {
25        "required": "This field is required",
26        "email": "Please enter a valid email"
27      }
28    }
29  },
30  "common": {
31    "actions": {
32      "save": "Save",
33      "cancel": "Cancel",
34      "delete": "Delete"
35    }
36  }
37}

2. TypeScript Typing

1// types/i18n.ts
2export interface TranslationKeys {
3  'welcome.title': string;
4  'welcome.description': string;
5  'navigation.home': string;
6  'navigation.about': string;
7  'forms.name.label': string;
8  'forms.email.
9}
10
11// Custom hook with typing
12import { useTranslation as useI18nTranslation } from 'react-i18next';
13
14export function useTranslation(namespace?: string) {
15  const { t, ...rest } = useI18nTranslation(namespace);
16
17  return {
18    t: (key: keyof TranslationKeys, options?: any) => t(key, options),
19    ...rest
20  };
21}
22
23// Components with typing
24interface TranslatedTextProps {
25  translationKey: keyof TranslationKeys;
26  values?: Record<string, any>;
27}
28
29function TranslatedText({ translationKey, values }: TranslatedTextProps) {
30  const { t } = useTranslation();
31  return <span>{t(translationKey, values)}</span>;
32}

React-i18next is a comprehensive solution for internationalizing React applications. It enables easy translation management, local data formatting, and creating applications accessible to users from around the world. The key to success is proper translation organization, performance optimization, and testing different language scenarios.

Go to CodeWorlds