Skip to content
fluenti

@fluenti/vite-plugin

The core Vite plugin that powers Fluenti’s compile-time transforms, virtual module system, and code splitting. You never import @fluenti/vite-plugin directly — instead, use the framework-specific wrapper for your project.

Each framework package re-exports the plugin factory with its own runtime generator and (optionally) framework-specific sub-plugins. The plugin ordering relative to the framework’s own Vite plugin matters.

vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import fluentiVue from '@fluenti/vue/vite-plugin'
export default defineConfig({
plugins: [
vue(),
fluentiVue(),
],
})

fluentiVue() adds a fluenti:vue-template sub-plugin that pre-transforms v-t, <Trans>, and <Plural> usage in Vue SFC templates before the core script transform runs.

Place fluentiVue() after vue() so that Vue’s SFC compiler runs first.

Every framework wrapper accepts a single optional FluentiPluginOptions object:

interface FluentiPluginOptions {
config?: string | FluentiBuildConfig
}

The config field accepts three forms:

// 1. Zero-config — auto-discovers fluenti.config.ts in the project root
fluentiVue()
// 2. Path to a specific config file
fluentiVue({ config: './i18n/fluenti.config.ts' })
// 3. Inline config object (merged with built-in defaults)
fluentiVue({
config: {
sourceLocale: 'en',
locales: ['en', 'ja', 'zh-CN'],
splitting: 'dynamic',
},
})

When passing an inline config object, the following FluentiBuildConfig fields are accepted. All are optional — defaults come from fluenti.config.ts or the built-in defaults shown below.

OptionTypeDefaultDescription
sourceLocalestring'en'Source locale used for descriptor fallback and build transforms
localesLocaleDefinition[]['en']Locale codes or objects (string | { code, name?, iso?, dir?, domain? }) available to the build
format'json' | 'po''po'Catalog file format
compileOutDirstring'./src/locales/compiled'Directory where compiled catalog files are read from
splitting'dynamic' | 'static' | falsefalseBuild-time locale chunking strategy (see Code splitting below)
defaultBuildLocalestringsourceLocaleLocale inlined for 'static' builds
includestring[]['./src/**/*.{vue,tsx,jsx,ts,js}']Source file patterns for dev watcher
excludestring[]['**/*.test.*', '**/*.spec.*', '**/__tests__/**', '**/*.d.ts']File patterns excluded from dev watcher
devAutoCompilebooleantrueAuto extract + compile on file change during dev
buildAutoCompilebooleantrueAuto extract + compile before production build
devAutoCompileDelaynumber300Debounce delay (ms) for dev auto-compile
parallelCompilebooleanfalseUse worker threads for compilation (effective with 5+ locales)
catalogExtensionstring'.js'File extension for compiled catalog files (e.g. '.mjs', '.json')
idGenerator(message: string, context?: string) => stringbuilt-in FNV-1aCustom message ID generator. Replaces the default hash-based ID strategy
onBeforeCompile() => boolean | void | Promise<...>Hook called before auto-compile. Return false to skip compilation
onAfterCompile() => void | Promise<void>Hook called after auto-compile completes

The plugin factory produces six ordered sub-plugins. The order is significant — each plugin depends on the output of the previous stage.

#Plugin nameEnforcePurpose
1fluenti:virtualResolves virtual:fluenti/* module IDs. Must be first.
2fluenti:vue-templatepre(Vue only) Pre-transforms v-t, <Trans>, <Plural> in SFC templates
3fluenti:script-transformpreAST scope-aware rewrite of t` ` tagged templates and <Trans> JSX optimization
4fluenti:build-compileTriggers extract + compile before production builds (buildStart hook)
5fluenti:build-splitRewrites t() calls to catalog references (dynamic/static)
6fluenti:devFile watcher + HMR for catalog changes during dev

The splitting option controls how translated messages are bundled for production.

All compiled catalogs are bundled together. Every locale’s messages ship in the initial bundle. Simplest setup, suitable for apps with few locales or small catalogs.

fluentiVue({
config: { splitting: false },
})

No virtual modules are generated. Your app imports compiled catalogs directly from compileOutDir.

Each locale is a separate chunk loaded on demand. Only the active locale is fetched; switching locales triggers a dynamic import().

fluentiVue({
config: {
splitting: 'dynamic',
locales: ['en', 'ja', 'zh-CN'],
},
})

At build time, the plugin:

  1. Rewrites t('message') calls to __catalog["<hash>"] references
  2. Injects import { __catalog } from 'virtual:fluenti/runtime' at the top of each transformed module
  3. Generates virtual:fluenti/runtime — a reactive module that exports __catalog, switchLocale(), and preloadLocale()
  4. Each locale’s catalog becomes a lazy chunk loaded via import()

This is the recommended strategy for multi-locale SPAs.

A single locale is inlined at build time. Other locales are stripped. Useful for pre-rendered or server-side builds that serve one locale per deployment.

fluentiVue({
config: {
splitting: 'static',
defaultBuildLocale: 'ja', // defaults to sourceLocale if omitted
},
})

At build time, the plugin:

  1. Rewrites t('message') calls to direct named imports (e.g. _a1b2c3)
  2. Injects import { _a1b2c3, ... } from 'virtual:fluenti/messages'
  3. Generates virtual:fluenti/messages — a re-export from the compiled catalog of defaultBuildLocale

This produces the smallest bundle for a single locale but requires a separate build per locale.

When splitting is enabled, the plugin serves generated code through Vite’s virtual module convention. These modules are implementation details — app code should use the framework runtime APIs (setLocale(), preloadLocale(), etc.) rather than importing virtual modules directly.

Virtual moduleSplitting modeContents
virtual:fluenti/runtime'dynamic'Reactive catalog object, switchLocale(), preloadLocale(), lazy import() loaders per locale
virtual:fluenti/messages'static'Re-export of all named exports from the defaultBuildLocale compiled catalog
virtual:fluenti/messages/<locale>anyRe-export of a specific locale’s compiled catalog

Virtual modules are resolved with the \0 prefix (Vite convention) so they are excluded from the file system and from other plugins’ transforms.

If your editor shows errors on virtual module imports, add a type declaration:

src/env.d.ts
declare module 'virtual:fluenti/runtime' {
export const __catalog: Record<string, (...args: unknown[]) => string>
export function switchLocale(locale: string): Promise<void>
export function preloadLocale(locale: string): Promise<void>
}
declare module 'virtual:fluenti/messages' {
const messages: Record<string, (...args: unknown[]) => string>
export = messages
}

The fluenti:script-transform plugin uses AST scope analysis to detect Fluenti’s t import and rewrite tagged templates at build time. Other variables named t are left untouched.

// Source (your code)
import { t } from '@fluenti/react'
const greeting = t`Hello ${name}`
// Build output (after transform)
const { t } = useI18n()
const greeting = t('Hello {name}', { name: name })

Supported import sources:

  • import { t } from '@fluenti/vue'
  • import { t } from '@fluenti/react'
  • import { t } from '@fluenti/solid'

Supported call forms:

  • t`Hello ${name}` — tagged template with interpolation
  • t({ message: 'Hello {name}', context: 'hero' }, { name }) — descriptor object form
  • t('literal string') — plain string (passed through for runtime lookup)

For JSX files (.tsx, .jsx), the plugin statically extracts the message string from <Trans> children, pre-computes the message ID, and injects __id, __message, and __components props. This eliminates the runtime cost of extractMessage() and hashMessage() without changing the component API.

// Source
<Trans>Hello <b>world</b></Trans>
// After transform — __id, __message, __components injected
<Trans __id="a1b2c3" __message="Hello <0>world</0>" __components={[({ children }) => <b>{children}</b>]}>
Hello <b>world</b>
</Trans>

The plugin adapts its behavior based on whether Vite is running serve (dev) or build (production).

FeatureBehavior
Auto extract + compileOn startup, runs extract then compile. On file changes matching include patterns, re-runs with debounce (devAutoCompileDelay ms). Disable with devAutoCompile: false.
HMRWhen compiled catalog files change in compileOutDir, the plugin invalidates all virtual:fluenti/* modules, triggering a hot update.
Script transformst` ` and <Trans> are transformed on every request (standard Vite transform pipeline).
Code splittingVirtual modules are served but splitting transforms are not applied — all catalogs are available for instant locale switching.
FeatureBehavior
Auto compileBefore buildStart, runs compile (extract is skipped — source files have not changed). Disable with buildAutoCompile: false.
Compile hooksonBeforeCompile runs before compilation. Return false to skip. onAfterCompile runs after successful compilation.
Script transformsSame as dev.
Code splittingfluenti:build-split rewrites t() calls to catalog references for dynamic/static splitting.
Parallel compileWhen parallelCompile: true, compilation uses worker threads (effective with 5+ locales).

Use onBeforeCompile and onAfterCompile to integrate with external tooling:

fluentiReact({
config: {
onBeforeCompile: async () => {
// Pull latest translations from a TMS
await fetchTranslationsFromCrowdin()
// Return false to skip compilation
// return false
},
onAfterCompile: async () => {
// Notify CI, run validation, etc.
console.log('Catalogs compiled successfully')
},
},
})

By default, Fluenti generates message IDs using FNV-1a hashing. You can replace this with a custom function:

fluentiVue({
config: {
idGenerator: (message, context) => {
// Your custom ID logic
const key = context ? `${context}::${message}` : message
return myCustomHash(key)
},
},
})

The idGenerator is used everywhere IDs are computed: extraction, compilation, and build-time code splitting transforms.

”Cannot find module ‘virtual:fluenti/runtime’”

Section titled “”Cannot find module ‘virtual:fluenti/runtime’””

This error appears when splitting is false (default) but your code imports from the virtual module. Virtual modules are only generated when splitting is set to 'dynamic' or 'static'. Either enable splitting or import compiled catalogs directly.

  1. Verify devAutoCompile is true (the default).
  2. Check that the changed source file matches the include patterns and is not excluded.
  3. Look for [fluenti] Extracting and compiling... done in the terminal. If it does not appear, the watcher is not triggering.
  4. Ensure compileOutDir matches the actual output directory of your compiled catalogs.

The auto-compile feature requires @fluenti/cli as a dev dependency. Install it:

Terminal window
pnpm add -D @fluenti/cli

If you manage compilation externally (e.g., in a CI step), disable auto-compile:

fluentiReact({
config: {
devAutoCompile: false,
buildAutoCompile: false,
},
})

Plugin ordering relative to the framework plugin matters:

  • Vue: vue() first, then fluentiVue() — Vue’s SFC compiler must process .vue files before Fluenti’s template transform runs.
  • React: fluentiReact() first, then react() — Fluenti’s JSX transforms must run before React’s JSX compilation.
  • Solid: solidPlugin() first, then fluentiSolid().

The scope transform only rewrites t when it is imported from a @fluenti/* package or destructured from useI18n(). Other variables named t (e.g., from a test library) are left untouched. If you still see unexpected rewrites, check that you do not have a conflicting import { t } from '...' statement.

Enable parallelCompile for projects with 5+ locales:

fluentiVue({
config: {
parallelCompile: true,
devAutoCompileDelay: 500, // increase debounce for heavy catalogs
},
})