Skip to content

Fix: Lingui Not Working — Messages Not Extracted, Translations Missing, or Macro Errors

FixDevs ·

Quick Answer

How to fix Lingui.js i18n issues — setup with React, message extraction, macro compilation, ICU format, lazy loading catalogs, and Next.js integration.

The Problem

The <Trans> component renders the message ID instead of translated text:

import { Trans } from '@lingui/react';

<Trans id="welcome">Welcome to our app</Trans>
// Renders: "welcome" instead of the translated text

Or message extraction produces an empty catalog:

npx lingui extract
# Catalog for "en": 0 messages extracted

Or the Lingui macro throws a build error:

SyntaxError: Using the macro requires configuring the Lingui compiler

Why This Happens

Lingui is a compile-time i18n framework that uses macros to extract translatable messages from source code:

  • Lingui macros require a compiler plugin@lingui/macro uses babel or SWC macros that transform <Trans> and t calls at build time. Without the compiler plugin, macros aren’t processed and throw syntax errors.
  • Messages must be extracted before translationlingui extract scans source files for macro usage and creates message catalogs (.po or .json). Without extraction, catalogs are empty.
  • Catalogs must be compiledlingui compile transforms human-readable catalogs into optimized JavaScript. Without compilation, the runtime can’t find translations.
  • The I18nProvider must wrap the app — without the provider and active locale, all messages render as their IDs or source text.

Fix 1: Setup with React and Vite

npm install @lingui/core @lingui/react
npm install -D @lingui/cli @lingui/macro @lingui/vite-plugin
// lingui.config.ts
import type { LinguiConfig } from '@lingui/conf';

const config: LinguiConfig = {
  locales: ['en', 'ja', 'es', 'fr'],
  sourceLocale: 'en',
  catalogs: [
    {
      path: '<rootDir>/src/locales/{locale}/messages',
      include: ['src'],
    },
  ],
  format: 'po',  // 'po' | 'json' | 'minimal'
};

export default config;
// vite.config.ts — add the Lingui plugin
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { lingui } from '@lingui/vite-plugin';

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: ['@lingui/babel-plugin-lingui-macro'],
      },
    }),
    lingui(),
  ],
});

// Or with SWC (faster):
import react from '@vitejs/plugin-react-swc';

export default defineConfig({
  plugins: [
    react({
      plugins: [['@lingui/swc-plugin', {}]],
    }),
    lingui(),
  ],
});
// src/i18n.ts — initialize i18n
import { i18n } from '@lingui/core';

export async function loadCatalog(locale: string) {
  const { messages } = await import(`./locales/${locale}/messages.ts`);
  i18n.load(locale, messages);
  i18n.activate(locale);
}

// Load default locale on startup
loadCatalog('en');

export { i18n };
// src/main.tsx — wrap app with provider
import { I18nProvider } from '@lingui/react';
import { i18n } from './i18n';

function App() {
  return (
    <I18nProvider i18n={i18n}>
      <MainContent />
    </I18nProvider>
  );
}

Fix 2: Use Macros in Components

// Macros are compiled away at build time — zero runtime overhead
import { Trans, Plural, Select } from '@lingui/react/macro';
import { t, msg, plural } from '@lingui/core/macro';

function WelcomePage({ user }: { user: User }) {
  return (
    <div>
      {/* Trans macro — JSX translation */}
      <h1><Trans>Welcome to our app</Trans></h1>

      {/* With variables */}
      <p><Trans>Hello, {user.name}!</Trans></p>

      {/* Pluralization */}
      <p>
        <Plural
          value={user.messageCount}
          zero="No messages"
          one="You have # message"
          other="You have # messages"
        />
      </p>

      {/* Gender/select */}
      <p>
        <Select
          value={user.gender}
          male={<Trans>{user.name} updated his profile</Trans>}
          female={<Trans>{user.name} updated her profile</Trans>}
          other={<Trans>{user.name} updated their profile</Trans>}
        />
      </p>

      {/* Rich text — HTML inside translations */}
      <p>
        <Trans>
          Read our <a href="/terms">Terms of Service</a> and{' '}
          <a href="/privacy">Privacy Policy</a>.
        </Trans>
      </p>
    </div>
  );
}

// t() macro — for non-JSX contexts (attributes, variables)
function SearchInput() {
  return (
    <input
      placeholder={t`Search...`}
      aria-label={t`Search the documentation`}
    />
  );
}

// In plain functions
function getErrorMessage(code: string): string {
  switch (code) {
    case 'NOT_FOUND': return t`Resource not found`;
    case 'UNAUTHORIZED': return t`You must be logged in`;
    default: return t`Something went wrong`;
  }
}

// msg() — define messages for later use
const messages = {
  title: msg`Dashboard`,
  subtitle: msg`Welcome back, ${name}`,
};

// Use later with i18n._(messages.title)

Fix 3: Extract and Translate

# Step 1: Extract messages from source code
npx lingui extract

# Output:
# Catalog statistics for en:
# ┌──────────┬─────────────┬─────────┐
# │ Language │ Total count │ Missing │
# ├──────────┼─────────────┼─────────┤
# │ en       │ 42          │ 0       │
# │ ja       │ 42          │ 42      │
# │ es       │ 42          │ 42      │
# └──────────┴─────────────┴─────────┘

# Step 2: Translate the catalogs
# Edit src/locales/ja/messages.po (or .json)

# Step 3: Compile catalogs to JavaScript
npx lingui compile

# This generates optimized .ts files from .po catalogs
# src/locales/ja/messages.po
msgid "Welcome to our app"
msgstr "アプリへようこそ"

msgid "Hello, {name}!"
msgstr "こんにちは、{name}さん!"

msgid "{0, plural, zero {No messages} one {You have # message} other {You have # messages}}"
msgstr "{0, plural, other {{0}件のメッセージ}}"

msgid "Search..."
msgstr "検索..."
// package.json — i18n scripts
{
  "scripts": {
    "i18n:extract": "lingui extract",
    "i18n:compile": "lingui compile",
    "i18n": "lingui extract && lingui compile",
    "prebuild": "npm run i18n:compile"
  }
}

Fix 4: Language Switching

'use client';

import { useLingui } from '@lingui/react';
import { loadCatalog } from '@/i18n';

function LanguageSwitcher() {
  const { i18n } = useLingui();

  async function changeLanguage(locale: string) {
    await loadCatalog(locale);
    // i18n is already activated in loadCatalog
  }

  const locales = [
    { code: 'en', label: 'English' },
    { code: 'ja', label: '日本語' },
    { code: 'es', label: 'Español' },
  ];

  return (
    <select
      value={i18n.locale}
      onChange={(e) => changeLanguage(e.target.value)}
    >
      {locales.map(({ code, label }) => (
        <option key={code} value={code}>{label}</option>
      ))}
    </select>
  );
}

Fix 5: Next.js App Router

// next.config.mjs
const nextConfig = {
  experimental: {
    swcPlugins: [['@lingui/swc-plugin', {}]],
  },
};

export default nextConfig;
// app/[lang]/layout.tsx
import { I18nProvider } from '@lingui/react';
import { loadCatalog, i18n } from '@/i18n';

export default async function LangLayout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: Promise<{ lang: string }>;
}) {
  const { lang } = await params;
  await loadCatalog(lang);

  return (
    <I18nProvider i18n={i18n}>
      {children}
    </I18nProvider>
  );
}

export function generateStaticParams() {
  return [{ lang: 'en' }, { lang: 'ja' }, { lang: 'es' }];
}

Fix 6: Integration with Translation Services

# Export for translation services (Crowdin, Lokalise, etc.)
npx lingui extract --format po
# .po files are the standard format for translation tools

# Or use JSON format
# lingui.config.ts: format: 'json'
npx lingui extract
# Generates .json catalogs compatible with most translation platforms

# After translators update the files:
npx lingui compile

Still Not Working?

Macro throws “requires configuring the Lingui compiler” — the babel or SWC plugin isn’t configured. For Vite, add @lingui/babel-plugin-lingui-macro to the React plugin’s babel plugins. For Next.js, add @lingui/swc-plugin to experimental.swcPlugins.

Extraction finds 0 messages — the include path in lingui.config.ts doesn’t match your source files. Check that include: ['src'] covers where your components are. Also verify macros are imported from @lingui/react/macro or @lingui/core/macro (not @lingui/react).

Translations show source text instead of translated text — catalogs aren’t compiled. Run npx lingui compile after translating. Also check that loadCatalog() was called with the correct locale and that i18n.activate() was called.

Runtime error: “i18n instance not found”I18nProvider is missing or doesn’t wrap the component using translations. Ensure the provider is in the layout above all translated components.

For related i18n issues, see Fix: i18next Not Working and Fix: next-intl Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles