Skip to content
fluenti

@fluenti/core

The framework-agnostic core engine. Zero runtime dependencies.

Create a Fluenti instance.

import { createFluentiCore } from '@fluenti/core'
const i18n = createFluentiCore({
locale: 'en',
fallbackLocale: 'en',
messages: {
en: { greeting: 'Hello {name}!' },
},
})
i18n.t('greeting', { name: 'World' }) // "Hello World!"

createFluentiCore() accepts a FluentConfigExtended object:

PropertyTypeDescription
localestringCurrent locale (validated — must be a non-empty string)
fallbackLocalestring?Fallback locale
messagesAllMessagesLocale-to-messages map
missing(locale, id) => string?Missing message handler
dateFormatsDateFormatOptions?Named date format styles
numberFormatsNumberFormatOptions?Named number format styles
fallbackChainRecord<string, Locale[]>?Locale fallback chains (e.g. { 'zh-TW': ['zh-CN', 'en'] })
transform(result, id, locale) => string?Post-translation transform hook (see below)
onLocaleChange(newLocale, prevLocale) => void?Callback fired when locale changes
formattersRecord<string, CustomFormatter>?Custom ICU function formatters (see below)
MethodDescription
t(id, values?)Translate a message
format(message, values?)Direct ICU interpolation without catalog lookup
d(value, style?)Format a date
n(value, style?)Format a number
setLocale(locale)Change active locale
loadMessages(locale, msgs)Add messages at runtime
getLocales()List available locales

Post-processing hook applied to every resolved message, after interpolation. Useful for pseudo-localization, profanity filtering, or analytics:

const i18n = createFluentiCore({
locale: 'en',
messages: { en },
transform: (result, id, locale) => {
// Example: pseudo-localize in dev
if (import.meta.env.DEV) return `[${result}]`
return result
},
})

Signature: (result: string, id: string, locale: Locale) => string

Fired whenever the locale changes via setLocale() or the locale property setter:

const i18n = createFluentiCore({
locale: 'en',
messages: { en, fr },
onLocaleChange: (newLocale, prevLocale) => {
document.documentElement.lang = newLocale
console.log(`Locale changed: ${prevLocale}${newLocale}`)
},
})

Signature: (newLocale: Locale, prevLocale: Locale) => void

Register custom ICU function handlers. When a {variable, functionName, style} node is encountered during interpolation, custom formatters are checked before the built-in Intl formatters:

const i18n = createFluentiCore({
locale: 'en',
messages: { en },
formatters: {
uppercase: (value, _style, _locale) => String(value).toUpperCase(),
list: (value, style, locale) =>
new Intl.ListFormat(locale, { type: style || 'conjunction' }).format(value as string[]),
},
})
// Usage in ICU messages:
// {name, uppercase} → "ALICE"
// {items, list, disjunction} → "apples, bananas, or cherries"

Type: Record<string, CustomFormatter> where CustomFormatter = (value: unknown, style: string, locale: Locale) => string

createFluentiCore() and setLocale() validate that the locale is a non-empty string. An error is thrown if the locale is empty, undefined, or not a string:

createFluentiCore({ locale: '', messages: {} })
// Error: [fluenti] createFluentiCore: locale must be a non-empty string, got ""
i18n.setLocale('')
// Error: [fluenti] setLocale: locale must be a non-empty string, got ""

Parse an ICU MessageFormat string into an AST.

import { parse } from '@fluenti/core'
const ast = parse('{count, plural, one {# item} other {# items}}')
ParameterTypeDescription
messagestringICU MessageFormat string

Returns: ASTNode[] — an array of AST nodes.

Node types:

  • text — Static text (TextNode)
  • variable{variable} placeholder (VariableNode)
  • plural{var, plural, ...} plural branches (PluralNode). Ordinal plurals use ordinal: true on the same node type
  • select{var, select, ...} selection branches (SelectNode)
  • function{var, functionName, style} formatting function (FunctionNode)

Compile an AST into a CompiledMessage.

import { parse, compile } from '@fluenti/core'
const ast = parse('Hello {name}!')
const fn = compile(ast)
// fn = (values) => `Hello ${values.name}!`
ParameterTypeDescription
astASTNode[]Parsed AST from parse()
localestring?Locale for plural rules (default: 'en')

Returns: CompiledMessage — either a string (for static messages) or (values?: Record<string, unknown>) => string (for dynamic messages with placeholders).

Parse, compile, and execute a message string in one call. Results are cached internally — repeated calls with the same message string skip parsing and compilation.

import { interpolate } from '@fluenti/core'
interpolate('{count, plural, one {# item} other {# items}}', { count: 5 }, 'en')
// → "5 items"
// Second call with same message string uses cached compiled function
interpolate('{count, plural, one {# item} other {# items}}', { count: 1 }, 'en')
// → "1 item"
ParameterTypeDescription
messagestringICU MessageFormat string
valuesRecord<string, unknown>?Interpolation values
localestring?Locale for plural rules (default: 'en')

Returns: string

Tagged template for lazy message descriptors. Use msg to define translatable messages outside of component render functions — in route metadata, constants, stores, or configuration.

import { msg } from '@fluenti/core'
// Creates a descriptor — does NOT translate immediately
const desc = msg`Hello ${name}`
// { id: 'abc123', message: 'Hello {0}' }
// With context for disambiguation
const desc2 = msg({ message: 'Save', context: 'button label' })
// { id: 'def456', message: 'Save', context: 'button label' }

A descriptor is a plain object with id and message properties. Pass it to t() at render time to get the translated string:

const { t } = useI18n()
t(desc) // → translated string

Compute the deterministic hash ID for a message string, with optional context for disambiguation:

import { resolveDescriptorId } from '@fluenti/core'
const id = resolveDescriptorId('Hello {name}', 'greeting')
  • detectLocale(options) — Detect locale from request context (cookie, query, path, Accept-Language header)
  • getSSRLocaleScript(locale, options?) — Generate a <script> tag that sets window.__FLUENTI_LOCALE__ for hydration. Pass { key: '__MY_APP_LOCALE__' } to customize the window variable name (useful for multi-instance / micro-frontends)
  • getHydratedLocale(fallback?, options?) — Read the locale set by the SSR script on the client side. Pass { key: '__MY_APP_LOCALE__' } to match a custom key used in getSSRLocaleScript
  • getDirection(locale) — Returns 'ltr' or 'rtl' for the given locale

negotiateLocale(requested, available, fallback?)

Section titled “negotiateLocale(requested, available, fallback?)”

Match user-preferred locale(s) against available locales. Matching strategy: exact match → language + region → language-only.

import { negotiateLocale } from '@fluenti/core'
negotiateLocale('zh-TW', ['en', 'zh-CN', 'ja']) // 'zh-CN' (language match)
negotiateLocale(['fr', 'en'], ['en', 'de']) // 'en' (second preference)
negotiateLocale('ko', ['en', 'ja'], 'en') // 'en' (fallback)
ParameterTypeDescription
requestedstring | string[]Single locale or ordered list of preferred locales
availablestring[]List of supported locales
fallbackstring?Fallback locale if no match is found

Returns: string — best matching locale.

Parse a BCP 47 locale string into its components.

import { parseLocale } from '@fluenti/core'
parseLocale('en') // { language: 'en' }
parseLocale('en-US') // { language: 'en', region: 'US' }
parseLocale('zh-Hans-CN') // { language: 'zh', script: 'Hans', region: 'CN' }

Returns: ParsedLocale{ language: string, script?: string, region?: string }

Check whether a locale uses a right-to-left script. Covers Arabic, Hebrew, Persian, Urdu, Pashto, Sindhi, Uyghur, Kurdish (Sorani), Dhivehi, Yiddish, and N’Ko.

import { isRTL } from '@fluenti/core'
isRTL('ar') // true
isRTL('ar-SA') // true
isRTL('en') // false

Validate that a locale string is a non-empty string. Throws an error if invalid.

import { validateLocale } from '@fluenti/core'
validateLocale('en', 'myFunction') // ok
validateLocale('', 'myFunction') // Error: [fluenti] myFunction: locale must be a non-empty string, got ""

Compute the deterministic FNV-1a hash ID for a message string with optional context.

import { hashMessage } from '@fluenti/core'
const id = hashMessage('Hello {name}')
const idWithCtx = hashMessage('Save', 'button label')

Build an ICU message string from tagged template parts. Used internally by t`...` and msg`...`.

import { buildICUMessage } from '@fluenti/core'
// Equivalent to what t`Hello ${name}, you have ${count} items` produces:
const icu = buildICUMessage(['Hello ', ', you have ', ' items'], ['Alice', 5])
// → 'Hello {arg0}, you have {arg1} items'

formatDate(value, locale, style?, styles?)

Section titled “formatDate(value, locale, style?, styles?)”

Format a date according to locale and an optional named style. Built-in styles: default, short, long, time, datetime, relative.

import { formatDate } from '@fluenti/core'
formatDate(new Date(), 'en') // default format
formatDate(new Date(), 'en', 'long') // "Thursday, March 19, 2026"
formatDate(new Date(), 'en', 'relative') // "now" (delegates to formatRelativeTime)

formatNumber(value, locale, style?, styles?)

Section titled “formatNumber(value, locale, style?, styles?)”

Format a number according to locale and an optional named style. Built-in styles: default, currency, percent, decimal.

import { formatNumber } from '@fluenti/core'
formatNumber(1234.5, 'en') // "1,234.5"
formatNumber(0.75, 'en', 'percent') // "75%"
formatNumber(99.99, 'en', 'currency') // "$99.99"

Format a date as a relative time string (e.g. “2 days ago”, “in 3 hours”). Automatically selects the best unit. Uses Intl.RelativeTimeFormat.

import { formatRelativeTime } from '@fluenti/core'
const yesterday = Date.now() - 86400000
formatRelativeTime(yesterday, 'en') // "yesterday"
import {
DEFAULT_DATE_FORMATS, // Built-in date format styles
DEFAULT_NUMBER_FORMATS, // Built-in number format styles
LOCALE_CURRENCY_MAP, // Locale → default currency code mapping
} from '@fluenti/core'

Clear all internal caches (interpolation, compilation, plural rules, date/number/relative-time formatters). Useful for long-running servers to prevent unbounded memory growth.

import { clearAllCaches } from '@fluenti/core'
clearAllCaches()
CacheDefault sizePurpose
Compiled messages500 (LRU)Avoids re-parsing identical ICU messages
Intl.NumberFormat200 (LRU)Caches formatter instances per locale + options
Intl.DateTimeFormat200 (LRU)Caches formatter instances per locale + options
Intl.RelativeTimeFormat200 (LRU)Caches formatter instances per locale

Use setMessageCacheSize(n) to adjust the compiled message cache. The value must be a positive integer (minimum 1).

In-memory catalog manager for compiled messages. Supports namespace-aware message IDs and locale enumeration.

import { Catalog } from '@fluenti/core'
const catalog = new Catalog()
catalog.set('en', { greeting: 'Hello!' })
catalog.get('en', 'greeting') // 'Hello!'
catalog.has('en', 'greeting') // true
catalog.getLocales() // ['en']
MethodDescription
get(locale, id)Retrieve a compiled message. Returns undefined if not found
set(locale, messages)Load messages for a locale (merges with existing)
has(locale, id)Check if a message exists
getLocales()List all locales with loaded messages

Error thrown when parsing an ICU MessageFormat string fails. Includes offset and source properties for diagnostic context.

import { parse, FluentParseError } from '@fluenti/core'
try {
parse('{count, plural, }')
} catch (e) {
if (e instanceof FluentParseError) {
console.log(e.offset) // position of the error
console.log(e.source) // original source string
}
}
ErrorCause
Expected identifierEmpty braces { } or invalid character after {
Unterminated placeholderMissing closing } (e.g. Hello {name)
Expected closing }Mismatched braces in function or plural/select
Plural/selectordinal must have an 'other' optionMissing required other branch
Maximum nesting depth of 10 exceededICU message nesting too deep
Identifier exceeds maximum length of 256Variable name longer than 256 characters

The ICU parser enforces the following limits:

  • Maximum nesting depth: 10 levels of nested plural/select
  • Maximum identifier length: 256 characters per variable name
  • Unterminated placeholders: Always detected and reported with position info

Built-in formatters (number, date, time) catch Intl errors and return a fallback placeholder in development mode with a console.warn. In production, the fallback is returned silently.

Custom formatters registered via createFluentiCore({ formatters }) are also wrapped in try-catch — a throwing formatter will not crash the render.

resolvePlural(count, options, locale, ordinal?)

Section titled “resolvePlural(count, options, locale, ordinal?)”

Resolve a plural branch from a set of options using Intl.PluralRules. Checks for exact matches (=0, =1, etc.) before CLDR categories.

resolvePluralCategory(count, options, locale, ordinal?)

Section titled “resolvePluralCategory(count, options, locale, ordinal?)”

Same as resolvePlural, but returns the matched category key (e.g. 'one', 'other') rather than the branch value.