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:
- User’s saved preference (from MMKV storage)
- Device locale (if Arabic, use Arabic)
- 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/:
en.json
{
"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.networkAdding New Keys
- Add the English key to
src/i18n/locales/en.json - Add the Arabic key to
src/i18n/locales/ar.json - 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-leftpaddingStart/paddingEndwork 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
}