| name | fluent-syntax-functions |
| description | Use when formatting numbers, dates, or currencies in FTL or registering custom Fluent functions. Prevents incorrect NUMBER()/DATETIME() option usage and malformed custom function signatures. Covers built-in functions, Intl formatting options, custom function registration, and bidirectional text isolation. Keywords: NUMBER, DATETIME, FluentFunction, Intl.NumberFormat, custom functions, RTL, useGrouping, format number, format date, currency, custom formatting, right-to-left text.
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires Fluent Syntax 1.0, @fluent/bundle 0.18+. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
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() });
}
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 = ().();
(hour < ) ;
(hour < ) ;
;
},
},
});
Corresponding FTL:
total = Total: { CURRENCY($amount, currency: "EUR") }
shout = { UPCASE("hello world") }
greeting = Good { GREETING_TIME() }!
Custom Function Rules
- ALWAYS register functions in the
functions option of the FluentBundle constructor
- Custom functions are merged with built-in
NUMBER and DATETIME -- naming a custom function NUMBER or DATETIME overrides the built-in
- Functions receive
positional (array of FluentValue) and named (record of FluentValue) arguments
- Functions MUST return a
FluentValue (string or FluentType<unknown>)
- If a function referenced in FTL is not registered, resolution produces a
ReferenceError and 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");
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" });
}
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
const bundle = new FluentBundle("en-US", { useIsolating: false });
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