| name | fluent-syntax-attributes |
| description | Use when adding attributes to FTL messages or terms for HTML element properties, form labels, or ARIA values. Prevents missing .attribute syntax errors and incorrect attribute access in TypeScript and React. Covers message attributes, term attributes, value-less messages, compound messages, and the Localized attrs prop. Keywords: FTL attributes, .placeholder, .aria-label, getMessage, attrs, compound message, translate placeholder, translate aria label, HTML attributes, input translation.
|
| 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-attributes
Quick Reference
Attribute Syntax Overview
| Concept | Syntax | Example |
|---|
| Message attribute | .attr = Value (indented) | .placeholder = Enter email |
| Term attribute | .attr = Value on -term | .gender = feminine |
| Value-less message | msg = followed by attributes only | login =\n .aria-label = Login |
| Compound message | Value + attributes | login = Log in\n .title = Click to log in |
| Attribute reference (FTL) | { msg.attr } or { -term.attr } | { -brand.gender } |
| TypeScript access | getMessage(id).attributes["attr"] | Returns Pattern | undefined |
| React access | <Localized attrs={{ title: true }}> | Explicit opt-in per attribute |
EBNF Grammar
Attribute ::= line_end blank? "." Identifier blank_inline? "=" blank_inline? Pattern
AttributeAccessor ::= "." Identifier
Message ::= Identifier blank_inline? "=" blank_inline? ((Pattern Attribute*) | (Attribute+))
Term ::= "-" Identifier blank_inline? "=" blank_inline? Pattern Attribute*
Critical Warnings
ALWAYS check msg.value for null before calling formatPattern() -- value-less messages (attributes-only) are valid FTL. Calling formatPattern(null!) causes a runtime error.
ALWAYS declare attrs explicitly in React's <Localized> component -- translations CANNOT inject arbitrary attributes onto DOM elements. Only attributes listed as true in the attrs prop are applied.
NEVER try to access term attributes at runtime via getMessage() -- term attributes are private and can ONLY be used as selectors inside FTL files. There is no public API to retrieve them.
NEVER assume a message has a value -- the grammar allows (Attribute+) without a Pattern. ALWAYS use the pattern if (msg?.value) { ... } before formatting.
Attribute Syntax
Message Attributes
Attributes are indented lines starting with a dot (.) that belong to the preceding message or term. They group multiple translatable strings under one identifier.
login-input = Predefined value
.placeholder = email@example.com
.aria-label = Login input value
.title = Type your login email
This creates one message (login-input) with a value and three attributes. Each attribute has its own Pattern that can contain placeables, variables, and references.
Value-less Messages (Attributes Only)
A message MAY have ONLY attributes and no value. This is valid FTL:
login-input =
.placeholder = email@example.com
.aria-label = Login input value
Here login-input has no value (msg.value is null), only attributes. Use this when a UI element has no visible text content but has accessible labels or HTML attributes that need translation.
Compound Messages (Value + Attributes)
A message MAY have both a value and attributes:
login-button = Log In
.title = Click to log in to your account
.aria-label = Log in button
The value (Log In) is the primary text. The attributes (.title, .aria-label) are secondary strings associated with the same UI element.
Term Attributes
Terms can also have attributes, but term attributes are private -- they are NOT accessible to the localization runtime and can ONLY be used as selectors within FTL.
-brand-name = Aurora
.gender = feminine
update-successful =
{ -brand-name.gender ->
[masculine] { -brand-name } has been updated.
[feminine] { -brand-name } has been updated.
*[other] { -brand-name } software has been updated.
}
The .gender attribute on -brand-name stores grammatical metadata. It is referenced as a selector via { -brand-name.gender -> ... } but CANNOT be retrieved by application code.
Key Difference: Message vs. Term Attributes
| Aspect | Message Attributes | Term Attributes |
|---|
| Runtime access | YES -- via getMessage().attributes | NO -- private, selector-only |
| Purpose | Translatable UI strings (placeholder, title, aria-label) | Grammatical metadata (gender, animacy, case) |
| Value requirement | Message can be attribute-only (no value) | Term MUST have a value |
| Use in selectors | YES -- via { msg.attr -> ... } | YES -- via { -term.attr -> ... } |
TypeScript Access
getMessage() Returns Attributes
The getMessage() method returns an object with value and attributes:
interface Message {
value: Pattern | null;
attributes: Record<string, Pattern>;
}
Accessing Attributes in TypeScript
import { FluentBundle, FluentResource } from "@fluent/bundle";
const bundle = new FluentBundle("en-US");
bundle.addResource(new FluentResource(`
login-input =
.placeholder = email@example.com
.aria-label = Login input value
.title = Type your login email
`));
const msg = bundle.getMessage("login-input");
if (msg) {
if (msg.value) {
const text = bundle.formatPattern(msg.value);
}
const placeholder = bundle.formatPattern(msg.attributes["placeholder"]);
const ariaLabel = bundle.formatPattern(msg.attributes["aria-label"]);
}
Compound Message Access
const bundle = new FluentBundle("en-US");
bundle.addResource(new FluentResource(`
submit-button = Submit Form
.title = Click to submit
.aria-label = Submit the registration form
`));
const msg = bundle.getMessage("submit-button");
if (msg) {
if (msg.value) {
const buttonText = bundle.formatPattern(msg.value);
}
const title = bundle.formatPattern(msg.attributes["title"]);
}
Safe Access Pattern
ALWAYS use this pattern when accessing messages that may or may not have values:
function getMessageValue(
bundle: FluentBundle,
id: string,
args?: Record<string, FluentVariable>
): string | null {
const msg = bundle.getMessage(id);
if (!msg) return null;
if (!msg.value) return null;
const errors: Error[] = [];
return bundle.formatPattern(msg.value, args, errors);
}
function getMessageAttribute(
bundle: FluentBundle,
id: string,
attr: string,
args?: Record<string, FluentVariable>
): string | null {
const msg = bundle.getMessage(id);
if (!msg) return null;
const pattern = msg.attributes[attr];
if (!pattern) return null;
: [] = [];
bundle.(pattern, args, errors);
}
React Access
The attrs Prop
In @fluent/react, the <Localized> component uses the attrs prop to control which FTL attributes are applied to the wrapped element:
import { Localized } from "@fluent/react";
<Localized id="login-input" attrs={{ placeholder: true, "aria-label": true }}>
<input
type="email"
placeholder="email@example.com"
aria-label="Login input"
/>
</Localized>
Corresponding FTL:
login-input =
.placeholder = email@example.com
.aria-label = Login input value
Compound Messages in React
When a message has both a value and attributes:
<Localized id="submit-button" attrs={{ title: true }}>
<button title="Click to submit">Submit Form</button>
</Localized>
submit-button = Submit Form
.title = Click to submit
The value replaces the button's text content. The .title attribute replaces the title prop -- but ONLY because attrs={{ title: true }} explicitly allows it.
Imperative Attribute Access
Use the useLocalization hook for imperative access:
import { useLocalization } from "@fluent/react";
function DynamicInput() {
const { l10n } = useLocalization();
const bundle = l10n.getBundle("login-input");
if (bundle) {
const msg = bundle.getMessage("login-input");
if (msg) {
const placeholder = bundle.formatPattern(
msg.attributes["placeholder"]
);
}
}
return <input placeholder={placeholder} />;
}
Decision Trees
When to Use Attributes vs. Separate Messages
Is the string associated with the SAME UI element?
├── YES → Use attributes on one message
│ ├── Element has visible text + HTML attributes → Compound message
│ └── Element has ONLY HTML attributes (no text) → Value-less message
└── NO → Use separate messages
When to Use Term Attributes
Is the metadata needed for grammatical agreement?
├── YES → Use term attribute (.gender, .animacy, .case)
│ └── Reference as selector: { -term.attr -> ... }
└── NO → Is it a translatable string?
├── YES → Use message attribute
└── NO → Store as a comment instead
Reference Links
- references/methods.md -- EBNF grammar for Attribute and AttributeAccessor; getMessage().attributes API
- references/examples.md -- Complete examples for message attributes, term attributes, value-less messages, compound messages, TypeScript and React access
- references/anti-patterns.md -- Common mistakes with attributes: null value checks, missing attrs in React, term attribute access
Official Sources