ReadyNative

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 lingui

Six 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:

  1. Create src/locales/<code>.json next to the shipped ones (a test checks every locale has the same keys as en.json).
  2. Register it: add the code to LANGUAGES (and RTL_LANGUAGES for a right-to-left script), a native name to LANGUAGE_LABELS and messages in src/lib/i18n.ts, and to locales in lingui.config.ts. LANGUAGES is the single source of truth - the Settings picker (and /examples/i18n, with --with-examples) render from it.
  3. 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-localization config plugin keeps supportsRTL on (the default). Set ["expo-localization", { supportsRTL: false }] in your app config if you never ship an RTL language.
  • Write layouts with start/end in mind: marginLeft/paddingRight mirror automatically, absolute left/right positions and direction-bearing icons (arrows, chevrons) need a look.

Gotchas

  • Expo Go works - it's pure JS plus expo-localization.
  • No macros. babel.config.js belongs to the UI module, so @lingui/babel-plugin-lingui-macro isn't installed. Catalogs are raw JSON compiled at runtime through setMessagesCompiler(compileMessage). Use i18n.t / useT() / <Trans id="…" />; the t and Trans macros will not work.
  • bun run i18n:extract runs npx lingui extract, but it only sees literal i18n.t("…") / i18n._("…") calls - not t() from useT() - and writes ids verbatim ({{var}}). Treat it as a catalog normaliser; it never deletes ids. __tests__ and shims are excluded.
  • Swapping stacks: bun run setup --i18n none restores the shim re-export and removes the locales, lingui.config.ts, the example, the i18n:extract script 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-modules

In a finalized tree setup is a stub, so you undo it by hand. Here is everything this module added:

  1. 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.
  2. Replace, don't delete src/lib/i18n.ts: core code imports it, so swap in the no-op version from modules/i18n/none/files/ of a fresh clone of your tier repo - same exports, nothing behind them.
  3. Uninstall the dependencies: bun remove @lingui/core @lingui/message-utils @lingui/react expo-localization @lingui/cli @lingui/format-json. Keep any of expo-localization (also used by analytics/posthog, consent/consent) that another module you picked still needs.
  4. Drop the config plugin expo-localization from .readynative.json → modules.app.expo.plugins (that is where app.config.ts reads it from), then rebuild the dev build.
  5. Unwrap the provider: delete <I18nProvider> and its import from src/providers.tsx.
  6. Delete the script i18n:extract from package.json.
  7. Check it: bun run typecheck and bun run lint point at anything that still imports the removed files; bun run gen:graph refreshes docs/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 lingui

Module id: i18n/lingui.

Dependencies

PackageVersionKind
@lingui/core^5.9.5dependency
@lingui/message-utils^5.9.5dependency
@lingui/react^5.9.5dependency
expo-localization~57.0.2dependency (expo install)
@lingui/cli^5.9.5devDependency
@lingui/format-json^5.9.5devDependency

Config plugins

Merged into app.config.ts through .readynative.json (modules.app):

  • expo-localization

Providers

Rendered in src/providers.tsx (lower order = outermost):

OrderProviderFrom
60I18nProvider@/lib/i18n

Compatibility

After setup

  1. 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:extract cannot see t() calls.

Files

11 files copied to the project root
  • lingui.config.ts
  • src/app/examples/i18n.tsx
  • src/lib/__tests__/i18n.test.tsx
  • src/lib/i18n.ts
  • 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

On this page

Get ReadyNative