@fluenti/vue
v0.6.3
Published
Vue 3 compile-time i18n — v-t directive, Trans/Plural/Select components, useI18n composable
Maintainers
Readme
@fluenti/vue
Compile-time i18n for Vue 3. Zero runtime parsing. Type-safe templates.
Your translations are compiled at build time into optimized functions — no ICU parser ships to the browser, no runtime overhead, just fast string lookups. Write natural language in your templates; Fluenti handles the rest.
The Magic: Write Natural Language, Ship Optimized Code
The v-t directive (compiled away at build time)
v-t is not a runtime directive. It is a Vue compiler transform that rewrites your templates during build. What you write and what ships to production are two different things:
<!-- What you write -->
<template>
<h1 v-t>Welcome to our app!</h1>
<p v-t>Hello, {name}!</p>
<p v-t.plural="count">one apple | {count} apples</p>
<img v-t.alt alt="Hero banner" src="/hero.png" />
</template><!-- What ships to the browser (conceptual) -->
<template>
<h1>{{ $t('a1b2c3') }}</h1>
<p>{{ $t('d4e5f6', { name }) }}</p>
<p>{{ $t('{count, plural, one {# apple} other {# apples}}', { count }) }}</p>
<img :alt="$t('f7g8h9')" src="/hero.png" />
</template>No parsing. No interpretation. Just a hash lookup into a pre-compiled catalog. This is what compile-time i18n means.
The t tagged template (in <script setup>)
For programmatic translations outside of templates, use msg with useI18n():
<script setup lang="ts">
import { useI18n } from '@fluenti/vue'
const { t } = useI18n()
const greeting = t('Hello, {name}!', { name: 'World' })
const formatted = t('You have {count} items', { count: 42 })
</script>Quick Start
1. Install
pnpm add @fluenti/core @fluenti/vue2. Configure Vite
// vite.config.ts
import vue from '@vitejs/plugin-vue'
import fluentiVue from '@fluenti/vue/vite-plugin'
export default {
plugins: [vue(), fluentiVue()],
}3. Create the plugin
// main.ts
import { createApp } from 'vue'
import { createFluenti } from '@fluenti/vue'
import App from './App.vue'
import en from './locales/compiled/en'
import ja from './locales/compiled/ja'
const fluent = createFluenti({
locale: 'en',
fallbackLocale: 'en',
messages: { en, ja },
})
const app = createApp(App)
app.use(fluent)
app.mount('#app')4. Use in your SFC
<script setup>
import { useI18n } from '@fluenti/vue'
const { locale, setLocale } = useI18n()
</script>
<template>
<h1 v-t>Hello, world!</h1>
<p v-t>Welcome to our application.</p>
<button @click="setLocale('ja')">日本語</button>
</template>That's it. Four files, zero boilerplate.
Components
<Trans> — Rich Text with HTML and Components
Translate text that contains inline HTML elements or Vue components. Child elements are preserved through the translation round-trip:
<template>
<Trans>Read the <a href="/docs">documentation</a></Trans>
<Trans>Click <RouterLink to="/next">here</RouterLink> to continue.</Trans>
</template>| Prop | Type | Default | Description |
|-------|----------|----------|-----------------|
| tag | string | 'span' | Wrapper element |
<Plural> — Plural Forms
Full ICU plural support as a component. Use string props for simple cases, or named slots for rich text:
<template>
<!-- Simple: string props -->
<Plural :value="count" zero="No items" one="1 item" other="{count} items" />
<!-- Rich: named slots -->
<Plural :value="count">
<template #zero>No <strong>items</strong> left</template>
<template #one><em>1</em> item remaining</template>
<template #other><strong>{{ count }}</strong> items remaining</template>
</Plural>
</template>| Prop | Type | Default | Description |
|---------|----------|----------|--------------------------------------|
| value | number | required | The count to pluralize on |
| zero | string | -- | Text for zero items (ICU =0) |
| one | string | -- | Singular form |
| two | string | -- | Dual form |
| few | string | -- | Few form (some languages) |
| many | string | -- | Many form (some languages) |
| other | string | '' | Default/fallback form |
| tag | string | 'span' | Wrapper element |
Slots: #zero, #one, #two, #few, #many, #other -- each receives { count } as slot props.
<Select> — Gender / Option Selection
ICU select patterns as a component:
<template>
<!-- String props -->
<Select :value="gender" male="He liked this" female="She liked this" other="They liked this" />
<!-- Options map (recommended for dynamic keys) -->
<Select :value="role" :options="{ admin: 'Administrator', editor: 'Editor' }" other="Viewer" />
<!-- Rich text via slots -->
<Select :value="gender">
<template #male><strong>He</strong> liked this</template>
<template #female><strong>She</strong> liked this</template>
<template #other><em>They</em> liked this</template>
</Select>
</template>| Prop | Type | Default | Description |
|-----------|---------------------------|----------|------------------------------------|
| value | string | required | The value to match against |
| options | Record<string, string> | -- | Named options map |
| other | string | '' | Fallback when no option matches |
| tag | string | 'span' | Wrapper element |
Slots: Named slots matching option keys, each receives { value } as slot props.
v-t Directive Reference
| Modifier | Effect |
|----------------|-----------------------------------|
| (none) | Translates text content |
| .plural | Enables plural forms (pipe syntax)|
| .alt | Translates the alt attribute |
| .placeholder | Translates the placeholder attribute |
| .title | Translates the title attribute |
| .{attr} | Translates any named attribute |
<template>
<h1 v-t>Hello, world!</h1>
<p v-t.plural="count">one apple | {count} apples</p>
<img v-t.alt alt="Welcome banner" src="banner.png" />
<input v-t.placeholder placeholder="Search..." />
</template>useI18n() Composable
The full API surface, available inside any component with the plugin installed:
<script setup>
import { useI18n } from '@fluenti/vue'
const { t, d, n, locale, setLocale } = useI18n()
</script>
<template>
<p>{{ t('Hello, {name}!', { name: 'World' }) }}</p>
<p>{{ d(new Date(), 'long') }}</p>
<p>{{ n(1234.5, 'currency') }}</p>
<button @click="setLocale('ja')">日本語</button>
</template>| Method | Signature | Description |
|-----------------|----------------------------------------------------------------|----------------------------------------------------------|
| t | (id: string \| MessageDescriptor, values?) => string | Translate a key with optional interpolation |
| d | (value: Date \| number, style?) => string | Format a date using dateFormats presets or Intl |
| n | (value: number, style?) => string | Format a number using numberFormats presets or Intl |
| format | (message: string, values?) => string | Format an ICU message string directly (no catalog lookup)|
| te | (key: string, locale?) => boolean | Check if a translation key exists |
| tm | (key: string, locale?) => CompiledMessage \| undefined | Get the raw compiled message without interpolation |
| locale | Ref<string> (readonly) | Reactive ref for the current locale |
| setLocale | (locale: string) => Promise<void> | Change locale (async when lazy locale loading is enabled) |
| loadMessages | (locale: string, messages: Messages) => void | Dynamically add messages for a locale at runtime |
| getLocales | () => string[] | Get all locales that have loaded messages |
| preloadLocale | (locale: string) => void | Preload a locale chunk in the background |
| isLoading | Ref<boolean> (readonly) | Whether a locale chunk is currently being loaded |
| loadedLocales | Ref<ReadonlySet<string>> (readonly) | Set of locales whose messages have been loaded |
Plugin Options
const fluent = createFluenti({
// Required
locale: 'en',
messages: { en, ja, zh },
// Optional
fallbackLocale: 'en',
fallbackChain: { 'zh-TW': ['zh', 'en'], '*': ['en'] },
dateFormats: {
short: { year: 'numeric', month: 'short', day: 'numeric' },
long: { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' },
relative: 'relative',
},
numberFormats: {
currency: { style: 'currency', currency: 'USD' },
percent: { style: 'percent', minimumFractionDigits: 1 },
},
missing: (locale, id) => `[missing: ${id}]`,
componentPrefix: 'I18n', // registers I18nTrans, I18nPlural, I18nSelect
lazyLocaleLoading: true,
chunkLoader: (locale) => import(`./locales/${locale}.js`),
// Post-translation transform applied to every resolved message
transform: (result, id, locale) => result,
// Callback fired whenever the locale changes
onLocaleChange: (newLocale, prevLocale) => {
document.documentElement.lang = newLocale
},
// Custom ICU function formatters (e.g. {items, list})
formatters: {
list: (value, style, locale) =>
new Intl.ListFormat(locale, { type: style || 'conjunction' }).format(value),
},
})| Option | Type | Default | Description |
|-------------------|------------------------------------|---------|----------------------------------------------------------|
| locale | string | -- | Active locale code (required) |
| messages | Record<string, Messages> | -- | Pre-loaded message catalogs (required) |
| fallbackLocale | string | -- | Locale to try when a key is missing |
| fallbackChain | Record<string, string[]> | -- | Locale-specific fallback chains ('*' for default) |
| dateFormats | Record<string, DateTimeFormatOptions \| 'relative'> | -- | Named date format presets for d() |
| numberFormats | Record<string, NumberFormatOptions> | -- | Named number format presets for n() |
| missing | (locale, id) => string \| undefined | -- | Handler called when a translation key is not found |
| componentPrefix | string | '' | Prefix for globally registered components |
| lazyLocaleLoading | boolean | false | Enable async locale loading through chunkLoader |
| chunkLoader | (locale) => Promise<Messages> | -- | Async loader for locale chunks (requires lazyLocaleLoading) |
| transform | (result: string, id: string, locale: string) => string | -- | Post-interpolation hook applied to every resolved message |
| onLocaleChange | (newLocale: string, prevLocale: string) => void | -- | Callback fired whenever the locale changes via setLocale() |
| formatters | Record<string, CustomFormatter> | -- | Custom ICU function formatters keyed by function name |
Code Splitting
For large apps, load locale messages on demand instead of bundling everything upfront:
const fluent = createFluenti({
locale: 'en',
messages: { en }, // only the default locale is bundled
lazyLocaleLoading: true,
chunkLoader: (locale) => import(`./locales/compiled/${locale}.js`),
})
// Later: switching locale triggers async loading
await fluent.global.setLocale('ja') // loads ja chunk, then switchesPreload locales in the background for instant switching:
<script setup>
const { setLocale, isLoading, preloadLocale } = useI18n()
</script>
<template>
<button @mouseenter="preloadLocale('ja')" @click="setLocale('ja')" :disabled="isLoading">
{{ isLoading ? 'Loading...' : '日本語' }}
</button>
</template>SSR / SSG Support
createFluenti() creates entirely fresh state per call -- no module-level singletons. Call it once per SSR request to avoid locale leaking between users.
For server-side i18n in Nuxt or custom Vue SSR setups, use the dedicated server utilities:
import { createServerI18n, detectLocale, getSSRLocaleScript } from '@fluenti/vue/server'
const { setLocale, getI18n } = createServerI18n({
loadMessages: (locale) => import(`./locales/compiled/${locale}.js`),
fallbackLocale: 'en',
})SSR utilities included:
createServerI18n()-- per-request i18n with lazy message loading and cachingdetectLocale()-- locale detection from cookies, headers, path, or querygetSSRLocaleScript()-- inline script tag for hydrationgetHydratedLocale()-- read the hydrated locale on the clientisRTL()/getDirection()-- RTL/LTR detection
Documentation
Full documentation at fluenti.dev.
