| name | fluent-core-architecture |
| description | Use when setting up or understanding Project Fluent architecture. Prevents misconceptions about the bundle-resource-message model and asymmetric localization. Covers design philosophy, FTL format overview, and how Fluent differs from ICU MessageFormat. Keywords: Project Fluent, FTL, FluentBundle, FluentResource, asymmetric localization, i18n architecture, what is Fluent, how Fluent works, Fluent vs ICU, translation system.
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires @fluent/bundle 0.18+. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
fluent-core-architecture
Quick Reference
Fluent Package Ecosystem
| Package | Version | Purpose |
|---|
@fluent/bundle | 0.19.1 | Core runtime: parse FTL, format messages |
@fluent/react | 0.15.2 | React bindings: <Localized>, useLocalization() |
@fluent/langneg | 0.7.0 | Locale negotiation between user prefs and available locales |
@fluent/syntax | 0.19.0 | Full AST parser/serializer for tooling (linting, codegen) |
Runtime Model (data flow)
FTL string ──► FluentResource ──► FluentBundle ──► getMessage(id) ──► formatPattern(pattern, args) ──► string
(parse) (store) (lookup) (resolve + format)
Key Terminology
| Term | Definition |
|---|
| Message | A named unit of translation: identifier + value (pattern) + optional attributes |
| Term | A reusable fragment prefixed with -; NEVER accessible via getMessage() |
| Pattern | The translatable content of a message or attribute |
| Placeable | { } syntax that embeds variables, references, or function calls into text |
| Variant | One branch of a select expression, keyed by string or plural category |
| Attribute | A .name = value sub-entry on a message or term |
| Selector | A -> expression that chooses a variant based on a runtime value |
| Resource | A parsed FTL file containing messages and terms |
| Bundle | A locale-bound container that holds resources and formats messages |
Critical Warnings
NEVER confuse Fluent with ICU MessageFormat. They are fundamentally different systems. Fluent uses asymmetric localization (each locale develops independently); ICU uses symmetric patterns (source language grammar leaks to all translations). See the comparison table below.
NEVER use @fluent/syntax for runtime message formatting. It is a tooling library for linting, AST manipulation, and codegen. ALWAYS use @fluent/bundle for runtime formatting.
NEVER skip the two-step lookup: ALWAYS call getMessage(id) first, then formatPattern(pattern, args). There is no single-call format method.
NEVER pass raw FTL strings to addResource(). It requires a FluentResource instance: bundle.addResource(new FluentResource(ftl)).
ALWAYS pass an errors array to formatPattern() in production. Without it, the method throws on the first resolution error instead of returning a best-effort string.
Design Philosophy
Project Fluent is Mozilla's localization system built on one core principle: asymmetric localization. Translators can use the full expressive power of their language without asking developers for permission.
Asymmetric Localization
Traditional i18n systems (gettext, Java properties, ICU MessageFormat) enforce a one-to-one mapping between source strings and translations. The grammar of the source language limits what translators can express. Fluent eliminates this constraint:
- A simple English string can map to a complex multi-variant Polish translation with grammatical cases.
- The Polish translator adds selectors independently -- no code changes, no English file modifications.
- Locale-specific logic is isolated. Complexity in one locale NEVER leaks to another.
Progressive Enhancement
Each locale develops independently. English may use welcome = Welcome, { $name }! while Polish uses a selector on grammatical gender. The developer writes one getMessage("welcome") call. Fluent handles the rest.
Simplicity for Simple Cases
The basic syntax is as simple as any properties file:
hello = Hello, world!
Complexity (selectors, terms, attributes) is added only when a locale needs it.
FTL Format Overview
FTL (Fluent Translation List) is the file format for Fluent translations. Syntax version: Fluent Syntax 1.0 (released April 2019).
Messages
The basic unit of translation. An identifier, =, and a value (pattern):
hello = Hello, world!
welcome = Welcome, { $name }!
Terms
Reusable fragments prefixed with -. ALWAYS use terms for brand names and shared vocabulary:
-brand-name = Firefox
about = About { -brand-name }.
Terms accept parameters and can store grammatical metadata as attributes. Terms are NEVER accessible via the public getMessage() API.
Attributes
Multiple translatable strings grouped under one message using .attribute syntax:
login-input = Predefined value
.placeholder = email@example.com
.aria-label = Login input value
Selectors
Choose translation variants based on runtime values. Every select expression MUST have exactly one default variant (marked with *):
emails =
{ $count ->
[one] You have one unread email.
*[other] You have { $count } unread emails.
}
Comments
Three levels: # (message-bound), ## (section group), ### (file-level resource comment).
The Runtime Model
FluentBundle -> FluentResource -> Message -> Pattern -> formatPattern
The runtime model follows a strict pipeline:
Step 1: Create a bundle for a specific locale:
import { FluentBundle, FluentResource } from "@fluent/bundle";
const bundle = new FluentBundle("en-US");
Step 2: Parse FTL into a resource and add it to the bundle:
const resource = new FluentResource(`
welcome = Welcome, { $name }!
-brand = Acme Corp
`);
const errors = bundle.addResource(resource);
Step 3: Look up a message by its identifier:
const msg = bundle.getMessage("welcome");
The returned Message object has this shape:
interface Message {
value: Pattern | null;
attributes: Record<string, Pattern>;
}
Step 4: Format the pattern with runtime arguments:
if (msg?.value) {
const text = bundle.formatPattern(msg.value, { name: "Anna" });
}
Key API Methods
| Method | Signature | Purpose |
|---|
constructor | new FluentBundle(locales, options?) | Create a locale-bound bundle |
addResource | (resource, options?) => Error[] | Add parsed FTL; returns parse errors |
getMessage | (id) => Message | undefined | Look up a message by ID |
hasMessage | (id) => boolean | Check if a message exists |
formatPattern | (pattern, args?, errors?) => string | Format a pattern to a string |
Constructor Options
| Option | Type | Default | Purpose |
|---|
functions | Record<string, FluentFunction> | {} | Custom functions available in FTL |
useIsolating | boolean | true | Wrap placeables in Unicode bidi isolation marks |
transform | (text: string) => string | identity | Transform applied to all text elements |
Fluent vs ICU MessageFormat
| Aspect | ICU MessageFormat | Project Fluent |
|---|
| Localization model | Symmetric (1:1 mapping) | Asymmetric (1:N mapping) |
| Complexity leakage | Source language grammar leaks to all translations | Locale logic is isolated per language |
| File format | Embedded in code or properties files | Dedicated .ftl files |
| Terms/brands | No built-in concept | First-class -term syntax |
| Attributes | Not supported | .attribute syntax for multi-value messages |
| Comments | Not standardized | Three-level comment system (#, ##, ###) |
| Error handling | Typically throws errors | Graceful fallback (shows message ID or {???}) |
| Plural syntax | {count, plural, one{...} other{...}} | { $count -> [one] ... *[other] ... } |
| Gender syntax | {gender, select, male{...} female{...} other{...}} | Uses term attributes with selectors |
| Nesting | Deep nesting common and hard to read | Flat structure, complexity in variants |
| Translator autonomy | Often requires developer coordination | Translators add complexity independently |
The most significant difference: in ICU, if Polish needs grammatical cases, the developer MUST modify source code to pass case parameters and update the English source string. In Fluent, the Polish translator adds a selector independently -- the English file stays simple and no code changes are needed.
Decision Tree: Which Package Do I Need?
Need to format messages at runtime in an app?
├── YES -> @fluent/bundle (core runtime)
│ ├── Building a React app? -> ALSO add @fluent/react
│ └── Need locale negotiation? -> ALSO add @fluent/langneg
└── NO
├── Linting or validating FTL files? -> @fluent/syntax
├── Programmatically generating FTL? -> @fluent/syntax
└── Building editor tooling? -> @fluent/syntax
NEVER use @fluent/syntax for runtime formatting. It produces a full AST and is significantly heavier than the optimized runtime parser in @fluent/bundle.
Reference Links
Official Sources