@lingui/macro, react-i18next, and react-intl are the leading solutions for adding multi-language support to React applications. @lingui/macro focuses on developer experience by allowing translations to live directly in the code using Babel macros. react-i18next provides flexible React bindings for the i18next ecosystem, supporting various backends and middleware. react-intl is the React integration for FormatJS, offering robust ICU message formatting and strong separation of code and content. Each tool solves the same core problem but takes a different approach to syntax, workflow, and runtime behavior.
Adding multi-language support to a React app is more than just swapping text. You need to handle plurals, dates, currencies, and dynamic content without slowing down your site. @lingui/macro, react-i18next, and react-intl are the top choices for this task. They all get the job done, but they work differently under the hood. Let's compare how they tackle common engineering challenges.
How you write translations in your code affects daily developer experience. Some teams prefer functions, others prefer components, and some want macros.
@lingui/macro uses Babel macros to let you write translations as template literals.
t and wrap strings directly.// @lingui/macro: Direct template literal
import { t } from '@lingui/macro';
function Welcome({ name }) {
return <h1>{t`Hello ${name}`}</h1>;
}
react-i18next relies on a hook or a higher-order component to access the translation function.
t('key') with a string identifier.// react-i18next: Hook-based access
import { useTranslation } from 'react-i18next';
function Welcome({ name }) {
const { t } = useTranslation();
return <h1>{t('welcome.message', { name })}</h1>;
}
react-intl uses React components to wrap translated content.
<FormattedMessage> with an ID and values.// react-intl: Component-based declaration
import { FormattedMessage } from 'react-intl';
function Welcome({ name }) {
return (
<h1>
<FormattedMessage id="welcome.message" values={{ name }} />
</h1>
);
}
Where do the actual translation strings live? This impacts how translators work and how you update content.
@lingui/macro extracts messages from your source code automatically.
# Lingui: Extract messages from code
lingui extract
react-i18next typically uses manual JSON files or a backend service.
en.json, fr.json, etc.// react-i18next: Manual JSON structure
{
"welcome": {
"message": "Hello {{name}}"
}
}
react-intl supports both manual JSON and extraction via CLI tools.
// react-intl: Extract using FormatJS CLI
formatjs extract 'src/**/*.ts' --out-file dist/en.json
Handling "1 item" vs "2 items" or currency formatting varies by library.
@lingui/macro supports ICU plurals directly in the macro syntax.
// @lingui/macro: ICU plural support
import { Plural } from '@lingui/macro';
function Cart({ count }) {
return <p><Plural value={count} one="# item" other="# items" /></p>;
}
react-i18next uses a custom interpolation syntax for plurals.
key_one, key_other).// react-i18next: Plural keys in JSON
// JSON: { "items_one": "# item", "items_other": "# items" }
function Cart({ count }) {
const { t } = useTranslation();
return <p>{t('items', { count })}</p>;
}
react-intl is built on the ICU MessageFormat standard.
// react-intl: ICU plural in message definition
// Message: "{count, plural, one {# item} other {# items}}"
function Cart({ count }) {
return (
<FormattedMessage
id="cart.items"
values={{ count }}
/>
);
}
Loading all translations at once can slow down initial page load. All three support splitting.
@lingui/macro compiles to lightweight functions that support dynamic imports.
// @lingui/macro: Dynamic catalog loading
async function loadLanguage(lang) {
const { messages } = await import(`./locales/${lang}.js`);
i18n.loadAndActivate({ lang, messages });
}
react-i18next has built-in support for lazy loading namespaces.
useTranslation with namespace arguments.// react-i18next: Lazy loading namespaces
import { useTranslation } from 'react-i18next';
function Dashboard() {
const { t } = useTranslation(['dashboard', 'common']);
return <div>{t('dashboard:title')}</div>;
}
react-intl requires manual setup for lazy loading providers.
IntlProvider wrapper per route.// react-intl: Manual provider setup
async function RouteComponent({ lang }) {
const messages = await import(`./locales/${lang}.json`);
return (
<IntlProvider locale={lang} messages={messages}>
<App />
</IntlProvider>
);
}
Despite different approaches, these libraries solve the same core problems with similar tools. Here are key overlaps:
// Shared pattern: Context usage
// All libraries wrap the app in a Provider at the root
<Provider i18n={i18n}><App /></Provider>
// Shared pattern: Changing language
i18n.changeLanguage('fr'); // Works in all three ecosystems
// Shared pattern: Typed translation functions
const { t } = useTranslation(); // Infers types in all three
// Shared pattern: Backend loading
// All can fetch translations from an API endpoint
backend: { loadPath: '/locales/{{lng}}.json' }
| Feature | Shared by All Three |
|---|---|
| Core Tech | βοΈ React Hooks & Context |
| Switching | π Dynamic language change |
| Types | π οΈ TypeScript definitions |
| Loading | β‘ Lazy loading support |
| Formatting | π Date & Number formatting |
| Feature | @lingui/macro | react-i18next | react-intl |
|---|---|---|---|
| Syntax | βοΈ Template macros (t\...``) | πͺ Hooks (t('key')) | π§© Components (<FormattedMessage>) |
| Workflow | π Auto-extraction from code | π Manual JSON or Backend | π CLI Extraction or Manual |
| Plurals | π °οΈ ICU Standard | π’ Custom Key Suffixes | π °οΈ ICU Standard |
| Setup | π οΈ Requires Babel Plugin | βοΈ Flexible Configuration | ποΈ Provider Wrapper Required |
| Focus | π§βπ» Developer Experience | π Ecosystem Flexibility | π’ Enterprise Standards |
@lingui/macro is like a developer-first toolkit π§° β great for teams that want translations close to the code and automated workflows. Ideal for modern React apps where developers manage content directly.
react-i18next is like a swiss army knife πͺ β perfect for teams who need flexibility, backend integration, and a vast plugin ecosystem. Shines in complex apps with dynamic content needs.
react-intl is like a precision instrument π β best for enterprise projects requiring strict ICU compliance and clear separation between code and content. The go-to for large-scale, regulated applications.
Final Thought: All three libraries are mature and capable. The best choice depends on whether you prioritize developer workflow (@lingui/macro), ecosystem flexibility (react-i18next), or standard compliance (react-intl).
Choose @lingui/macro if your team prefers keeping translation strings close to the component logic and wants automatic extraction via CLI. It is ideal for projects that value a clean codebase without scattered JSON files and want strong TypeScript support out of the box. This approach works well when developers want to see context directly in the source code rather than managing external keys.
Choose react-i18next if you need a highly flexible ecosystem with support for various storage backends, language detection, and middleware. It is suitable for applications that require dynamic language switching, backend-driven translations, or integration with existing i18next infrastructure. This library shines when you need granular control over how and where translation files are loaded.
Choose react-intl if your project requires strict adherence to the ICU message format for complex plurals, genders, and select statements. It is best for enterprise applications where separation of concerns between developers and translators is critical. This package is the right choice when you need robust formatting for dates, numbers, and currencies across many locales.
Babel Macros which transforms tagged template literals and JSX components to ICU MessageFormat.
@lingui/macro is part of LinguiJS. See the documentation for all information, tutorials and examples.
See the reference documentation.