Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

React-i18next - Internacjonalizacja aplikacji React

React-i18next to potężna biblioteka do internacjonalizacji (i18n) aplikacji React, umożliwiająca łatwe dodawanie obsługi wielu języków, formatowania dat, liczb i walut. To kompleksowe rozwiązanie bazujące na popularnej bibliotece i18next.

Instalacja i podstawowa konfiguracja

Instalacja

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

Podstawowa konfiguracja

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) // Ładowanie tłumaczeń z serwera
9  .use(LanguageDetector) // Automatyczne wykrywanie języka
10  .use(initReactI18next) // Integracja z React
11  .init({
12    // Konfiguracja fallback
13    fallbackLng: 'en',
14    
15    // Debugowanie w development
16    debug: process.env.NODE_ENV === 'development',
17    
18    // Interpolacja
19    interpolation: {
20      escapeValue: false // React już zabezpiecza przed XSS
21    },
22    
23    // Konfiguracja Backend
24    backend: {
25      loadPath: '/locales/{{lng}}/{{ns}}.json'
26    },
27    
28    // Konfiguracja detectora języka
29    detection: {
30      order: ['localStorage', 'navigator', 'htmlTag'],
31      caches: ['localStorage']
32    }
33  });
34
35export default i18n;

Integracja z aplikacją React

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

Podstawowe użycie

Hook useTranslation

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;

Pliki tłumaczeń

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}

Zaawansowane funkcjonalności

1. Interpolacja i pluralizacja

1function UserProfile({ user, messageCount }) {
2  const { t } = useTranslation();
3  
4  return (
5    <div>
6      {/* Interpolacja */}
7      <h1>{t('user.greeting', { name: user.name })}</h1>
8      
9      {/* Pluralizacja */}
10      <p>{t('messages.count', { count: messageCount })}</p>
11      
12      {/* Formatowanie dat */}
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// Pliki tłumaczeń z pluralizacją
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 (przestrzenie nazw)

1// Konfiguracja z namespace
2i18n.init({
3  //
4  defaultNS: 'common',
5  ns: ['common', 'navigation', 'forms', 'errors']
6});
7
8// Użycie różnych namespace
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    // Zapisz wybór w localStorage
16    localStorage.setItem('preferredLanguage', newLang);
17    
18    // Zmień kierunek tekstu dla języków RTL
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}

Formatowanie dat, liczb i walut

1. Formatowanie dat

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// Użycie
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. Formatowanie liczb i walut

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 tłumaczeń

1. Dynamiczne ładowanie namespace

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// Użycie
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      // Symulacja ładowania
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 i 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 z react-i18next

1. Setup testów

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. Testowanie komponentów

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    // Zmień język
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 z 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 dla tłumaczeń
17const loadTranslations = async (language) => {
18  const translations = await import(`../locales/${language}/common.json`);
19  return translations.default;
20};

Najlepsze praktyki

1. Organizacja kluczy tłumaczeń

1// Dobra struktura
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. Typowanie z TypeScript

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 z typowaniem
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// Komponenty z typowaniem
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 to kompleksowe rozwiązanie do internacjonalizacji aplikacji React. Umożliwia łatwe zarządzanie tłumaczeniami, formatowanie lokalnych danych i tworzenie aplikacji dostępnych dla użytkowników z całego świata. Kluczem do sukcesu jest odpowiednia organizacja tłumaczeń, optymalizacja wydajności i testowanie różnych scenariuszy językowych.

Vai a CodeWorlds