Reach is used in 70+ countries and supports over 50 languages. Making the app work in English, Spanish, and French is straightforward. Making it work in Arabic, Dari, Amharic, and Tigrinya, with users who may have never used a smartphone before, is a different problem entirely.
i18next setup
We use react-i18next with a namespace structure:
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
i18n.use(initReactI18next).init({
lng: 'en',
fallbackLng: 'en',
ns: ['common', 'sessions', 'auth', 'settings', 'errors'],
defaultNS: 'common',
interpolation: {
escapeValue: false,
},
resources: {
en: {
common: require('./locales/en/common.json'),
sessions: require('./locales/en/sessions.json'),
auth: require('./locales/en/auth.json'),
},
// Other languages loaded on demand
},
});
Namespaces keep the translation files manageable. common has UI strings shared across screens. sessions has strings specific to the session flow. errors has error messages. Each namespace is a separate JSON file, so translators can work on one domain at a time.
For languages that aren't pre-loaded, we load them from local storage on language switch:
async function changeLanguage(lang: string) {
if (!i18n.hasResourceBundle(lang, 'common')) {
const resources = await loadTranslationsFromStorage(lang);
Object.entries(resources).forEach(([ns, translations]) => {
i18n.addResourceBundle(lang, ns, translations);
});
}
await i18n.changeLanguage(lang);
}
RTL layout
Arabic, Hebrew, Dari, Pashto, and Urdu are right-to-left languages. React Native supports RTL through I18nManager:
import { I18nManager } from 'react-native';
function setRTL(isRTL: boolean) {
if (I18nManager.isRTL !== isRTL) {
I18nManager.forceRTL(isRTL);
// Requires app restart to take effect
RNRestart.restart();
}
}
When RTL is enabled, React Native automatically mirrors most layout properties: flexDirection: 'row' flows right-to-left, marginLeft becomes marginRight, textAlign: 'left' becomes textAlign: 'right'.
Components that need explicit RTL handling:
- Navigation gestures: the back swipe gesture goes right-to-left. React Navigation handles this automatically with RTL enabled.
- Icons with directionality: arrows, chevrons, progress indicators. We use
I18nManager.isRTLto flip these icons:
const chevronStyle = {
transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }],
};
- Chat message alignment: messages from the current user appear on the right in LTR and on the left in RTL. This is intentional and matches platform conventions.
- Sliders and progress bars: a progress bar filling from left to right needs to fill from right to left in RTL.
Components that handle RTL automatically (no changes needed):
- Text alignment (auto-mirrors)
- Flexbox row direction (auto-mirrors)
- Margin and padding in the inline direction (auto-mirrors)
- ScrollView content direction (auto-mirrors)
Locale-specific formatting
Dates and numbers format differently across locales:
// Date formatting
function formatDate(date: Date, locale: string): string {
return new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(date);
}
// "March 5, 2025" (en-US)
// "5 mars 2025" (fr-FR)
// "٥ مارس ٢٠٢٥" (ar-SA) — note the Arabic-Indic numerals
// Number formatting
function formatNumber(num: number, locale: string): string {
return new Intl.NumberFormat(locale).format(num);
}
// "1,234.56" (en-US)
// "1.234,56" (de-DE)
// "١٬٢٣٤٫٥٦" (ar-SA)
We use the Intl API for formatting rather than string manipulation. The Intl API handles the details (digit systems, separator characters, month name translations) that manual formatting would miss.
On React Native with Hermes, Intl support requires the intl polyfill for some features. We include @formatjs/intl-pluralrules, @formatjs/intl-numberformat, and @formatjs/intl-datetimeformat polyfills.
Pluralisation
English has two plural forms: singular ("1 message") and plural ("2 messages"). Other languages have more:
- Arabic has six plural forms (zero, one, two, few, many, other)
- Polish has three (one, few, many)
- Japanese has one (no plural distinction)
i18next handles this with the ICU message format:
{
"messages_count": "{count, plural, =0 {No messages} one {# message} two {# messages} few {# messages} many {# messages} other {# messages}}"
}
Naive string interpolation (${count} message${count === 1 ? '' : 's'}) breaks for every language except English. Using the ICU format with i18next's plural resolver handles all CLDR plural rules correctly.
Font fallbacks
Our primary font (the app uses a custom sans-serif) doesn't cover all character sets. Arabic, Amharic, Tigrinya, and several other languages need fonts that support their script.
const fontFamily = Platform.select({
ios: getFontForLanguage(currentLanguage),
android: getFontForLanguage(currentLanguage),
});
function getFontForLanguage(lang: string): string {
const arabicScriptLanguages = ['ar', 'fa', 'ps', 'ur'];
const ethiopicLanguages = ['am', 'ti'];
if (arabicScriptLanguages.includes(lang)) return 'NotoSansArabic';
if (ethiopicLanguages.includes(lang)) return 'NotoSansEthiopic';
return 'Inter';
}
We bundle Noto Sans variants for Arabic, Ethiopic, and Devanagari scripts. These add about 2MB to the app bundle, which is acceptable given the user base.
Designing for low-tech-familiarity users
Some users of Reach have limited experience with smartphones. They're hospital staff in regions where personal smartphone ownership is recent, or they're community interpreters who use the app infrequently.
Product decisions we made:
- Large touch targets: minimum 48x48 points (larger than Apple's 44pt guideline). Buttons have generous padding.
- Minimal text in primary actions: the "Request Interpreter" button uses an icon alongside text. The icon communicates the action even if the text is in an unfamiliar script.
- No gestures for primary actions: swipe-to-delete, long-press menus, and pull-to-refresh are discoverable features that new smartphone users may not know about. All primary actions are explicit buttons.
- Visual feedback for every action: tapping a button shows a visible state change (colour change, loading indicator) within 100ms. Users who are unfamiliar with touchscreens may tap multiple times if they don't see immediate feedback.
- Onboarding with visuals: the first-time user experience shows screenshots of the app with annotations pointing to key actions, rather than text-heavy tutorial screens.
These decisions aren't technically complex. They're product decisions informed by understanding who the users are and what their constraints are. The technical implementation is straightforward. The insight that drives it, that your users might be using a smartphone application for the first time, is the part that requires empathy and domain knowledge.
Building for 70 countries isn't about translating strings. It's about understanding that the user experience you designed for users in California doesn't work for users in a hospital in rural Afghanistan, and redesigning accordingly.