·Chapter 22
Internationalization (i18n) in React Native
This chapter will guide you through implementing internationalization in your React Native application, ensuring it's accessible to users worldwide.
Setting Up i18next
Basic Configuration
// src/i18n/config.ts import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import * as RNLocalize from 'react-native-localize'; interface LocaleConfig { languageTag: string; isRTL: boolean; } // Get the user's preferred locale const getLocale = (): LocaleConfig => { const locales = RNLocalize.getLocales(); if (locales.length === 0) { return { languageTag: 'en', isRTL: false }; } return { languageTag: locales[0].languageTag, isRTL: locales[0].isRTL, }; }; // Initialize i18next export const initializeI18n = async () => { const { languageTag } = getLocale(); await i18n .use(initReactI18next) .init({ lng: languageTag, fallbackLng: 'en', debug: __DEV__, interpolation: { escapeValue: false, }, react: { useSuspense: false, }, }); return i18n; }; export default i18n;
Translation Resources
// src/i18n/resources/en.ts export default { common: { ok: 'OK', cancel: 'Cancel', save: 'Save', delete: 'Delete', error: 'Error', success: 'Success', }, auth: { login: 'Login', signup: 'Sign Up', email: 'Email', password: 'Password', forgotPassword: 'Forgot Password?', }, validation: { required: '{{field}} is required', email: 'Please enter a valid email', minLength: '{{field}} must be at least {{count}} characters', }, }; // src/i18n/resources/ar.ts export default { common: { ok: 'موافق', cancel: 'إلغاء', save: 'حفظ', delete: 'حذف', error: 'خطأ', success: 'نجاح', }, auth: { login: 'تسجيل الدخول', signup: 'إنشاء حساب', email: 'البريد الإلكتروني', password: 'كلمة المرور', forgotPassword: 'نسيت كلمة المرور؟', }, validation: { required: '{{field}} مطلوب', email: 'يرجى إدخال بريد إلكتروني صحيح', minLength: 'يجب أن يحتوي {{field}} على {{count}} أحرف على الأقل', }, }; // src/i18n/resources/index.ts import en from './en'; import ar from './ar'; export const resources = { en: { translation: en }, ar: { translation: ar }, };
RTL Support
RTL Layout Configuration
// src/utils/rtl.ts import { I18nManager } from 'react-native'; import * as RNRestart from 'react-native-restart'; export const configureRTL = async (isRTL: boolean) => { if (I18nManager.isRTL !== isRTL) { await I18nManager.forceRTL(isRTL); RNRestart.Restart(); } }; // Usage in App.tsx useEffect(() => { const { isRTL } = getLocale(); configureRTL(isRTL); }, []);
RTL-Aware Styles
// src/utils/styles.ts import { I18nManager, StyleSheet } from 'react-native'; type StyleDirection = { start?: number; end?: number; marginStart?: number; marginEnd?: number; paddingStart?: number; paddingEnd?: number; }; export const createRTLStyles = (styles: StyleDirection) => { const rtlStyles: any = {}; Object.entries(styles).forEach(([key, value]) => { switch (key) { case 'start': rtlStyles[I18nManager.isRTL ? 'right' : 'left'] = value; break; case 'end': rtlStyles[I18nManager.isRTL ? 'left' : 'right'] = value; break; case 'marginStart': rtlStyles[I18nManager.isRTL ? 'marginRight' : 'marginLeft'] = value; break; case 'marginEnd': rtlStyles[I18nManager.isRTL ? 'marginLeft' : 'marginRight'] = value; break; case 'paddingStart': rtlStyles[I18nManager.isRTL ? 'paddingRight' : 'paddingLeft'] = value; break; case 'paddingEnd': rtlStyles[I18nManager.isRTL ? 'paddingLeft' : 'paddingRight'] = value; break; default: rtlStyles[key] = value; } }); return StyleSheet.create(rtlStyles); }; // Usage in components const styles = createRTLStyles({ container: { paddingStart: 16, paddingEnd: 16, }, icon: { start: 0, marginEnd: 8, }, });
Localized Components
Text Component
// src/components/LocalizedText.tsx import React from 'react'; import { Text, TextProps } from 'react-native'; import { useTranslation } from 'react-i18next'; interface LocalizedTextProps extends TextProps { i18nKey: string; values?: Record<string, any>; } export const LocalizedText: React.FC<LocalizedTextProps> = ({ i18nKey, values, ...props }) => { const { t } = useTranslation(); return ( <Text {...props}> {t(i18nKey, values)} </Text> ); }; // Usage <LocalizedText i18nKey="validation.minLength" values={{ field: 'Password', count: 8 }} />
Input Component
// src/components/LocalizedInput.tsx import React from 'react'; import { TextInput, TextInputProps, View } from 'react-native'; import { useTranslation } from 'react-i18next'; import { createRTLStyles } from '../utils/styles'; interface LocalizedInputProps extends TextInputProps { labelI18nKey: string; errorI18nKey?: string; errorValues?: Record<string, any>; } export const LocalizedInput: React.FC<LocalizedInputProps> = ({ labelI18nKey, errorI18nKey, errorValues, style, ...props }) => { const { t } = useTranslation(); return ( <View style={styles.container}> <LocalizedText style={styles.label}> {t(labelI18nKey)} </LocalizedText> <TextInput style={[styles.input, style]} textAlign={I18nManager.isRTL ? 'right' : 'left'} {...props} /> {errorI18nKey && ( <LocalizedText style={styles.error}> {t(errorI18nKey, errorValues)} </LocalizedText> )} </View> ); }; const styles = createRTLStyles({ container: { marginBottom: 16, }, label: { marginBottom: 8, fontSize: 16, fontWeight: '600', }, input: { borderWidth: 1, borderColor: '#ccc', borderRadius: 8, paddingStart: 12, paddingEnd: 12, paddingVertical: 8, }, error: { color: 'red', marginTop: 4, fontSize: 12, }, });
Number and Date Formatting
Localized Formatters
// src/utils/formatters.ts import { NativeModules, Platform } from 'react-native'; interface FormatOptions { locale?: string; currency?: string; minimumFractionDigits?: number; maximumFractionDigits?: number; } export class LocalizedFormatter { private locale: string; constructor(locale: string = getDeviceLocale()) { this.locale = locale; } formatNumber(value: number, options: FormatOptions = {}): string { return new Intl.NumberFormat(options.locale || this.locale, { minimumFractionDigits: options.minimumFractionDigits, maximumFractionDigits: options.maximumFractionDigits, }).format(value); } formatCurrency(value: number, options: FormatOptions = {}): string { return new Intl.NumberFormat(options.locale || this.locale, { style: 'currency', currency: options.currency || 'USD', }).format(value); } formatDate(date: Date, options: Intl.DateTimeFormatOptions = {}): string { return new Intl.DateTimeFormat(this.locale, options).format(date); } formatRelativeTime(date: Date): string { const rtf = new Intl.RelativeTimeFormat(this.locale, { numeric: 'auto', }); const diff = date.getTime() - new Date().getTime(); const days = Math.round(diff / (1000 * 60 * 60 * 24)); if (Math.abs(days) < 1) { const hours = Math.round(diff / (1000 * 60 * 60)); if (Math.abs(hours) < 1) { const minutes = Math.round(diff / (1000 * 60)); return rtf.format(minutes, 'minute'); } return rtf.format(hours, 'hour'); } return rtf.format(days, 'day'); } } // Usage const formatter = new LocalizedFormatter(); <Text>{formatter.formatNumber(1234.567)}</Text> <Text>{formatter.formatCurrency(99.99, { currency: 'EUR' })}</Text> <Text>{formatter.formatDate(new Date(), { dateStyle: 'full' })}</Text> <Text>{formatter.formatRelativeTime(new Date('2024-03-25'))}</Text>
Language Switcher
Language Selection Component
// src/components/LanguageSwitcher.tsx import React from 'react'; import { View, TouchableOpacity } from 'react-native'; import { useTranslation } from 'react-i18next'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { configureRTL } from '../utils/rtl'; const SUPPORTED_LANGUAGES = [ { code: 'en', name: 'English', isRTL: false }, { code: 'ar', name: 'العربية', isRTL: true }, ]; export const LanguageSwitcher: React.FC = () => { const { i18n } = useTranslation(); const changeLanguage = async (languageCode: string) => { const language = SUPPORTED_LANGUAGES.find(lang => lang.code === languageCode); if (!language) return; try { await AsyncStorage.setItem('userLanguage', languageCode); await i18n.changeLanguage(languageCode); await configureRTL(language.isRTL); } catch (error) { console.error('Failed to change language:', error); } }; return ( <View style={styles.container}> {SUPPORTED_LANGUAGES.map(language => ( <TouchableOpacity key={language.code} style={[ styles.languageButton, i18n.language === language.code && styles.activeLanguage, ]} onPress={() => changeLanguage(language.code)} > <LocalizedText style={styles.languageText}> {language.name} </LocalizedText> </TouchableOpacity> ))} </View> ); }; const styles = StyleSheet.create({ container: { flexDirection: 'row', justifyContent: 'center', padding: 16, }, languageButton: { padding: 8, marginHorizontal: 8, borderRadius: 4, backgroundColor: '#f0f0f0', }, activeLanguage: { backgroundColor: '#007AFF', }, languageText: { color: '#333', fontSize: 16, }, activeLanguageText: { color: '#fff', }, });
Next Steps
Now that you understand internationalization in React Native, you can:
- Implement multi-language support
- Handle RTL layouts properly
- Create localized components
- Format numbers and dates
- Add language switching functionality
- Test your app in different locales