Announcing Lingui 6.0
We're announcing Lingui 6.0! ๐
This release focuses primarily on technical improvements and modernization of the codebase. It includes the transition to ESM-only distribution, reduced dependency graph, removal of deprecated APIs, and improved TypeScript support. It also introduces a few new features. In this post, we'll highlight the key changes in this release.
In line with the principles of Semantic Versioning, this release contains breaking changes that we have thoroughly documented in the v6 migration guide.

Table of Contentsโ
What is Lingui?โ
Lingui is an open-source JavaScript library for internationalization (i18n) and localization (l10n). Designed to make it easy for developers to build fully translated, multilingual applications, it offers support for React, React Native, Vue.js, Node.js, TypeScript. It is also compatible with Astro and Svelte via community-supported packages.
Progress Highlightsโ
The journey from Lingui 5.0 includes 22 version updates (minor and patch), more than 130 closed issues, and more than 170 merged pull requests.
Before we dive into the changes in 6.0, let's take a look at the project's metrics since version 5.0. The project has grown significantly, with GitHub stars up 24% and substantially increased downloads across all packages:
(The download numbers are based on the npm-stat.com data)
Additionally, eslint-plugin-lingui has seen strong adoption, with monthly downloads growing from ~107k to ~791k (+639%) over the same period.
Recapโ
Let's quickly walk through some of the major changes that have happened between 5.0 and 6.0.
Ecosystemโ
We've introduced a new example project featuring Lingui + TanStack Start! Add internationalization to your TanStack Start apps using Lingui's powerful localization tools with Vite and Babel plugin integration.
๐ View the TanStack Start example.
Embracing the AI Eraโ
With the growing prevalence of AI-powered development tools, we've worked to ensure Lingui integrates seamlessly with this new paradigm.
We now provide llms.txt and llms-full.txt documentation files following the llms.txt specification, optimized for LLM context windows. Context7 provides Lingui documentation via MCP, allowing AI assistants to fetch up-to-date docs directly into prompts.
We've also released Lingui Skills - Agent Skills that help AI coding assistants implement internationalization correctly. Available skills cover best practices and other helpful tips.
๐ See the i18n with AI documentation page for more details.
CLI Multithreadingโ
The CLI received significant performance improvements with worker thread support. Multithreading is now available across all CLI commands: extract, compile, extract-template, and extract-experimental (the dependency-tree based extractor). Each command processes files, catalogs, or locales in parallel using a worker pool.
You can configure the number of worker threads with the --workers flag:
lingui extract --workers 4
By default, Lingui uses CPU cores - 1, capped at 8. Use --workers 1 to disable multithreading and run in a single process.
Explicit Placeholder Labels Macroโ
The ph() macro allows labeling placeholders with meaningful names. Without it, complex
expressions become positional placeholders ({0}, {1}). With ph(), you can assign names that provide
better context for translators:
- t`Hello ${getUserName()}`;
+ t`Hello ${ph({ name: getUserName() })}`;
- msgid "Hello {0}"
- msgstr "Hello {0}"
+ msgid "Hello {name}"
+ msgstr "Hello {name}"
Named placeholders help translators understand what the value represents, so they can choose the correct grammar or wording for the target language.
๐ Read more about the ph() macro in the macro documentation.
What's New in 6.0?โ
ESM-Only Distributionโ
Lingui 6.0 is distributed as ESM-only (ECMAScript Modules). ESM is the official, standardized module system for JavaScript, and the ecosystem has largely converged on it. After years of shipping dual ESM/CommonJS builds, we've taken the step to simplify.
Why ESM-only? Dual builds came with real costs: they nearly doubled our package sizes, added maintenance complexity (conditionals, workarounds, and separate entry points), and occasionally led to subtle bugs from module duplication and dependency resolution. Going ESM-only makes Lingui smaller, simpler to maintain, and aligned with where the ecosystem is headed. With Node.js supporting require(esm) in recent versions, the transition is smooth for most users.
Node.js v22.19+ (or v24+) is now required. This aligns with the ESM-only move and lets us rely on modern Node behavior without legacy workarounds.
Reduced Package Sizeโ
Lingui 6.0 is noticeably lighter to install and maintain. Across the core packages, we cut both disk usage and transitive dependency count.
To make this representative of real projects, the numbers are calculated for @lingui/core, @lingui/react, and @lingui/cli together. Most apps use this combination, so measuring them as a set captures the practical install footprint better than showing each package in isolation.
node_modules Footprintโ
The combined install of @lingui/core + @lingui/react + @lingui/cli dropped from 62 MB to 35 MB - a ~44% reduction. That means faster npm install, lighter CI caches, and less disk clutter on every developer's machine.
Dependency Countโ
The transitive dependency tree shrank from 146 packages down to 104 - 42 fewer packages to resolve, download, and audit on every install. The graph below shows just how much the tree thinned out:

Configurable JSX Placeholder Names in <Trans>โ
When <Trans> includes JSX elements, extracted messages traditionally used numeric placeholders like <0>...</0>. This works, but creates two translation problems:
- Numeric tags provide little context to translators, especially in longer messages with several nested elements.
- Reordering or refactoring JSX can change placeholder indices, producing noisy catalog diffs and increasing the chance of unnecessary re-translation.
Lingui 6.0 introduces two new macro config options to make placeholders semantic and more stable:
macro.jsxPlaceholderAttributefor explicit local naming (for example,_t="link").macro.jsxPlaceholderDefaultsfor project-wide default names by element/tag.
import { defineConfig } from "@lingui/cli";
export default defineConfig({
macro: {
jsxPlaceholderAttribute: "_t",
jsxPlaceholderDefaults: {
a: "link",
strong: "bold",
},
},
});
<Trans>
Open <a href="/">docs</a> and read the <strong>important</strong> note.
</Trans>
- "Open <0>docs</0> and read the <1>important</1> note."
+ "Open <link>docs</link> and read the <bold>important</bold> note."
You can also set an explicit name directly in markup:
<Trans>
Read the{" "}
<a _t="docsLink" href="/docs">
documentation
</a>
.
</Trans>
- "Read the <0>documentation</0>."
+ "Read the <docsLink>documentation</docsLink>."
The result is more human-readable messages, better translator context, and fewer accidental translation breaks when UI markup changes.
The same capability is also available in @lingui/swc-plugin, so teams using either Babel or SWC can keep placeholder naming behavior consistent.
Vue 3 Reactivity Transform in Vue Extractorโ
The Vue extractor now supports Vue's Reactivity Transform (reactive props destructure in <script setup>). In Vue 3, destructuring props from defineProps() is compiled in a way that can change how variables appear in the generated code. If the extractor runs on the raw source while your app runs on the compiled output, message IDs can diverge and translations may not resolve at runtime.
To align extraction with your build, use the new createVueExtractor() factory and enable the reactivityTransform option when your project uses the transform:
import { defineConfig } from "@lingui/cli";
import { createVueExtractor } from "@lingui/extractor-vue";
export default defineConfig({
locales: ["en", "cs"],
extractors: [createVueExtractor({ reactivityTransform: true })],
});
The option is opt-in (reactivityTransform: false by default) so existing setups keep working.
Vite Plugin Improvementsโ
@lingui/vite-plugin now supports Vite 8 and ships linguiTransformerBabelPreset - a shortcut for wiring Rolldown and Babel when you use Lingui macros in a Vite-based build:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import babel from "@rolldown/plugin-babel";
import { lingui, linguiTransformerBabelPreset } from "@lingui/vite-plugin";
export default defineConfig({
plugins: [
react(),
lingui(),
babel({
presets: [linguiTransformerBabelPreset()],
}),
],
});
Stronger Type Safetyโ
This release tightens TypeScript types for better safety across core packages.
Several packages now use stricter nullability behavior, and optional values are handled consistently with TypeScript conventions (e.g. undefined instead of null where appropriate). We've also clarified the separation between extracted-message shapes and loaded-catalog message shapes for better type accuracy in custom extractors, formatters, or tooling.
Most apps are unaffected, but custom integrations may need small updates.
New Exampleโ
We now have a React Webpack Po-Gettext example in the repo. It uses the po-gettext formatter for plurals, a minimal webpack setup for React and TypeScript, and dynamic loading of compiled JSON catalogs. It is listed on our Examples page alongside the other starter projects.
Conclusionโ
We're excited to continue improving Lingui and hope you enjoy using Lingui 6.0. We look forward to hearing your feedback!
A huge thank you to everyone who helped with this release - it simply wouldn't have been possible without the amazing community and contributors!
