12 Nov 2026 · 5 min read

Expo i18n: i18next or Lingui, compared

Expo i18n with i18next or Lingui: plurals, ICU vs JSON catalogs, expo-localization for the device locale, Expo Go support, and when to pick each.

An Expo app has two i18n jobs: find the device language, and show your strings in that language. expo-localization handles the first. ReadyNative's i18n category handles the second with one of three options: i18next, Lingui or none. For most apps, pick i18next. Pick Lingui when you want ICU message syntax and a catalog CLI. Pick none while you are still English-only.

The choice matters less than it looks. Screens call the same functions under all three, and both libraries run in Expo Go, so the translation layer never needs a development build.

Where the device language goesThe device language from expo-localization and a saved choice under readynative:language feed src/lib/i18n.ts, which calls either i18next or Lingui behind the same t function.Device languageexpo-localization · getLocales()Saved choicereadynative:languagesrc/lib/i18n.tsi18n.t(key, vars) · useT() · setLanguage()i18nextJSON keys · count pluralsdefaultLinguiICU MessageFormatcompiled at runtimeBoth libraries run in Expo Go, which includes expo-localization.

The comparison in one table

i18nextLingui
Message syntaxJSON keys and {{name}}; ICU only through the i18next-icu pluginICU MessageFormat in the catalog; screens still write {{name}}
Pluralscount option with _one and _other keysplural and select in ICU, with CLDR categories
Catalogsrc/locales/<code>.json, keyed by the English sentencesrc/locales/<code>.json, flat id to string
Catalog CLINone in the module; i18next-cli exists upstreambun run i18n:extract, which runs lingui extract
Runs in Expo GoYesYes
MacrosNoneNone in ReadyNative; no Babel macro
EcosystemThe bigger plugin set, per the module docsRuntime API, React bindings and the CLI
DefaultYes, recommended in the modulePick it at setup

i18next: the default, with the bigger ecosystem

i18next runs through react-i18next. It keeps the surface your screens already use: i18n.t(key, vars) and useT(). The keys are the English sentences, so a screen reads the same with or without the module:

import { useT } from "@/lib/i18n";
import { Text } from "@/components/ui";

const t = useT();

<Text>{t("Version {{version}}", { version })}</Text>;

Add a string by writing the English sentence in code, then add it to every src/locales/*.json. A missing key falls back to English, and then to the key itself. Plurals use the count option with _one and _other keys in the catalog, as i18next documents.

The module turns off keySeparator and nsSeparator. i18next's defaults are a dot and a colon, so turning them off keeps sentences that contain either one as valid keys.

Lingui: ICU messages and a catalog CLI

Lingui's runtime API, @lingui/core with @lingui/react, fills the same slot in src/lib/i18n.ts. Its catalogs use ICU MessageFormat, the syntax Lingui documents for plurals, select and ordinals, and the plural categories follow CLDR. Screens still write {{name}}; the module converts it to {name} at the t boundary, so the catalog entry reads:

{
  "Attachment {name} saved": "Adjunto {name} guardado"
}

A plural in Lingui's syntax looks like this:

{count, plural, one {# message} other {# messages}}

Two details shape the work. The module ships no Babel macro, so the t and Trans macros do not work. You write i18n.t, useT() or the Trans component with an id, and edit the catalogs by hand. The bun run i18n:extract script runs lingui extract. It normalises catalogs and never deletes ids, but it only sees literal i18n.t and i18n._ calls, not the t() from useT().

Runtime compilation has a cost. Lingui's docs say the message compiler is usually left out of production builds to keep the bundle small. The module compiles catalogs at runtime through setMessagesCompiler(compileMessage), so expect that code in your app.

expo-localization: the device locale

Both options read the device language with expo-localization, which Expo Go includes. getLocales() returns the user's locales in the order they set them, and the list always has at least one entry:

import { getLocales } from "expo-localization";

const deviceLanguage = getLocales()[0].languageCode ?? "en";

languageCode can be null, so keep a fallback. languageTag keeps the region, as in en-US or es-419, and is never null.

The platforms differ once the app is running. On iOS the values stay the same while the app runs, and a language change resets the app. On Android the user can change the language without a restart, so Expo's guide says to call getLocales() again whenever AppState reports the app back in the foreground. If a component needs the current value, the useLocales() hook rerenders when the OS settings change.

Expo's guide also says there is still no way to read the temperature unit from user preferences. On Android it suggests a lookup table by locale, and it notes that iOS users can change the unit themselves.

Plurals depend on Intl: test on a device

Plural forms come from Intl.PluralRules, and the docs do not agree on whether Hermes has it. Expo's localization guide says that with Hermes you can use the Intl API on all platforms. The i18next plurals page says the Hermes engine still does not implement Intl.PluralRules, and asks for a polyfill. Lingui's React Native tutorial, written as of 08/2024, recommends polyfilling Intl.PluralRules and Intl.Locale.

Check each plural on a device in every language you ship, not only in English. Until the docs agree, the device is the only proof.

When each one wins

How ReadyNative wires it

The option is chosen at setup. Either library adds expo-localization and its config plugin, writes src/lib/i18n.ts, registers I18nProvider at order 60, and stores the user's language under readynative:language through your storage adapter. Pick Lingui with:

bun run setup --i18n lingui

Pass i18next for the default, or none for the shim. With --with-examples, setup adds the /examples/i18n route. To change the language in code, call setLanguage(code). useLanguage() reads the current one, and deviceLanguage() reports what the OS says.

The option pages are on the stack pages: i18next and Lingui.

Read next: the i18n module docs, and Expo's localization guide.

FAQ

Should I use i18next or Lingui in Expo?

For most apps, i18next. It has the bigger plugin ecosystem, and both libraries run in Expo Go. Pick Lingui when you want ICU MessageFormat messages and the lingui extract CLI, and you can edit the catalogs by hand.

How do I get the device language in Expo?

Install expo-localization with npx expo install expo-localization, then read getLocales()[0].languageCode. The value can be null, so fall back to a default such as 'en'. On Android, call getLocales() again when the app returns to the foreground.

Do plurals work in React Native i18n?

Plurals depend on Intl.PluralRules. Expo says the Intl API works on all platforms with Hermes, but i18next's docs say Hermes still lacks Intl.PluralRules and needs a polyfill. Check each plural on a device in every language you ship.

How do I change the app language at runtime?

Call setLanguage(code), and read the current language with useLanguage(). For Arabic, setLanguage('ar') also flips the layout to right-to-left and restarts the app.

More guides