Skip to Content
Core ConceptsInternationalization

Internationalization

RNCopilot ships with react-i18next configured for English and Arabic, including full RTL (right-to-left) support. Every user-facing string in the app goes through the useTranslation() hook — no exceptions.

Setup

The i18n configuration lives in src/i18n/config.ts:

// src/i18n/config.ts import { getLocales } from 'expo-localization'; import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import { getItem, STORAGE_KEYS } from '@/utils/storage'; import ar from './locales/ar.json'; import en from './locales/en.json'; const savedLang = getItem<string>(STORAGE_KEYS.preferences.language); let initialLang = 'en'; if (savedLang.success && savedLang.data) { initialLang = savedLang.data; } else { const deviceLocale = getLocales()[0]?.languageCode; if (deviceLocale === 'ar') { initialLang = 'ar'; } } i18n.use(initReactI18next).init({ compatibilityJSON: 'v4', resources: { en: { translation: en }, ar: { translation: ar }, }, lng: initialLang, fallbackLng: 'en', interpolation: { escapeValue: false }, });

Language priority:

  1. User’s saved preference (from MMKV storage)
  2. Device locale (if Arabic, use Arabic)
  3. Fallback to English

Using Translations

The useTranslation Hook

import { useTranslation } from 'react-i18next'; import { Text } from '@/common/components/Text'; export function WelcomeMessage() { const { t } = useTranslation(); return ( <> <Text variant="h1">{t('home.welcome')}</Text> <Text variant="body">{t('home.subtitle')}</Text> </> ); }

Never hardcode UI strings. Every piece of text the user sees must use t('key').

// WRONG <Text>Welcome back!</Text> <Button title="Sign In" /> // CORRECT <Text>{t('home.welcome')}</Text> <Button title={t('auth.login')} />

Translation Files

Translation files live in src/i18n/locales/:

{ "common": { "loading": "Loading...", "cancel": "Cancel", "save": "Save", "delete": "Delete", "confirm": "Confirm" }, "home": { "welcome": "Welcome", "subtitle": "Your app is ready to go." }, "settings": { "title": "Settings", "darkMode": "Dark Mode", "language": "Language" }, "validation": { "required": "This field is required", "emailInvalid": "Please enter a valid email address", "passwordMin": "Password must be at least 8 characters" } }

Key Rules for Translation Files

Both files must be updated together. When you add a key to en.json, you must add the corresponding key to ar.json in the same commit. Missing keys will fall back to English, which breaks the Arabic user experience.

Dot Notation Keys

Keys use dot notation with semantic grouping:

feature.section.item home.welcome home.subtitle settings.darkMode auth.login auth.forgotPassword validation.required validation.emailInvalid errors.generic errors.network

Adding New Keys

  1. Add the English key to src/i18n/locales/en.json
  2. Add the Arabic key to src/i18n/locales/ar.json
  3. Use the key in your component via t('feature.keyName')
// en.json { "products": { "title": "Products", "empty": "No products found", "addNew": "Add Product" } } // ar.json { "products": { "title": "المنتجات", "empty": "لا توجد منتجات", "addNew": "إضافة منتج" } }

Type-Safe Translations

The i18n config declares a module augmentation for i18next, which gives you autocomplete on translation keys:

// src/i18n/config.ts declare module 'i18next' { interface CustomTypeOptions { defaultNS: 'translation'; resources: { translation: typeof en; }; } }

This means t('home.welcme') (typo) will produce a TypeScript error if strict i18n typing is enabled in your IDE.

RTL Support

RTL layout is handled globally in app/_layout.tsx. You do not need to manually flip layouts or conditionally change flexDirection:

// app/_layout.tsx const isArabic = i18n.language === 'ar'; if (Platform.OS !== 'web') { I18nManager.allowRTL(isArabic); I18nManager.forceRTL(isArabic); }

When the language is Arabic:

  • flexDirection: 'row' automatically becomes right-to-left
  • paddingStart / paddingEnd work correctly
  • Icons and navigation arrows are mirrored

RTL changes require an app restart on native platforms. Switching from English to Arabic (or vice versa) at runtime will take effect after the next app launch.

Zod Validation Messages as i18n Keys

Validation schemas use i18n keys as error messages, not raw strings:

import { z } from 'zod/v4'; export const loginSchema = z.object({ email: z.email('validation.emailInvalid'), password: z.string().min(8, 'validation.passwordMin'), });

The FormField component translates these keys when rendering error messages:

// The error message "validation.emailInvalid" is passed through t() // and displayed as "Please enter a valid email address" (EN) // or "يرجى إدخال بريد إلكتروني صحيح" (AR)

Changing Language at Runtime

import i18n from '@/i18n/config'; import { setItem, STORAGE_KEYS } from '@/utils/storage'; function changeLanguage(lang: 'en' | 'ar') { i18n.changeLanguage(lang); setItem(STORAGE_KEYS.preferences.language, lang); // Note: RTL changes require app restart on native }
Last updated on