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.
Prefer Compile-Time APIs
Section titled “Prefer Compile-Time APIs”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:
| Priority | API | Where | Why |
|---|---|---|---|
| 1 | v-t directive | Vue templates | Zero-runtime — compiled away entirely |
| 2 | t`…` tagged template | <script setup> / JS modules | Auto-reactive, auto-extracted, natural syntax |
| 3 | <Trans> / <Plural> / <Select> | Templates (all frameworks) | Rich text with embedded elements |
| 4 | msg tagged template | Outside components (routes, stores) | Lazy descriptor — resolved later by t() |
| 5 | t() function call | Fallback | Manual — 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 yourselfconst pageTitle = computed(() => t('Welcome back, {name}!', { name: name.value }))</script>React & Solid: use t`…` and <Trans>
Section titled “React & Solid: use t`…` and <Trans>”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> )}When t() is appropriate
Section titled “When t() is appropriate”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 functionsfunction formatError(code: number) { return t(`Error ${code}: something went wrong`)}Message Authoring
Section titled “Message Authoring”Use natural language as keys
Section titled “Use natural language as keys”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.
Use msg for messages outside components
Section titled “Use msg for messages outside components”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` } },]Never concatenate translated strings
Section titled “Never concatenate translated strings”// ❌ Breaks in Japanese, Arabic, German…t`Hello` + ' ' + name + ', ' + t`you have` + ' ' + count + ' ' + t`items`
// ✅ One message, one translation unitt`Hello ${name}, you have ${count} items`For plurals, use <Plural> or ICU {count, plural, …} syntax instead of
hand-rolling count === 1 ? … : … conditionals.
Performance
Section titled “Performance”Compile-time advantage
Section titled “Compile-time advantage”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 │└──────────────┘ └──────────────────────┘Lazy-load non-default locales
Section titled “Lazy-load non-default locales”Only the source locale should be bundled. All others should load on demand:
FluentiPlugin({ sourceLocale: 'en', splitting: 'dynamic', // non-en locales loaded on demand})Cache translations in tight loops
Section titled “Cache translations in tight loops”// ❌ Resolves on every iterationitems.map((item) => `${t`Item`}: ${item.name}`)
// ✅ Resolve once outside the loop, reuse the valueconst label = t`Item`items.map((item) => `${label}: ${item.name}`)SSR & Hydration
Section titled “SSR & Hydration”Detection priority
Section titled “Detection priority”cookie → query → path → Accept-Language → fallbackAlways set <html lang> and <html dir>
Section titled “Always set <html lang> and <html dir>”import { getDirection } from '@fluenti/core'const dir = getDirection(locale) // 'ltr' or 'rtl'Persist with a cookie
Section titled “Persist with a cookie”For framework-specific SSR setup, see:
- SSR & Hydration — cross-framework concepts
- Next.js · Nuxt · SolidStart
Testing
Section titled “Testing”Mock catalogs in unit tests
Section titled “Mock catalogs in unit tests”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] },})import { render } from '@testing-library/react'import { I18nProvider } from '@fluenti/react'
function renderWithI18n(ui, { locale = 'en', messages = {} } = {}) { return render( <I18nProvider locale={locale} messages={{ [locale]: messages }}> {ui} </I18nProvider> )}
renderWithI18n(<Greeting name="Alice" />, { messages: { greeting: 'Hello {name}' },})import { render } from '@solidjs/testing-library'import { I18nProvider } from '@fluenti/solid'
render(() => ( <I18nProvider config={{ locale: 'en', messages: { en: { greeting: 'Hello {name}' } }, }}> <Greeting name="Alice" /> </I18nProvider>))Verify locale switching updates all translations
Section titled “Verify locale switching updates all translations”await setLocale('ja')expect(screen.getByText('こんにちは Alice')).toBeTruthy()Snapshot rich-text translations
Section titled “Snapshot rich-text translations”const { container } = render(<Trans>Read the <a href="/docs">docs</a></Trans>)expect(container.innerHTML).toMatchSnapshot()Translation Workflow
Section titled “Translation Workflow”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 │└─────────────────────────────────────────┘Track progress with fluenti stats
Section titled “Track progress with fluenti stats”$ fluenti stats
Locale Total Translated %────── ───── ────────── ──────en 142 142 100%ja 142 128 90%zh-CN 142 97 68%Run compilation in CI, not locally
Section titled “Run compilation in CI, not locally”Integrate with translation platforms
Section titled “Integrate with translation platforms”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.
Common Mistakes
Section titled “Common Mistakes”| Anti-pattern | Fix |
|---|---|
Using t() function calls everywhere | Use v-t, t`…`, <Trans> — compile-time first |
| Concatenating translated strings | One message per translation unit: t`Hello ${name}` |
| Global mutable i18n state in SSR | Create a new instance per request |
| Runtime ICU parsing | Use fluenti compile — parsing is build-only |
| Eagerly importing every locale | Use splitting: 'dynamic' for lazy loading |
Hardcoding dir="ltr" | Use getDirection(locale) to handle RTL |
Storing locale in localStorage for SSR apps | Use cookies — visible to both server and client |
| Committing compiled output | Gitignore locales/compiled/, compile in CI |