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-i18nextyarn add @better-i18n/expo i18next react-i18nextpnpm add @better-i18n/expo i18next react-i18nextbun add @better-i18n/expo i18next react-i18nextRecommended Optional Dependencies
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-storageBring 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:
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".
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;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.
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.
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 />;
}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:
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.
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.