Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Implement internationalization with Lingui in React and JavaScript applications. Use when adding i18n, translating UI, working with Trans/useLingui/Plural, extracting messages, compiling catalogs, or when the user mentions Lingui, internationalization, i18n, translations, locales, message extraction, ICU MessageFormat, or working with .po files.
.claude/skills/platformplatform-lingui-best-practices/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 57% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 125% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 52% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 205% | 0% |
| case-22 | ✗→✓ | ▲ Improved | 339% | 0% |
Lingui is a powerful internationalization (i18n) framework for JavaScript. This skill covers best practices for implementing i18n in React and vanilla JavaScript applications.
The standard Lingui workflow consists of these steps:
I18nProviderTrans, t, etc.)lingui extractlingui compileImport from these packages:
jsx// React macros (recommended) import { Trans, Plural, Select, useLingui } from "@lingui/react/macro"; // Core macros for vanilla JS import { t, msg, plural, select } from "@lingui/core/macro"; // Runtime (rarely used directly) import { I18nProvider } from "@lingui/react"; import { i18n } from "@lingui/core";
Wrap your application with I18nProvider:
jsximport { I18nProvider } from "@lingui/react"; import { i18n } from "@lingui/core"; import { messages } from "./locales/en/messages"; i18n.load("en", messages); i18n.activate("en"); function App() { return ( <I18nProvider i18n={i18n}> {/* Your app */} </I18nProvider> ); }
The Trans macro is the primary way to translate JSX:
jsximport { Trans } from "@lingui/react/macro"; // Simple text <Trans>Hello World</Trans> // With variables <Trans>Hello {userName}</Trans> // With components (rich text) <Trans> Read the <a href="/docs">documentation</a> for more info. </Trans> // Extracted as: "Read the <0>documentation</0> for more info."
When to use: For any translatable text in JSX elements.
For strings outside JSX (attributes, alerts, function calls):
jsximport { useLingui } from "@lingui/react/macro"; function MyComponent() { const { t } = useLingui(); const handleClick = () => { alert(t`Action completed!`); }; return ( <div> <img src="..." alt={t`Image description`} /> <button onClick={handleClick}>{t`Click me`}</button> </div> ); }
When to use: Element attributes, alerts, function parameters, any non-JSX string.
When you need to define messages at module level or in arrays/objects:
jsximport { msg } from "@lingui/core/macro"; import { useLingui } from "@lingui/react"; // Module-level constants const STATUSES = { active: msg`Active`, inactive: msg`Inactive`, pending: msg`Pending`, }; function StatusList() { const { _ } = useLingui(); return Object.entries(STATUSES).map(([key, message]) => ( <div key={key}>{_(message)}</div> )); }
When to use: Module-level constants, arrays of messages, conditional message selection.
Use the Plural macro for quantity-dependent messages:
jsximport { Plural } from "@lingui/react/macro"; <Plural value={messageCount} one="You have # message" other="You have # messages" />
The # placeholder is replaced with the actual value.
Use _N syntax for exact number matches (takes precedence over plural forms):
jsx<Plural value={count} _0="No messages" one="One message" other="# messages" />
Combine with Trans for complex messages:
jsx<Plural value={count} one={`You have # message, ${userName}`} other={ <Trans> You have <strong>#</strong> messages, {userName} </Trans> } />
Use i18n.date() and i18n.number() for locale-aware formatting:
jsximport { useLingui } from "@lingui/react/macro"; function MyComponent() { const { i18n } = useLingui(); const lastLogin = new Date(); return ( <Trans> Last login: {i18n.date(lastLogin)} </Trans> ); }
These use the browser's Intl API for proper locale formatting.
Provide a custom ID for stable message keys:
jsx<Trans id="header.welcome">Welcome to our app</Trans>
When the same text has different meanings, use context:
jsx<Trans context="direction">right</Trans> <Trans context="correctness">right</Trans>
These create separate catalog entries.
Add context for translators:
jsx<Trans comment="Greeting shown on homepage">Hello World</Trans>
Basic lingui.config.js:
jsimport { defineConfig } from "@lingui/cli"; export default defineConfig({ sourceLocale: "en", locales: ["en", "es", "fr", "de"], catalogs: [ { path: "<rootDir>/src/locales/{locale}/messages", include: ["src"], exclude: ["**/node_modules/**"], }, ], });
For detailed configuration patterns, see configuration.md.
Prefer macros over runtime components. Macros are compiled at build time, reducing bundle size:
jsx// ✅ Good - uses macro import { Trans } from "@lingui/react/macro"; // ❌ Avoid - runtime only import { Trans } from "@lingui/react";
Avoid complex expressions in messages - they'll be replaced with placeholders:
jsx// ❌ Bad - loses context <Trans>Hello {user.name.toUpperCase()}</Trans> // Extracted as: "Hello {0}" // ✅ Good - clear variable name const userName = user.name.toUpperCase(); <Trans>Hello {userName}</Trans> // Extracted as: "Hello {userName}"
Choose the right tool:
jsx// ✅ For JSX content <h1><Trans>Welcome</Trans></h1> // ✅ For string values const { t } = useLingui(); <img alt={t`Profile picture`} />
Macros need component context - use msg instead:
jsx// ❌ Bad - won't work import { t } from "@lingui/core/macro"; const LABELS = [t`Red`, t`Green`, t`Blue`]; // ✅ Good - use msg for lazy translation import { msg } from "@lingui/core/macro"; const LABELS = [msg`Red`, msg`Green`, msg`Blue`];
Install and configure eslint-plugin-lingui to catch common mistakes automatically:
bashnpm install --save-dev eslint-plugin-lingui
js// eslint.config.js import pluginLingui from "eslint-plugin-lingui"; export default [ pluginLingui.configs["flat/recommended"], ];
jsximport { i18n } from "@lingui/core"; async function changeLocale(locale) { const { messages } = await import(`./locales/${locale}/messages`); i18n.load(locale, messages); i18n.activate(locale); }
jsximport { useEffect } from "react"; import { i18n } from "@lingui/core"; function loadCatalog(locale) { return import(`./locales/${locale}/messages`); } function App() { useEffect(() => { loadCatalog("en").then(catalog => { i18n.load("en", catalog.messages); i18n.activate("en"); }); }, []); return <I18nProvider i18n={i18n}>{/* ... */}</I18nProvider>; }
When using memoization, use the t function from the macro version:
jsximport { useLingui } from "@lingui/react/macro"; import { msg } from "@lingui/core/macro"; import { useMemo } from "react"; const welcomeMessage = msg`Welcome!`; function MyComponent() { const { t } = useLingui(); // Macro version - reference changes with locale // ✅ Safe - t reference updates with locale const message = useMemo(() => t(welcomeMessage), [t]); return <div>{message}</div>; }
If you encounter issues:
include patterns in lingui.config.jslingui compileI18nProvider wraps your applingui compile --typescript for TypeScript projectsFor detailed common mistakes and pitfalls, see common-mistakes.md.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 14,900 | 9,281 | -38% | 1 | 1 | 0% | 2,741 | 4,315 | +57% | 0 | 0 | — |
case-02 | fail→pass | 9,149 | 9,242 | +1% | 1 | 1 | 0% | 1,867 | 4,210 | +125% | 0 | 0 | — |
case-03 | fail→pass | 16,625 | 12,624 | -24% | 1 | 1 | 0% | 3,214 | 4,896 | +52% | 0 | 0 | — |
case-04 | pass→pass | 10,200 | 7,621 | -25% | 1 | 1 | 0% | 1,669 | 3,664 | +120% | 0 | 0 | — |
case-05 | pass→fail | 12,741 | 12,594 | -1% | 1 | 1 | 0% | 2,261 | 4,879 | +116% | 0 | 0 | — |
case-06 | pass→pass | 13,509 | 6,890 | -49% | 1 | 1 | 0% | 2,772 | 3,734 | +35% | 0 | 0 | — |
case-07 | fail→pass | 6,117 | 2,864 | -53% | 1 | 1 | 0% | 939 | 2,867 | +205% | 0 | 0 | — |
case-08 | pass→pass | 6,817 | 3,286 | -52% | 1 | 1 | 0% | 1,156 | 2,946 | +155% | 0 | 0 | — |
case-09 | pass→pass | 11,915 | 3,524 | -70% | 1 | 1 | 0% | 2,046 | 2,937 | +44% | 0 | 0 | — |
case-22 | fail→pass | 3,567 | 2,265 | -37% | 1 | 1 | 0% | 615 | 2,698 | +339% | 0 | 0 | — |
case-10 | fail→pass | 9,855 | 8,680 | -12% | 1 | 1 | 0% | 1,770 | 3,853 | +118% | 0 | 0 | — |
case-11 | fail→pass | 12,097 | 4,874 | -60% | 1 | 1 | 0% | 2,030 | 3,229 | +59% | 0 | 0 | — |
case-12 | pass→pass | 10,244 | 4,898 | -52% | 1 | 1 | 0% | 1,818 | 3,167 | +74% | 0 | 0 | — |
case-13 | fail→pass | 9,084 | 3,785 | -58% | 1 | 1 | 0% | 1,641 | 3,004 | +83% | 0 | 0 | — |
case-14 | fail→pass | 13,420 | 5,310 | -60% | 1 | 1 | 0% | 2,474 | 3,343 | +35% | 0 | 0 | — |
case-15 | pass→pass | 9,769 | 8,319 | -15% | 1 | 1 | 0% | 1,852 | 3,939 | +113% | 0 | 0 | — |
case-16 | pass→pass | 7,400 | 3,498 | -53% | 1 | 1 | 0% | 1,347 | 2,944 | +119% | 0 | 0 | — |
case-17 | pass→pass | 11,914 | 5,835 | -51% | 1 | 1 | 0% | 2,056 | 3,251 | +58% | 0 | 0 | — |
case-18 | pass→pass | 12,897 | 6,193 | -52% | 1 | 1 | 0% | 1,969 | 3,285 | +67% | 0 | 0 | — |
case-19 | fail→pass | 12,012 | 8,160 | -32% | 1 | 1 | 0% | 2,159 | 3,904 | +81% | 0 | 0 | — |
case-20 | pass→pass | 6,648 | 3,443 | -48% | 1 | 1 | 0% | 1,041 | 2,951 | +183% | 0 | 0 | — |
case-21 | fail→pass | 10,034 | 8,621 | -14% | 1 | 1 | 0% | 1,748 | 4,001 | +129% | 0 | 0 | — |
case-23 | pass→pass | 14,450 | 8,886 | -39% | 1 | 1 | 0% | 2,402 | 3,831 | +59% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +43 percentage points is the difference between those two pass rates over the 23 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.