React Internationalization (i18n) Guide

Learn to build React applications that work seamlessly across languages and cultures. Master i18n setup, translation management, formatting, and optimization techniques.

Advertisement Space - top-i18n

Google AdSense: horizontal

7.5B

Global Population

7,000+

Languages Worldwide

60%

Non-English Internet Users

75%

Prefer Native Language

React i18n Setup & Configuration

Set up internationalization in your React application with popular libraries

Internationalization (i18n) enables your React app to support multiple languages and locales. Learn to configure i18n libraries and structure your app for global audiences.

Implementation

1// Setting up react-i18next
2// 1. Install dependencies: npm install react-i18next i18next i18next-browser-languagedetector
3
4// i18n.ts - Configuration file
5import i18n from 'i18next';
6import { initReactI18next } from 'react-i18next';
7import LanguageDetector from 'i18next-browser-languagedetector';
8
9// Translation resources
10const resources = {
11 en: {
12 translation: {
13 welcome: {
14 title: 'Welcome to {{appName}}',
15 subtitle: 'Build amazing React applications',
16 description: 'Start your journey with React today'
17 },
18 navigation: {
19 home: 'Home',
20 about: 'About',
21 products: 'Products',
22 contact: 'Contact',
23 language: 'Language'
24 },
25 common: {
26 loading: 'Loading...',
27 error: 'Something went wrong',
28 retry: 'Try again',
29 save: 'Save',
30 cancel: 'Cancel',
31 delete: 'Delete',
32 edit: 'Edit',
33 search: 'Search',
34 noResults: 'No results found'
35 },
36 forms: {
37 email: 'Email',
38 password: 'Password',
39 confirmPassword: 'Confirm Password',
40 required: 'This field is required',
41 invalidEmail: 'Please enter a valid email',
42 passwordMismatch: 'Passwords do not match',
43 minLength: 'Must be at least {{count}} characters',
44 maxLength: 'Must be no more than {{count}} characters'
45 },
46 messages: {
47 success: 'Operation completed successfully',
48 error: 'An error occurred: {{error}}',
49 saved: 'Your changes have been saved',
50 deleted: 'Item deleted successfully',
51 confirmDelete: 'Are you sure you want to delete this item?'
52 }
53 }
54 },
55 ja: {
56 translation: {
57 welcome: {
58 title: '{{appName}}へようこそ',
59 subtitle: '素晴らしいReactアプリケーションを構築',
60 description: '今日からReactの旅を始めましょう'
61 },
62 navigation: {
63 home: 'ホーム',
64 about: '概要',
65 products: '製品',
66 contact: 'お問い合わせ',
67 language: '言語'
68 },
69 common: {
70 loading: '読み込み中...',
71 error: 'エラーが発生しました',
72 retry: '再試行',
73 save: '保存',
74 cancel: 'キャンセル',
75 delete: '削除',
76 edit: '編集',
77 search: '検索',
78 noResults: '結果が見つかりません'
79 },
80 forms: {
81 email: 'メールアドレス',
82 password: 'パスワード',
83 confirmPassword: 'パスワード確認',
84 required: 'この項目は必須です',
85 invalidEmail: '有効なメールアドレスを入力してください',
86 passwordMismatch: 'パスワードが一致しません',
87 minLength: '{{count}}文字以上で入力してください',
88 maxLength: '{{count}}文字以下で入力してください'
89 },
90 messages: {
91 success: '操作が正常に完了しました',
92 error: 'エラーが発生しました: {{error}}',
93 saved: '変更が保存されました',
94 deleted: 'アイテムが削除されました',
95 confirmDelete: 'このアイテムを削除してもよろしいですか?'
96 }
97 }
98 },
99 es: {
100 translation: {
101 welcome: {
102 title: 'Bienvenido a {{appName}}',
103 subtitle: 'Construye aplicaciones React increíbles',
104 description: 'Comienza tu viaje con React hoy'
105 },
106 navigation: {
107 home: 'Inicio',
108 about: 'Acerca de',
109 products: 'Productos',
110 contact: 'Contacto',
111 language: 'Idioma'
112 },
113 common: {
114 loading: 'Cargando...',
115 error: 'Algo salió mal',
116 retry: 'Intentar de nuevo',
117 save: 'Guardar',
118 cancel: 'Cancelar',
119 delete: 'Eliminar',
120 edit: 'Editar',
121 search: 'Buscar',
122 noResults: 'No se encontraron resultados'
123 }
124 // ... more translations
125 }
126 }
127};
128
129// Initialize i18next
130i18n
131 .use(LanguageDetector) // Detects user language
132 .use(initReactI18next) // Passes i18n instance to react-i18next
133 .init({
134 resources,
135 fallbackLng: 'en',
136 debug: process.env.NODE_ENV === 'development',
137
138 interpolation: {
139 escapeValue: false // React already escapes values
140 },
141
142 detection: {
143 order: ['querystring', 'cookie', 'localStorage', 'navigator', 'htmlTag'],
144 caches: ['localStorage', 'cookie']
145 }
146 });
147
148export default i18n;
149
150// App.tsx - Wrap your app with I18nextProvider
151import React from 'react';
152import { I18nextProvider } from 'react-i18next';
153import i18n from './i18n';
154
155function App() {
156 return (
157 <I18nextProvider i18n={i18n}>
158 <YourApp />
159 </I18nextProvider>
160 );
161}
162
163// Using translations in components
164import { useTranslation } from 'react-i18next';
165
166function WelcomeComponent() {
167 const { t } = useTranslation();
168
169 return (
170 <div>
171 <h1>{t('welcome.title', { appName: 'React Learning' })}</h1>
172 <p>{t('welcome.subtitle')}</p>
173 <p>{t('welcome.description')}</p>
174 </div>
175 );
176}

Advanced Implementation

1// Advanced i18n configuration with namespaces and lazy loading
2// i18n-advanced.ts
3import i18n from 'i18next';
4import { initReactI18next } from 'react-i18next';
5import HttpApi from 'i18next-http-backend';
6import LanguageDetector from 'i18next-browser-languagedetector';
7
8// Namespace configuration
9const namespaces = ['common', 'home', 'products', 'forms', 'errors'];
10
11i18n
12 .use(HttpApi) // Load translations from backend
13 .use(LanguageDetector)
14 .use(initReactI18next)
15 .init({
16 backend: {
17 loadPath: '/locales/{{lng}}/{{ns}}.json',
18 addPath: '/locales/add/{{lng}}/{{ns}}'
19 },
20
21 ns: namespaces,
22 defaultNS: 'common',
23
24 fallbackLng: {
25 'en-US': ['en'],
26 'es-ES': ['es'],
27 'ja-JP': ['ja'],
28 default: ['en']
29 },
30
31 debug: false,
32
33 interpolation: {
34 escapeValue: false,
35 format: function(value, format, lng) {
36 // Custom formatting
37 if (format === 'uppercase') return value.toUpperCase();
38 if (format === 'currency') {
39 return new Intl.NumberFormat(lng, {
40 style: 'currency',
41 currency: format.split(':')[1] || 'USD'
42 }).format(value);
43 }
44 if (format === 'date') {
45 return new Intl.DateTimeFormat(lng).format(value);
46 }
47 return value;
48 }
49 },
50
51 react: {
52 useSuspense: true,
53 bindI18n: 'languageChanged loaded',
54 bindI18nStore: 'added removed',
55 transEmptyNodeValue: '',
56 transSupportBasicHtmlNodes: true,
57 transKeepBasicHtmlNodesFor: ['br', 'strong', 'i', 'p']
58 },
59
60 saveMissing: true,
61 saveMissingTo: 'current',
62
63 keySeparator: '.',
64 nsSeparator: ':',
65
66 pluralSeparator: '_',
67 contextSeparator: '_',
68
69 // Cache options
70 load: 'languageOnly',
71 preload: ['en'],
72
73 // Performance optimizations
74 cleanCode: true,
75 nonExplicitSupportedLngs: true
76 });
77
78export default i18n;
79
80// Lazy loading translations for specific routes
81import { useEffect } from 'react';
82import { useTranslation } from 'react-i18next';
83
84function ProductsPage() {
85 const { t, i18n } = useTranslation(['products', 'common']);
86
87 useEffect(() => {
88 // Load namespace if not already loaded
89 i18n.loadNamespaces('products');
90 }, [i18n]);
91
92 return (
93 <div>
94 <h1>{t('products:title')}</h1>
95 <button>{t('common:save')}</button>
96 </div>
97 );
98}

Translation Management & Best Practices

Organize translations, handle plurals, and implement context-aware translations

Effective translation management is crucial for maintaining scalable internationalized applications. Learn patterns for organizing translations and handling complex scenarios.

Implementation

1// Translation organization and management
2// locales/en/common.json
3{
4 "brand": {
5 "name": "React Learning Hub",
6 "tagline": "Master React Development"
7 },
8 "actions": {
9 "save": "Save",
10 "save_and_continue": "Save and Continue",
11 "save_as_draft": "Save as Draft",
12 "cancel": "Cancel",
13 "delete": "Delete",
14 "delete_confirm": "Are you sure you want to delete {{item}}?"
15 },
16 "status": {
17 "loading": "Loading...",
18 "saving": "Saving...",
19 "saved": "Saved",
20 "error": "Error",
21 "success": "Success"
22 },
23 "validation": {
24 "required": "{{field}} is required",
25 "email": "Please enter a valid email address",
26 "min": "{{field}} must be at least {{min}} characters",
27 "max": "{{field}} must be no more than {{max}} characters",
28 "pattern": "{{field}} format is invalid",
29 "unique": "{{field}} must be unique"
30 },
31 "time": {
32 "just_now": "Just now",
33 "minutes_ago": "{{count}} minute ago",
34 "minutes_ago_plural": "{{count}} minutes ago",
35 "hours_ago": "{{count}} hour ago",
36 "hours_ago_plural": "{{count}} hours ago",
37 "days_ago": "{{count}} day ago",
38 "days_ago_plural": "{{count}} days ago"
39 }
40}
41
42// Using pluralization
43import { useTranslation } from 'react-i18next';
44
45function NotificationBadge({ count }: { count: number }) {
46 const { t } = useTranslation();
47
48 return (
49 <div className="notification-badge">
50 <span className="count">{count}</span>
51 <span className="label">
52 {t('notifications.count', { count })}
53 {/* Will use:
54 - "You have 0 notifications"
55 - "You have 1 notification"
56 - "You have 5 notifications"
57 */}
58 </span>
59 </div>
60 );
61}
62
63// Context-based translations
64function UserGreeting({ user, timeOfDay }: { user: User; timeOfDay: string }) {
65 const { t } = useTranslation();
66
67 // Context separator is '_' by default
68 return (
69 <h1>
70 {t(`greeting_${timeOfDay}`, {
71 name: user.name,
72 defaultValue: t('greeting', { name: user.name })
73 })}
74 </h1>
75 );
76}
77
78// Translation with rich content
79function TermsAndConditions() {
80 const { t } = useTranslation();
81
82 return (
83 <p>
84 {t('terms.agreement', {
85 interpolation: { escapeValue: false },
86 components: {
87 link1: <a href="/terms" />,
88 link2: <a href="/privacy" />,
89 bold: <strong />
90 },
91 defaultValue:
92 'By clicking continue, you agree to our <link1>Terms of Service</link1> and <link2>Privacy Policy</link2>.'
93 })}
94 </p>
95 );
96}
97
98// Trans component for complex translations
99import { Trans } from 'react-i18next';
100
101function ComplexMessage({ user, count }: { user: User; count: number }) {
102 return (
103 <Trans
104 i18nKey="user.message"
105 values={{ name: user.name, count }}
106 components={{
107 bold: <strong />,
108 italic: <em />,
109 link: <a href="/profile" />
110 }}
111 >
112 Hello <bold>{{name}}</bold>, you have <italic>{{count}}</italic> new <link>messages</link>.
113 </Trans>
114 );
115}
116
117// Translation hooks with TypeScript
118import { useTranslation } from 'react-i18next';
119
120// Define translation keys type
121type TranslationKeys = {
122 'welcome.title': { appName: string };
123 'welcome.subtitle': never;
124 'user.greeting': { name: string; role: string };
125 'items.count': { count: number };
126 'error.message': { error: string };
127};
128
129// Typed translation hook
130function useTypedTranslation() {
131 const { t: originalT, ...rest } = useTranslation();
132
133 const t = <K extends keyof TranslationKeys>(
134 key: K,
135 ...args: TranslationKeys[K] extends never
136 ? []
137 : [options: TranslationKeys[K]]
138 ): string => {
139 return originalT(key, ...(args as any));
140 };
141
142 return { t, ...rest };
143}
144
145// Usage with type safety
146function TypedComponent() {
147 const { t } = useTypedTranslation();
148
149 return (
150 <div>
151 {t('welcome.title', { appName: 'React App' })} {/* ✓ Type safe */}
152 {t('welcome.subtitle')} {/* ✓ No params needed */}
153 {/* {t('welcome.title')} */} {/* ✗ TS Error: Missing params */}
154 </div>
155 );
156}

Advanced Implementation

1// Advanced translation patterns
2// Dynamic namespace loading based on routes
3import { useEffect } from 'react';
4import { useTranslation } from 'react-i18next';
5import { useParams } from 'react-router-dom';
6
7function DynamicPage() {
8 const { category } = useParams<{ category: string }>();
9 const { t, i18n } = useTranslation(category);
10
11 useEffect(() => {
12 // Load namespace dynamically
13 if (category) {
14 i18n.loadNamespaces(category);
15 }
16 }, [category, i18n]);
17
18 return (
19 <div>
20 <h1>{t(`${category}:title`)}</h1>
21 <p>{t(`${category}:description`)}</p>
22 </div>
23 );
24}
25
26// Translation with data fetching
27interface TranslatedContent {
28 [key: string]: {
29 title: string;
30 content: string;
31 metadata: Record<string, any>;
32 };
33}
34
35function useTranslatedContent(contentKey: string) {
36 const { i18n } = useTranslation();
37 const [content, setContent] = useState<TranslatedContent | null>(null);
38 const [loading, setLoading] = useState(true);
39
40 useEffect(() => {
41 async function fetchContent() {
42 try {
43 setLoading(true);
44 const response = await fetch(
45 `/api/content/${contentKey}?lang=${i18n.language}`
46 );
47 const data = await response.json();
48 setContent(data);
49 } catch (error) {
50 console.error('Failed to fetch translated content:', error);
51 } finally {
52 setLoading(false);
53 }
54 }
55
56 fetchContent();
57 }, [contentKey, i18n.language]);
58
59 return { content, loading };
60}
61
62// Markdown translation support
63import ReactMarkdown from 'react-markdown';
64
65function MarkdownContent({ translationKey }: { translationKey: string }) {
66 const { t } = useTranslation();
67 const markdownContent = t(translationKey);
68
69 return (
70 <ReactMarkdown
71 components={{
72 a: ({ href, children }) => (
73 <a href={href} target="_blank" rel="noopener noreferrer">
74 {children}
75 </a>
76 ),
77 code: ({ children }) => (
78 <code className="inline-code">{children}</code>
79 )
80 }}
81 >
82 {markdownContent}
83 </ReactMarkdown>
84 );
85}
86
87// Translation provider with fallbacks
88interface TranslationProviderProps {
89 children: React.ReactNode;
90 fallbackComponent?: React.ComponentType;
91}
92
93function TranslationProvider({
94 children,
95 fallbackComponent: FallbackComponent = DefaultFallback
96}: TranslationProviderProps) {
97 const { ready } = useTranslation();
98
99 if (!ready) {
100 return <FallbackComponent />;
101 }
102
103 return <>{children}</>;
104}
105
106function DefaultFallback() {
107 return (
108 <div className="translation-loading">
109 <div className="spinner" />
110 <p>Loading translations...</p>
111 </div>
112 );
113}

Date, Time & Number Formatting

Format dates, times, numbers, and currencies for different locales

Proper formatting of dates, times, numbers, and currencies is essential for international users. Learn to use Intl API and libraries for consistent formatting.

Implementation

1// Date and time formatting with i18n
2import { useTranslation } from 'react-i18next';
3import { format, formatDistance, formatRelative } from 'date-fns';
4import { enUS, ja, es, de, fr } from 'date-fns/locale';
5
6// Locale mapping for date-fns
7const locales = {
8 en: enUS,
9 ja: ja,
10 es: es,
11 de: de,
12 fr: fr
13};
14
15// Custom hook for formatted dates
16function useFormattedDate() {
17 const { i18n } = useTranslation();
18 const locale = locales[i18n.language] || enUS;
19
20 const formatDate = (date: Date, formatStr: string = 'PPP') => {
21 return format(date, formatStr, { locale });
22 };
23
24 const formatRelativeTime = (date: Date) => {
25 return formatDistance(date, new Date(), {
26 addSuffix: true,
27 locale
28 });
29 };
30
31 const formatRelativeDate = (date: Date) => {
32 return formatRelative(date, new Date(), { locale });
33 };
34
35 return {
36 formatDate,
37 formatRelativeTime,
38 formatRelativeDate
39 };
40}
41
42// Component using formatted dates
43function EventCard({ event }: { event: Event }) {
44 const { formatDate, formatRelativeTime } = useFormattedDate();
45 const { t } = useTranslation();
46
47 return (
48 <div className="event-card">
49 <h3>{event.title}</h3>
50 <p>
51 {t('event.date')}: {formatDate(event.date, 'PPP')}
52 </p>
53 <p>
54 {t('event.time')}: {formatDate(event.date, 'p')}
55 </p>
56 <p className="relative-time">
57 {formatRelativeTime(event.date)}
58 </p>
59 </div>
60 );
61}
62
63// Number and currency formatting
64function useNumberFormat() {
65 const { i18n } = useTranslation();
66
67 const formatNumber = (
68 value: number,
69 options?: Intl.NumberFormatOptions
70 ) => {
71 return new Intl.NumberFormat(i18n.language, options).format(value);
72 };
73
74 const formatCurrency = (
75 value: number,
76 currency: string = 'USD'
77 ) => {
78 return new Intl.NumberFormat(i18n.language, {
79 style: 'currency',
80 currency,
81 minimumFractionDigits: 0,
82 maximumFractionDigits: 2
83 }).format(value);
84 };
85
86 const formatPercent = (value: number) => {
87 return new Intl.NumberFormat(i18n.language, {
88 style: 'percent',
89 minimumFractionDigits: 0,
90 maximumFractionDigits: 2
91 }).format(value);
92 };
93
94 const formatCompact = (value: number) => {
95 return new Intl.NumberFormat(i18n.language, {
96 notation: 'compact',
97 compactDisplay: 'short'
98 }).format(value);
99 };
100
101 return {
102 formatNumber,
103 formatCurrency,
104 formatPercent,
105 formatCompact
106 };
107}
108
109// Pricing component with currency
110function PricingCard({ price, currency, discount }: PricingProps) {
111 const { formatCurrency, formatPercent } = useNumberFormat();
112 const { t } = useTranslation();
113
114 const finalPrice = price * (1 - discount);
115
116 return (
117 <div className="pricing-card">
118 <div className="original-price">
119 {discount > 0 && (
120 <span className="strikethrough">
121 {formatCurrency(price, currency)}
122 </span>
123 )}
124 </div>
125
126 <div className="current-price">
127 {formatCurrency(finalPrice, currency)}
128 </div>
129
130 {discount > 0 && (
131 <div className="discount">
132 {t('pricing.save')} {formatPercent(discount)}
133 </div>
134 )}
135 </div>
136 );
137}
138
139// Statistics with number formatting
140function Statistics({ data }: { data: StatsData }) {
141 const { formatNumber, formatCompact } = useNumberFormat();
142 const { t } = useTranslation();
143
144 return (
145 <div className="statistics">
146 <div className="stat-card">
147 <h3>{t('stats.totalUsers')}</h3>
148 <p className="stat-value">
149 {formatCompact(data.totalUsers)}
150 </p>
151 <p className="stat-detail">
152 {formatNumber(data.totalUsers)}
153 </p>
154 </div>
155
156 <div className="stat-card">
157 <h3>{t('stats.revenue')}</h3>
158 <p className="stat-value">
159 {formatCurrency(data.revenue, data.currency)}
160 </p>
161 </div>
162
163 <div className="stat-card">
164 <h3>{t('stats.growthRate')}</h3>
165 <p className="stat-value">
166 {formatPercent(data.growthRate)}
167 </p>
168 </div>
169 </div>
170 );
171}
172
173// Custom date picker with localization
174import DatePicker from 'react-datepicker';
175import 'react-datepicker/dist/react-datepicker.css';
176
177function LocalizedDatePicker({ value, onChange }: DatePickerProps) {
178 const { i18n } = useTranslation();
179 const locale = locales[i18n.language] || enUS;
180
181 // Register locale with date picker
182 useEffect(() => {
183 import(`date-fns/locale/${i18n.language}/index.js`)
184 .then(module => {
185 // Locale loaded
186 })
187 .catch(() => {
188 console.warn(`Locale ${i18n.language} not found`);
189 });
190 }, [i18n.language]);
191
192 return (
193 <DatePicker
194 selected={value}
195 onChange={onChange}
196 locale={locale}
197 dateFormat="P"
198 showMonthDropdown
199 showYearDropdown
200 dropdownMode="select"
201 placeholderText={t('forms.selectDate')}
202 />
203 );
204}

Advanced Implementation

1// Advanced formatting with custom formatters
2// Currency with locale-specific formatting
3interface CurrencyConfig {
4 [locale: string]: {
5 [currency: string]: {
6 position: 'before' | 'after';
7 separator: string;
8 decimal: string;
9 thousand: string;
10 precision: number;
11 };
12 };
13}
14
15const currencyConfig: CurrencyConfig = {
16 en: {
17 USD: { position: 'before', separator: '', decimal: '.', thousand: ',', precision: 2 },
18 EUR: { position: 'before', separator: '', decimal: '.', thousand: ',', precision: 2 }
19 },
20 ja: {
21 JPY: { position: 'before', separator: '', decimal: '.', thousand: ',', precision: 0 },
22 USD: { position: 'before', separator: ' ', decimal: '.', thousand: ',', precision: 2 }
23 },
24 de: {
25 EUR: { position: 'after', separator: ' ', decimal: ',', thousand: '.', precision: 2 }
26 }
27};
28
29function useAdvancedCurrencyFormat() {
30 const { i18n } = useTranslation();
31
32 const formatCurrencyCustom = (
33 amount: number,
34 currency: string,
35 options?: {
36 showSymbol?: boolean;
37 useGrouping?: boolean;
38 minimumFractionDigits?: number;
39 }
40 ) => {
41 const config = currencyConfig[i18n.language]?.[currency] ||
42 currencyConfig.en.USD;
43
44 // Get currency symbol
45 const formatter = new Intl.NumberFormat(i18n.language, {
46 style: 'currency',
47 currency,
48 currencyDisplay: 'symbol'
49 });
50
51 const parts = formatter.formatToParts(amount);
52 const symbol = parts.find(part => part.type === 'currency')?.value || currency;
53
54 // Format number
55 const numberFormatter = new Intl.NumberFormat(i18n.language, {
56 minimumFractionDigits: options?.minimumFractionDigits ?? config.precision,
57 maximumFractionDigits: config.precision,
58 useGrouping: options?.useGrouping ?? true
59 });
60
61 const formattedNumber = numberFormatter.format(amount);
62
63 // Combine based on position
64 if (options?.showSymbol === false) {
65 return formattedNumber;
66 }
67
68 return config.position === 'before'
69 ? `${symbol}${config.separator}${formattedNumber}`
70 : `${formattedNumber}${config.separator}${symbol}`;
71 };
72
73 return { formatCurrencyCustom };
74}
75
76// Time zone aware formatting
77function useTimeZoneFormat() {
78 const { i18n } = useTranslation();
79 const [userTimeZone, setUserTimeZone] = useState(
80 Intl.DateTimeFormat().resolvedOptions().timeZone
81 );
82
83 const formatInTimeZone = (
84 date: Date,
85 timeZone: string = userTimeZone,
86 options?: Intl.DateTimeFormatOptions
87 ) => {
88 return new Intl.DateTimeFormat(i18n.language, {
89 timeZone,
90 ...options
91 }).format(date);
92 };
93
94 const formatMeeting = (date: Date, timeZones: string[]) => {
95 return timeZones.map(tz => ({
96 timeZone: tz,
97 time: formatInTimeZone(date, tz, {
98 hour: 'numeric',
99 minute: 'numeric',
100 timeZoneName: 'short'
101 })
102 }));
103 };
104
105 return {
106 userTimeZone,
107 setUserTimeZone,
108 formatInTimeZone,
109 formatMeeting
110 };
111}
112
113// Meeting scheduler with multiple time zones
114function MeetingScheduler({ meeting }: { meeting: Meeting }) {
115 const { formatMeeting } = useTimeZoneFormat();
116 const { t } = useTranslation();
117
118 const timeZones = ['America/New_York', 'Europe/London', 'Asia/Tokyo'];
119 const meetingTimes = formatMeeting(meeting.date, timeZones);
120
121 return (
122 <div className="meeting-scheduler">
123 <h3>{meeting.title}</h3>
124 <div className="time-zones">
125 {meetingTimes.map(({ timeZone, time }) => (
126 <div key={timeZone} className="time-zone-item">
127 <span className="timezone">{timeZone}:</span>
128 <span className="time">{time}</span>
129 </div>
130 ))}
131 </div>
132 </div>
133 );
134}
135
136// List formatting for different locales
137function useListFormat() {
138 const { i18n } = useTranslation();
139
140 const formatList = (
141 items: string[],
142 type: 'conjunction' | 'disjunction' | 'unit' = 'conjunction'
143 ) => {
144 // Polyfill for older browsers
145 if (!Intl.ListFormat) {
146 return items.join(type === 'conjunction' ? ' and ' : ' or ');
147 }
148
149 return new Intl.ListFormat(i18n.language, {
150 style: 'long',
151 type
152 }).format(items);
153 };
154
155 return { formatList };
156}
157
158// Component using list formatting
159function TagList({ tags }: { tags: string[] }) {
160 const { formatList } = useListFormat();
161 const { t } = useTranslation();
162
163 return (
164 <p>
165 {t('tags.selected')}: {formatList(tags)}
166 </p>
167 );
168}

Language Switching & Persistence

Implement language switchers and persist user language preferences

Allow users to switch languages seamlessly and remember their preferences across sessions. Implement smooth transitions and handle dynamic content updates.

Implementation

1// Language Switcher Component
2import { useTranslation } from 'react-i18next';
3import { useState, useEffect } from 'react';
4
5interface Language {
6 code: string;
7 name: string;
8 nativeName: string;
9 flag: string;
10 rtl?: boolean;
11}
12
13const languages: Language[] = [
14 { code: 'en', name: 'English', nativeName: 'English', flag: '🇬🇧' },
15 { code: 'ja', name: 'Japanese', nativeName: '日本語', flag: '🇯🇵' },
16 { code: 'es', name: 'Spanish', nativeName: 'Español', flag: '🇪🇸' },
17 { code: 'fr', name: 'French', nativeName: 'Français', flag: '🇫🇷' },
18 { code: 'de', name: 'German', nativeName: 'Deutsch', flag: '🇩🇪' },
19 { code: 'ar', name: 'Arabic', nativeName: 'العربية', flag: '🇸🇦', rtl: true },
20 { code: 'zh', name: 'Chinese', nativeName: '中文', flag: '🇨🇳' },
21 { code: 'ko', name: 'Korean', nativeName: '한국어', flag: '🇰🇷' }
22];
23
24function LanguageSwitcher() {
25 const { i18n } = useTranslation();
26 const [isOpen, setIsOpen] = useState(false);
27 const currentLanguage = languages.find(lang => lang.code === i18n.language) || languages[0];
28
29 const changeLanguage = async (langCode: string) => {
30 try {
31 await i18n.changeLanguage(langCode);
32 setIsOpen(false);
33
34 // Update document direction for RTL languages
35 const language = languages.find(lang => lang.code === langCode);
36 document.documentElement.dir = language?.rtl ? 'rtl' : 'ltr';
37 document.documentElement.lang = langCode;
38
39 // Save to localStorage
40 localStorage.setItem('preferredLanguage', langCode);
41
42 // Update meta tags
43 updateMetaTags(langCode);
44
45 // Trigger custom event for other components
46 window.dispatchEvent(new CustomEvent('languageChanged', {
47 detail: { language: langCode }
48 }));
49 } catch (error) {
50 console.error('Failed to change language:', error);
51 }
52 };
53
54 // Load saved language preference
55 useEffect(() => {
56 const savedLanguage = localStorage.getItem('preferredLanguage');
57 if (savedLanguage && savedLanguage !== i18n.language) {
58 changeLanguage(savedLanguage);
59 }
60 }, []);
61
62 return (
63 <div className="language-switcher">
64 <button
65 onClick={() => setIsOpen(!isOpen)}
66 className="language-button"
67 aria-label="Change language"
68 aria-expanded={isOpen}
69 >
70 <span className="flag">{currentLanguage.flag}</span>
71 <span className="name">{currentLanguage.nativeName}</span>
72 <span className="arrow"></span>
73 </button>
74
75 {isOpen && (
76 <div className="language-dropdown">
77 {languages.map(language => (
78 <button
79 key={language.code}
80 onClick={() => changeLanguage(language.code)}
81 className={`language-option ${
82 language.code === i18n.language ? 'active' : ''
83 }`}
84 lang={language.code}
85 >
86 <span className="flag">{language.flag}</span>
87 <span className="name">{language.nativeName}</span>
88 {language.code === i18n.language && (
89 <span className="checkmark"></span>
90 )}
91 </button>
92 ))}
93 </div>
94 )}
95 </div>
96 );
97}
98
99// Advanced language detection and persistence
100function useLanguageDetection() {
101 const { i18n } = useTranslation();
102
103 useEffect(() => {
104 // Priority order for language detection
105 const detectLanguage = async () => {
106 // 1. Check URL parameter
107 const urlParams = new URLSearchParams(window.location.search);
108 const urlLang = urlParams.get('lang');
109
110 // 2. Check localStorage
111 const savedLang = localStorage.getItem('preferredLanguage');
112
113 // 3. Check cookie
114 const cookieLang = getCookie('language');
115
116 // 4. Check browser language
117 const browserLang = navigator.language.split('-')[0];
118
119 // 5. Check geolocation (if available)
120 const geoLang = await detectLanguageByGeolocation();
121
122 // Determine final language
123 const detectedLang = urlLang || savedLang || cookieLang || geoLang || browserLang || 'en';
124
125 // Validate language is supported
126 const supportedLang = languages.some(lang => lang.code === detectedLang)
127 ? detectedLang
128 : 'en';
129
130 // Change language if different
131 if (supportedLang !== i18n.language) {
132 await i18n.changeLanguage(supportedLang);
133 }
134
135 // Persist the choice
136 localStorage.setItem('preferredLanguage', supportedLang);
137 setCookie('language', supportedLang, 365);
138 };
139
140 detectLanguage();
141 }, [i18n]);
142}
143
144// Smooth language transition with loading state
145function LanguageTransition({ children }: { children: React.ReactNode }) {
146 const { i18n } = useTranslation();
147 const [isChanging, setIsChanging] = useState(false);
148
149 useEffect(() => {
150 const handleLanguageChanging = () => setIsChanging(true);
151 const handleLanguageChanged = () => {
152 setTimeout(() => setIsChanging(false), 300);
153 };
154
155 i18n.on('languageChanging', handleLanguageChanging);
156 i18n.on('languageChanged', handleLanguageChanged);
157
158 return () => {
159 i18n.off('languageChanging', handleLanguageChanging);
160 i18n.off('languageChanged', handleLanguageChanged);
161 };
162 }, [i18n]);
163
164 return (
165 <div className={`language-transition ${isChanging ? 'changing' : ''}`}>
166 {children}
167 {isChanging && (
168 <div className="language-loading">
169 <div className="spinner" />
170 </div>
171 )}
172 </div>
173 );
174}
175
176// Cookie utilities
177function setCookie(name: string, value: string, days: number) {
178 const expires = new Date();
179 expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000);
180 document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/`;
181}
182
183function getCookie(name: string): string | null {
184 const nameEQ = name + "=";
185 const ca = document.cookie.split(';');
186 for (let i = 0; i < ca.length; i++) {
187 let c = ca[i];
188 while (c.charAt(0) === ' ') c = c.substring(1, c.length);
189 if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
190 }
191 return null;
192}
193
194// Geolocation-based language detection
195async function detectLanguageByGeolocation(): Promise<string | null> {
196 try {
197 const response = await fetch('https://ipapi.co/json/');
198 const data = await response.json();
199
200 // Map country codes to languages
201 const countryToLanguage: Record<string, string> = {
202 US: 'en', GB: 'en', AU: 'en', CA: 'en',
203 ES: 'es', MX: 'es', AR: 'es',
204 FR: 'fr', BE: 'fr',
205 DE: 'de', AT: 'de', CH: 'de',
206 JP: 'ja',
207 CN: 'zh', TW: 'zh',
208 KR: 'ko',
209 SA: 'ar', AE: 'ar', EG: 'ar'
210 };
211
212 return countryToLanguage[data.country_code] || null;
213 } catch (error) {
214 console.error('Geolocation detection failed:', error);
215 return null;
216 }
217}
218
219// Update meta tags for SEO
220function updateMetaTags(language: string) {
221 // Update HTML lang attribute
222 document.documentElement.lang = language;
223
224 // Update meta tags
225 const metaDescription = document.querySelector('meta[name="description"]');
226 const metaKeywords = document.querySelector('meta[name="keywords"]');
227
228 // You would typically fetch these from your translations
229 const metaContent = {
230 en: {
231 description: 'Learn React development with our comprehensive guides',
232 keywords: 'react, javascript, web development'
233 },
234 ja: {
235 description: 'Reactの開発を学ぶための包括的なガイド',
236 keywords: 'react, javascript, ウェブ開発'
237 }
238 // ... more languages
239 };
240
241 if (metaDescription) {
242 metaDescription.setAttribute('content', metaContent[language]?.description || '');
243 }
244
245 if (metaKeywords) {
246 metaKeywords.setAttribute('content', metaContent[language]?.keywords || '');
247 }
248}

Advanced Implementation

1// Language-aware routing
2import { useEffect } from 'react';
3import { useNavigate, useLocation } from 'react-router-dom';
4import { useTranslation } from 'react-i18next';
5
6function LanguageRouter({ children }: { children: React.ReactNode }) {
7 const { i18n } = useTranslation();
8 const navigate = useNavigate();
9 const location = useLocation();
10
11 useEffect(() => {
12 // Extract language from URL path
13 const pathSegments = location.pathname.split('/');
14 const urlLang = pathSegments[1];
15
16 // Check if URL language is valid
17 const isValidLang = languages.some(lang => lang.code === urlLang);
18
19 if (isValidLang && urlLang !== i18n.language) {
20 // Update i18n language to match URL
21 i18n.changeLanguage(urlLang);
22 } else if (!isValidLang && pathSegments.length > 1) {
23 // Redirect to URL with current language
24 const newPath = `/${i18n.language}${location.pathname}`;
25 navigate(newPath, { replace: true });
26 }
27 }, [location.pathname, i18n, navigate]);
28
29 // Update URL when language changes
30 useEffect(() => {
31 const handleLanguageChange = (lng: string) => {
32 const pathSegments = location.pathname.split('/');
33 const currentLang = pathSegments[1];
34
35 if (languages.some(lang => lang.code === currentLang)) {
36 // Replace language in URL
37 pathSegments[1] = lng;
38 const newPath = pathSegments.join('/');
39 navigate(newPath, { replace: true });
40 } else {
41 // Add language to URL
42 const newPath = `/${lng}${location.pathname}`;
43 navigate(newPath, { replace: true });
44 }
45 };
46
47 i18n.on('languageChanged', handleLanguageChange);
48 return () => i18n.off('languageChanged', handleLanguageChange);
49 }, [i18n, navigate, location.pathname]);
50
51 return <>{children}</>;
52}
53
54// Language-specific route configuration
55const createLocalizedRoutes = (language: string) => {
56 const t = (key: string) => {
57 // This would normally use your i18n instance
58 const routes = {
59 en: {
60 home: '/',
61 about: '/about',
62 products: '/products',
63 contact: '/contact'
64 },
65 ja: {
66 home: '/',
67 about: '/について',
68 products: '/製品',
69 contact: '/お問い合わせ'
70 },
71 es: {
72 home: '/',
73 about: '/acerca',
74 products: '/productos',
75 contact: '/contacto'
76 }
77 };
78
79 return routes[language]?.[key] || routes.en[key];
80 };
81
82 return {
83 home: t('home'),
84 about: t('about'),
85 products: t('products'),
86 contact: t('contact')
87 };
88};
89
90// Persistent language preference across subdomains
91class LanguagePreferenceSync {
92 private channel: BroadcastChannel | null = null;
93
94 constructor() {
95 if ('BroadcastChannel' in window) {
96 this.channel = new BroadcastChannel('language_sync');
97 this.setupListener();
98 }
99 }
100
101 setupListener() {
102 if (!this.channel) return;
103
104 this.channel.onmessage = (event) => {
105 if (event.data.type === 'language_changed') {
106 const { language } = event.data;
107 // Update language without triggering another broadcast
108 i18n.changeLanguage(language, () => {
109 console.log('Language synced from another tab:', language);
110 });
111 }
112 };
113 }
114
115 broadcastLanguageChange(language: string) {
116 if (!this.channel) return;
117
118 this.channel.postMessage({
119 type: 'language_changed',
120 language,
121 timestamp: Date.now()
122 });
123 }
124
125 destroy() {
126 this.channel?.close();
127 }
128}
129
130// Usage in app
131const languageSync = new LanguagePreferenceSync();
132
133// When language changes
134i18n.on('languageChanged', (lng) => {
135 languageSync.broadcastLanguageChange(lng);
136});
137
138// Cleanup on unmount
139window.addEventListener('beforeunload', () => {
140 languageSync.destroy();
141});

SEO & Performance Optimization

Optimize internationalized React apps for SEO and performance

Internationalization can impact SEO and performance. Learn strategies to optimize your multilingual React application for search engines and fast loading.

Implementation

1// SEO optimization for i18n
2import { Helmet } from 'react-helmet-async';
3import { useTranslation } from 'react-i18next';
4import { useLocation } from 'react-router-dom';
5
6// SEO Meta component
7function SEOMeta({
8 titleKey,
9 descriptionKey,
10 keywordsKey,
11 image,
12 type = 'website'
13}: SEOMetaProps) {
14 const { t, i18n } = useTranslation();
15 const location = useLocation();
16
17 const title = t(titleKey);
18 const description = t(descriptionKey);
19 const keywords = t(keywordsKey);
20
21 // Generate alternate language URLs
22 const alternateUrls = languages.map(lang => ({
23 hrefLang: lang.code,
24 href: `https://example.com/${lang.code}${location.pathname}`
25 }));
26
27 // Add x-default for language selection page
28 alternateUrls.push({
29 hrefLang: 'x-default',
30 href: `https://example.com${location.pathname}`
31 });
32
33 return (
34 <Helmet>
35 <html lang={i18n.language} />
36 <title>{title}</title>
37 <meta name="description" content={description} />
38 <meta name="keywords" content={keywords} />
39
40 {/* Open Graph tags */}
41 <meta property="og:title" content={title} />
42 <meta property="og:description" content={description} />
43 <meta property="og:type" content={type} />
44 <meta property="og:url" content={`https://example.com${location.pathname}`} />
45 <meta property="og:locale" content={i18n.language} />
46 {image && <meta property="og:image" content={image} />}
47
48 {/* Alternate languages for SEO */}
49 {alternateUrls.map(({ hrefLang, href }) => (
50 <link
51 key={hrefLang}
52 rel="alternate"
53 hrefLang={hrefLang}
54 href={href}
55 />
56 ))}
57
58 {/* Canonical URL */}
59 <link
60 rel="canonical"
61 href={`https://example.com/${i18n.language}${location.pathname}`}
62 />
63
64 {/* Twitter Card tags */}
65 <meta name="twitter:card" content="summary_large_image" />
66 <meta name="twitter:title" content={title} />
67 <meta name="twitter:description" content={description} />
68 {image && <meta name="twitter:image" content={image} />}
69 </Helmet>
70 );
71}
72
73// Lazy loading translations for performance
74import { Suspense, lazy } from 'react';
75
76// Create lazy loaded components for each language
77const createLazyComponent = (componentPath: string) => {
78 return languages.reduce((acc, lang) => {
79 acc[lang.code] = lazy(() =>
80 import(`./localized/${lang.code}/${componentPath}`)
81 );
82 return acc;
83 }, {} as Record<string, React.LazyExoticComponent<any>>);
84};
85
86// Language-specific component loader
87function LocalizedComponentLoader({
88 componentName,
89 fallback = <div>Loading...</div>,
90 ...props
91}: LocalizedComponentProps) {
92 const { i18n } = useTranslation();
93 const components = createLazyComponent(componentName);
94 const Component = components[i18n.language] || components.en;
95
96 return (
97 <Suspense fallback={fallback}>
98 <Component {...props} />
99 </Suspense>
100 );
101}
102
103// Performance monitoring for translations
104function useTranslationPerformance() {
105 const { i18n } = useTranslation();
106
107 useEffect(() => {
108 // Measure translation loading time
109 const startTime = performance.now();
110
111 const handleLoaded = () => {
112 const loadTime = performance.now() - startTime;
113
114 // Report to analytics
115 if (window.gtag) {
116 window.gtag('event', 'translation_loaded', {
117 language: i18n.language,
118 load_time: loadTime,
119 namespace_count: i18n.options.ns?.length || 1
120 });
121 }
122
123 // Log performance warning if too slow
124 if (loadTime > 1000) {
125 console.warn(`Translations took ${loadTime}ms to load`);
126 }
127 };
128
129 i18n.on('loaded', handleLoaded);
130 return () => i18n.off('loaded', handleLoaded);
131 }, [i18n]);
132}
133
134// Preload critical translations
135function preloadTranslations(languages: string[], namespaces: string[]) {
136 const preloadLink = (href: string) => {
137 const link = document.createElement('link');
138 link.rel = 'preload';
139 link.as = 'fetch';
140 link.href = href;
141 link.crossOrigin = 'anonymous';
142 document.head.appendChild(link);
143 };
144
145 languages.forEach(lang => {
146 namespaces.forEach(ns => {
147 preloadLink(`/locales/${lang}/${ns}.json`);
148 });
149 });
150}
151
152// Call on app initialization
153preloadTranslations(['en', 'ja', 'es'], ['common', 'home']);
154
155// Translation bundling strategy
156// webpack.config.js
157module.exports = {
158 // ... other config
159 plugins: [
160 new webpack.ContextReplacementPlugin(
161 /date-fns[/\]locale$/,
162 new RegExp(`[/\\](${supportedLocales.join('|')})[/\\]index\.js$`)
163 ),
164 ],
165 optimization: {
166 splitChunks: {
167 cacheGroups: {
168 translations: {
169 test: /[\/]locales[\/]/,
170 name: (module, chunks, cacheGroupKey) => {
171 const moduleFileName = module
172 .identifier()
173 .split('/')
174 .reduceRight((item) => item);
175 const [lang] = moduleFileName.split('.');
176 return `translations.${lang}`;
177 },
178 chunks: 'all',
179 enforce: true
180 }
181 }
182 }
183 }
184};
185
186// Service Worker for offline translations
187// sw.js
188self.addEventListener('install', (event) => {
189 event.waitUntil(
190 caches.open('translations-v1').then((cache) => {
191 return cache.addAll([
192 '/locales/en/common.json',
193 '/locales/en/app.json',
194 '/locales/ja/common.json',
195 '/locales/ja/app.json',
196 // ... other critical translations
197 ]);
198 })
199 );
200});
201
202self.addEventListener('fetch', (event) => {
203 if (event.request.url.includes('/locales/')) {
204 event.respondWith(
205 caches.match(event.request).then((response) => {
206 return response || fetch(event.request).then((response) => {
207 // Cache new translations
208 if (response.status === 200) {
209 const responseClone = response.clone();
210 caches.open('translations-v1').then((cache) => {
211 cache.put(event.request, responseClone);
212 });
213 }
214 return response;
215 });
216 })
217 );
218 }
219});

Advanced Implementation

1// Server-side rendering with i18n
2// server.tsx
3import express from 'express';
4import React from 'react';
5import ReactDOMServer from 'react-dom/server';
6import { StaticRouter } from 'react-router-dom/server';
7import { I18nextProvider } from 'react-i18next';
8import i18next from 'i18next';
9import Backend from 'i18next-fs-backend';
10import { HelmetProvider } from 'react-helmet-async';
11
12const app = express();
13
14// Initialize i18next for SSR
15const initI18next = async (language: string) => {
16 const instance = i18next.createInstance();
17
18 await instance
19 .use(Backend)
20 .init({
21 lng: language,
22 fallbackLng: 'en',
23 ns: ['common', 'home'],
24 defaultNS: 'common',
25 backend: {
26 loadPath: './locales/{{lng}}/{{ns}}.json'
27 },
28 interpolation: {
29 escapeValue: false
30 },
31 react: {
32 useSuspense: false
33 }
34 });
35
36 return instance;
37};
38
39app.get('*', async (req, res) => {
40 // Detect language from request
41 const language = detectLanguageFromRequest(req);
42
43 // Initialize i18next with detected language
44 const i18n = await initI18next(language);
45
46 const helmetContext = {};
47
48 // Render app
49 const html = ReactDOMServer.renderToString(
50 <I18nextProvider i18n={i18n}>
51 <HelmetProvider context={helmetContext}>
52 <StaticRouter location={req.url}>
53 <App />
54 </StaticRouter>
55 </HelmetProvider>
56 </I18nextProvider>
57 );
58
59 const { helmet } = helmetContext;
60
61 // Generate HTML with proper language attributes
62 res.send(`
63 <!DOCTYPE html>
64 <html lang="${language}" dir="${isRTL(language) ? 'rtl' : 'ltr'}">
65 <head>
66 ${helmet.title.toString()}
67 ${helmet.meta.toString()}
68 ${helmet.link.toString()}
69 <script>
70 window.__INITIAL_I18N_STORE__ = ${JSON.stringify(i18n.store.data)};
71 window.__INITIAL_LANGUAGE__ = "${language}";
72 </script>
73 </head>
74 <body>
75 <div id="root">${html}</div>
76 <script src="/bundle.js"></script>
77 </body>
78 </html>
79 `);
80});
81
82// Client-side hydration
83// client.tsx
84import { hydrateRoot } from 'react-dom/client';
85import i18n from './i18n';
86
87// Restore SSR state
88if (window.__INITIAL_I18N_STORE__ && window.__INITIAL_LANGUAGE__) {
89 i18n.services.resourceStore.data = window.__INITIAL_I18N_STORE__;
90 i18n.changeLanguage(window.__INITIAL_LANGUAGE__);
91}
92
93const container = document.getElementById('root');
94hydrateRoot(
95 container,
96 <I18nextProvider i18n={i18n}>
97 <BrowserRouter>
98 <App />
99 </BrowserRouter>
100 </I18nextProvider>
101);
102
103// Static site generation with i18n
104// build-static.js
105const { renderToStaticMarkup } = require('react-dom/server');
106const fs = require('fs-extra');
107const path = require('path');
108
109async function buildStaticSite() {
110 const languages = ['en', 'ja', 'es'];
111 const routes = ['/', '/about', '/products', '/contact'];
112
113 for (const lang of languages) {
114 const i18n = await initI18next(lang);
115
116 for (const route of routes) {
117 const html = renderToStaticMarkup(
118 <I18nextProvider i18n={i18n}>
119 <StaticRouter location={route}>
120 <App />
121 </StaticRouter>
122 </I18nextProvider>
123 );
124
125 const outputPath = path.join('dist', lang, route, 'index.html');
126 await fs.outputFile(outputPath, wrapHtml(html, lang));
127 }
128 }
129}
130
131// CDN optimization for translations
132class TranslationCDN {
133 private cdnUrl: string;
134 private fallbackUrl: string;
135
136 constructor(cdnUrl: string, fallbackUrl: string) {
137 this.cdnUrl = cdnUrl;
138 this.fallbackUrl = fallbackUrl;
139 }
140
141 async loadTranslation(language: string, namespace: string) {
142 const urls = [
143 `${this.cdnUrl}/${language}/${namespace}.json`,
144 `${this.fallbackUrl}/${language}/${namespace}.json`
145 ];
146
147 for (const url of urls) {
148 try {
149 const response = await fetch(url, {
150 cache: 'force-cache',
151 headers: {
152 'Accept': 'application/json'
153 }
154 });
155
156 if (response.ok) {
157 return await response.json();
158 }
159 } catch (error) {
160 console.warn(`Failed to load from ${url}`, error);
161 }
162 }
163
164 throw new Error(`Failed to load translation ${language}/${namespace}`);
165 }
166}

Advertisement Space - mid-i18n

Google AdSense: rectangle

i18n Best Practices

Translation Organization

  • Use consistent key naming conventions
  • Organize translations by feature or page
  • Keep translation files small and focused
  • Use namespaces to avoid key collisions
  • Implement a translation management system
  • Version control your translations

Performance

  • Lazy load translations for routes
  • Use translation splitting for large apps
  • Cache translations in localStorage
  • Preload critical translations
  • Minimize translation bundle size
  • Use CDN for translation files

User Experience

  • Persist language preferences
  • Provide smooth language switching
  • Handle missing translations gracefully
  • Support RTL languages properly
  • Format dates and numbers correctly
  • Test with real translators

Common i18n Pitfalls to Avoid

❌ Hardcoded Text

Never hardcode user-facing text. Always use translation keys.

// Bad <button>Submit</button> // Good <button>{t('actions.submit')}</button>

❌ String Concatenation

Don't concatenate translated strings. Use interpolation.

// Bad t('welcome') + ' ' + userName // Good t('welcome', { name: userName })

❌ Assuming Text Length

Text can be 30-200% longer in different languages. Design flexibly.

Advertisement Space - bottom-i18n

Google AdSense: horizontal

Related Topics

Build Global React Applications

Reach users worldwide with properly internationalized React apps.

Learn Deployment →