Languages
src/lib/i18n/language.ts:
DEFAULT_LANGUAGE is he. getStoredLanguage() reads perform_language and falls back to the default for a missing or unknown value. getLanguageTag() returns the locale tag for date and number formatting.
The language is read with the synchronous SecureStore API (SecureStore.getItem), or localStorage on web. It has to be available at module load, before any component renders.
Strings
src/constants/strings.ts picks one dictionary at module load:
- Dictionaries are in
src/constants/locales/<code>.ts.en.tsis the shape. Every other locale is typed astypeof en, so a missing key fails the type check. stringsis a plain object, not a hook. Components readstrings.someKeydirectly.format(template, params)replaces{name}style placeholders.locales/muscles-ar.tsholds Arabic muscle names.
strings, fontFamily and the rtl.ts constants are all computed once at module load, a language change cannot apply in place. The app reloads.
Switching language
The profile tab’s App language row (features/profile/components/LanguageRow.tsx) opens an alert listing SUPPORTED_LANGUAGES and calls setLanguage(code):
reloadApp() calls reloadAppAsync('language-changed') from expo on native and location.reload() on web.
Native direction
MANUAL_RTL in src/lib/rtl.ts is the strategy switch. It is false: the app uses real native RTL through I18nManager, so React Native, Yoga and native chrome mirror together.
applyAppDirection() is called once at the top of src/app/_layout.tsx, before the first render.
Why the reload: I18nManager.allowRTL and forceRTL only persist a flag. The running surface keeps the direction it started with, so a mismatch is fixed by reloading into a new surface.
Why the guard: the attempted direction is written to perform_rtl_attempt before reloading. If the runtime refuses to change direction, the next launch sees the same tag and stops. The comment in the code records the incident behind it: an unguarded reload once restarted the Android bundle about twice a second until the app ran out of memory.
Two exported constants describe the state:
After removing such keys, run
expo prebuild --clean. A non-clean prebuild merges generated native files and does not remove keys it no longer manages.
Helpers in rtl.ts
With native RTL on, a normalrow already reverses and left / right in styles are swapped by React Native. The helpers exist so code reads the same under either strategy.
Props versus styles
This is the part that causes bugs. React Native mirrorsleft and right inside style objects when the surface is RTL. It does not touch props. A prop that takes a physical side keeps that side in every language.
So there are two families of helpers:
- For styles, use the logical helpers.
textAlign: rtlTextAlignStartresolves to'left', and React Native flips it to the right in RTL. - For props, use
surfaceStartandsurfaceEnd, which already resolve to the physical side for the direction the surface was laid out in.
surfaceStart is based on appliedRTL, not isRTL, so it stays correct during the one launch where the two differ.
Icons
Icons never flip by themselves on any platform. Use the exported names:iconBack is arrow-forward and iconChevronForward is chevron-back.
Content that stays left to right
Wrap a subtree inphysicalLtr when its layout is LTR in every language. The doc comment in rtl.ts names the cases: digit sequences that fill one by one such as the OTP and phone fields, measurement scales whose values ascend left to right, and captured story posters that must look identical in every locale. The reps picker overlay and the story share components are pinned this way too.
It is a design declaration, not an escape hatch. Anything that follows reading order should use the helpers above.
Tabs
- iOS native tabs receive the direction through
unstable_nativeProps={{ direction: isRTL ? 'rtl' : 'ltr' }}. - The Android tab bar lays its items out with
rtlRow. - The web tab layout reverses the tab array only when
MANUAL_RTL && isRTL, which is never true now.
Web
react-native-web stubs I18nManager: forceRTL does nothing and isRTL is always false. On web applyAppDirection() sets document.documentElement.dir and lang and returns. No reload is attempted.
Search in Hebrew
src/lib/search.ts folds both the query and the name before comparing: it strips niqqud and cantillation, maps final letter forms to medial ones, removes Latin accents, lowercases, and treats punctuation such as geresh and gershayim as word breaks. searchTerms() splits the query into words and matchesSearchTerms() requires every word to appear, in any order.
Checklist for a new screen
- Rows use
RoworflexDirection: rtlRow. - Text goes through
AppText. - Spacing between row children uses
gap. - Directional icons come from
rtl.ts. - Any
left/rightvalue passed as a prop usessurfaceStartorsurfaceEnd. - Numeric sequences and scales are wrapped in
physicalLtr. - Every new label exists in all six locale files.