Skip to content
fluenti

Best Practices

Fluenti’s compile-time architecture gives you a head start on performance, but the way you organise messages, structure catalogs, and wire up SSR still matters at scale. This guide collects the patterns that work best in production.

Fluenti’s core advantage is compile-time message transformation — the Vite plugin rewrites your code at build time so no ICU parsing happens in the browser. Always reach for compile-time APIs first:

PriorityAPIWhereWhy
1v-t directiveVue templatesZero-runtime — compiled away entirely
2t`…` tagged template<script setup> / JS modulesAuto-reactive, auto-extracted, natural syntax
3<Trans> / <Plural> / <Select>Templates (all frameworks)Rich text with embedded elements
4msg tagged templateOutside components (routes, stores)Lazy descriptor — resolved later by t()
5t() function callFallbackManual — use only when the above don’t fit

Vue: use v-t in templates, t`…` in script

Section titled “Vue: use v-t in templates, t`…` in script”

v-t is a compile-time nodeTransform, not a runtime directive. It rewrites your template during Vue’s SFC compilation — nothing reaches the browser:

<template>
<!-- ✅ v-t: compiled away, zero runtime cost -->
<h1 v-t>Welcome back, {{ name }}!</h1>
<p v-t>Read the <a href="/terms">terms</a> and <strong>conditions</strong></p>
<img v-t.alt src="/hero.jpg" alt="Welcome banner" />
<!-- ❌ Avoid: manual t() in templates -->
<h1>{{ t('Welcome back, {name}!', { name }) }}</h1>
</template>
<script setup>
// ✅ t`` tagged template: auto-reactive (wrapped in computed)
const pageTitle = t`Welcome back, ${name}!`
// ❌ Avoid: manual t() requires you to manage reactivity yourself
const pageTitle = computed(() => t('Welcome back, {name}!', { name: name.value }))
</script>
function Greeting({ name, count }) {
// ✅ t`` tagged template — natural syntax, auto-extracted
const greeting = t`Welcome back, ${name}!`
const itemLabel = t`${count} items in your cart`
return (
<div>
<h1>{greeting}</h1>
<p>{itemLabel}</p>
{/* ✅ <Trans> for rich text with embedded elements */}
<Trans>Read the <a href="/terms">terms</a> and <strong>conditions</strong></Trans>
{/* ✅ <Plural> for plural forms */}
<Plural value={count} one="# item" other="# items" />
</div>
)
}

Use t() only in situations where compile-time APIs don’t apply:

// Dynamic message IDs (rare — avoid if possible)
const key = getMessageKeyFromAPI()
t(key)
// Inside non-component utility functions
function formatError(code: number) {
return t(`Error ${code}: something went wrong`)
}

Natural-language keys are self-documenting and let the source locale work without a catalog lookup:

<!-- ✅ Natural key — readable, self-documenting -->
<p v-t>Welcome back, {{ name }}!</p>
<!-- ❌ Dot-path key — requires a catalog just to understand the UI -->
<p>{{ t('home.welcome_back', { name }) }}</p>

Keep messages close to where they are used

Section titled “Keep messages close to where they are used”

Colocate messages with the component that renders them. When a message lives three directories away, nobody updates it when the UI changes.

Route metadata, constants, and store definitions run outside the component lifecycle. Wrap them with msg so the CLI can still extract them:

import { msg } from '@fluenti/core'
export const routes = [
{ path: '/', meta: { title: msg`Home` } },
{ path: '/about', meta: { title: msg`About Us` } },
]
// ❌ Breaks in Japanese, Arabic, German…
t`Hello` + ' ' + name + ', ' + t`you have` + ' ' + count + ' ' + t`items`
// ✅ One message, one translation unit
t`Hello ${name}, you have ${count} items`

For plurals, use <Plural> or ICU {count, plural, …} syntax instead of hand-rolling count === 1 ? … : … conditionals.

Fluenti compiles ICU messages at build time, producing plain functions that execute with no parsing overhead at runtime.

Build time Runtime
┌──────────────┐ ┌──────────────────────┐
│ ICU source │──compile──► (values) => string │
│ "{count, │ │ // no parsing, just │
│ plural,…}" │ │ // string concat │
└──────────────┘ └──────────────────────┘

Only the source locale should be bundled. All others should load on demand:

vite.config.ts
FluentiPlugin({
sourceLocale: 'en',
splitting: 'dynamic', // non-en locales loaded on demand
})
// ❌ Resolves on every iteration
items.map((item) => `${t`Item`}: ${item.name}`)
// ✅ Resolve once outside the loop, reuse the value
const label = t`Item`
items.map((item) => `${label}: ${item.name}`)
cookie → query → path → Accept-Language → fallback
import { getDirection } from '@fluenti/core'
const dir = getDirection(locale) // 'ltr' or 'rtl'

For framework-specific SSR setup, see:

import { mount } from '@vue/test-utils'
import { createFluentVue } from '@fluenti/vue'
const i18n = createFluentVue({
locale: 'en',
messages: {
en: { greeting: 'Hello {name}' },
},
})
const wrapper = mount(MyComponent, {
global: { plugins: [i18n] },
})

Verify locale switching updates all translations

Section titled “Verify locale switching updates all translations”
await setLocale('ja')
expect(screen.getByText('こんにちは Alice')).toBeTruthy()
const { container } = render(<Trans>Read the <a href="/docs">docs</a></Trans>)
expect(container.innerHTML).toMatchSnapshot()

The extract → translate → compile loop

Section titled “The extract → translate → compile loop”
┌─────────────────────────────────────────┐
│ 1. fluenti extract │
│ Scan source → update PO/JSON files │
│ │
│ 2. Translate │
│ Translators edit PO files │
│ (or sync via Crowdin / Weblate) │
│ │
│ 3. fluenti compile │
│ PO → optimised JS modules │
└─────────────────────────────────────────┘
Terminal window
$ fluenti stats
Locale Total Translated %
────── ───── ────────── ──────
en 142 142 100%
ja 142 128 90%
zh-CN 142 97 68%

PO is the native format for Crowdin, Weblate, Lokalise, and Transifex. Point them at your locales/ directory and they will round-trip cleanly — no format conversion needed.

Anti-patternFix
Using t() function calls everywhereUse v-t, t`…`, <Trans> — compile-time first
Concatenating translated stringsOne message per translation unit: t`Hello ${name}`
Global mutable i18n state in SSRCreate a new instance per request
Runtime ICU parsingUse fluenti compile — parsing is build-only
Eagerly importing every localeUse splitting: 'dynamic' for lazy loading
Hardcoding dir="ltr"Use getDirection(locale) to handle RTL
Storing locale in localStorage for SSR appsUse cookies — visible to both server and client
Committing compiled outputGitignore locales/compiled/, compile in CI