Custom Bundlers
Fluenti ships first-class plugins for Vite (via @fluenti/vite-plugin) and Next.js (via @fluenti/next). For other bundlers — webpack, rspack, Parcel, esbuild, Rollup — use the @fluenti/core/transform API to build your own integration.
What the Build Plugin Does
Section titled “What the Build Plugin Does”A Fluenti build plugin handles three responsibilities:
- Transform — Rewrite
t`Hello ${name}`tagged templates into descriptor calls with hash-based IDs - Compile — Run
fluenti extract+fluenti compilebefore the build to generate per-locale message bundles - Resolve — Alias virtual module imports (e.g.,
@fluenti/messages/en) to compiled catalog files
Without a plugin, you handle steps 2 and 3 manually (CLI commands + import paths), and step 1 is skipped (use useI18n().t() instead of import { t }).
Minimal Setup (No Plugin)
Section titled “Minimal Setup (No Plugin)”If you don’t need compile-time t\“ transforms, you can use Fluenti with any bundler with zero plugin setup:
1. Install and configure
Section titled “1. Install and configure”pnpm add @fluenti/core @fluenti/react # or @fluenti/vue / @fluenti/solidpnpm add -D @fluenti/cliimport { defineConfig } from '@fluenti/cli'
export default defineConfig({ sourceLocale: 'en', locales: ['en', 'ja', 'zh-CN'], catalogDir: './locales', format: 'po', include: ['./src/**/*.tsx', './src/**/*.ts'], compileOutDir: './src/locales/compiled',})2. Extract and compile messages
Section titled “2. Extract and compile messages”fluenti extract # Scan source → create/update PO catalogsfluenti compile # Compile PO → JS bundles3. Import compiled catalogs directly
Section titled “3. Import compiled catalogs directly”import { I18nProvider, useI18n } from '@fluenti/react'import en from './locales/compiled/en'import ja from './locales/compiled/ja'
function App() { return ( <I18nProvider locale="en" messages={{ en, ja }}> <Greeting name="World" /> </I18nProvider> )}
function Greeting({ name }: { name: string }) { const { t } = useI18n() return <h1>{t('Hello {name}!', { name })}</h1>}<script setup>import { useI18n } from '@fluenti/vue'const { t } = useI18n()</script>
<template> <h1>{{ t('Hello {name}!', { name: 'World' }) }}</h1></template>import { I18nProvider, useI18n } from '@fluenti/solid'import en from './locales/compiled/en'import ja from './locales/compiled/ja'
function App() { return ( <I18nProvider locale="en" messages={{ en, ja }}> <Greeting name="World" /> </I18nProvider> )}
function Greeting(props: { name: string }) { const { t } = useI18n() return <h1>{t('Hello {name}!', { name: props.name })}</h1>}4. Add build scripts
Section titled “4. Add build scripts”{ "scripts": { "i18n": "fluenti extract && fluenti compile", "prebuild": "pnpm i18n", "build": "webpack" }}This approach works with any bundler. The trade-off: you use useI18n().t() instead of the direct-import t`…` syntax, and Trans components do their extraction at runtime instead of build time.
Full Plugin Integration
Section titled “Full Plugin Integration”For compile-time t`…` transforms and Trans optimization, build a loader/plugin using @fluenti/core/transform.
webpack / rspack
Section titled “webpack / rspack”webpack and rspack share the same loader API, so this works for both:
import { createTransformPipeline, hasScopeTransformCandidate,} from '@fluenti/core/transform'
const pipeline = createTransformPipeline({ framework: 'react' }) // or 'vue', 'solid'
export default function fluentLoader(source: string): string { const resourcePath: string = (this as any).resourcePath
// Only process source files if (!/\.[jt]sx?$/.test(resourcePath)) return source if (/node_modules/.test(resourcePath)) return source
// Quick regex check — skip files without Fluenti patterns if (!hasScopeTransformCandidate(source)) return source
const result = pipeline.transform(source, resourcePath) return result.transformed ? result.code : source}// webpack.config.ts (or rspack.config.ts)import { resolve } from 'node:path'
export default { module: { rules: [ { test: /\.[jt]sx?$/, exclude: /node_modules/, enforce: 'pre' as const, use: [{ loader: resolve(__dirname, './fluenti-loader.ts') }], }, // ... your other loaders (babel, ts-loader, etc.) ], }, resolve: { alias: { // Point virtual imports to compiled catalogs '@fluenti/messages/en': resolve(__dirname, 'src/locales/compiled/en.js'), '@fluenti/messages/ja': resolve(__dirname, 'src/locales/compiled/ja.js'), }, },}Parcel
Section titled “Parcel”Parcel uses Transformer plugins. Create a custom transformer:
import { Transformer } from '@parcel/plugin'import { createTransformPipeline, hasScopeTransformCandidate,} from '@fluenti/core/transform'
const pipeline = createTransformPipeline({ framework: 'react' })
export default new Transformer({ async transform({ asset }) { if (!asset.type.match(/^(js|ts|jsx|tsx)$/)) return [asset]
const source = await asset.getCode() if (!hasScopeTransformCandidate(source)) return [asset]
const result = pipeline.transform(source, asset.filePath) if (result.transformed) { asset.setCode(result.code) } return [asset] },})Register it in .parcelrc:
{ "extends": "@parcel/config-default", "transformers": { "*.{ts,tsx,js,jsx}": ["parcel-transformer-fluenti", "..."] }}esbuild
Section titled “esbuild”esbuild uses plugins with an onLoad callback:
import { readFile } from 'node:fs/promises'import type { Plugin } from 'esbuild'import { createTransformPipeline, hasScopeTransformCandidate,} from '@fluenti/core/transform'
const pipeline = createTransformPipeline({ framework: 'react' })
export function fluentPlugin(): Plugin { return { name: 'fluenti', setup(build) { build.onLoad({ filter: /\.[jt]sx?$/ }, async (args) => { if (args.path.includes('node_modules')) return undefined
const source = await readFile(args.path, 'utf-8') if (!hasScopeTransformCandidate(source)) return undefined
const result = pipeline.transform(source, args.path) if (!result.transformed) return undefined
return { contents: result.code, loader: args.path.endsWith('x') ? 'tsx' : 'ts', } }) }, }}import { fluentPlugin } from './esbuild-plugin-fluenti'
await esbuild.build({ entryPoints: ['src/index.tsx'], bundle: true, plugins: [fluentPlugin()],})Rollup
Section titled “Rollup”Rollup uses a transform hook:
import type { Plugin } from 'rollup'import { createTransformPipeline, hasScopeTransformCandidate,} from '@fluenti/core/transform'
const pipeline = createTransformPipeline({ framework: 'react' })
export function fluentPlugin(): Plugin { return { name: 'fluenti', transform(source, id) { if (!/\.[jt]sx?$/.test(id)) return null if (id.includes('node_modules')) return null if (!hasScopeTransformCandidate(source)) return null
const result = pipeline.transform(source, id) return result.transformed ? { code: result.code, map: null } : null }, }}Auto-Compile Integration
Section titled “Auto-Compile Integration”To run fluenti extract + fluenti compile automatically before each build, hook into your bundler’s lifecycle:
import { execSync } from 'node:child_process'
class FluentCompilePlugin { apply(compiler: any) { compiler.hooks.beforeCompile.tapPromise('fluenti', async () => { execSync('fluenti extract && fluenti compile', { stdio: 'inherit' }) }) }}
// In webpack.config.ts:export default { plugins: [new FluentCompilePlugin()],}import { execSync } from 'node:child_process'
// Run before esbuild.build()execSync('fluenti extract && fluenti compile', { stdio: 'inherit' })
await esbuild.build({ /* ... */ }){ "scripts": { "i18n": "fluenti extract && fluenti compile", "prebuild": "pnpm i18n", "build": "your-bundler build" }}Lazy Locale Loading
Section titled “Lazy Locale Loading”With any bundler that supports dynamic import(), you can lazy-load locales:
<I18nProvider locale="en" messages={{ en }} lazyLocaleLoading chunkLoader={(locale) => import(`./locales/compiled/${locale}.js`)}> <App /></I18nProvider>For webpack/rspack, this creates split chunks automatically. For esbuild, you may need splitting: true in the build config. For Parcel, dynamic imports work out of the box.
Comparison
Section titled “Comparison”| Feature | No plugin | With custom plugin |
|---|---|---|
useI18n().t() | ✅ Works | ✅ Works |
Trans, Plural, Select | ✅ Runtime | ✅ Optimized at build time |
import { t } + t\“ | ❌ Not available | ✅ Compile-time transform |
| Lazy locale loading | ✅ Via chunkLoader | ✅ Via chunkLoader |
| Auto extract + compile | Manual (prebuild script) | Can hook into bundler lifecycle |
| Setup effort | Minimal | Moderate (write loader/plugin) |
For the full transform API reference, see @fluenti/core/transform.