@fluenti/core
The framework-agnostic core engine. Zero runtime dependencies.
createFluentiCore(config)
Section titled “createFluentiCore(config)”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!"Config
Section titled “Config”createFluentiCore() accepts a FluentConfigExtended object:
| Property | Type | Description |
|---|---|---|
locale | string | Current locale (validated — must be a non-empty string) |
fallbackLocale | string? | Fallback locale |
messages | AllMessages | Locale-to-messages map |
missing | (locale, id) => string? | Missing message handler |
dateFormats | DateFormatOptions? | Named date format styles |
numberFormats | NumberFormatOptions? | Named number format styles |
fallbackChain | Record<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 |
formatters | Record<string, CustomFormatter>? | Custom ICU function formatters (see below) |
Instance methods
Section titled “Instance methods”| Method | Description |
|---|---|
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 |
transform hook
Section titled “transform hook”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
onLocaleChange callback
Section titled “onLocaleChange callback”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
Custom formatters
Section titled “Custom formatters”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
Locale validation
Section titled “Locale validation”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(message)
Section titled “parse(message)”Parse an ICU MessageFormat string into an AST.
import { parse } from '@fluenti/core'const ast = parse('{count, plural, one {# item} other {# items}}')| Parameter | Type | Description |
|---|---|---|
message | string | ICU 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 useordinal: trueon the same node typeselect—{var, select, ...}selection branches (SelectNode)function—{var, functionName, style}formatting function (FunctionNode)
compile(ast, locale?)
Section titled “compile(ast, locale?)”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}!`| Parameter | Type | Description |
|---|---|---|
ast | ASTNode[] | Parsed AST from parse() |
locale | string? | 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).
interpolate(message, values?, locale?)
Section titled “interpolate(message, values?, locale?)”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 functioninterpolate('{count, plural, one {# item} other {# items}}', { count: 1 }, 'en')// → "1 item"| Parameter | Type | Description |
|---|---|---|
message | string | ICU MessageFormat string |
values | Record<string, unknown>? | Interpolation values |
locale | string? | 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 immediatelyconst desc = msg`Hello ${name}`// { id: 'abc123', message: 'Hello {0}' }
// With context for disambiguationconst 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 stringresolveDescriptorId(message, context?)
Section titled “resolveDescriptorId(message, context?)”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')SSR Utilities
Section titled “SSR Utilities”detectLocale(options)— Detect locale from request context (cookie, query, path, Accept-Language header)getSSRLocaleScript(locale, options?)— Generate a<script>tag that setswindow.__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 ingetSSRLocaleScriptgetDirection(locale)— Returns'ltr'or'rtl'for the given locale
Locale Utilities
Section titled “Locale Utilities”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)| Parameter | Type | Description |
|---|---|---|
requested | string | string[] | Single locale or ordered list of preferred locales |
available | string[] | List of supported locales |
fallback | string? | Fallback locale if no match is found |
Returns: string — best matching locale.
parseLocale(locale)
Section titled “parseLocale(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 }
isRTL(locale)
Section titled “isRTL(locale)”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') // trueisRTL('ar-SA') // trueisRTL('en') // falsevalidateLocale(locale, context)
Section titled “validateLocale(locale, context)”Validate that a locale string is a non-empty string. Throws an error if invalid.
import { validateLocale } from '@fluenti/core'
validateLocale('en', 'myFunction') // okvalidateLocale('', 'myFunction') // Error: [fluenti] myFunction: locale must be a non-empty string, got ""Message ID Utilities
Section titled “Message ID Utilities”hashMessage(message, context?)
Section titled “hashMessage(message, context?)”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')buildICUMessage(strings, expressions)
Section titled “buildICUMessage(strings, expressions)”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'Formatting
Section titled “Formatting”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 formatformatDate(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"formatRelativeTime(value, locale)
Section titled “formatRelativeTime(value, locale)”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() - 86400000formatRelativeTime(yesterday, 'en') // "yesterday"Formatting Constants
Section titled “Formatting Constants”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'clearAllCaches()
Section titled “clearAllCaches()”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()Internal cache sizes
Section titled “Internal cache sizes”| Cache | Default size | Purpose |
|---|---|---|
| Compiled messages | 500 (LRU) | Avoids re-parsing identical ICU messages |
Intl.NumberFormat | 200 (LRU) | Caches formatter instances per locale + options |
Intl.DateTimeFormat | 200 (LRU) | Caches formatter instances per locale + options |
Intl.RelativeTimeFormat | 200 (LRU) | Caches formatter instances per locale |
Use setMessageCacheSize(n) to adjust the compiled message cache. The value must be a positive integer (minimum 1).
Catalog
Section titled “Catalog”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') // truecatalog.getLocales() // ['en']| Method | Description |
|---|---|
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 |
FluentParseError
Section titled “FluentParseError”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 }}Common parse errors
Section titled “Common parse errors”| Error | Cause |
|---|---|
Expected identifier | Empty braces { } or invalid character after { |
Unterminated placeholder | Missing closing } (e.g. Hello {name) |
Expected closing } | Mismatched braces in function or plural/select |
Plural/selectordinal must have an 'other' option | Missing required other branch |
Maximum nesting depth of 10 exceeded | ICU message nesting too deep |
Identifier exceeds maximum length of 256 | Variable name longer than 256 characters |
Parser limits
Section titled “Parser limits”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
Error handling in formatters
Section titled “Error handling in formatters”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.
Plural Resolution
Section titled “Plural Resolution”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.