Skip to main content

Common i18n patterns in React

This page describes the most common i18n patterns in React. It's a follow-up to the tutorial with practical examples. See the API reference for detailed information about all components.

Macros

Using jsx macros is the most straightforward way to translate your React components.

Trans handles translations of messages including variables and other React components:

import { Trans } from "@lingui/macro";

function render() {
return (
<>
<h1>
<Trans>LinguiJS example</Trans>
</h1>
<p>
<Trans>
Hello <a href="/profile">{name}</a>.
</Trans>
</p>
</>
);
}

You don't need anything special to use Trans inside your app (except of wrapping the root component in I18nProvider).

Element attributes and string-only translations

Sometimes you can't use Trans component, for example when translating element attributes:

<img src="..." alt="Image caption" />

In such case you need to use the useLingui() hook with the msg macro.

import { msg } from "@lingui/macro";
import { useLingui } from "@lingui/react";

export default function ImageWithCaption() {
const { _ } = useLingui();

return <img src="..." alt={_(msg`Image caption`)} />;
}

Translations outside React components

Sometimes, you may need to access translations outside React components, which is another common pattern. You can use t macro outside React context as usual:

import { t } from "@lingui/macro";

export function showAlert() {
alert(t`...`);
}
caution

When you use t macro (and plural, select, selectOrdinal), it uses a global i18n instance. While this generally works, there are situations, like in server-side rendering (SSR) applications, where it may not be the best fit.

For better control and flexibility, it's a good idea to avoid the global i18n instance and instead use a specific instance tailored to your needs.

import { msg } from "@lingui/macro";
import { I18n } from "@lingui/core";

export function showAlert(i18n: I18n) {
alert(t(i18n)`...`);
}

function MyComponent() {
// get i18n instance from React Context
const { i18n } = useLingui();

// pass instance outside
showAlert(i18n);
}
note

All js macros such as t plural, select, selectOrdinal cannot be used on the module level.

import { t } from "@lingui/macro";

// ❌ Bad! This won't work because the `t` macro is used at the module level.
// The `t` macro returns a string, and once this string is assigned, it won't react to locale changes.
const colors = [t`Red`, t`Orange`, t`Yellow`, t`Green`];

// ✅ Good! Every time the function is executed, the `t` macro will be re-executed as well,
// and the correctly translated color labels will be returned.
function getColors() {
return [t`Red`, t`Orange`, t`Yellow`, t`Green`];
}

There is an ESLint Rule designed to check for this misuse.

A better option would be to use the Lazy Translations pattern described in the following paragraph.

Lazy Translations

You don't need to declare messages at the same code location where they are displayed. Tag a string with the msg macro, and you've created a "message descriptor", which can then be passed around as a variable, and can be displayed as a translated string by passing its id to Trans as its id prop:

import { msg } from "@lingui/macro";
import { Trans } from "@lingui/react";

const favoriteColors = [msg`Red`, msg`Orange`, msg`Yellow`, msg`Green`];

export default function ColorList() {
return (
<ul>
{favoriteColors.map((color) => (
<li>
<Trans id={color.id} />
</li>
))}
</ul>
);
}
note

Note that we import <Trans> component from @lingui/react, because we want to use the runtime Trans component here, not the (compile-time) macro.

To render the message descriptor as a string-only translation, pass it to the i18n._() method:

import { i18n } from "@lingui/core";
import { msg } from "@lingui/macro";

const favoriteColors = [msg`Red`, msg`Orange`, msg`Yellow`, msg`Green`];

export function getTranslatedColorNames() {
return favoriteColors.map((color) => i18n._(color));
}

Passing messages as props

It's often convenient to pass messages around as component props, for example as a "label" prop on a button. The easiest way to do this is to pass a Trans element as the prop:

import { Trans } from "@lingui/macro";

export default function FancyButton(props) {
return <button>{props.label}</button>;
}

export function LoginLogoutButtons(props) {
return (
<div>
<FancyButton label={<Trans>Log in</Trans>} />
<FancyButton label={<Trans>Log out</Trans>} />
</div>
);
}

If you need the prop to be displayed as a string-only translation, you can pass a message tagged with the msg macro:

import { msg } from "@lingui/macro";
import { useLingui } from "@lingui/react";

export default function ImageWithCaption(props) {
return <img src="..." alt={props.caption} />;
}

export function HappySad(props) {
const { _ } = useLingui();

return (
<div>
<ImageWithCaption caption={_(msg`I'm so happy!`)} />
<ImageWithCaption caption={_(msg`I'm so sad.`)} />
</div>
);
}

Picking a message based on a variable

Sometimes you need to pick between different messages to display, depending on the value of a variable. For example, imagine you have a numeric "status" code that comes from an API, and you need to display a message representing the current status.

A simple way to do this is to create an object that maps the possible values of "status" to message descriptors (tagged with the msg macro), and render them as needed with deferred translation:

import { msg } from "@lingui/macro";
import { useLingui } from "@lingui/react";

const statusMessages = {
["STATUS_OPEN"]: msg`Open`,
["STATUS_CLOSED"]: msg`Closed`,
["STATUS_CANCELLED"]: msg`Cancelled`,
["STATUS_COMPLETED"]: msg`Completed`,
};

export default function StatusDisplay({ statusCode }) {
const { _ } = useLingui();
return <div>{_(statusMessages[statusCode])}</div>;
}

Memoization pitfall

In the following contrived example, we document how a welcome message will or will not be updated when locale changes.

The documented behavior may not be intuitive at first, but it is expected, because of how useMemo dependencies work.

To avoid bugs with stale translations, use the _ function returned from useLingui: it is safe to use with memoization because its reference changes whenever the Lingui context updates. We are open to accepting solutions to make working with the Lingui context easier.

Keep in mind that useMemo is primarily a performance optimization tool in React. Because of this, there might be no need to memoize your translations. Additionally, this issue is not present when using the Trans component which we recommend to use when possible.

import { msg } from "@lingui/macro";
import { i18n } from "@lingui/core";

const welcomeMessage = msg`Welcome!`;

// ❌ Bad! This code won't work
export function Welcome() {
const buggyWelcome = useMemo(() => {
return i18n._(welcomeMessage);
}, []);

return <div>{buggyWelcome}</div>;
}

// ❌ Bad! This code won't work either because the reference to i18n does not change
export function Welcome() {
const { i18n } = useLingui();

const buggyWelcome = useMemo(() => {
return i18n._(welcomeMessage);
}, [i18n]);

return <div>{buggyWelcome}</div>;
}

// ✅ Good! `useMemo` has i18n context in the dependency
export function Welcome() {
const linguiCtx = useLingui();

const welcome = useMemo(() => {
return linguiCtx.i18n._(welcomeMessage);
}, [linguiCtx]);

return <div>{welcome}</div>;
}

// 🤩 Better! `useMemo` consumes the `_` function from the Lingui context
export function Welcome() {
const { _ } = useLingui();

const welcome = useMemo(() => {
return _(welcomeMessage);
}, [_]);

return <div>{welcome}</div>;
}

Server components

Lingui can be easily be used with server components. There are however some considerations to be made so we can make optimal use of server components and it's capabilities.

  1. We want to avoid shipping to much js to the browser if it is not needed
  2. We want to be able to use both the Trans and TransNoContext inside our components. The difference been that Trans will be shipped as part of the js bundle and TransNoContext not.

Setup in Next.js

The example below shows how you can setup Lingui to work with sever components in Next.js.

In Next.js 13+ it is a common pattern to add a [locale] dir at the root of your app to facilitate i18n. This parameter will correspond the the language of the page visited by the user. e.g. english en and french fr. You can use the Next.js middleware to redirect to the default language if the root / is visited.

In the layout file of [locale] you can setup the I18nProvider component.

// src/app/[locale]/layout.jsx
import I18nProvider from '@/components/I18nProvider';
import { loadMessages, setI18n } from '@/utils/locales';

export async function generateStaticParams() {
return [{ locale: 'en' }, { locale: 'fr' }];
}

export default function RootLayout({ params, children }) {
const { locale } = params;
const messages = loadMessages(locale);
setI18n(locale);
return (
<html lang={locale}>
<body>
<I18nProvider locale={locale} messages={messages}>
{children}
</I18nProvider>
</body>
</html>
);
}

As you can see in the example above we slight modified the default I18nProvider with our own implementation. Here we pass the active message catalog as a prop. This is to avoid importing all catalogs inside the I18nProvider component itself and thus making them part of the client bundle. For the people who are wondering: Why we are not just passing an instance of i18n the original provider component? Server components can pass data to client components but the caveat is that this needs to be serializable data and i18n is not.

// src/components/I18nProvider.js
'use client'

import { setupI18n } from '@lingui/core';
import { I18nProvider as LinguiProvider } from '@lingui/react';

export default function I18nProvider({ locale, messages, ...props }) {
return (
<LinguiProvider
i18n={setupI18n({
locale,
messages: { [locale]: messages },
})}
{...props}
/>
);
}

If we take a closer look at loadMessages and setI18n function in the layout file. The loadMessages function will just return the correct catalog based on the locale. The function setI18n is more exotic. This uses a new react feature called cache. The React cache function allows you to memoize the return value of a function, allowing you to call the same function multiple times while only executing it once. This is useful in that we can set an instance of i18n for later usage in our nested components.

note

The file locales.js below imports all catalogs so it is important that you don't import this file into a client component otherwise als catalogs will be part of the js bundle.

// src/utils/locales.js
import { cache } from 'react';
import { setupI18n } from '@lingui/core';
import { messages as en } from '@/locales/en.js';
import { messages as fr } from '@/locales/fr.js';

export function loadMessages(locale) {
if (locale === 'fr') {
return fr;
}
return en;
}

export function setI18n(locale) {
const messages = loadMessages(locale);
getLinguiContext().current = setupI18n({
locale,
messages: { [locale]: messages },
});
return getLinguiContext().current;
}

export function getI18n() {
return getLinguiContext().current;
}

const getLinguiContext = cache(() => ({
current: setupI18n({
locale: 'en',
messages: { en },
}),
}))

Usage

Below you can see how we can use the Trans component and the getI18n and setI18n helpers inside pages and components.

note

In Next.js pages are rendered before the wrapping layout. This means that we need to call setI18n both in our wrapping layout and in each page so nested components can make use of the helper function. Read more

// src/[locale]/page.js
import { Trans } from '@lingui/react'
import { TransNoContext } from '@lingui/react/server'
import { getI18n } from '@/utils/locales'
import Header from '@/components/Header'

export default function Home({ params }) {
const { locale } = params;
const i18n = setI18n(locale)
return (
<main>
<Header>
<div>
<Trans id="Hello" /> {/* ⚠️ Trans is rendered on the server but will be part of the js bundle */}
<TransNoContext id="World" lingui={{ i18n }} /> {/* ✅ Will not be part of the js bundle */}
</div>
</main>
)
}
// src/components/Header.js
import { Trans } from '@lingui/react'
import { TransNoContext } from '@lingui/react/server'
import { getI18n } from '@/utils/locales'

export default function Header() {
const i18n = getI18n() // will get the i18n instance set by page or layout component
return (
<nav>
<ul>
<li>
<Trans id="Home" /> {/* ⚠️ Trans is rendered on the server but will be part of the js bundle */}
</li>
<li>
{i18n._('Users')} {/* ✅ Will not be part of the js bundle */}
</li>
<li>
<TransNoContext id="Setting" lingui={{ i18n }} /> {/* ✅ Will not be part of the js bundle */}
</li>
</ul>
</nav>
)
}