An SWC transform plugin for Intlayer β the open-source i18n framework for React, Next.js, Vue, Svelte, and more.
Intlayer is a modern i18n solution for web and mobile apps. Itβs framework-agnostic, AI-powered, and includes a free CMS & visual editor. With per-locale content files, TypeScript autocompletion, tree-shakable dictionaries, and CI/CD integration, Intlayer makes internationalization faster, cleaner, and smarter.
π Key Features
Intlayer provides a variety of features to help you scale your internationalization efforts.
- Cross-Framework Support: Compatible with all major frameworks and libraries, including Next.js, React, Vite, Vue.js, Nuxt, Preact, Express, and more.
- JavaScript-Powered Content Management: Harness the flexibility of JavaScript to define and manage your content efficiently.
- Per-Locale Content Declaration File: Speed up your development by declaring your content once, before auto generation.
- Compiler: The Intlayer Compiler extracts automatically the content from the components and generates the dictionary files.
- Type-Safe Environment: Leverage TypeScript to ensure your content definitions and code are error-free, while also benefiting from IDE autocompletion.
- Simplified Setup: Get up and running quickly with minimal configuration. Adjust settings for internationalization, routing, AI, build, and content handling with ease.
- Simplified Content Retrieval: No need to call your
tfunction for each piece of content. Retrieve all your content directly using a single hook. - Consistent Server Component Implementation: Perfectly suited for Next.js server components, use the same implementation for both client and server components.
- Organized Codebase: Keep your codebase more organized: 1 component = 1 dictionary in the same folder.
- Enhanced Routing: Full support of app routing, adapting seamlessly to complex application structures, for Next.js, React, Vite, Vue.js, etc.
- Markdown Support: Import and interpret locale files and remote Markdown for multilingual content like privacy policies, documentation, etc.
- Free Visual Editor & CMS: A free visual editor and CMS are available for content writers, removing the need for a localization platform.
- Tree-shakable Content: Tree-shakable content, reducing the size of the final bundle. Loads content per component, excluding any unused content from your bundle.
- Static Rendering: Doesn't block Static Rendering in Next.js.
- AI-Powered Translation: Transform your website into 231 languages with just one click using Intlayer's advanced AI-powered translation tools.
- MCP Server Integration: Provides an MCP server for IDE automation, enabling seamless content management and i18n workflows.
- VSCode Extension: Intlayer provides a VSCode extension to help you manage your content and translations.
- Interoperability: Allow interoperability with react-i18next, next-i18next, next-intl, react-intl, vue-i18n.
- Performances & Benchmark: Uses advanced tree-shaking and dynamic loading to boost performances and keep the solution as light as possible.
π οΈ How the plugin works
The plugin rewrites useIntlayer / getIntlayer / useTranslations call arguments at compile time, replacing string dictionary keys with pre-loaded dictionary imports. This eliminates runtime registry lookups and enables tree-shaking for per-locale bundles.
Before (source code):
import { useIntlayer } from "react-intlayer";
const t = useIntlayer("locale-switcher");
After (transformed output):
import _FsHhNfuhm85 from "../../.intlayer/dictionaries/locale-switcher.json" with { type: "json" };
import { useDictionary as useIntlayer } from "react-intlayer";
const t = useIntlayer(_FsHhNfuhm85);
Three import modes are supported:
| Mode | Helper function | Import type |
|---|---|---|
static |
useDictionary |
JSON import assertion |
dynamic |
useDictionaryDynamic |
Dynamic .mjs import |
fetch |
useDictionaryDynamic |
Fetch .mjs import |
Field renaming (build.minify)
When the compiled dictionaries have been minified β every user-defined content field renamed to a short alphabetic alias β the plugin rewrites the matching source accesses so both sides keep agreeing:
Before
const { title } = useIntlayer("about");
const content = useIntlayer("about");
content.section.subtitle;
After
const { d: title } = useIntlayer("about");
const content = useIntlayer("about");
content.b.a;
Destructuring (including nested patterns, local aliases and defaults), member chains, optional chaining, static computed accesses (content["title"]), array indexes (content.list[0].title) and signal-style accessors (content().title) are all handled. Dynamic accesses (content[key]) stop the rewrite, leaving the rest of the chain untouched.
The rename tables come from the fieldRenameMap option. Deciding which fields are unused and what alias each gets requires reading every component source file and rewriting the dictionary JSON β file I/O and cross-file state a per-file Wasm transform cannot do β so that analysis runs in Node (in @intlayer/babel, driven by withIntlayer) and only its result is handed to this plugin.
Build reporting
The plugin transforms one file at a time with no cross-file state, so all it can report is a line per file. That is a tracing aid, not build output β the purge and minify summaries a build normally prints (which dictionaries were pruned, which fields were removed, what was partially minified) come from the Node-side pipeline and follow log.mode in intlayer.config.*, exactly as in a Vite build.
logLevel is therefore "off" unless you ask for tracing:
| Level | Output |
|---|---|
"off" |
Nothing (default) |
"info" |
One line per transformed file: injected imports and renamed field count |
"debug" |
Everything above, plus skipped files and the emitted code of each transform |
With next-intlayer, set INTLAYER_SWC_LOG_LEVEL=info (or debug) to turn it on; log.mode: "disabled" silences it regardless.
Next.js compatibility
An SWC Wasm plugin can only be loaded by a host that speaks its swc_ecma_ast
schema. Next.js 16.1.0 is the minimum: it is the first release built on
SWC's forward-compatible plugin ABI, where the AST travels as self-describing
CBOR instead of rkyv, so one binary keeps working on later releases. Earlier
releases require an exact schema match and reject the plugin.
| Next.js | swc_ecma_ast |
Plugin loads |
|---|---|---|
| 14.2.x | 0.112.7 | β rkyv ABI |
| 15.5.x | 14.0.0 | β rkyv ABI |
| 16.0.x | 16.0.0 | β rkyv ABI |
| 16.1.x | 19.0.0 | β exact match |
| 16.2.x | 20.0.1 | β |
| 16.3.x | 25.0.0 | β |
You do not have to check this yourself: withIntlayer from
next-intlayer reads the Next.js
version from your project and simply does not register the plugin below 16.1.0.
Those builds succeed, they just run without the bundle optimisation instead of
failing with failed to invoke plugin.
This is also why the crate pins swc_core to the 54.x line rather than the
latest release: 54.x is the newest swc_core still on swc_ecma_ast 19, the
schema Next.js 16.1 ships. Moving the pin forward would raise the minimum
supported Next.js version with it.
Usage: Next.js / SWC Wasm plugin (recommended)
The Wasm binary is distributed via npm as @intlayer/swc.
You do not need to add this Rust crate as a dependency for that use-case.
# or
Configure in next.config.ts:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
experimental: {
swcPlugins: [
[
"@intlayer/swc",
{
dictionariesDir: "/absolute/path/.intlayer/dictionaries",
dictionariesEntryPath: "/absolute/path/.intlayer/dictionaries.mjs",
dynamicDictionariesDir:
"/absolute/path/.intlayer/dynamic_dictionaries",
fetchDictionariesDir: "/absolute/path/.intlayer/fetch_dictionaries",
importMode: "static", // "static" | "dynamic" | "fetch"
replaceDictionaryEntry: false,
filesList: [], // empty = transform all files
dictionaryModeMap: {}, // per-key overrides, e.g. { "heavy-dict": "dynamic" }
fieldRenameMap: {}, // minified field aliases, e.g. { about: { title: { shortName: "a", children: {} } } }
logLevel: "off", // "off" | "info" | "debug"
},
],
],
},
};
export default nextConfig;
In practice you should use the @intlayer/webpack or @intlayer/vite plugin, which configures the SWC plugin automatically based on your intlayer.config.* file.
Usage: native Rust library
Add to Cargo.toml:
[]
= "7"
Then call [process_transform] directly from your own SWC pipeline:
use ;
use Program;
Building the Wasm plugin yourself
The plugin Cargo feature enables the #[plugin_transform] Wasm entry point. Without it the crate compiles as a standard native Rust library.
# Uses the alias defined in .cargo/config.toml
# equivalent to:
Build through the alias, or at least through this crate's .cargo/config.toml:
it sets --cfg=swc_ast_unknown for wasm32 targets, which is what opts the
binary into the forward-compatible ABI. Built without it, the plugin only loads
on hosts sharing its exact swc_ecma_ast version, and the first Next.js release
that adds an AST node breaks every build using it.
Plugin configuration reference
All fields correspond to the JSON object passed as the second element of each swcPlugins tuple.
| Field | Type | Default | Description |
|---|---|---|---|
dictionariesDir |
string |
required | Absolute path to compiled .json dictionaries |
dictionariesEntryPath |
string |
required | Absolute path to the generated entry .mjs file |
dynamicDictionariesDir |
string |
required | Absolute path for dynamic .mjs modules |
fetchDictionariesDir |
string |
required | Absolute path for fetch .mjs modules |
importMode |
"static" | "dynamic" | "fetch" |
"static" |
Global import strategy |
replaceDictionaryEntry |
boolean |
false |
Replace entry file with empty stubs |
filesList |
string[] |
[] |
Allowlist of absolute file paths; empty = all files |
dictionaryModeMap |
Record<string, string> |
{} |
Per-dictionary import mode overrides |
nestingDictionaryKeys |
string[] |
[] |
Keys imported through their nested/ companion |
extraCallers |
ExtraCallerConfig[] |
[] |
Compat-adapter callers to rewrite like useIntlayer |
fieldRenameMap |
Record<string, FieldRenameMap> |
{} |
Minified field aliases, per dictionary key |
logLevel |
"off" | "info" | "debug" |
"off" |
Build-time reporting verbosity |
FieldRenameMap is a recursive object mapping each original field name to { shortName: string; children: FieldRenameMap }.
Public Rust API
The following symbols are exported by this crate:
PluginConfigβ configuration struct (mirrors the JSON options above). ImplementsDefault, so..PluginConfig::default()keeps call sites stable as options are added.ExtraCallerConfig/NamespaceOptionConfigβ compat-adapter caller descriptors.FieldRenameMap/FieldRenameNodeβ the minified field alias tables.LogLevelβ build-time reporting verbosity.process_transform(program, cfg, filename) -> Programβ core transform function; accepts and returns an SWCProgramAST.normalize_path(path: &str) -> Stringβ normalises Windows-style backslash paths to forward slashes for cross-platform path diffing.
Crate layout
| Module | Responsibility |
|---|---|
config |
Plugin option types and their wire format |
ast |
Small helpers for reading and building AST nodes |
paths |
Path normalisation and relative module specifiers |
packages |
Recognised package specifiers and generated-file conventions |
extra_caller |
Namespace resolution for compat-adapter callers |
field_rename |
Source-side content field renaming (build.minify) |
pre_pass |
Caller discovery and the file-level dynamic/static decision |
optimize |
Call-site and import-specifier rewriting |
imports |
Injection of the dictionary imports the rewrite created |
dictionary_entry |
Emptying of the generated dictionaries entry module |
logger |
Build-time reporting |
Documentation
Explore our comprehensive documentation to get started with Intlayer and learn how to integrate it into your projects.
Related packages
| Package | Description |
|---|---|
@intlayer/webpack |
Webpack plugin (Babel-based transform) |
@intlayer/vite |
Vite plugin |
react-intlayer |
React hooks |
next-intlayer |
Next.js integration |
License
Apache-2.0 Β© Aymeric Pineau