Lesson 14-React Native Internationalization

Internationalization Basic Configuration

Installing Required Dependencies

# Core Library
npm install i18next react-i18next i18next-react-native-language-detector

# Optional: Support for Plural Forms and Interpolation
npm install i18next-plural-rules

Basic i18n Configuration

// i18n.js
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-react-native-language-detector';

// Import Translation Resources
import en from './locales/en.json';
import zh from './locales/zh.json';
import ja from './locales/ja.json';

i18n
  .use(LanguageDetector) // Use Language Detector
  .use(initReactI18next) // Bind to React
  .init({
    resources: {
      en: { translation: en },
      zh: { translation: zh },
      ja: { translation: ja },
    },
    fallbackLng: 'en', // Default Language
    debug: __DEV__, // Enable Debugging in Development Mode
    interpolation: {
      escapeValue: false, // React Handles XSS Protection
    },
    compatibilityJSON: 'v3', // Resolve iOS 13+ Compatibility Issues
  });

export default i18n;

Translation Resource File Example

// locales/en.json
{
  "welcome": "Welcome to our app",
  "login": {
    "title": "Login",
    "username": "Username",
    "password": "Password"
  },
  "errors": {
    "network": "Network error, please try again"
  }
}

// locales/zh.json
{
  "welcome": "Welcome to our app",
  "login": {
    "title": "Login",
    "username": "Username",
    "password": "Password"
  },
  "errors": {
    "network": "Network error, please try again"
  }
}

Using in React Components

Basic Text Translation

// LoginScreen.js
import React from 'react';
import { View, Text } from 'react-native';
import { useTranslation } from 'react-i18next';

const LoginScreen = () => {
  const { t } = useTranslation();

  return (
    <View>
      <Text>{t('welcome')}</Text>
      <Text>{t('login.title')}</Text>
      <Text>{t('login.username')}</Text>
      <Text>{t('login.password')}</Text>
    </View>
  );
};

export default LoginScreen;

Dynamic Parameter Interpolation

// ProductDetails.js
import React from 'react';
import { Text } from 'react-native';
import { useTranslation } from 'react-i18next';

const ProductDetails = ({ price, discount }) => {
  const { t } = useTranslation();

  return (
    <Text>
      {t('product.price', { 
        price: price.toFixed(2),
        discount: discount > 0 ? `-${discount}%` : '' 
      })}
    </Text>
  );
};

Plural Form Handling

// locales/en.json
{
  "items": "You have {{count}} item",
  "items_plural": "You have {{count}} items"
}

// Usage in Component
<Text>{t('items', { count: itemCount })}</Text>

Language Switching Implementation

Language Selector Component

// LanguageSwitcher.js
import React from 'react';
import { View, Picker } from 'react-native';
import { useTranslation } from 'react-i18next';

const LanguageSwitcher = () => {
  const { i18n } = useTranslation();

  const changeLanguage = (lng) => {
    i18n.changeLanguage(lng);
    // Optional: Persist User Selection
    AsyncStorage.setItem('userLanguage', lng);
  };

  return (
    <Picker
      selectedValue={i18n.language}
      onValueChange={changeLanguage}
    >
      <Picker.Item label="English" value="en" />
      <Picker.Item label="Chinese" value="zh" />
      <Picker.Item label="Japanese" value="ja" />
    </Picker>
  );
};

Persisting User Language Preference

// Add to i18n Initialization
import AsyncStorage from '@react-native-async-storage/async-storage';

i18n
  .use({
    type: 'languageDetector',
    async: true,
    detect: async (callback) => {
      try {
        const savedLanguage = await AsyncStorage.getItem('userLanguage');
        callback(savedLanguage || undefined); // undefined Lets Detector Auto-Detect
      } catch (err) {
        callback(undefined);
      }
    },
    init: () => {},
    cacheUserLanguage: async (lng) => {
      await AsyncStorage.setItem('userLanguage', lng);
    },
  })
  .use(initReactI18next)
  .init({ /* ... */ });

Handling Plural Forms and Context

Advanced Plural Form Usage

// locales/en.json
{
  "apple": "apple | apples",
  "banana": "no bananas | one banana | {{count}} bananas"
}

// Usage in Component
<Text>{t('apple', { count: 5 })}</Text>
<Text>{t('banana', { count: 0 })}</Text>
<Text>{t('banana', { count: 1 })}</Text>
<Text>{t('banana', { count: 10 })}</Text>

Context Translation

// locales/en.json
{
  "read": "read | read (present perfect) | read (past perfect)",
  "read_context": {
    "present": "read",
    "presentPerfect": "have read",
    "pastPerfect": "had read"
  }
}

// Usage in Component
<Text>{t('read', { context: 'present' })}</Text>
<Text>{t('read_context.presentPerfect')}</Text>

Date, Number, and Currency Formatting

Using Intl API for Formatting

// utils/formatters.js
import { Platform } from 'react-native';

export const formatDate = (date) => {
  return new Intl.DateTimeFormat(i18n.language, {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
  }).format(new Date(date));
};

export const formatCurrency = (amount) => {
  return new Intl.NumberFormat(i18n.language, {
    style: 'currency',
    currency: i18n.language === 'ja' ? 'JPY' : 'USD',
  }).format(amount);
};

Using Formatting in Components

// OrderSummary.js
import React from 'react';
import { Text } from 'react-native';
import { formatDate, formatCurrency } from '../utils/formatters';
import { useTranslation } from 'react-i18next';

const OrderSummary = ({ date, amount }) => {
  const { t } = useTranslation();

  return (
    <View>
      <Text>{t('order.date')}: {formatDate(date)}</Text>
      <Text>{t('order.total')}: {formatCurrency(amount)}</Text>
    </View>
  );
};

Handling RTL (Right-to-Left) Layouts

Detecting and Setting RTL

// In App.js or Entry File
import { I18nManager } from 'react-native';
import i18n from './i18n';

// Detect if Language Requires RTL Layout
const isRTL = ['ar', 'he', 'fa'].includes(i18n.language);
I18nManager.forceRTL(isRTL);
I18nManager.allowRTL(isRTL);

// Note: Set as Early as Possible in App Startup

RTL Layout Adaptation Example

// styles.js
import { StyleSheet, I18nManager } from 'react-native';

export default StyleSheet.create({
  container: {
    flexDirection: I18nManager.isRTL ? 'row-reverse' : 'row',
    justifyContent: 'space-between',
  },
  text: {
    writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
  },
});

Dynamic Import of Translation Resources (On-Demand Loading)

Code Splitting for Translation Resources

// i18n.js
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';

i18n
  .use(initReactI18next)
  .init({
    lng: 'en',
    fallbackLng: 'en',
    debug: __DEV__,
    interpolation: { escapeValue: false },
    react: { useSuspense: true }, // Enable Suspense
  });

// Dynamically Load Language Resources
export const loadLocale = async (locale) => {
  try {
    const module = await import(`./locales/${locale}.json`);
    i18n.addResourceBundle(locale, 'translation', module.default);
    i18n.changeLanguage(locale);
  } catch (error) {
    console.error(`Failed to load ${locale} translations:`, error);
  }
};

export default i18n;

Using Dynamic Loading in Components

// LanguageSwitcher.js
import React, { Suspense } from 'react';
import { Button } from 'react-native';
import { useTranslation } from 'react-i18next';
import { loadLocale } from '../i18n';

const LanguageSwitcher = () => {
  const { i18n } = useTranslation();

  const changeLanguage = async (lng) => {
    if (i18n.language !== lng) {
      await loadLocale(lng);
    }
  };

  return (
    <Suspense fallback={<ActivityIndicator />}>
      <Button title="English" onPress={() => changeLanguage('en')} />
      <Button title="Chinese" onPress={() => changeLanguage('zh')} />
    </Suspense>
  );
};

Testing Internationalization Features

Jest Testing Example

// __tests__/i18n.test.js
import i18n from '../i18n';
import { render, waitFor } from '@testing-library/react-native';
import LoginScreen from '../screens/LoginScreen';

jest.mock('../i18n', () => ({
  t: (key) => key, // Mock Translation Function
  language: 'en',
}));

describe('LoginScreen i18n', () => {
  it('renders with English translations', () => {
    const { getByText } = render(<LoginScreen />);
    expect(getByText('welcome')).toBeTruthy();
  });

  it('switches to Chinese translations', async () => {
    i18n.language = 'zh';
    const { getByText } = render(<LoginScreen />);
    await waitFor(() => {
      expect(getByText('login.title')).toBeTruthy();
    });
  });
});

Detox E2E Testing Example

// e2e/i18n.spec.js
describe('Internationalization', () => {
  beforeAll(async () => {
    await device.launchApp();
  });

  it('should display English by default', async () => {
    await expect(element(by.text('Welcome'))).toBeVisible();
  });

  it('should switch to Chinese', async () => {
    await element(by.id('language-switcher')).tap();
    await element(by.text('Chinese')).tap();
    await expect(element(by.text('Login'))).toBeVisible();
  });
});
Share your love