Skip to content
better-i18n.com
Sur cette page

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

Installation #

npm install @better-i18n/expo i18next react-i18next

For offline caching and device locale detection:

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

For persistent storage, install one of:

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

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

Configuration #

Create i18n Config #

Create a file to initialize i18next with initBetterI18n:

Basic

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

i18n.use(initReactI18next); // [!code highlight]

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

export default i18n;

Offline-First (Recommended)

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'; // [!code highlight]

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

i18n.use(initReactI18next);

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

export default i18n;

AsyncStorage

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'; // [!code highlight]

i18n.use(initReactI18next);

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

export default i18n;

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.

Expo Router

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'; // [!code ++]

// Hold the splash screen until the app is ready
SplashScreen.preventAutoHideAsync(); // [!code ++]

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

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

  if (!ready) return null; // [!code ++]

  return <Stack />;
}

Expo (No Router)

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

// Hold the splash screen until the app is ready
SplashScreen.preventAutoHideAsync(); // [!code ++]

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

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

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

  return <HomeScreen />;
}

Bare React Native

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'; // [!code highlight]

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

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

Next Steps #