Internationalization (i18n)
Comprehensive guide for handling multiple languages and localization in the Expo (React Native) application.
The boilerplate uses a robust internationalization (i18n) system powered by i18next and react-i18next. This approach provides a flexible, scalable, and industry-standard way to manage translations in React Native applications.
Overview
The localization system consists of four main components:
- JSON Locale Files: Translation strings stored as JSON objects in
mobile-app/locales/. - i18next Configuration: Initialization and setup in
mobile-app/services/i18n.ts. - State Management:
usePreferencesStore(Zustand) manages the current language state and handles server synchronization. - React Integration: The
useTranslationhook provides access to translation functions within components.
Configuration
The i18n service is configured using i18next and integrates with expo-localization to detect the device's native language settings.
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import { getLocales } from 'expo-localization';
import en from '../locales/en.json';
import id from '../locales/id.json';
const resources = {
en: { translation: en },
id: { translation: id },
};
const deviceLocales = getLocales();
const deviceLanguage = deviceLocales[0]?.languageCode ?? 'en';
i18n.use(initReactI18next).init({
resources,
lng: deviceLanguage, // Use device language as default
fallbackLng: 'en',
interpolation: {
escapeValue: false, // React already escapes values
},
});
export default i18n;Locale Files
Translation files are located in mobile-app/locales/. The boilerplate supports:
en.json: Englishid.json: Bahasa Indonesia
Example Structure
{
"auth": {
"login_title": "Welcome Back",
"login_button": "Sign In"
},
"general": {
"title": "General"
}
}Usage in Code
Using the Hook
The most common way to access translations is through the useTranslation hook.
import { useTranslation } from 'react-i18next';
export default function MyComponent() {
const { t } = useTranslation();
return (
<Text>{t('auth.login_title')}</Text>
);
}With Interpolation
You can pass variables to your translations:
- JSON:
"welcome": "Welcome, {{name}}!" - Code:
t('welcome', { name: 'John' })
With Pluralization
-
JSON:
"item_count_one": "{{count}} item", "item_count_other": "{{count}} items" -
Code:
t('item_count', { count: 5 })
Formatting Dates and Numbers
For formatting dates according to the user's locale, the boilerplate uses the date-fns library.
import { formatDistanceToNow } from 'date-fns';
import { enUS, id } from 'date-fns/locale';
import { useTranslation } from 'react-i18next';
export function RelativeTime({ date }: { date: Date }) {
const { i18n } = useTranslation();
// Select the date-fns locale based on current language
const locale = i18n.language === 'id' ? id : enUS;
return (
<Text>
{formatDistanceToNow(date, { addSuffix: true, locale })}
</Text>
);
}State Management & Persistence
The application's language preference is managed by usePreferencesStore and is persisted both locally (via AsyncStorage) and on the server.
Persistence Flow
- Selection: User selects a language in
app/settings/general.tsx. - Update:
changeLanguage(lang)is called, which:- Updates
i18nextinstance:i18n.changeLanguage(lang). - Updates Zustand store:
setLanguageInStore(lang).
- Updates
- Local Storage: The store automatically saves the preference to
AsyncStorage. - Server Sync: Changes are debounced and asynchronously synced to the backend API (
PUT /user/preferences). - Initialization: On app launch,
usePreferencesStore.getState().initialize()restores the saved language.
Changing Language Programmatically
const { setLanguage } = usePreferencesStore();
const { i18n } = useTranslation();
const handleLanguageChange = async (lang: string) => {
await i18n.changeLanguage(lang);
await setLanguage(lang);
};Adding a New Language
To add support for a new language (e.g., Japanese - ja):
- Create JSON File: Create
mobile-app/locales/ja.jsonand copy keys fromen.json. - Register Resource: Import and add the new file to
resourcesinmobile-app/services/i18n.ts. - Update UI: Add the new language option to the
BottomSheetModalinmobile-app/app/settings/general.tsx. - Update Labels: Update
getLanguageLabelandgetFullLanguageLabelhelpers in the settings screen.
Best Practices
- Nested Keys: Use logical nesting (e.g.,
auth.login.title) to keep the locale files organized. - Fallback: Always provide a value in
en.json(the fallback language) when adding new keys. - No Hardcoding: Never use hardcoded strings for user-facing text. Even small labels should be localized.
- Context: If a word has multiple meanings depending on context, use specific keys like
actions.savevsfile.save. - Reuse: Reuse common strings (like "Cancel", "Save", "Error") by placing them in a
commonorgeneralsection.
