@fluenti/core
v0.6.3
Published
Framework-agnostic compile-time i18n core — ICU parser, compiler, interpolation, formatters
Maintainers
Readme
@fluenti/core
Compile-time i18n that disappears from your bundle.
Fluenti compiles ICU MessageFormat strings into optimized functions at build time -- no parser ships to the browser, no runtime overhead, fully type-safe. What you write in development becomes raw string concatenation in production.
What the compiler does
You write expressive i18n using tagged templates or ICU syntax:
// Source — what you write
import { msg } from '@fluenti/core'
const greeting = msg`Hello ${name}, you have ${count} messages`
i18n.t(greeting, { name: 'Yuki', count: 3 })At build time, Fluenti compiles your messages into minimal functions -- the ICU parser never ships to the browser:
// Output — what runs in production
const greeting = (v) => "Hello " + v.name + ", you have " + v.count + " messages"Static messages with no variables compile down to plain strings. Zero overhead, zero waste.
Features
- ICU MessageFormat -- plurals, selects, nested arguments, ordinals,
#substitution - Compile-time transforms -- messages become optimized functions; no runtime parsing
- Hash-based message IDs -- deterministic FNV-1a hashes from content, no manual key management
- CLDR plural rules -- per-locale plural category resolution (
zero,one,two,few,many,other) - Intl formatters -- thin wrappers around
Intl.DateTimeFormat,Intl.NumberFormat,Intl.RelativeTimeFormat - SSR-safe -- locale detection from cookies, query params, URL paths, or headers
- Dual-mode
tfunction --t('message.id', { values })for catalog lookup ort`Hello ${name}`as a tagged template literal; returned fromuseI18n()in all framework packages - Tree-shakeable -- import only what you use; dead code is eliminated
- Fallback chains -- locale-specific and wildcard (
*) fallback resolution - Custom number/date styles -- define reusable format presets per locale
Quick start
pnpm add @fluenti/coreMost users install a framework package instead (
@fluenti/vue,@fluenti/solid,@fluenti/react,@fluenti/next) which includes@fluenti/coreas a dependency.
Create an instance
import { createFluentiCore } from '@fluenti/core'
const i18n = createFluentiCore({
locale: 'en',
fallbackLocale: 'en',
messages: {
en: {
greeting: 'Hello {name}!',
items: (v) => `You have ${new Intl.PluralRules('en').select(v.count) === 'one' ? '1 item' : v.count + ' items'}`,
},
ja: {
greeting: 'こんにちは {name}!',
},
},
})
i18n.t('greeting', { name: 'World' }) // "Hello World!"Advanced configuration
createFluentiCore() accepts additional options on FluentConfigExtended:
const i18n = createFluentiCore({
locale: 'en',
fallbackLocale: 'en',
messages: { en },
// Post-translation transform applied to every resolved message
transform: (result, id, locale) => result.toUpperCase(),
// 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 as string[]),
},
})| Option | Type | Description |
|--------|------|-------------|
| transform | (result: string, id: string, locale: Locale) => string | Post-interpolation hook applied to every resolved message |
| onLocaleChange | (newLocale: Locale, prevLocale: Locale) => void | Callback fired on setLocale() or locale property assignment |
| formatters | Record<string, CustomFormatter> | Custom ICU function formatters keyed by function name |
Parse and compile ICU messages
import { parse, compile } from '@fluenti/core'
const ast = parse('{count, plural, one {# item} other {# items}}')
const message = compile(ast, 'en')
message({ count: 1 }) // "1 item"
message({ count: 42 }) // "42 items"Tagged template literals
import { msg } from '@fluenti/core'
// Generates a deterministic hash ID + ICU message automatically
const desc = msg`Hello ${name}`
// { id: 'abc123', message: 'Hello {0}' }
// Or declare explicit descriptors for extraction
const explicit = msg.descriptor({
id: 'welcome.title',
message: 'Welcome back, {name}!',
})Formatting
i18n.d(new Date(), 'long') // "March 17, 2026"
i18n.n(1234.5, 'currency') // "$1,234.50"SSR locale detection
import { detectLocale } from '@fluenti/core'
const locale = detectLocale({
cookie: request.headers.get('cookie'),
headers: request.headers,
available: ['en', 'ja', 'zh-CN'],
fallback: 'en',
})ICU MessageFormat examples
Simple: Hello {name}!
Plural: {count, plural, one {# item} other {# items}}
Ordinal: {pos, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}
Select: {gender, select, male {He} female {She} other {They}} left
Nested: {count, plural, one {1 message from {sender}} other {# messages from {sender}}}
Number: {price, number, currency}
Date: {when, date, short}Framework integrations
| Package | Framework |
|---------|-----------|
| @fluenti/vue | Vue 3 -- <Trans>, <Plural>, <Select>, useI18n() |
| @fluenti/solid | SolidJS -- <Trans>, <Plural>, <Select>, <DateTime>, <NumberFormat>, useI18n() |
| @fluenti/react | React 19 -- <Trans>, <Plural>, <Select>, <DateTime>, <NumberFormat>, useI18n() |
| @fluenti/next | Next.js 15 -- withFluenti(), RSC support, streaming |
| @fluenti/nuxt | Nuxt 3 -- locale-prefixed routing, SEO, auto locale detection |
| @fluenti/vue-i18n-compat | vue-i18n migration bridge -- run both side by side |
Documentation
Full docs at fluenti.dev.
