Better I18NBetter I18N
Expo

Setup

Installation and configuration for Expo and React Native

This guide walks you through installing @better-i18n/expo and setting up i18next in your Expo or React Native app.

Prerequisite: Create a project at dash.better-i18n.com. Your project identifier will be in the format org/project (e.g., my-company/mobile-app).

Installation

npm install @better-i18n/expo i18next react-i18next
yarn add @better-i18n/expo i18next react-i18next
pnpm add @better-i18n/expo i18next react-i18next
bun add @better-i18n/expo i18next react-i18next

For offline caching and device locale detection:

npx expo install expo-localization
  • expo-localization - Enables device locale detection via useDeviceLocale

For persistent storage, install one of:

# Fastest — MMKV (recommended)
npx expo install react-native-mmkv

# Most common — AsyncStorage
npx expo install @react-native-async-storage/async-storage

Bring your own storage. Pass any storage instance — MMKV or AsyncStorage — through storageAdapter(). The adapter detects the type automatically and normalizes it. No storage passed? Falls back to in-memory (great for demos and testing).

Configuration

Create i18n Config

Create a file to initialize i18next with initBetterI18n:

lib/i18n/index.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import { initBetterI18n } from '@better-i18n/expo'; 

i18n.use(initReactI18next); 

// Start at module level — the promise is cached, so multiple imports won't re-run init
export const i18nReady = initBetterI18n({ 
  project: 'your-org/your-project', 
  i18n,
  defaultLocale: 'en',
  debug: __DEV__,
});

export default i18n;

This config has no offline support and no locale persistence. The app always starts in defaultLocale — if a user previously selected Turkish, they'll see English for one frame before switching. For production apps, use the Offline-First tab.

Pass a storageAdapter to enable offline caching and locale persistence. Translations are cached locally and the user's language choice survives app restarts — no "English flash".

lib/i18n/index.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import { MMKV } from 'react-native-mmkv';
import { initBetterI18n, storageAdapter } from '@better-i18n/expo'; 

const mmkv = new MMKV({ id: 'app' });

i18n.use(initReactI18next);

export const i18nReady = initBetterI18n({
  project: 'your-org/your-project',
  i18n,
  storage: storageAdapter(mmkv, { localeKey: '@app:locale' }), 
  defaultLocale: 'en',
  debug: __DEV__,
});

export default i18n;
lib/i18n/index.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { initBetterI18n, storageAdapter } from '@better-i18n/expo'; 

i18n.use(initReactI18next);

export const i18nReady = initBetterI18n({
  project: 'your-org/your-project',
  i18n,
  storage: storageAdapter(AsyncStorage, { localeKey: '@app:locale' }), 
  defaultLocale: 'en',
  debug: __DEV__,
});

export default i18n;

localeKey nedir? storageAdapter'a localeKey geçildiğinde, kullanıcının seçtiği dil her changeLanguage() çağrısında storage'a yazılır ve uygulama yeniden açıldığında otomatik okunur.

localeKey olmadan: uygulama her açılışta defaultLocale veya cihaz diline başlar — kullanıcı Türkçe seçmiş olsa bile, uygulamanın ilk açılışında 1 frame'lik "English flash" riski vardır.

localeKey ile: son seçilen dil her zaman kazanır — kayıtlı dil, cihaz dilinden ve defaultLocale'den önce gelir.

Need the language list? Use getLanguages() anywhere in your app after init completes — no context provider needed.

Initialize in App Entry

Await i18nReady before rendering your app. Use SplashScreen to keep the native splash visible during init — the user sees no loading indicator at all.

app/_layout.tsx
import { useEffect, useState } from 'react';
import * as SplashScreen from 'expo-splash-screen';
import { Stack } from 'expo-router';
import { i18nReady } from '~/lib/i18n'; 

// Hold the splash screen until the app is ready
SplashScreen.preventAutoHideAsync(); 

export default function RootLayout() {
  const [ready, setReady] = useState(false);

  useEffect(() => { 
    i18nReady.then(() => { 
      setReady(true); 
      SplashScreen.hideAsync(); 
    }); 
  }, []); 

  if (!ready) return null; 

  return <Stack />;
}

SplashScreen.preventAutoHideAsync() is called at module level so it takes effect before the first render. The native splash stays visible while i18nReady resolves — no spinner, no blank screen, no English flash.

App.tsx
import { useEffect, useState } from 'react';
import * as SplashScreen from 'expo-splash-screen';
import { i18nReady } from './src/i18n'; 
import { HomeScreen } from './src/screens/HomeScreen';

// Hold the splash screen until the app is ready
SplashScreen.preventAutoHideAsync(); 

export default function App() {
  const [ready, setReady] = useState(false);

  useEffect(() => { 
    i18nReady.then(() => { 
      setReady(true); 
      SplashScreen.hideAsync(); 
    }); 
  }, []); 

  // SplashScreen is visible, so the user sees nothing during init
  if (!ready) return null; 

  return <HomeScreen />;
}
index.ts
import { registerRootComponent } from 'expo';
import { i18nReady } from './src/i18n';

// The App component is only mounted after i18n is ready — no loading state needed
i18nReady.then(() => {
  const { App } = require('./src/App');
  registerRootComponent(App);
});

Use Translations

Use the standard react-i18next hooks — no changes to your components:

src/screens/HomeScreen.tsx
import { useTranslation } from 'react-i18next';
import { Text, View } from 'react-native';

export function HomeScreen() {
  const { t } = useTranslation();

  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text style={{ fontSize: 24 }}>{t('welcome')}</Text>
      <Text>{t('description')}</Text>
    </View>
  );
}

Language Picker Example

Use i18n.changeLanguage() to switch languages at runtime. Translations are pre-loaded before the switch — no loading spinners or English flash.

components/LanguagePicker.tsx
import { useTranslation } from 'react-i18next';
import { FlatList, Pressable, Text } from 'react-native';
import { getLanguages } from '@better-i18n/expo'; 

export function LanguagePicker() {
  const { i18n } = useTranslation();

  return (
    <FlatList
      data={getLanguages()} 
      keyExtractor={(item) => item.code}
      renderItem={({ item }) => (
        <Pressable onPress={() => i18n.changeLanguage(item.code)}>
          <Text>{item.nativeName ?? item.name}</Text>
        </Pressable>
      )}
    />
  );
}

Language switching is instant — initBetterI18n overrides changeLanguage() to pre-load translations from CDN (or cache) before the switch happens. No loading spinners or English flash.

Next Steps

On this page