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.
1npm install react-i18next i18next i18next-browser-languagedetector i18next-http-backend1// 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;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 />);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;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}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}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}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}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}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}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}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}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;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}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;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});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}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};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}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.