BabelizeBabelize
Integrations

React Native & Expo

How Babelize translates Expo Router, classic Expo and bare React Native apps — what it extracts, where translations are bundled, how the device language is picked, and current limitations.

React Native support is in beta: it works, but review the diff before merging.

Babelize translates Expo Router, classic Expo and bare React Native apps. It uses the same runtime as React on the web, react-i18next with ICU messages, but a native app can't download translations from a server or read a browser language, so the setup is different:

  • translations are bundled with the app as JSON files,
  • the device language picks the translation (expo-localization in Expo apps, react-native-localize in bare React Native),
  • i18next is initialized before the first screen renders, so t() never shows a raw key.

Detection

A project is treated as React Native when its package.json depends on expo or react-native. A Next.js app that also uses react-native-web stays a Next.js project.

The whole project is scanned, because React Native apps keep code in App.tsx, screens/, navigation/, hooks/ and so on. Skipped: the native ios/ and android/ projects, .expo, web-build, scripts/, the locale folder and config files (app.config.ts, babel.config.js, metro.config.js, …), plus everything Babelize always skips (node_modules, tests, type declarations).

What gets translated

WhereBecomes
<Text>Welcome back</Text><Text>{t("home.welcomeBack")}</Text>
A sentence with nested <Text> and variables: <Text>Signed in as <Text style={s.bold}>{name}</Text></Text>one message, rendered with <Trans> so the inner <Text> keeps its style
placeholder, title (<Button title>), accessibilityLabel, accessibilityHintplaceholder={t("…")}
Screen titles in navigation options: title, headerTitle, headerBackTitle, tabBarLabel, drawerLabel in options / screenOptions (objects or functions)options={{ title: t("…") }} — other options (animation: 'fade', presentation: 'modal') are left alone
Alert.alert(title, message, buttons) / Alert.promptthe title, the message and each button's text, including two-letter labels such as OK
Template literals and concatenations: `We charged ${total}.`t("…", { total })
A default prop value: ({ label = 'New' })({ label = t("…") })

Components get const { t } = useTranslation(). Text in a lowercase render helper (function renderEmpty() { return <Text>…</Text> }) or at module level uses the t exported by the generated i18n.ts instead, because a hook there would break the rules of hooks.

Platform files share their translations: tabs.tsx, tabs.ios.tsx and tabs.web.tsx use one key group.

Left alone: testID, nativeID, style, placeholderTextColor, keyboardType, textContentType, autoComplete, accessibilityRole, route names, navigation calls (navigation.navigate('Details')), object keys, file paths, and everything else that is data rather than copy.

Files Babelize adds

With a src/ folder (Expo Router's default) everything goes there; otherwise at the project root. Nothing new is ever added inside Expo Router's app/ folder, where every file is a route.

FileWhat it is
src/locales/<lang>.jsonthe translations, one file per language
src/locales/index.tsimports every locale file. Rewritten on every run, so adding or removing a language needs no manual step
src/i18n.tsinitializes i18next with the bundled translations and the device language. Written once; you can edit it
package.jsoni18next, react-i18next, i18next-icu, intl-messageformat, and expo-localization (pinned to your Expo SDK, as npx expo install would) or react-native-localize

The i18n config is imported as the first line of the app's entry point:

  • Expo Router ("main": "expo-router/entry"): the root layout, src/app/_layout.tsx or app/_layout.tsx
  • classic Expo: the file main points to (index.ts)
  • bare React Native: index.js

TypeScript projects get .ts files, JavaScript projects .js. If your app already sets up i18next itself, Babelize doesn't add a second config: it tells you how to load the bundled translations into yours.

After merging, install the new dependencies (npx expo install --fix keeps Expo modules on your SDK's versions). Bare React Native apps also need cd ios && pod install for react-native-localize.

Language

The app starts in the device's preferred language that has translations: the full tag first (pt-BR, zh-Hant), then the language (pt), falling back to the source language. To let people choose a language, the project page in the dashboard has a copyable switcher (buttons calling i18n.changeLanguage). To remember their choice across launches, store it and pass it to changeLanguage at startup:

import AsyncStorage from '@react-native-async-storage/async-storage';
import i18n from './i18n';

i18n.on('languageChanged', (lng) => AsyncStorage.setItem('language', lng));
AsyncStorage.getItem('language').then((lng) => lng && i18n.changeLanguage(lng));

Hardcoded prices and dates that Babelize rewrites to Intl.NumberFormat / Intl.DateTimeFormat follow the app's language (i18next.language).

Requirements

  • Expo SDK 50 or later, or React Native 0.73 or later with Hermes. Message formatting uses Intl (plural rules, number formatting), which Hermes provides from these versions.
  • TypeScript projects need resolveJsonModule, which expo/tsconfig.base and @react-native/typescript-config already enable.

Limitations

  • Review the diff before merging. Files Babelize edits are reprinted, so run your formatter (Prettier, expo lint --fix) on the pull request.
  • Right-to-left languages (Arabic, Hebrew, …) are translated, but switching the layout direction is up to you: call I18nManager.forceRTL(true) for RTL languages; it takes effect after the app reloads.
  • Native strings — the app name, iOS permission prompts (NSCameraUsageDescription, …), Android strings.xml — are not translated. Expo apps can localize them with the locales config.
  • Monorepos: each Babelize project translates one app. For an apps/mobile app in a monorepo, run babelize translate from inside its folder.
  • Text that only exists at runtime (API data, user content) isn't in your code, so it isn't translated.

Last updated: 2026-10-15

How is this guide?

On this page