fluent-syntax-functions
Quick Reference
Built-in Functions
| Function | Purpose | Backed By | Usage Context |
|---|---|---|---|
NUMBER() |
Format numeric values | Intl.NumberFormat |
Placeables and selectors |
DATETIME() |
Format date/time values | Intl.DateTimeFormat |
Placeables only |
Function Call Grammar (EBNF)
FunctionReference ::= Identifier CallArguments
CallArguments ::= blank? "(" blank? argument_list blank? ")"
argument_list ::= (Argument blank? "," blank?)* Argument?
Argument ::= NamedArgument | InlineExpression
NamedArgument ::= Identifier blank? ":" blank? (StringLiteral | NumberLiteral)
Function names are plain Identifier values -- ALWAYS use uppercase by convention: NUMBER, DATETIME, MY_FUNC.
FluentFunction Type Signature
type FluentFunction = (
positional: Array<FluentValue>,
named: Record<string, FluentValue>
) => FluentValue;
Where FluentValue = FluentType<unknown> | string.
Critical Warnings
NEVER pass objects, arrays, or booleans as FluentVariable values -- only string, number, Date, FluentType, or TemporalObject are valid. Passing unsupported types causes a TypeError at resolution time.
NEVER disable useIsolating for bundles that handle RTL/LTR mixed content -- Unicode isolation marks (U+2068/U+2069) prevent bidirectional text corruption.
NEVER use lowercase for function names in FTL -- the parser treats lowercase identifiers as message references, not function calls. ALWAYS use UPPERCASE.
NEVER pass variable references as named argument values in FTL -- named arguments MUST be string literals or number literals: NUMBER($x, minimumFractionDigits: 2) is correct, NUMBER($x, minimumFractionDigits: $digits) is NOT valid.
NUMBER() Function
In Placeables (Display Formatting)
# Basic usage -- explicit formatting
emails = You have { NUMBER($count) } unread emails.
# With formatting options
price = Total: { NUMBER($amount, minimumFractionDigits: 2, maximumFractionDigits: 2) }
file-size = Size: { NUMBER($bytes, useGrouping: 0) } bytes
balance = Balance: { NUMBER($value, minimumIntegerDigits: 2) }
In Selectors (Plural Category Determination)
# Cardinal plurals (default)
emails =
{ $count ->
[one] You have one unread email.
*[other] You have { $count } unread emails.
}
# Ordinal plurals
your-rank = { NUMBER($pos, type: "ordinal") ->
[1] You finished first!
[one] You finished {$pos}st
[two] You finished {$pos}nd
[few] You finished {$pos}rd
*[other] You finished {$pos}th
}
# Formatted number as selector
your-score =
{ NUMBER($score, minimumFractionDigits: 1) ->
[0.0] You scored zero points.
*[other] You scored { NUMBER($score, minimumFractionDigits: 1) } points.
}
Exact numeric matches (like [1]) are checked BEFORE CLDR plural category matches. ALWAYS place exact matches above category matches.
NUMBER() Options (Complete)
See references/methods.md for the full options table.
DATETIME() Function
Basic Usage
# Short date
today = Today is { DATETIME($date, month: "long", day: "numeric", year: "numeric") }.
# Time display
current-time = Time: { DATETIME($now, hour: "numeric", minute: "2-digit") }.
# Full date-time with style shortcuts
last-login = Last login: { DATETIME($date, dateStyle: "long", timeStyle: "short") }.
# Weekday display
meeting-day = Meeting on { DATETIME($date, weekday: "long") }.
TypeScript -- Passing Date Variables
const msg = bundle.getMessage("today");
if (msg?.value) {
const text = bundle.formatPattern(msg.value, { date: new Date() });
// "Today is March 19, 2026."
}
ALWAYS pass Date objects (or number timestamps or Temporal objects) as date variables. NEVER pass pre-formatted date strings -- let Fluent handle locale-aware formatting.
DATETIME() Options (Complete)
See references/methods.md for the full options table.
Custom Functions
Registration via FluentBundle Constructor
import { FluentBundle, FluentResource } from "@fluent/bundle";
const bundle = new FluentBundle("en-US", {
functions: {
CURRENCY: (positional, named) => {
const value = positional[0];
const num = typeof value === "number"
? value
: Number(value);
const currency = named["currency"];
const currencyCode = typeof currency === "string"
? currency
: "USD";
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: currencyCode,
}).format(num);
},
UPCASE: (positional, named) => {
const val = positional[0];
return typeof val === "string"
? val.toUpperCase()
: String(val).toUpperCase();
},
GREETING_TIME: (positional, named) => {
const hour = new Date().getHours();
if (hour < 12) return "morning";
if (hour < 18) return "afternoon";
return "evening";
},
},
});
Corresponding FTL:
total = Total: { CURRENCY($amount, currency: "EUR") }
shout = { UPCASE("hello world") }
greeting = Good { GREETING_TIME() }!
Custom Function Rules
- ALWAYS register functions in the
functionsoption of theFluentBundleconstructor - Custom functions are merged with built-in
NUMBERandDATETIME-- naming a custom functionNUMBERorDATETIMEoverrides the built-in - Functions receive
positional(array ofFluentValue) andnamed(record ofFluentValue) arguments - Functions MUST return a
FluentValue(stringorFluentType<unknown>) - If a function referenced in FTL is not registered, resolution produces a
ReferenceErrorand the fallback{FUNC_NAME()}is displayed
Decision Tree -- When to Use Custom Functions
Need locale-aware number formatting?
├── YES → Use NUMBER() with Intl.NumberFormat options
│ ├── Need currency? → NUMBER($val, style: "currency") or custom CURRENCY()
│ ├── Need percentage? → Custom PERCENT() function
│ └── Need ordinals? → NUMBER($val, type: "ordinal") in selector
│
Need locale-aware date formatting?
├── YES → Use DATETIME() with Intl.DateTimeFormat options
│
Need text transformation?
├── YES → Custom function (UPCASE, LOWERCASE, CAPITALIZE)
│
Need runtime logic (time of day, platform detection)?
├── YES → Custom function returning a string for selector matching
│
Need formatting not covered by Intl?
├── YES → Custom function with your own formatting logic
Bidirectional Text Isolation
How useIsolating Works
When useIsolating is true (the default), every placeable value is wrapped in Unicode isolation marks:
- U+2068 (First Strong Isolate) -- inserted BEFORE the placeable
- U+2069 (Pop Directional Isolate) -- inserted AFTER the placeable
This prevents RTL text in placeables from corrupting the surrounding LTR text layout (and vice versa).
Example -- With Isolation (Default)
const bundle = new FluentBundle("en-US"); // useIsolating: true (default)
const resource = new FluentResource(`welcome = Welcome, { $name }!`);
bundle.addResource(resource);
const msg = bundle.getMessage("welcome");
if (msg?.value) {
const text = bundle.formatPattern(msg.value, { name: "Ahmed" });
// Internal: "Welcome, \u2068Ahmed\u2069!"
// Renders correctly in both LTR and RTL contexts
}
When to Disable useIsolating
ONLY disable useIsolating in these cases:
- Unit testing -- Unicode marks interfere with string equality assertions
- Unidirectional content -- ALL content is the same direction (e.g., English-only app)
- Pre-isolated content -- Placeables already contain isolation marks
// For testing
const bundle = new FluentBundle("en-US", { useIsolating: false });
// For production with mixed directionality -- KEEP DEFAULT
const bundle = new FluentBundle("ar", { useIsolating: true });
NEVER disable useIsolating for Arabic, Hebrew, or any locale that mixes RTL and LTR scripts. Doing so causes placeable values to reorder surrounding text unpredictably.
Reference Links
- references/methods.md -- NUMBER options table, DATETIME options table, FluentFunction signature, FunctionReference grammar
- references/examples.md -- NUMBER formatting, DATETIME formatting, custom function registration, bidirectional text
- references/anti-patterns.md -- Wrong function syntax, passing objects as args, disabling useIsolating carelessly
Official Sources
- https://projectfluent.org/fluent/guide/builtins.html
- https://projectfluent.org/fluent/guide/functions.html
- https://github.com/projectfluent/fluent.js/blob/main/fluent-bundle/src/builtins.ts
- https://github.com/projectfluent/fluent.js/blob/main/fluent-bundle/src/bundle.ts
- https://raw.githubusercontent.com/projectfluent/fluent/master/spec/fluent.ebnf