Lingui
Set up Lingui for i18n in an Expo app with ReadyNative: @lingui/core + @lingui/react runtime API, 6 languages bundled incl. Arabic (RTL) · Expo Go OK
Expo Go: yes
Runs in Expo Go; no dev build needed for this module.
Lingui's runtime API (@lingui/core setupI18n plus @lingui/react) replaces the core i18n shim in src/lib/i18n.ts, with the same surface as everywhere else: i18n.t(key, vars) and useT(). I'd pick it when you want ICU message syntax and a catalog CLI (bun run i18n:extract); pick i18next for the bigger plugin ecosystem, and none while you're still English-only. Screens don't change shape: they keep the shim's {{name}} placeholders, and toIcu() converts them to {name} at the t boundary.
Setup
bun run setup --i18n linguiSix languages ship - English, Spanish, Russian, Chinese (Simplified), Portuguese and Arabic (src/locales/{en,es,ru,zh,pt,ar}.json, Lingui "minimal" catalogs: id → string, ICU {var} placeholders); the device language wins, and Settings has a language picker that renders from LANGUAGES. To add a locale:
- Create
src/locales/<code>.jsonnext to the shipped ones (a test checks every locale has the same keys asen.json). - Register it: add the code to
LANGUAGES(andRTL_LANGUAGESfor a right-to-left script), a native name toLANGUAGE_LABELSandmessagesinsrc/lib/i18n.ts, and tolocalesinlingui.config.ts.LANGUAGESis the single source of truth - the Settings picker (and/examples/i18n, with--with-examples) render from it. - Normalise catalogs whenever you like:
bun run i18n:extract.
bun run gen screen --tab appends new tab titles to every locale file (English value, translate later). Locale files are yours: setup/clean never remove them.
Setup also adds expo-localization and its config plugin, persists the language override under readynative:language through your storage adapter, registers I18nProvider (order 60), and adds a test (plus the /examples/i18n route with --with-examples). The instance is exported as lingui if you want Trans or useLingui().
Usage
import { useT } from "@/lib/i18n";
import { Text } from "@/components/ui";
const t = useT();
<Text>{t("Version {{version}}", { version })}</Text>;Add a key by writing the English sentence in code and adding it to every src/locales/*.json with ICU {var} placeholders. A missing id renders the key (English) with interpolation.
setLanguage(code) switches at runtime, useLanguage() reads the current one, deviceLanguage() reports the OS language. Both hooks subscribe to the instance directly, so they work outside I18nProvider too - tests render screens bare.
Right-to-left
Arabic is in RTL_LANGUAGES. setLanguage("ar") calls I18nManager.forceRTL(true) and restarts the app with reloadAppAsync(), because React Native applies a new layout direction only on launch; switching back to a left-to-right language does the reverse. Rows, stacks, tab bars and back buttons mirror on their own. On web it flips <html dir> in place, with no restart. isRTL(language) tells you the direction of any language.
- Expo Go resets the direction whenever it opens a project, so the text turns Arabic but the layout stays left-to-right there - check RTL in a dev build.
- The
expo-localizationconfig plugin keepssupportsRTLon (the default). Set["expo-localization", { supportsRTL: false }]in your app config if you never ship an RTL language. - Write layouts with
start/endin mind:marginLeft/paddingRightmirror automatically, absoluteleft/rightpositions and direction-bearing icons (arrows, chevrons) need a look.
Gotchas
- Expo Go works - it's pure JS plus
expo-localization. - No macros.
babel.config.jsbelongs to the UI module, so@lingui/babel-plugin-lingui-macroisn't installed. Catalogs are raw JSON compiled at runtime throughsetMessagesCompiler(compileMessage). Usei18n.t/useT()/<Trans id="…" />; thetandTransmacros will not work. bun run i18n:extractrunsnpx lingui extract, but it only sees literali18n.t("…")/i18n._("…")calls - nott()fromuseT()- and writes ids verbatim ({{var}}). Treat it as a catalog normaliser; it never deletes ids.__tests__andshimsare excluded.- Swapping stacks:
bun run setup --i18n nonerestores the shim re-export and removes the locales,lingui.config.ts, the example, thei18n:extractscript and the five packages. Screens keep working -t(key)returns the English key.
Remove it
While modules/ exists (a tree set up with --keep-modules), setup does all of it:
bun run setup --i18n none --yes --keep-modulesIn a finalized tree setup is a stub, so you undo it by hand. Here is everything this module added:
- Delete the files that are still there (the demo screens are gone already unless you set up with
--with-examples):lingui.config.ts,src/app/examples/i18n.tsx,src/lib/__tests__/i18n.test.tsx,src/locales/ar.json,src/locales/en.json,src/locales/es.json,src/locales/pt.json,src/locales/ru.json,src/locales/zh.json,src/screens/examples/i18n-example-screen.tsx. - Replace, don't delete
src/lib/i18n.ts: core code imports it, so swap in the no-op version frommodules/i18n/none/files/of a fresh clone of your tier repo - same exports, nothing behind them. - Uninstall the dependencies:
bun remove @lingui/core @lingui/message-utils @lingui/react expo-localization @lingui/cli @lingui/format-json. Keep any ofexpo-localization(also used byanalytics/posthog,consent/consent) that another module you picked still needs. - Drop the config plugin
expo-localizationfrom.readynative.json→modules.app.expo.plugins(that is whereapp.config.tsreads it from), then rebuild the dev build. - Unwrap the provider: delete
<I18nProvider>and its import fromsrc/providers.tsx. - Delete the script
i18n:extractfrompackage.json. - Check it:
bun run typecheckandbun run lintpoint at anything that still imports the removed files;bun run gen:graphrefreshesdocs/ARCHITECTURE.md.
Reference
Everything below is generated from modules/i18n/lingui/module.json - the same file bun run setup reads, so it is what actually lands in your repo.
Install
bun run setup --i18n linguiModule id: i18n/lingui.
Dependencies
| Package | Version | Kind |
|---|---|---|
@lingui/core | ^5.9.5 | dependency |
@lingui/message-utils | ^5.9.5 | dependency |
@lingui/react | ^5.9.5 | dependency |
expo-localization | ~57.0.2 | dependency (expo install) |
@lingui/cli | ^5.9.5 | devDependency |
@lingui/format-json | ^5.9.5 | devDependency |
Config plugins
Merged into app.config.ts through .readynative.json (modules.app):
expo-localization
Providers
Rendered in src/providers.tsx (lower order = outermost):
| Order | Provider | From |
|---|---|---|
| 60 | I18nProvider | @/lib/i18n |
Compatibility
- Requires
storage=kv-store,mmkv,async-storage
After setup
- Lingui runs its runtime API only (no babel macro: babel.config.js belongs to the UI stack). Edit src/locales/*.json by hand;
bun run i18n:extractcannot see t() calls.
Files
11 files copied to the project root
lingui.config.tssrc/app/examples/i18n.tsxsrc/lib/__tests__/i18n.test.tsxsrc/lib/i18n.tssrc/locales/ar.jsonsrc/locales/en.jsonsrc/locales/es.jsonsrc/locales/pt.jsonsrc/locales/ru.jsonsrc/locales/zh.jsonsrc/screens/examples/i18n-example-screen.tsx