This guide helps you migrate from Lingui to FormatJS / react-intl. Both libraries use ICU MessageFormat for message syntax, making migration relatively straightforward compared to other i18n libraries.

Concept mapping

LinguiFormatJSNotes
<I18nProvider><IntlProvider>React context provider
<Trans> macro<FormattedMessage>Declarative component
t macrointl.formatMessage()Imperative API
useLingui()useIntl()React hook
@lingui/cli extract@formatjs/cli extractMessage extraction
@lingui/cli compile@formatjs/cli compileMessage compilation
lingui.config.tsBundler plugin configSWC, Babel, Vite
.po / .json catalogs.json message filesFormatJS uses flat JSON
@lingui/macro (compile-time)babel-plugin-formatjs / SWC pluginBuild-time transforms
plural() macroICU {count, plural, ...} syntaxSame ICU standard
select() macroICU {val, select, ...} syntaxSame ICU standard

Syntax comparison

Since both libraries use ICU MessageFormat, the runtime message syntax is nearly identical. The main difference is in how you author messages in source code.

Simple messages

// Lingui — uses macros that are compiled away
import {Trans, t} from '@lingui/macro'

;<Trans>Hello, world!</Trans>
const msg = t`Hello, world!`

// FormatJS — uses components and imperative API
import {FormattedMessage, useIntl} from 'react-intl'

;<FormattedMessage defaultMessage="Hello, world!" />
const intl = useIntl()
const msg = intl.formatMessage({defaultMessage: 'Hello, world!'})

Interpolation

// Lingui
<Trans>Hello, {name}!</Trans>

// FormatJS
<FormattedMessage defaultMessage="Hello, {name}!" values={{name}} />

Plurals

// Lingui — plural() macro
import {plural} from '@lingui/macro'
const msg = plural(count, {
  one: '# item',
  other: '# items',
})

// FormatJS — ICU syntax in message string
const msg = intl.formatMessage(
  {defaultMessage: '{count, plural, one {# item} other {# items}}'},
  {count}
)

Rich text

// Lingui
<Trans>
  Read the <a href="/docs">documentation</a>
</Trans>

// FormatJS
<FormattedMessage
  defaultMessage="Read the <link>documentation</link>"
  values={{
    link: chunks => <a href="/docs">{chunks}</a>,
  }}
/>

Lingui macros allow JSX elements directly in <Trans>. FormatJS uses XML-like tags in the message string with render functions in values.

CLI comparison

Extraction

# Lingui
lingui extract

# FormatJS
formatjs extract 'src/**/*.{ts,tsx}' --out-file messages/en.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'

Compilation

# Lingui — compiles .po to optimized JS catalogs
lingui compile

# FormatJS — compiles JSON to AST-optimized JSON
formatjs compile messages/en.json --ast --out-file compiled/en.json

FormatJS uses JSON throughout. Lingui supports PO (gettext), PO + JSON, and its own JSON format.

Step-by-step migration

1. Install FormatJS

npm i react-intl
npm i -D @formatjs/cli

2. Convert message catalogs

If you use PO files, convert them to FormatJS JSON:

# Export from Lingui to JSON first (if using PO)
lingui extract --format json

# Then reshape the JSON to FormatJS flat format:
# Lingui: { "messageId": { "message": "Hello", "origin": [...] } }
# FormatJS: { "messageId": "Hello" }

If you already use Lingui's JSON format, the conversion is straightforward — extract just the message field from each entry.

3. Replace the provider

- import {I18nProvider} from '@lingui/react'
- import {i18n} from '@lingui/core'
+ import {IntlProvider} from 'react-intl'
+ import messages from './messages/en.json'

  function App() {
    return (
-     <I18nProvider i18n={i18n}>
+     <IntlProvider locale="en" messages={messages}>
        <MyApp />
-     </I18nProvider>
+     </IntlProvider>
    )
  }

4. Replace macros with components/hooks

- import {Trans, t} from '@lingui/macro'
- import {useLingui} from '@lingui/react'
+ import {FormattedMessage, useIntl} from 'react-intl'

  function MyComponent() {
-   const {i18n} = useLingui()
+   const intl = useIntl()

    return (
      <div>
-       <Trans>Welcome back</Trans>
+       <FormattedMessage defaultMessage="Welcome back" />
-       <p>{t`You have ${count} notifications`}</p>
+       <p>
+         {intl.formatMessage(
+           {defaultMessage: 'You have {count} notifications'},
+           {count}
+         )}
+       </p>
      </div>
    )
  }

5. Set up a bundler plugin

Replace the Lingui Babel/SWC plugin with the FormatJS equivalent:

These handle automatic ID generation and AST pre-compilation (with ast: true).

6. Update extraction scripts

  // package.json scripts
  {
-   "extract": "lingui extract",
-   "compile": "lingui compile"
+   "extract": "formatjs extract 'src/**/*.{ts,tsx}' --out-file messages/en.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'",
+   "compile": "formatjs compile messages/en.json --ast --out-file compiled/en.json"
  }

7. Incremental migration

Both libraries can coexist during migration:

  1. Keep @lingui/react and react-intl providers in your component tree simultaneously.
  2. Migrate components one at a time — replace <Trans> with <FormattedMessage> and t macro calls with intl.formatMessage().
  3. Once all components are migrated, remove @lingui/core, @lingui/react, @lingui/macro, and lingui.config.ts.