@better-i18n/expo
v0.10.0
Published
Better i18n backend plugin for i18next in Expo/React Native
Maintainers
Readme
@better-i18n/expo
Better i18n integration for Expo and React Native. Fetches translations from the CDN with offline caching and auto-detected device locale.
Features
- CDN-Powered — Translations served from the edge via Better i18n CDN
- Offline-First — Network-first strategy with automatic persistent cache fallback
- Persistent Storage — Pass
storageAdapter()with MMKV or AsyncStorage for offline caching - Device Locale — Detect device language via
expo-localization - i18next Compatible — Works as a backend plugin or one-call init helper
- Namespace Auto-Discovery — Automatically registers all namespaces from CDN data
- Lightweight — Built on
@better-i18n/core(~2KB)
Installation
npm install @better-i18n/expo i18next react-i18next
# or
bun add @better-i18n/expo i18next react-i18nextOptional Peer Dependencies
| Package | Purpose |
| --- | --- |
| expo-localization | Auto-detect device locale |
| react-native-mmkv | Fastest persistent storage |
| @react-native-async-storage/async-storage | Common persistent storage |
Pass storageAdapter() with either package to enable persistent offline caching. Without it, the SDK uses in-memory storage (no persistence across restarts).
Quick Start
Recommended: initBetterI18n
One-call setup that fetches translations and initializes i18next with all namespaces pre-loaded:
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import { initBetterI18n } from "@better-i18n/expo";
i18n.use(initReactI18next);
const { core, languages } = await initBetterI18n({
project: "acme/my-app",
i18n,
useDeviceLocale: true, // auto-detect device language
});
// `languages` contains available languages with metadata
// [{ code: "en", name: "English", ... }, { code: "tr", name: "Turkish", ... }]Then use translations in any component:
import { useTranslation } from "react-i18next";
function HomeScreen() {
// Colon-notation (namespace explicit):
const { t } = useTranslation("common");
return <Text>{t("welcome")}</Text>;
// Dot-notation (default namespace):
// const { t } = useTranslation();
// return <Text>{t("common.welcome")}</Text>;
}Translation Patterns
initBetterI18n supports both i18next usage styles — choose one and use it consistently:
Colon-notation — t('namespace:key')
// Scoped to a namespace via hook argument
const { t } = useTranslation("auth");
t("login") // ✓
t("auth:login") // ✓ (namespace explicit)Dot-notation — t('section.key')
// Default namespace, dot-separated path
const { t } = useTranslation();
t("auth.login") // ✓
t("common.welcome") // ✓Both patterns work simultaneously with no extra configuration — all CDN namespaces are merged into the "translation" default namespace in addition to being registered individually.
### Alternative: Backend Plugin
For more control, use `BetterI18nBackend` as an i18next backend plugin:
```ts
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import { BetterI18nBackend } from "@better-i18n/expo";
i18n
.use(BetterI18nBackend)
.use(initReactI18next)
.init({
backend: {
project: "acme/my-app",
defaultLocale: "en",
},
lng: "en",
fallbackLng: "en",
ns: ["common", "auth"],
defaultNS: "common",
interpolation: { escapeValue: false },
});API
initBetterI18n(options)
One-call setup that fetches translations from CDN and initializes i18next.
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| project | string | Required | Project identifier in org/project format |
| i18n | i18n | Required | i18next instance to configure |
| defaultLocale | string | "en" | Fallback locale |
| storage | TranslationStorage | In-memory | Persistent storage via storageAdapter() — see Offline & Caching |
| staticData | Record \| () => Promise | undefined | Bundled translations for offline-first (e.g., first launch in airplane mode) |
| fetchTimeout | number | 10000 | CDN fetch timeout in ms |
| retryCount | number | 1 | Retry attempts on CDN failure |
| useDeviceLocale | boolean | false | Auto-detect device locale via expo-localization |
| debug | boolean | false | Enable debug logging |
| i18nextOptions | Partial<InitOptions> | {} | Additional i18next init options (SDK always sets defaultNS and fallbackNS to "translation") |
Returns Promise<BetterI18nResult>:
| Property | Type | Description |
| --- | --- | --- |
| core | I18nCore | Core instance for accessing manifest, languages, etc. |
| languages | LanguageOption[] | Available languages from the CDN manifest |
BetterI18nBackend
i18next backend plugin class. Pass options via backend in i18n.init():
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| project | string | Required | Project identifier |
| defaultLocale | string | "en" | Fallback locale |
| cdnBaseUrl | string | "https://cdn.better-i18n.com" | Custom CDN URL |
| cacheExpiration | number | 86400000 (24h) | In-memory cache TTL in ms |
| storage | TranslationStorage | In-memory | Persistent storage via storageAdapter() — see Offline & Caching |
| staticData | Record \| () => Promise | undefined | Bundled translations for offline-first |
| fetchTimeout | number | 10000 | CDN fetch timeout in ms |
| retryCount | number | 1 | Retry attempts on CDN failure |
| debug | boolean | false | Enable debug logging |
getDeviceLocale(options?)
Get the device's primary locale using expo-localization.
import { getDeviceLocale } from "@better-i18n/expo";
const locale = getDeviceLocale({ fallback: "en" });
// "tr" on a Turkish device, "en" if detection failsgetDeviceLocales()
Get all device locales as language codes.
import { getDeviceLocales } from "@better-i18n/expo";
const locales = getDeviceLocales();
// ["tr", "en", "de"]createMemoryStorage()
Create an in-memory storage adapter (no persistence). Useful for testing.
import { createMemoryStorage } from "@better-i18n/expo";
const storage = createMemoryStorage();Caching Strategy
The SDK uses a network-first strategy with automatic fallback:
1. Check in-memory cache (TtlCache) → avoids redundant fetches within a session
2. Fetch from CDN (with timeout + retry) → always get fresh translations
3. On CDN failure → try staticData (bundled translations)
4. On staticData miss → fall back to persistent storage (MMKV/AsyncStorage)
5. No cache, no CDN, no static data → throw errorOffline-First with Static Data
For apps that must work on first launch without network (e.g., airplane mode):
import en from "./locales/en.json"; // { auth: {...}, common: {...} }
import tr from "./locales/tr.json";
const { core, languages } = await initBetterI18n({
project: "acme/my-app",
i18n,
// Pass locale messages directly — same format as CDN response
staticData: { en, tr },
// Or lazy-load to keep bundle small:
// staticData: () => import('./locales/fallback.json'),
fetchTimeout: 5000, // fail fast on slow networks
retryCount: 2, // retry twice before falling back
});Persistent Storage
Pass storageAdapter() with your preferred storage library:
import { MMKV } from 'react-native-mmkv';
import { storageAdapter } from '@better-i18n/expo';
storage: storageAdapter(new MMKV({ id: 'app' }))Without storage, in-memory fallback is used (no offline support). See Offline & Caching for details.
Language Switching
When using initBetterI18n, language switching is handled automatically. New translations are fetched from CDN on demand:
import { useTranslation } from "react-i18next";
function LanguagePicker({ languages }) {
const { i18n } = useTranslation();
return (
<FlatList
data={languages}
renderItem={({ item }) => (
<Pressable onPress={() => i18n.changeLanguage(item.code)}>
<Text>{item.nativeName}</Text>
</Pressable>
)}
/>
);
}Custom Storage Adapter
Implement the TranslationStorage interface to use any key-value store:
import type { TranslationStorage } from "@better-i18n/expo";
const myStorage: TranslationStorage = {
getItem: async (key) => { /* ... */ },
setItem: async (key, value) => { /* ... */ },
removeItem: async (key) => { /* ... */ },
};
await initBetterI18n({
project: "acme/my-app",
i18n,
storage: myStorage,
});Part of Better i18n
This package is part of the Better i18n ecosystem:
| Package | Description |
| --- | --- |
| @better-i18n/core | Framework-agnostic core (CDN client, caching, locale utilities) |
| @better-i18n/use-intl | React/TanStack Start integration via use-intl |
| @better-i18n/next | Next.js integration via next-intl |
| @better-i18n/expo | Expo/React Native integration via i18next |
| @better-i18n/cli | CLI for detecting hardcoded strings |
| @better-i18n/mcp | MCP server for AI-powered translation management |
Documentation
Full documentation at docs.better-i18n.com/frameworks/expo
License
MIT © Better i18n
