Curly Message Format
Version 3 — Stable
| Format name | Curly Message Format |
| Machine-readable identifier | curly-message |
| Versioned identifier | curly-message-3 |
| Status | Stable |
Version 3 is stable. The body of this document is normative: it states what a conforming implementation must do. Within
curly-message-3, what a message resolves to is settled — a message written against this document resolves the same way against every later revision of it, and an amendment that would change that belongs to a later version of the format rather than to this one. The promise is about messages: a caller that supplied none wrote nothing for this document to settle, and what an implementation answers where there is no message to resolve is outside it. What a later revision may add is what leaves the messages written today alone: a modifier under a name no earlier revision defined, a conformance level an implementation opts into, and wording that states more precisely what the body already required. The promise has one exception, the one addition outside that list: section 13's bound on the text one serialization writes, which a revision of this document added. A value whose serialization writes more text than the bound an implementation documents is one no conversion can describe, however few nodes it visits, as a value the host's serializer refuses already was.This document states version 3 and nothing else. Three documents beside it carry what it does not, and none of them is part of this specification:
CHANGELOG.mdrecords every revision,MIGRATIONS.mdis the path from an earlier version of the format, andRULINGS.mdsays why a rule reads as it does.
1. Scope
This document specifies the Curly Message Format: a syntax for translatable messages in which values are substituted into double-curly placeholders, and a placeholder may carry a modifier, a list of options and a fallback.
It specifies the message syntax and the result of resolving a message against a payload, props and a locale (section 4). It does not specify a host API, a file format for message catalogues, an id-namespacing scheme, or how an implementation reports diagnostics.
It describes a message as text to be resolved. A companion document,
CST.md, describes the same message as text to be shown: a
concrete syntax tree over the grammar of section 6, for a tool that highlights,
completes or lints a message rather than resolving it. It adds nothing this
document requires, and an implementation conforms without offering a tree.
The format is deliberately small. It has no gender selection, and the plural categories it selects by are the locale's own rather than rules a message writes (section 11.5). Formatting that depends on a locale is delegated to the host platform's internationalization facilities. A placeholder may hold a placeholder in an option value, and there it is one construct holding another rather than an argument syntax: what is nested is written in the message, never supplied with the payload (sections 12, 14.1).
2. Conformance
The key words MUST, MUST NOT, REQUIRED, SHOULD, SHOULD NOT and MAY are to be interpreted as described in RFC 2119.
An implementation conforms to this specification at one or more levels:
| Level | Sections | Requirement |
|---|---|---|
| Core | 4-10, 11 (excluding 11.2, 11.3 and 11.5), 12, 13, 14 | Grammar, escaping, whitespace, resolution, the fallback chain, what a modifier receives and returns, the comparison modifiers, nesting and its limits, security properties and error behavior. |
| Intl | 11.2, 11.5 | The locale-dependent formatting modifiers number, date, ago and currency, and the plural selections plural and ordinal. |
| Extensions | 11.3 | Host-defined modifiers. |
Core is REQUIRED. An implementation MUST report which levels it satisfies. An implementation that does not satisfy Intl MUST treat the names of those six modifiers as unknown modifiers (section 11.4), not as ordinary option keys: an implementation that lacks those modifiers has not defined them, and a host is free to define them itself, because a host's own configuration overrides the format's (section 11.3).
Conformance is tested by the implementation-independent fixture set
@curly-message/conformance, which targets a stated version of this document.
That set is developed in the conformance/ directory of this document's
repository and released from it.
An implementation states the levels it satisfies, and the limits it permits that the set derives cases from, to that set through the adapter of section 14.3.
3. Terminology
message : Text that may contain placeholders. A message a host wrote as something else is converted to text before anything reads it (sections 4 and 5).
payload : A mapping from string keys to entries, supplied by the caller, from which placeholder values are resolved. An entry is a value, or the value's own configuration (section 4.1).
props : Caller-supplied properties for the modifiers that read them — the formatting modifiers, the plural selections and host-defined modifiers (sections 11.2, 11.3, 11.5) — grouped by modifier name. Distinct from the payload: the payload carries data, props carry presentation. Distinct from an option, which is written in the placeholder: props are supplied by the caller (section 11.2).
locale : A language tag identifying the target language, used by the modifiers that depend on one (sections 11.2, 11.5).
id (of a message) : The identifier under which a message was requested. Nothing in resolution reads it; reports name it, so that a report says which message went looking (section 14.3).
placeholder
: A {{ … }} construct within a message that section 6 derives as one. A
construct it does not derive is literal text (section 9.1).
selector : The leading part of a placeholder: a payload key and a modifier, both optional (section 9.1).
option
: A key:value pair within a placeholder, offered to the modifier for
selection.
inline default
: The option-shaped segment (section 6) whose key is default. It is not an
option; it is the placeholder's own fallback (section 9.3).
absent : A value is absent when the payload has no own entry for the key, or when the entry it owns is the host's undefined. A value that is present but empty, zero, false or the host's null is not absent; a present value that no conversion can describe is treated as absent (section 9.2).
4. Data model
Resolving a message takes four inputs — a message, a payload, props and a locale — and produces a string. A fifth, the message's id, is optional and reaches no step of resolution: reports name it (section 14.3), and nothing else reads it.
A caller that supplies no message — in a host with an undefined, a message that is the host's undefined — has supplied nothing to resolve, and the resolution is the empty string. So has one whose message is present but that no conversion below can describe, the same way such a value is absent (section 9.2). Nothing else fails to exist: a message that exists resolves normally, even when it is empty, and one that is zero, false or the host's null resolves as the text this section converts it to. What a host shows in the place of a message its catalogue does not hold is the host's own, which section 1 declines along with the catalogue itself.
The host's undefined is ECMAScript's word for nothing, and a host with a single
null has no second value to spell it with. There absence is an entry the
mapping does not hold, an argument the caller omitted, or a stand-in the
implementation documents for a place where a value must be written, and that is
what this document means by the host's undefined. The host's null stays a value
wherever this document holds it present — a message, a payload entry, a
wrapper's value, a property a layer names (sections 4.1, 9.2, 11.2) — so a
payload entry holding it is present. Where the host's functions answer nothing
with that null, it is also section 11's answer that is nothing at all.
Everything the format carries is text. A payload value MAY be of any host type, but it reaches a modifier, an option comparison and the output as text, never at the type it was authored with.
A value that is a plain object or a plain array MUST be converted using the host's JSON serialization, so that a structured value survives into a host-defined modifier that reads it back. Every other value MUST be converted using the host's ordinary string conversion, so a date, a pattern, a set or a class instance keeps whatever text it describes itself as.
That serialization holds no insignificant whitespace, as sections 4.1 and 7
write it: { a: 1, b: [2] } converts to {"a":1,"b":[2]}. An object's members
come in the order the host's mapping holds them in, or, where the mapping holds
no order, in the order the serializer writes them. Everything else in the text
is the host serializer's: how it escapes a character, how it spells a number,
and which values it refuses, a refused value being one no conversion can
describe (below). A host with no JSON serialization of its own converts with an
RFC 8259 serialization the implementation documents.
Plain object and plain array MUST both be read narrowly, and by the value's own type: a value of the type a host offers for arbitrary keyed data, or of the type it offers for an arbitrary ordered sequence, carrying no meaning beyond the entries it holds. A type an application declared for data of its own — a class, a struct, a record, a sequence derived from the host's own — is not one, and neither is a type the host offers for something more specific than data, so a date, a pattern and a set convert as strings however the host would serialize them. Where a host offers no such type it holds no value of that shape, and what one would have carried converts as a string like everything else.
In ECMAScript that reading is the prototype: an object whose prototype is the
running realm's Object.prototype, or null, and an array whose prototype is
the running realm's Array.prototype. A value built in another realm has a
prototype of its own by that test, whatever it was authored as, and so does one
of a type an application derived; both convert like every other value.
A host may offer one type for both shapes, or several for one. A type that
holds both — a mapping that is also a sequence — is plain, and the host's
serialization decides which shape a value of it takes, so an empty one may
write []. Where several of the host's types fit one shape, the implementation
documents which it reads as plain, and the types the host's JSON decoding
produces for an object and an array are among them. A host with no type for
dynamic data, where a payload reaches the implementation only as a value model
the implementation defines, has that model stand for the host's types: the
implementation documents which of its variants are the plain object and the
plain array, and a value of any other type it accepts converts by the ordinary
string conversion.
The narrow reading is what keeps a value authored as a date instance formattable
by date (section 11.2), and section 4.1 recognizes a wrapper by the same
reading.
A value that no conversion can describe — a serialization that raises, yields nothing or reaches the conversion limit of section 13, a string conversion that raises — MUST be treated as absent, MUST NOT raise, and SHOULD be reported (section 14.2). A serialization the host's serializer refuses for the depth of the value — at a nesting bound of its own, or where the call stack it recurses on runs out — is one that raises or yields nothing, however few nodes section 13 would count in it. Where the call stack running out would end the process rather than raise, the implementation refuses the value at a depth of its own short of that. A depth at which the implementation refuses a value, there or in a serialization it documents, is a refusal of the serializer's like the host's: a serialization that raises or yields nothing, at a depth section 13 sets no minimum for.
How many times a value is converted while a message resolves is itself bounded (section 13), and so is what one serialization visits. What one ordinary string conversion costs is the value's own type's, which section 13 does not bound.
The text a value's conversion produces is data, and nothing removes escape sequences from it (section 7). A backslash it carries is a backslash, so a serialization reaches the output parsable as the format it was made in. A message is the exception, because it is not a value the walk reads but the text the walk is over: section 5 converts it before anything reads it, and what the conversion produced is then the message's own text.
Wherever a value is compared numerically, the implementation MUST convert it using the host's ordinary numeric conversion, and a conversion that does not yield a number MUST be treated as a failed comparison, never as an error. A conversion that yields NaN yields no number, though the host may type NaN as one: it orders against nothing, so it cannot be compared.
An implementation MUST return a string, including when the message it was given was not one.
4.1 Value wrappers
A payload entry MAY be the value's own configuration instead of the value.
An entry is a wrapper when it is a plain object, owns at least one key, and
every own key is one of value, default and props. Anything else is a
value — including a plain object that owns any other key alongside them.
{ value: 1 } wrapper, value 1
{ value: 1, default: 'D' } wrapper
{ default: 'D' } wrapper, no value
{ props: { number: … } } wrapper, no value
{ unit: 'kg', value: 1 } value -> {"unit":"kg","value":1}
{} value -> {}
A wrapper's value is the placeholder's value (section 9.2), its default
joins the fallback chain (section 10), and its props is the topmost layer of
formatting properties (section 11.2).
Unwrapping happens exactly once: a wrapper's value is a value, never
itself a wrapper. A wrapper that owns no value key, or whose value is the
host's undefined, has no value.
Recognizing a wrapper asks an entry for its own keys. An entry that refuses to name them — the host raises where they are enumerated — is not a wrapper, and the placeholder does not read it as its value either: it is treated as a value no conversion can describe (section 4), absent, and is not converted after the refusal, so the one read meets the condition once.
The payload's root default entry is an ordinary value, never a wrapper.
Recognition is exact rather than opportunistic because a payload cannot be
constrained: message ids are namespaced dotted segments, and payload values are
frequently plain objects taken straight from an API. { value: 1, unit: 'kg' }
is data, and it must stay data.
Exactness is not enough on its own, because { value: … } is itself a common
shape and a caller that does not control what its payload holds cannot rule one
out. An implementation SHOULD therefore offer a way to turn recognition off for
a resolution. With it off an entry of that shape is a value like any other and
converts as one (section 4), which is where a caller holding untrusted data
passes it (section 14.1). Recognition is on where the caller says nothing: a
payload written for this section reads as it is written.
5. Interpolation model
A message is resolved in one walk. Section 6 parses it once into the text it is written with and the placeholders it declares; the walk then emits that text and, for each placeholder, what section 9 resolves it to.
The walk reaches the outermost placeholders first. A placeholder written inside an option value (section 12) is reached only where the enclosing placeholder selects the option that holds it, so an option the modifier passes over is never rendered: a placeholder in it is not resolved, a payload entry it names is not read, and a modifier it names is not called.
A message reaches that walk as text: it is converted by section 4's rules before anything reads it, not after everything has. A host that wrote its message as something other than a string therefore gets it parsed and unescaped like any other.
Message text is syntax. Payload text is data. What a placeholder resolves to
is emitted as it stands. It is not parsed, so a {{ it contains opens no
placeholder and a ; it contains ends no segment; and it is not unescaped, so a
backslash it contains stands for itself. Escape sequences (section 7) are
removed from the message's own text and from the names the message writes, where
section 6 derives them, and from nothing else.
That is the whole of the model, and the rest of this document leans on it. The walk emits each part of the message once and reads nothing it has emitted, so there is no second pass to perform and nothing for one to find. What bounds the walk is section 13.
6. Grammar
The grammar is given in ISO/IEC 14977 EBNF. It is defined over Unicode code points.
message = { escape | literal-char | placeholder } ;
placeholder = "{{" , selector , { ";" , segment } , "}}" ;
selector = key , [ ":" , modifier-name ] ;
segment = option-key , [ ":" , value ] ;
key = { text-unit } ;
option-key = { text-unit } ;
modifier-name = { value-unit } ;
value = { value-unit | placeholder } ;
text-unit = inner-escape | text-char ;
value-unit = inner-escape | value-char ;
escape = "\" , any-char ;
inner-escape = "\" , inner-char ;
(* A code point stands for itself at any position where no complete escape and
no complete placeholder begins; see note 7. *)
literal-char = ? any code point at such a position ? ;
(* character classes *)
line-term = ? U+000A | U+000D | U+2028 | U+2029 ? ;
whitespace = ? line-term | U+0009 | U+000B | U+000C | U+0020 |
U+00A0 | U+1680 | U+2000 | U+2001 | U+2002 |
U+2003 | U+2004 | U+2005 | U+2006 | U+2007 |
U+2008 | U+2009 | U+200A | U+202F | U+205F |
U+3000 | U+FEFF ? ;
any-char = ? any code point ? ;
inner-char = ? any code point except line-term ? ;
text-char = ? any code point except ":" ";" "\" line-term,
and not starting the sequence "{{" or "}}" ? ;
value-char = ? any code point except ";" "\" line-term,
and not starting the sequence "{{" or "}}" ? ;
Notes on the grammar, all normative:
A placeholder MUST NOT contain a line terminator. A
{{ … }}construct spanning a line terminator is literal text, and escaping the terminator does not make it a placeholder: onlyinner-escapeoccurs inside one, andinner-charexcludes every line terminator. A nested placeholder (note 10) is bound by the same characters, being a placeholder itself. Which substrings of a message are placeholders MUST NOT depend on the payload, or on what any placeholder resolves to (note 7), and what a placeholder resolved to is never read as message text at all (section 5).A key MAY contain a colon or a semicolon only as an escape sequence (
\:,\;), and a brace only where it does not form a delimiter (note 6). A key MAY otherwise contain any character, including spaces, dots, digits, backslashes and non-ASCII text; it MUST NOT contain a line terminator in any form (note 1). Keys are compared by exact code-point equality after unescaping.The selector's colon is the first unescaped colon in the selector. Everything before it is the key; everything after it, up to the selector's end, is the modifier name. The name is not otherwise constrained: one this format does not specify and the host has not registered is a message error (section 11.4), not literal text. A colon with nothing after it names no modifier (section 8).
An option's colon is the first unescaped colon in the segment. Everything after it, up to the segment's end, is the value. A value MAY therefore contain unescaped colons.
A value MUST NOT contain an unescaped semicolon outside a placeholder it holds, which ends the segment. A semicolon within a nested placeholder is that placeholder's own separator and ends nothing in the segment around it (note 10).
A backslash consumes the character that follows it, so a brace it consumed is text and cannot be half of a delimiter.
{{and}}are delimiters only where the two braces stand adjacent and neither has been consumed, which makes\{{v}},{\{v}},{{v}\}and{{v\}}all literal text: in each, one brace of a pair was taken by a backslash and the brace left over stands alone. To write either delimiter as literal text, escape both of its braces:\{\{,\}\}. Consuming one brace disturbs no other, so{{v\}}}is a placeholder whose key isv}— the backslash takes the first}and the remaining two close the placeholder, which is what lets a key end in a closing brace. A}that starts no pair needs no escape at all:{{a}b}}already names the keya}b. And{{\{v}}is a placeholder whose key is{v, its opening pair intact because the backslash took only the third brace.A message has exactly one derivation, and one left-to-right scan finds it. At each position at most one of
escapeandplaceholdercan begin — the first opens on\, the second on{{— and a code point where neither **completes** is aliteral-char. Both need what follows them. An escape needs a character to consume, so a backslash at the end of a message consumes nothing and is aliteral-charitself. And a placeholder begins only where a **complete** one derives: the scan reads forward from the opening pair and either reaches a closing pair or does not. That reading is recursive, because an option value may hold a placeholder (note 10) and the inner construct must derive completely or the enclosing one does not derive. Where it does not, the opening brace is aliteral-charand the scan resumes at the very next code point — one brace, not two — so{{a{{b}}is the literal text{{afollowed by the placeholder{{b}}, while{{{{a}}is the literal text{followed by a placeholder whose key is{a.Inside a placeholder every boundary is forced by a character class rather than by choice:
text-charexcludes:, so the selector's colon is the first one no backslash consumed;text-charandvalue-charboth exclude;, so a segment ends at the first semicolon; and both exclude a code point that starts}}, so the closing pair is the first one left standing that no nested placeholder has claimed. Nothing in the scan consults the payload, or what any placeholder resolves to, so the same message always yields the same placeholders.The whitespace class is fixed by this document. It is the twenty-five code points enumerated above, and it is neither a host language's whitespace class nor a Unicode property; an implementation MUST NOT substitute either for it. Wherever this document says whitespace it means this class: in the escapable characters of section 7, in the significance rules of section 8, in the blank text of section 11.2, which is read in a payload value, not in message text, and in the whitespace around a number of that section, which is read in a payload value and in an option key a numeric comparison converts. The class names
line-termrather than repeating its four code points, so every line terminator is a member, and an amendment toline-termis an amendment to this class that reaches all three sections just named.No production requires whitespace. The grammar admits it wherever it admits text, and admits it only as text: the spaces in
{{ value; }}derive astext-unit, which is what leaves that placeholder one derivation (note 7). Section 8 decides which whitespace a placeholder's parts keep, and section 7 decides which of it an escape sequence makes text.A placeholder derives inside an option value and nowhere else.
valueis the only production that admits one, so a{{in a key, in an option key or in a modifier name opens no placeholder. That is deliberate: it keeps all three readable without a payload, which is what lets a tool answer which entries a message names and which modifiers it calls before anything is resolved (section 12).A
{{in a value must open a complete placeholder or the enclosing construct does not derive at all, becausevalue-charexcludes a code point that starts{{. There is no third reading in which the braces are content: to write a delimiter as text, escape both of its braces (note 6). So{{v; a:{{n}}; default:D}}is one placeholder holding another, while{{v; a:{{n}}is the literal text{{v; a:followed by the placeholder{{n}}(note 7).Nesting is not otherwise limited: a placeholder MAY hold a placeholder to any depth, and how deep a message nests is a fact about the message alone. What an implementation resolves is bounded by section 13, which bounds resolution and not derivation.
A verdict is final. Whether a complete placeholder derives at a given position is a function of the message and that position alone. It does not depend on where the scan reached the position from, on how deeply the construct is nested, or on anything an implementation configures. A scan that has answered once for a position therefore answers the same way at every later attempt, and an implementation MAY record that answer.
It should. Note 7 has the scan resume at the very next code point where a placeholder does not derive, so the attempts overlap; without the record, a message that nests deeply costs time growing faster than any polynomial in its length, and an attacker writes one in a few hundred characters. With it, the scan costs time proportional to the length of the message. Section 14.1 requires the bound, not the technique.
7. Escaping
A backslash cancels the structural meaning of the character that follows it, and the pair denotes that character as literal text. A character that has no structural meaning where it appears is unchanged by a preceding backslash: both characters stand, and the backslash denotes itself. A backslash at the end of a message has no character to cancel and denotes itself likewise.
The rule is uniform across the whole message string: a backslash consumes the
character after it wherever it appears, and the same characters are escapable
everywhere. A position decides reach, not membership — a line terminator is
escapable like any other member of whitespace, but an escape sequence that
carries one is never inside a placeholder (section 6, note 1).
The characters that carry structural meaning are :, ;, {, }, \ and
the members of whitespace (section 6). So \:, \;, \{ and \} write
those characters as text, \\ writes a single backslash, and an escaped space
writes a space that the insignificance rules of section 8 will not take. Every
member of whitespace escapes that way, the line terminators among them, so
\ before one is an escape sequence like any other — though none carries a
line terminator into a placeholder (section 6, note 1).
Escaping is defined at the level of the message string, not at the level of the file that carries it. A message stored in JSON must additionally satisfy JSON's own escaping rules, so a single format-level backslash is written as two characters in a JSON source file:
| Intent | In the message string | In a JSON catalogue |
|---|---|---|
literal : |
\: |
"\\:" |
literal ; |
\; |
"\\;" |
literal { |
\{ |
"\\{" |
literal } |
\} |
"\\}" |
literal {{ |
\{\{ |
"\\{\\{" |
literal }} |
\}\} |
"\\}\\}" |
literal \ |
\\ |
"\\\\" |
| a space trimming keeps | \ followed by a space |
"\\ " |
A literal backslash in front of a structural character is written by escaping
both — \\\: yields \:.
Implementations MUST remove escape sequences from the message, and from
nothing else. A sequence is removed where section 6 derives it — in the text of
the message and in the names a placeholder writes — and it is removed once, when
the message is parsed, before anything is resolved (section 5). Removing one
leaves what the first paragraph of this section says it denotes: the escaped
character alone where the backslash cancelled a structural meaning, and both
characters where it cancelled none, so \a renders as \a.
That rule governs the message, not the spellings it reaches its parts by. A key and a modifier name are each matched by name against something a host wrote — a payload entry, a registered modifier — so each is compared by exact code-point equality against the name the single removal above left (section 6, note 2); nothing unescapes a name a second time. An option key is unescaped the same way, but it looks nothing up: it is compared against the value, and that comparison belongs to the modifier that performs it (section 11.1). All three are what their author wrote, not the spelling a reserved character forced. An option value is unescaped by the same single removal, and where an option key stands for its own value (section 9.4) that value is the message's own text and is unescaped with the rest of it.
Payload text is not escaped text. A value, a props value, a payload
default, a wrapper's default and a modifier's return value are data, and an
implementation MUST NOT remove escape sequences from any of them (sections 5,
14.1). A backslash a value carries is a backslash. That is what lets a value
hold a Windows path, a regular expression or a serialization without the
message's escaping rules reaching into it, and it is why the text section 4
produces is the text the output carries:
{ a: 'C:\U' } serializes to {"a":"C:\\U"} and renders {"a":"C:\\U"}
A caller that needs a machine-readable serialization in the result therefore needs to do nothing for it: what the conversion produced is what the output carries, parsable as the conversion made it. A host-defined modifier that reads a serialized value back (section 4) receives that same text.
8. Whitespace
Within a placeholder:
- Whitespace surrounding the key is not significant.
- Whitespace surrounding the modifier name is not significant.
- Whitespace surrounding an option key or an inline default key is not significant.
- Whitespace surrounding an option value or an inline default value is not significant. Whitespace inside one is.
These rules take the members of whitespace (section 6), and nothing else. A
code point outside the class is ordinary text: a key padded with one is a
different key, and an option value that holds only one is not empty.
Whitespace that an escape sequence claims is text, not padding: it belongs to the key, the option key or the value it appears in, and the rules above do not remove it (section 7).
These rules are read over the spelling, before anything is resolved. What
is padding is decided by the characters the message writes, so a placeholder an
option value holds is content wherever it stands, and the text it resolves to is
never padding however it is spelled: in {{a; x: {{b}} {{c}} ;}} the space
before {{b}} and the one after {{c}} are padding, the one between them is
content, and a value of {{b}} that resolves to three spaces contributes three
spaces. That is what keeps section 5's rule whole — a value cannot be trimmed by
a rule it was never text for.
An option value that consists only of unescaped whitespace therefore trims away
to nothing, so x: and x: are equivalent: both declare the empty string
(section 9.4).
A modifier name or an option key that trims away to nothing, or that is
empty to begin with, names nothing: the placeholder declares no modifier, and
the segment declares no option. So {{v:}} is a plain substitution (section
9.5), and {{v;;}} and {{v; }} have no options.
Consequently {{value}}, {{value;}}, {{ value }} and {{ value; }} are
equivalent.
9. Resolution
A placeholder resolves in the following order.
The bounds of section 13 and the message errors of section 14.2 have their places among the steps below. The read limit is tested first, before section 9.2 reads anything: a placeholder reached once the budget is spent resolves to the empty string, however deeply it nests and whatever it names. Every other placeholder looks its value up and converts it at section 9.2, which spends the budget and reports what the conversion reports, and only then are the message errors decided — the nesting limit first (section 13), then a modifier nobody registered (section 11.4), then a selection with no options (section 9.5) — before section 9.5 selects a result. A placeholder that meets one of them takes the fallback chain and meets none after it.
9.1 Parse
Split the placeholder into its selector and segments per section 6. There is no
third state between a placeholder and text: a {{ … }} construct that section 6
does not derive as a placeholder is literal text, rendered as it stands, and it
MUST NOT be used to look up any payload key or reach any later step of this
section.
A selector MAY name no key. {{}}, {{ }} and {{:eq}} are placeholders with
nothing to look up, so they resolve to the fallback chain (section 10), and they
MUST NOT resolve the payload keys "", "null" or "undefined".
9.2 Look up the value
Look up the key in the payload.
The key is matched whole, by exact code-point equality after unescaping
(section 6, note 2). It is a name and not a path: {{user.name}} names the
payload key user.name, and a payload carrying a user entry with a name
inside it owns no entry under that name, so the placeholder takes the fallback
chain. No placeholder reaches inside a value either — one that is a plain
object or a plain array resolves whole, as the text section 4 converts it to.
That text is data: it is emitted as it stands, so a {{ it carries opens no
placeholder and a backslash it carries is a backslash (sections 5, 7).
The lookup MUST consider only the payload's own entries. Members inherited
from a prototype, class or base mapping MUST NOT resolve. In a host where
mappings inherit members, constructor, toString and __proto__ are
therefore absent unless the payload carries them as own entries. This is a
security requirement, not an optimization (section 14).
A value is absent when there is no own entry for the key, or when that
entry is the host's undefined — the host's own word for nothing here, which
section 4.1 already reads that way in a wrapper's value. An undefined entry
is absent, not a value no conversion could describe, so there is nothing to
report (section 14.2). Nothing else is absent: a value of zero, empty string,
false or the host's null MUST be treated as present. A value that is present
but that no conversion can describe (section 4) is treated as absent. So is an
own entry, of the payload or of a wrapper, the host raises on where it is read:
what cannot be read cannot be described, and the implementation MUST NOT raise
for it either.
If the entry is a wrapper (section 4.1), the value is the wrapper's value, and
the wrapper's default and props join resolution as sections 10 and 11.2
describe. The own-property requirement applies to the wrapper's own keys too: a
value, default or props that the wrapper only inherits MUST NOT resolve.
9.3 Determine the default
The inline default is the first segment whose key is exactly default, compared
case-sensitively. The default it declares is that segment's value, read the way
section 9.4 reads an option's: default:x declares x, default: declares the
empty string, and default alone — no colon — declares the key itself, the text
default.
default is also a reserved payload key: it is the fallback for every
placeholder in the message that resolves to no value, and {{default}} reads
that same fallback rather than a placeholder of its own.
The placeholder's default is the first link of the chain in section 10 that
yields text. The payload's default takes precedence over the inline
default:, and a wrapper's default takes precedence over both. This step
names where the default comes from, not when the chain is walked: section 10
walks it only where the placeholder uses its result.
The inline default is an option value, so it may hold a placeholder (section 6, note 10) and it is read the way section 9.4 reads any option's value: only where the placeholder uses it. A default nobody falls back to is never rendered.
If no link yields text, the default is the empty string.
9.4 Collect the options
Every segment other than the inline default that names an option key is an option, in source order. A segment whose option key is empty or trims away to nothing (section 8) declares no option, with or without a value.
key:valueyields that key and value.keyalone — no colon — yields the key as both key and value.key:yields that key and the empty string. The colon declares a value, so a value that ends at the colon — or that is only unescaped whitespace (section 8) — is empty rather than absent.- Where two options share a key, the first MUST win wherever a modifier selects by key (section 11.1). Collection keeps every option in source order, duplicates included, so a host-defined modifier is handed the list as the placeholder wrote it.
Collecting an option does not read its value. An option's value is the text the message writes there with its escape sequences removed and the placeholders it holds resolved (section 6, note 10), and that text is produced only where something reads it — which, for a selection, is the one option the modifier chose (section 11). An option the modifier passes over costs nothing: no payload entry a placeholder in it names is read, no modifier such a placeholder names is called, and no report it would have made is made.
The reserved key default MUST NOT appear among the options.
9.5 Select the result
- If the placeholder has no modifier and no options, it is a plain substitution: the result is the value, or the default (section 10) if the value is absent.
- Otherwise it is a selection: the result is produced by the modifier
(section 11), which is
eqwhen no modifier is named.
Giving a placeholder that carries no modifier its first option therefore changes
what it asks for, and not only what it may answer. {{v}} asks for the value;
{{v; a:A;}} asks which option matches it, and where none does the result is
the fallback chain (section 11.1) and never the value itself. Over the value
RAW the first renders RAW and the second the empty string, or the declared
default where there is one.
A placeholder that names no key has nothing to compare, so it resolves at
section 9.1 and is neither a plain substitution nor a selection. A modifier
name it carries is still subject to section 11.4: {{:zz}} resolves to the
fallback chain and is a message error for naming a modifier nobody registered.
A selection that names a comparison modifier (section 11.1) or a plural
selection (section 11.5) and no options is a message error (section 14.2): the
author asked which option matches and offered none. A formatting modifier
selects nothing, so {{n:number}} is complete as it stands, and whether a
host-defined modifier needs options is that modifier's own business. A host
that registered its own modifier under one of those names (section 11.3) has
replaced the format's, so a placeholder naming it asks a host-defined modifier:
{{v:eq}} over that registration is no message error.
An implementation SHOULD report that error, naming it missing-options
(section 14.2), and the placeholder takes the fallback chain (section 10) as
every message error does. It takes it before any modifier is asked, as an
absent value does: the locale is not tested and the value is not compared, so
{{n:plural; default:D;}} renders D where no locale is available and reports
nothing but missing-options. What the placeholder declares is what makes the
error, so the report does not turn on the payload: {{v:eq}} is reported over
a payload that supplies v and over one that leaves it absent alike, though an
absent value takes the chain before any modifier is asked (section 11.1). It
turns on what answers to the name: the format's own eq is reported, and one a
host registered in its place is not.
10. The fallback chain
A placeholder that resolves to no value takes the first of the following that yields text:
- the
defaultof the wrapper the payload entry was, where it was one (section 4.1); - the payload's own
defaultentry; - the inline
default:of that placeholder; - the empty string.
A link that is absent, or whose value no conversion can describe (section 4), is skipped.
The chain is read only where the placeholder uses its result. A placeholder that resolves to a value never reaches it, and a modifier that answers without its default leaves it unread, so a link nothing needed is neither converted (section 13) nor reported for a value it cannot describe (section 14.2). Nor is a placeholder inside link 3 resolved there (section 12).
The links are not all the same kind of text. Links 1 and 2 come from the payload and are data: each is emitted as it stands, unscanned and unescaped (sections 5, 7, 14.1). Link 3 is the message's own — an option value like any other — so its escape sequences are removed with the rest of the message's and a placeholder it holds is resolved where the chain reaches it (sections 7, 12).
A link yields text where it is present and can be described. The empty
string is text: an inline default: declaring nothing, or a wrapper default
holding the empty string, yields it and the chain stops there rather than
stepping past.
The payload outranks the message: a message declares the default it was written with, and the application overrides that default where it needs to, so the more specific statement wins.
Over one message that declares its own default, the links rank as follows. The placeholder is a plain substitution (section 9.5), so it reaches the chain only where the value is absent, and the empty string in the second row is a value and not a missing one:
Hello, {{name; default:Guest;}}!
payload { name: 'Alice', default: 'Friend' } -> "Hello, Alice!"
payload { name: '', default: 'Friend' } -> "Hello, !"
payload { name: { default: 'You' },
default: 'Friend' } -> "Hello, You!"
payload { default: 'Friend' } -> "Hello, Friend!"
payload {} -> "Hello, Guest!"
11. Modifiers
A modifier receives the value, the options, the default, the locale and the props, and returns an answer.
An option's value and the default reach it unread. Producing either is work — an option value may hold a placeholder (section 6, note 10), and the default is a chain to walk (section 10) — and a modifier that selects one option does not pay for the rest. So an implementation MUST NOT resolve a placeholder inside an option's value, MUST NOT call a modifier such a placeholder names, and MUST NOT walk the fallback chain, except where the modifier reads that value or that default. A comparison reads one option's value; a modifier that reads every option's value is entitled to, and pays for all of them.
The value and the default both reach the modifier as text (section 4): the default is the text the chain in section 10 resolved to, and the value is the text the payload entry converted to. No modifier sees a value at the type it was authored with.
A modifier's answer is a host value in turn, and becomes text by that same conversion: a structured answer serializes rather than collapsing to whatever the host calls an object, so a modifier writes the text a payload value of that shape would. Neither an answer no conversion can describe nor an answer that is nothing at all is an answer, and the placeholder takes the fallback chain (section 10) — the treatment a value that is not a value gets. An answer that is nothing at all is the host's undefined, and on a host whose functions answer nothing with their null, that null (section 4).
That text is data, like a payload value: it is emitted as it stands, never parsed and never unescaped (sections 5, 7, 14.1). A modifier that builds its answer out of the value it was handed therefore cannot build syntax with it, and a host extension is not a way around the rule that data is not syntax.
Modifier names are case-sensitive. eq is a modifier; EQ is not.
11.1 Comparison modifiers (Core)
| Name | Selects the first option whose key |
|---|---|
eq |
equals the value, compared as text, case-insensitively |
ne |
differs from the value, compared as text, case-insensitively |
lt |
is greater than the value, comparing numerically |
lte |
equals the value, compared as text, case-insensitively, otherwise as lt |
gt |
is less than the value, comparing numerically |
gte |
equals the value, compared as text, case-insensitively, otherwise as gt |
Case-insensitively names one comparison: the option key and the value are
each mapped to lower case by the host's ordinary, locale-independent case
conversion, and the two results are compared by code-point equality. Section 7
defers the comparison here, and this is the whole of it: an implementation MUST
NOT substitute a collation, a normalization or a case fold for it, and MUST NOT
tailor the conversion to a locale. The comparison modifiers are Core
(section 2), so they resolve alike in every locale and where no locale is
available at all — a conversion a locale tailors reads I and i as one text
in most languages and as two in Turkish, and a Core selection cannot turn on
which. The mapping is not a case fold, and it does not always leave apart what a
fold would join. The lower case of STRASSE is strasse and not straße, so
an option keyed STRASSE does not select for the value straße. But Unicode
gives some characters a single-character lower case that lands on another
character, so U+212A KELVIN SIGN lowercases to k, U+1E9E LATIN CAPITAL LETTER SHARP S to ß, U+2126 OHM SIGN to ω and U+212B ANGSTROM SIGN to
å, and an option keyed with either member of such a pair selects for a value
spelled with the other. Those selections are required rather than an artifact of
a host: the mapping is the one Unicode specifies, and an implementation MUST NOT
narrow it to the ASCII letters.
The mapping is Unicode's full lower-case mapping, the one its default case
conversion applies: the context it gives a final sigma included, and none of
the mappings it tailors to a language. It parts from the simple mapping at few
code points: under the full one U+0130 LATIN CAPITAL LETTER I WITH DOT ABOVE
lowercases to i followed by U+0307 COMBINING DOT ABOVE, and a capital sigma
to ς in the Final_Sigma casing context, which Unicode's default case
algorithms define, where the simple one gives i and σ. So
{{v:eq; ΟΔΟΣ:X; default:D}} renders X over οδος, and
{{v:eq; σ:X; default:D}} renders X over a lone Σ, which has no cased
letter before it. Which version of Unicode the mapping follows is the host's
ordinary conversion's, and the implementation documents it. A host whose
ordinary conversion maps the ASCII letters alone, or applies the simple
mapping, has none this section can use, and its implementation supplies the
full mapping in its place.
An option key is compared against the value's own text (section 11), never
against what another placeholder makes of it. The count and the noun are two
placeholders: {{n}} {{n; 1:file; default:files;}} renders 1 file over
{ n: 1 } and 5 files over { n: 5 }. At an English locale
{{n:number}} {{n; 1,000:K; default:files;}} over { n: 1000 } renders
1,000 files: the grouping belongs to the placeholder that formats it, and the
one that selects still compares against 1000, which the key 1,000 does not
equal. This comparison sees only whether the count is one; a language whose
forms turn on more than that selects by the count's plural category instead
(section 11.5).
lt and lte MUST consider options in ascending key order; gt and gte in
descending key order. Ordering is by numeric value of the key; an option whose
key is not numeric MUST NOT be selected by a numeric comparison, and a key whose
conversion yields NaN is not numeric (section 4). So
{{v:lt; 10:A; NaN:N; 5:B; default:D}} renders B over 3 and D over
NaN, on a host whose conversion reads NaN as NaN and on one that reads it as
no number alike. Two keys spelled differently that carry the same numeric value
— 2 and 2.0 — tie, and a tie MUST be broken by source order (section 9.4),
so an ordering that is not stable does not conform. Implementations MUST NOT
reorder the caller's option list observably.
lte and gte compare for equality first, over every option in source order
(section 9.4), and reach the key order above only where that comparison selects
nothing. The equality leg is a text comparison rather than a numeric one, so the
option it selects need not carry a numeric key: {{v:lte; abc:X}} selects X
for the value abc, which the numeric leg on its own passes over.
If no option is selected, the result is the fallback chain (section 10).
ne has no special handling for an absent value: like every other modifier, an
absent value takes the fallback chain.
11.2 Formatting modifiers (Intl)
These delegate to the host's internationalization facilities and require a
locale. If no locale is available, the result MUST be the empty string and not
the fallback chain (section 10): a declared default does not stand in for a
locale nobody supplied. A locale is not available where the caller supplied
none, and where what it supplied is empty. One the caller did supply that the
host then rejects is available but unusable, which is the formatting failure at
the end of this section and takes the chain like any other. An implementation
SHOULD report a locale that is not available, naming it missing-locale
(section 14.2): the empty string is what such a placeholder renders, and the
report is what says why it rendered nothing.
The locale is tested first among what the modifier does, before it reads the value as a number or a date: the value has already been looked up and converted at section 9.2, spending the read budget (section 9), and where no locale is available the result is the empty string whatever that value is, so the two rules below that send a placeholder to the fallback chain — a value that is blank, and an input the modifier cannot format — are reached only where a locale is. The test is reached in turn only where the placeholder reaches a modifier at all: an absent value (section 9.2) takes the chain before any modifier is asked, whether a locale is available or not. A placeholder whose value is absent therefore reports no missing locale, over a caller that supplied one and over a caller that supplied none alike.
| Name | Input | Formats as |
|---|---|---|
number |
a number | a locale-formatted number, at most 2 fraction digits by default |
date |
milliseconds since the Unix epoch; failing that, text the host can parse as a date | a locale-formatted date |
ago |
a signed millisecond delta relative to now — negative is past | a locale-formatted relative time |
currency |
a number, multiplied by a ratio property defaulting to 1 |
a locale-formatted currency amount |
number, ago and currency take a number only. Because every value arrives
as text (section 4), date also accepts text the host's own date parsing
understands; that is what keeps a value authored as a date instance formattable.
Which text that parsing reads is the host's, and an implementation documents the
forms it reads. It SHOULD read two of them: the text the host's ordinary string
conversion writes for a value of the host's own date type, as the instant that
text writes, and an RFC 3339 date-time spelled with an uppercase T, its year
from 0001 to 9999, its seconds from 00 to 59, a fraction of a second or
none, and an explicit offset, Z or one of +hh:mm and -hh:mm —
2024-03-05T10:00:00.000Z and
2024-03-05T11:00:00+01:00 are one instant. It SHOULD NOT read as a date a text
whose instant depends on when it is read, tomorrow or next monday. Every
other form is the host's, and two hosts differ over it: a date-time written
with no offset, or a bare date, reads as UTC on one and as local time on
another, so a value that must format alike everywhere is a timestamp or a
date-time written with its offset.
Text that is empty or consists only of whitespace (section 6) is not a
number, whatever the host's numeric conversion makes of it, and is not a date
either. No formatting modifier can format it, so the placeholder MUST take the
fallback chain (section 10). The class is the one section 8 applies to message
text, applied here to a payload value: an implementation MUST NOT substitute
its host's own notion of a blank string. This governs formatting alone: a
numeric comparison (section 11.1) converts blank text like any other text.
What makes any other value a number is the host's ordinary numeric conversion
(section 4), applied to the whole of the text and yielding something finite.
The conversion reads the whole value and not a leading part of it, so 12px is
not a number; whitespace around a number is not part of it, so 12 is one; a
sign and an exponent are part of it, so +12 and 1e3 are. An infinity is not
a number a formatting modifier can format either, so {{v:number}} over a
value the host's conversion reads as an infinity — Infinity in ECMAScript —
takes the fallback chain while {{v:gt; 5:X}} still selects X for that same
value; and currency applies the test a second time to the product of the
value and its ratio, so a product that overflows to an infinity takes the
chain too.
The whitespace around a number is the class of section 6 and nothing else, so
12 with U+3000 IDEOGRAPHIC SPACE or U+FEFF on either side is a number,
while 5 after U+0085 NEXT LINE, which the class does not hold, is not. An
implementation converts the text that remains once that class is removed from
both ends, and a code point outside the class is part of that text whatever
the host's conversion makes of it: where the conversion does not skip the class
around the text, the implementation removes it first, and where the conversion
also skips a code point outside the class, as Python's float skips U+0085,
the implementation reads text that begins or ends with one as no number. This
holds for a formatting modifier and for a numeric comparison (section 4) alike,
and for an option key a numeric comparison orders by (section 11.1) as for the
value it compares, so {{v:gt; 1:X; default:D}} renders D over U+0085
followed by 5. Whitespace an escape sequence claims stays in the key
(section 8), so {{v:lt; \ 5:A; default:D}} renders A over 3, and so does
the same message with U+3000 after the backslash. ECMAScript's conversion
skips exactly that class, so an implementation there does neither. What the
conversion reads in what remains, its whitespace aside, is the host's, as the
next paragraph says.
Beyond that the test is the host's, and deliberately so: it is the same
conversion section 4 requires of a numeric comparison, and a second test
written here would leave {{v:number}} and {{v:date}} disagreeing about what
a number is. Only the conversion carries over: where it yields no number a
comparison merely fails (section 4), while a formatting modifier resolves to
the fallback chain. Two hosts whose conversions read different literal forms —
a hexadecimal one, say — therefore differ over such a value, and a message
that must format alike everywhere writes a decimal number.
date reads its value as a timestamp first: only a value that test does
not read as a number reaches the host's date parsing. The two readings overlap,
because a bare run of digits is both a count of milliseconds and a year, and
the order is what settles which one a message gets. {{v:date}} over 2024
formats the 2024th millisecond after the epoch and not the first day of 2024;
over 99 it formats the 99th and not a day in 1999. The order reaches past the
digits, too: 2024 is a number with whitespace around it and +1000 is a
signed one, so both are timestamps, though a host's date parsing reads a year
in each. An implementation that parsed dates first would render a different
date for the same payload, so this order is normative and not a tie a host may
break.
Formatting properties are read from props under the modifier's own name,
layered over implementation-configured defaults, which are layered over the
defaults this section states — number's fraction digits and currency's
ratio above, ago's format and numeric below. A wrapper's props
(section 4.1) is layered over all of them.
A property is not an option (section 3): an option is a segment written in the placeholder and offered to the modifier for selection, and a formatting modifier selects nothing (section 9.5). The layers above are the only place a formatting modifier reads a property from, and a placeholder is not one of them, so a placeholder segment spelled like one is an option that reaches no layer.
Every layer composes per property: a layer overrides only the properties it names, and the properties it does not name keep whatever the layer beneath it gave them. A layer MUST NOT reset a property it does not name. A property a layer holds the host's null under is one it names: null is a value (section 3), and it is what reaches the formatting request. A property the layer holds the host's undefined under, like one it does not hold, is one it does not name.
So is a property, or a whole layer, the host raises on where it is read: what
cannot be read names nothing, and the modifier is handed what the other layers
name. Props are the caller's, and a property that cannot be read is a defect in
them rather than an input the modifier cannot process: it is reported as a
value no conversion can describe (section 14.2), once for the placeholder,
however many properties or layers refused (section 14.3). The props are read
where a modifier is handed them and nowhere else, so a plain substitution
(section 9.5), or a placeholder whose value is absent, never reads a wrapper's
props.
implementation defaults number: { maximumFractionDigits: 4, useGrouping: false }
props number: { useGrouping: true }
wrapper props number: { maximumFractionDigits: 1 }
effective { maximumFractionDigits: 1, useGrouping: true }
Each layer is read from its own entries, the way section 9.2 reads the payload. A property a layer merely inherits MUST NOT reach the formatting request (section 14.1).
The two fraction digits number formats by default are a default maximum,
not a cap. Where a layer names a minimumFractionDigits above it, the default
maximum widens to reach that minimum instead of contradicting it; a layer that
names the maximum itself decides it. The widening runs after the layers have
composed, and it is the one place a property no layer named does not keep what
the layer beneath it gave. A default that held at two would make a minimum
above two unformattable, and the placeholder would take the fallback chain over
properties the caller wrote deliberately.
currency formats in the currency style. That style is what the modifier is
rather than one of the properties it layers, so a layer MUST NOT replace it; an
implementation applies it over every layer.
The currency to format in is a property like any other: it comes from the layers above and from nowhere else, and a placeholder segment spelled like one is an option that reaches no layer. A message cannot name it, so where the host's facility requires one and no layer supplies it, the modifier cannot format its input and the placeholder takes the fallback chain by the last rule of this section. An amount's currency comes from the caller, never from the message.
ago selects a unit automatically unless one is named. The selection chooses
among second, minute, hour, day, week, month and year. A unit is
named by a format property in the layers above, holding one of those units,
written in the singular or the plural, rather than a name from the host's whole
relative-time vocabulary; the value auto, which is what a layer naming none
leaves in place, is that automatic selection. Both the unit and the count are
chosen from the magnitude of the delta, and the sign is applied to the
result: a delta and its negation MUST select the same unit and the same count
with opposite signs. A host rounding rule that takes a half in one direction —
toward positive infinity, say — reads "half an hour from now" and "half an hour
ago" as different distances, which no reader of a relative time expects.
A format property that names none of those units is a property the modifier
cannot process: the placeholder resolves to the fallback chain (section 10),
and the implementation SHOULD report the failure as failed-modifier
(section 14.2), the way an input the modifier cannot format does by the last
rule of this section. The unit names are compared exactly, so a spelling that
differs by case names none of them: a placeholder whose format is YEAR
takes the chain, where one whose format is year or years names the year.
The automatic selection climbs those units in order, each step a fixed multiple
of the one below it: 1000 milliseconds to the second, 60 seconds to the minute,
60 minutes to the hour, 24 hours to the day, 7 days to the week, 13/3 weeks —
four weeks and a third — to the month, and 12 months to the year. The delta is
divided by 1000 and rounded to a whole count on its magnitude; that is the
count of seconds, and the climb starts there whatever the count is, so a delta
under half a second is a count of zero seconds. Each further step divides the
count it was handed by that step's multiple and rounds it the same way, and is
taken only where the rounded count is one or more in magnitude. The first step
that rounds below one is not taken, the climb stops at the unit beneath it, and
year is where it runs out in any case.
The rounding is applied at every step rather than once at the end, so a unit's
count is the rounded count of the unit beneath it divided again, and the two do
not always agree: three and a half days is four days before it is a week, and
so formats as one week rather than as the half week the arithmetic alone gives,
and twenty-six days is four weeks and then one month. A half rounds away from
zero, so a step is taken as soon as the count beneath it reaches half that
step's multiple: 30 seconds make a minute, 30 minutes an hour, 12 hours a day,
4 days a week, 3 weeks a month and 6 months a year. A named unit climbs the
same ladder with the same rounding and stops at the unit it names whatever the
count is there: format: 'day' at a delta of three hours is a count of zero
days, and format: 'second' at a delta of 800 days is a count of 69 120 000
seconds.
ago asks the host for its non-numeric phrasing wherever the host has one, so
a count of one day reads as "yesterday" in English rather than as "1 day ago",
and the count of zero seconds a small delta rounds to reads as "now". The
phrasing is a numeric property in the layers above, and where no layer names
one it is auto — not whatever the host's own formatter would default to,
which in ECMAScript is always, spelling every count out. A layer naming
numeric decides it like any other property.
The properties are named in the vocabulary of ECMAScript's internationalization facilities. That vocabulary is this format's way of describing a request rather than a call into any one language: a host reads each name onto its own facility, and a caller that writes the same properties asks for the same formatting wherever it runs.
That vocabulary also says which requests are valid, and an implementation
SHOULD judge a request by it, here and in section 11.5, rather than by what its
host's facility happens to accept. A locale that is not a well-formed Unicode
BCP 47 locale identifier (UTS #35) as ECMAScript's facilities read one — en_US,
i-klingon — and a property one of their constructors refuses — a currency
that is not three ASCII letters, or none at all in the currency style — are
then a request the host rejects however lenient its facility is: the modifier
cannot format its input, and the placeholder takes the fallback chain by the
last rule of this section rather than rendering en_US in some locale or an
amount with no currency.
A platform with no internationalization facility of its own has the one its implementation formats through. There a library, with the implementation's own code applying that library's data, is the host's facility, here and in section 11.5, and the implementation documents which library it is and at which version. What the library's data lacks — a non-numeric phrase, a narrow currency symbol, a locale's minimum for grouping — is locale data, and an output that differs from another implementation's by it differs by locale data alone (below). What the library cannot be asked is a property the facility cannot express, documented and stated as the next paragraph requires.
A property the host's facility cannot express is not an input the modifier cannot format. The modifier formats with the properties the facility does express, and the placeholder does not take the fallback chain on a property's account: a number formatted without the grouping a caller asked to suppress is still the number, and a fallback in its place is not. The same holds where the facility expresses a property but not every value of it, or not alongside every value of the request's other properties: a facility that formats an amount with its currency's own digits cannot express the digit properties in the currency style, and formats a decimal number with them all the same. What it cannot express there goes unapplied, and the rest is formatted. An implementation MUST document what it cannot express — each property or value of one, and, where it cannot express it in some requests only, the values of the other properties that make a request one of them — and states it to the conformance set through the adapter of section 14.3, so that what it did not apply is observable rather than only written down.
Because the output of these modifiers depends on the host's locale data, an output string on its own tests that data as much as it tests this format. An implementation MAY expose the formatting request it makes — the operation, its properties and its input — to the conformance adapter (section 14.3); an implementation that does not exposes only the output, and an output differing from another implementation's by locale data alone is not a non-conformance.
A formatting modifier that cannot format its input MUST NOT raise; it resolves to the fallback chain and SHOULD report the failure.
11.3 Host-defined modifiers (Extensions)
A host MAY register additional modifiers. A host's own configuration overrides the format's, so a host-defined modifier MAY replace a modifier named in this specification.
What a host registers under a name MUST be a modifier. A name a message may write is one this specification names or one a host registered a modifier under, so registering anything else registers no modifier: it does not make the name one a message may write, and it does not replace a modifier already answering to it. Where nothing else answers to that name, a message writing it names a modifier nobody registered, and section 11.4 governs; where this specification names a modifier under it, that modifier answers as it did.
A host-defined modifier receives its properties the way a formatting modifier does (section 11.2): the props written under its own name, layered as that section layers them — the implementation-configured defaults, then the call's props, then the wrapper's props — with every layer overriding only the properties it names and every layer read from its own entries. It receives what the layers wrote under its own name and nothing they wrote under another's, and where no layer wrote under its name it receives no properties at all.
It receives the locale the caller supplied, verbatim: an empty locale reaches it empty, and where the caller supplied none it receives none. Section 11.2's test for a locale that is not available governs the modifiers that section and section 11.5 name — the format defines those, so it must say what they do without one — and does not reach here. What a host-defined modifier needs of a locale is the host's own, and an implementation substitutes nothing for it: it supplies no locale of its own and does not collapse an empty one into none, so a modifier that treats the two differently can.
Because a host's own registration overrides the format's, a name a later version of this format defines costs a host that already registered its own modifier under that name nothing: its messages resolve as they did, and the only thing out of reach is the modifier the new version defines.
A host-defined modifier may keep what it was handed and read it later. Section 13 says which reads made during the resolution count toward a bound an implementation keeps, and which are refused and answer the empty string. Once the resolution has returned, an option's value or the default it kept answers the text a read of it during the resolution produced, and the empty string where no read produced it then: after a resolution returns, nothing resolves, reports or runs host code on its account.
A host-defined modifier that raises MUST be contained: the placeholder resolves to the fallback chain and the failure SHOULD be reported. Section 14.2 says what a failure is, and that the host interrupting the process is not one.
11.4 Unknown modifiers
A modifier name that is neither specified nor registered is a message error (section 14.2). The placeholder resolves to the fallback chain. A name this document specifies only at a level an implementation does not satisfy is not specified for that implementation, so the names sections 11.2 and 11.5 define are unknown to one that does not satisfy Intl (section 2).
Implementations MUST NOT silently treat an unknown modifier as eq. A message
written today as {{d:duration}} must not render as an equality selection now
and silently change meaning when a later version of this format defines
duration.
11.5 Plural selections (Intl)
plural and ordinal select an option by the plural category a locale puts a
number in. The categories are those of the Unicode CLDR plural rules, as the
host's internationalization facilities expose them — in ECMAScript,
Intl.PluralRules. plural selects by the cardinal rules, which say how many
of something there are; ordinal selects by the ordinal rules, which say where
something stands. Each is a selection, as the comparisons of section 11.1 are,
and each depends on the locale, as the formatting modifiers of section 11.2 do,
so both are Intl (section 2).
A comparison does not do their work. Most languages with more than two forms for a count choose among them by the remainder of the count rather than by its size — in Russian 1, 21 and 101 take one form, 2, 22 and 102 another, and 5, 11 and 12 a third — and no set of keys a comparison orders can spell a remainder.
There are six categories: zero, one, two, few, many and other.
Every locale has other, and each has its own subset of the rest. A category
is a form of the language rather than a number: in Latvian zero is 0, 10 to
20, 30 and 100; in French one is 0, 1 and 1.5; and English has no zero, so
0 is other there. Which category a number is in is the host's locale data, as
the text a formatting modifier produces is (section 11.2).
An option key is a category, a number, or neither:
- A category is one of the six names, compared exactly as written:
oneis a category andOneis not, asdefaultis the inline default andDEFAULTis not (section 9.3). - A number is a key that section 11.2's test reads as one, applied to the key's
own text.
0,234,1.5,-1and1e3are numbers;1,000,Infinityandtwelveare not. - A key that is neither never selects.
The two kinds are not interchangeable, and a message may write both. 0
selects for the number zero in every locale; zero selects for the numbers a
locale puts in that category, which in Latvian is not zero alone and in English
is none.
A placeholder that names either modifier, declares an option and has a present value resolves as follows. One that declares no option takes the fallback chain as section 9.5 says, and an absent value takes it before any modifier is asked (section 11.2).
- The locale is tested first, as section 11.2 tests it. Where none is
available the result is the empty string, and the implementation SHOULD
report
missing-locale(section 14.2). - The value MUST be a number by section 11.2's test, and for
ordinalan integer: an ordinal of a fraction names no position, and the ordinal rules are written for whole numbers. A value that is not is an input the modifier cannot process, so the placeholder takes the fallback chain and the implementation SHOULD reportfailed-modifier. - The first option in source order (section 9.4) whose key is a number equal
to the value, compared numerically, is selected.
2and2.0are equal, and the one written first wins. A key is compared with the value and not with the number as it would be shown, so2does not select for1.999. - Where no such option exists, the host is asked for the category the locale puts the value in, with the properties below, and the first option whose key is that category is selected.
- Where nothing is selected, the result is the fallback chain (section 10).
A number therefore wins over a category wherever the two are written, which is
the one place these modifiers depart from section 11.1's first match. 0: is
how a message gives zero its own text in every locale, and what it renders must
not turn on where its author wrote it:
{{n:plural; one:{{n}} file; other:{{n}} files; 0:No files;}}
At an English locale this renders No files over { n: 0 } — though 0 is
other in English and other: is written first — 1 file over { n: 1 } and
5 files over { n: 5 }.
The host is asked for a category at step 4 and nowhere else. A resolution that stops earlier — at a number that matched, a value that is not one, a locale that is not available — asks it nothing. An implementation that exposes the formatting requests it makes (section 11.2) exposes the one a plural selection makes the same way, with the category the host answered, because that category is what chose the option.
other is a category like the rest: it selects where the locale puts the value
in it. default is not a category. It is the inline default (section 9.3),
reached through the fallback chain wherever nothing is selected — the value
absent, no option declared, a value the modifier cannot take (for ordinal, a
number that is not an integer), a request the host cannot make, or a category
the placeholder writes no option for — and never where a present value meets a
locale that is not available, which renders the empty string. Over a
placeholder that writes every category its locale has, the two part only where
no category is asked for, and there they say different things: other that the
count is many, default that it is not known.
{{n:plural; one:{{n}} soubor; few:{{n}} soubory; many:{{n}} souboru; other:{{n}} souborů; default:neznámý počet;}}
At a Czech locale this renders 5 souborů over { n: 5 } and 1.5 souboru
over { n: 1.5 }, and neznámý počet over {} and over { n: 'abc' }. A
default that wrote {{n}} would render it exactly where n is missing.
plural takes its category from the number as number would show its digits,
because the plural rules are written over a number as it is shown, fraction
digits included: Czech says 1 soubor but 1,0 souboru, and English 1 file
but 1.0 files. A count and the word that agrees with it are two placeholders
here — {{n:number}} decides how the number is shown, {{n:plural}} which form
the word takes — and a category taken from other digits than the ones shown
would disagree with what the reader reads.
{{n:number}} {{n:plural; one:soubor; few:soubory; many:souboru; other:souborů;}}
At a Czech locale this renders 1,5 souboru over { n: 1.5 } and 2 soubory
over { n: 1.999 }. number shows at most two fraction digits by default, so
1.999 is shown as 2, and it is the category of 2 that is asked for; asked
of 1.999 itself, the Czech rules answer many.
plural therefore reads, from the layers of section 11.2 under the name
number, the properties that decide which digits a number is shown with —
minimumIntegerDigits, minimumFractionDigits, maximumFractionDigits,
minimumSignificantDigits, maximumSignificantDigits, roundingIncrement,
roundingMode, roundingPriority and trailingZeroDisplay, the properties
ECMAScript's Intl.PluralRules takes from Intl.NumberFormat so that a
selection can be made from a number as formatted — and no other property of
number's. Over them it layers the properties under its own name, composed
from the same layers, each overriding the property of the same name from
number. Every layer composes per property and is read from its own entries,
as section 11.2 requires. number's default maximum, and its widening, then
apply to what composed as they apply to number (section 11.2), so that where
no layer names a maximum the category is taken at the fraction digits number
shows. A style or a notation under number that scales or abbreviates what it
shows — percent, compact — is not read, and over one the category is taken
from the value's digits rather than from the number shown.
ordinal reads the properties under its own name and nothing of number's. It
takes integers, which show no fraction, so there is nothing for it to agree
with.
The rule type is what each modifier is rather than a property it layers, as
currency's style is (section 11.2): plural asks for the cardinal rules and
ordinal for the ordinal rules over every layer, and a layer MUST NOT replace
either. In ECMAScript the rule type is the type of Intl.PluralRules,
cardinal or ordinal.
A property the host's facility cannot express is treated as section 11.2 treats
one. A selection the host cannot make — its facility refusing the properties it
was asked with, a minimum a layer named above a maximum another named, say —
MUST NOT raise: the placeholder takes the fallback chain and the implementation
SHOULD report failed-modifier.
A message that shows a count is best written to show it with {{n:number}},
which writes it in the locale's own digits and separators, and whose digits are
what plural takes its category from. A plain {{n}} shows the value's own
text. The two agree where that text is the number number shows — an integer
written without a fraction or an exponent, under layers that neither add digits
nor remove them — and part elsewhere: {{n}} over 1.999 shows 1.999, and
plural selects for the 2 that number would have shown; over 1.0 it shows
1.0, and plural selects for 1.
12. Nesting
A placeholder MAY hold a placeholder, in an option value and nowhere else (section 6, note 10). The inner one is part of the message, found by the same scan, and resolved by section 9 like any other — but only where the option that holds it is the one selected (sections 5, 11).
{{count:gt; 0:{{count:number;}} items; default:no items;}}
Over { count: 5 } the comparison selects the option keyed 0, that option's
value is read, and reading it resolves {{count:number;}}, so the message
renders 5 items at an English locale. Over { count: 0 } the comparison
selects nothing, the default is read instead, and the inner placeholder is never
resolved: no payload entry is read for it, no formatting modifier is called, and
no report it would have made is made. Note also which construct the semicolon
in {{count:number;}} belongs to — the inner placeholder writes it, so it is
the inner one's separator and ends no segment of the outer.
A key, an option key and a modifier name hold no placeholder, so what a message looks up and what it calls are readable without a payload. A tool can answer which payload entries a message names and which modifiers it calls from the message alone, and its answer covers the nested placeholders as it covers the enclosing ones.
What a placeholder resolves to is never nested in anything. A value, a props
value, a payload default, a wrapper's default and a modifier's answer are
data (sections 5, 7, 14.1). A value holding the nine characters {{count}}
renders those nine characters; a value holding ; ends no segment; a value
holding }} closes nothing. So what a message resolves to cannot change what
the message means, and no payload can reach a branch the message did not select
for it, or write a construct the message did not spell.
{{state:eq; draft:{{note}}; live:Published; default:?;}}
Over { state: 'live', note: 'X; live:Leaked' } this renders Published, and
note is never read at all: the option holding it is not the one the
comparison selected, so the segment its text spells is never a segment.
How deeply a message nests is a fact about the message alone, and nothing in section 6 bounds it. What an implementation resolves is bounded by section 13.
13. Limits
Resolution is bounded. A payload is frequently attacker-influenced, and so sometimes is a message — a host that takes translations from its users takes message text from them — so neither may buy unbounded work.
An implementation MUST enforce all four of the following:
- An output limit. At least 100 000 characters of placeholder result MUST be permitted, counted in the unit the host measures its strings in — a UTF-16 code unit in ECMAScript, so a character outside the Basic Multilingual Plane counts twice. What is counted is what the output carries, so each result is counted once: a placeholder written inside an option value reaches the output through the result of the placeholder around it and is counted there rather than again on its own. A placeholder whose result would carry the total past the limit resolves to the empty string instead, and the walk carries on. The message's own text is not counted against it and always renders: that text is the caller's and is bounded by the message it was read from, while a placeholder's result is bounded only by the payload. Section 5 builds the output once, so nothing is discarded and nothing is cut mid-character.
- A read limit. At least 100 000 characters of value text MUST be permitted, in the same unit, counting every text a placeholder reads whether or not any of it reaches the output. Output alone bounds no work: a thousand placeholders that each read a hundred-thousand-character value and select nothing from it produce nothing and read a hundred million characters. A placeholder reached once the limit is spent resolves to the empty string, and the walk carries on — the limit is tested before the placeholder reads, so a placeholder that finds a budget and spends it past the limit still resolves, and the next one pays. Value text is what a placeholder reads from the payload: the value it names, and each link of its fallback chain the payload supplies (section 10). A value no conversion can describe produces no text and spends none of the budget, however much its conversion wrote before it was abandoned. The message's own text is not value text — an option value and an inline default are bounded by the message they were read from — and neither is what a modifier answers with, which is the placeholder's result and is bounded by the output limit above.
- A conversion limit. A value's serialization MUST NOT be abandoned while it has visited fewer than 100 000 nodes and written fewer than 100 000 characters of text, in the unit the output limit counts. A serialization that reaches either limit the implementation imposes MUST be treated as a conversion that cannot describe the value (section 4). The node minimum binds the limit the implementation imposes on the nodes it visits, not the depth at which the host's serializer refuses a value, or the implementation refuses one in the serializer's place: that refusal is a serialization that raises or yields nothing (section 4). This document sets no minimum for that depth, and the implementation documents it with what it permits.
- A nesting limit. At least 8 levels of placeholder MUST be resolved (section 12), counting the outermost as the first. A placeholder nested deeper than an implementation resolves is a message error (section 14.2): it takes the fallback chain, and the walk carries on. Only a placeholder the walk reaches counts: an option the modifier passed over is never read, so however deeply it nests it costs nothing, reports nothing and reaches no limit (section 9.4).
These are minima. An implementation MAY permit more, and MUST document what it permits.
A placeholder reaches at most one of the first two. One reached once the read limit is spent resolves to the empty string and produces nothing, so it cannot also reach the output limit, and one that reaches the output limit had the read budget to be resolved. The conversion limit is not reached by a placeholder at all: it is met inside a conversion, and what the placeholder sees is a value no conversion can describe (section 4).
The nesting limit bounds resolution and not derivation. Section 6 admits nesting to any depth, and note 11 is what keeps the scan linear in the length of the message without a bound to lean on, so which constructs are placeholders stays a fact about the message and never about the implementation. Were it otherwise, two conforming implementations would disagree about where a placeholder ends: the content of a construct nested too deep would be read as further segments of the construct around it, a different option would be selected, and the braces the scan refused would stand in the output. A message error is contained instead — one placeholder takes its fallback chain, and every other part of the message resolves as written.
No limit here raises, and none of them ends the walk. On reaching one an implementation MUST resolve the placeholder that met it — to the empty string, or to its fallback chain for the nesting limit — MUST NOT raise, and SHOULD report that a limit was reached (section 14.2). The walk then carries on, so every part of the message no limit reached still renders. A single walk has nowhere to leave an unresolved placeholder, and an unresolved placeholder in the output is the message's own syntax reaching a reader.
The conversion limit bounds the work of producing a text, which the other three cannot: each of them measures a string that already exists. Serialization follows a shared reference again every time it meets one, so a value naming the same child twice at each of twenty-four levels holds twenty-five objects and describes sixteen million leaves — nothing circular, so nothing a serializer refuses. The limit is what a single conversion may spend: a value that visits more nodes than a resolvable output could hold is read as one no conversion can describe, which is where it takes the output limit's number from. The two do not measure each other, though — a serialization visits a member it then omits, so a value can visit any number of nodes and still describe itself in two characters — and the output limit cannot stand in for a bound on the work. Nor does a count of nodes bound the text a serialization writes, since a node may be a string of any length: an array naming one string of a hundred thousand characters five thousand times visits five thousand and one nodes and writes more than five hundred million characters. So the limit measures that text too, and takes the output limit's number for it as it does for the nodes.
A node, for that count, is a value the walk visits: the value being converted, and then every member it reaches, counted once for each time it reaches one. Two things follow from counting visits rather than values. A member the walk visits and then omits is counted, so an object whose members all hold the host's undefined describes itself in two characters and still spends a node on each of them. And a value reached twice is counted twice, because the walk follows a shared reference again every time it meets one rather than recording where it has been — which is what makes the twenty-five objects above twenty-five distinct values and more than sixteen million visits.
One conversion is not a resolution. A value is read once for every placeholder that names it and reaches it, so a limit on a single conversion bounds a resolution only if its conversions cannot multiply with its reads: resolving a message MUST NOT convert a given value twice observably, and every later read of that value MUST answer with the text the first conversion produced — the answer that no conversion can describe it included (section 4). What a resolution spends converting is then set by the distinct values it reaches, not by the reads the message makes of them. A value the payload builds afresh on each read is a new value each time, and is converted each time.
Recognizing a wrapper (section 4.1) enumerates an entry's own keys, which is work that grows with the entry as a serialization's does, and an entry is read once for every placeholder that names it, so the same holds of recognition: resolving a message MUST NOT enumerate a given entry twice observably to recognize a wrapper, and every later read MUST answer with the verdict the first enumeration reached — the answer that the entry refused to be enumerated included, which is reported (section 14.2) at every read that meets it, as a value no conversion can describe is.
The requirement is written over conversion rather than over serialization because section 4 defines two ways to reach a text and a value takes whichever its type selects. The ordinary string conversion runs host code the same way a serialization does, and it can fail the same way, so bounding only the JSON path would leave a value that raises on conversion absent at one placeholder and present at the next within one resolution, with the reports following. A value whose conversion is not deterministic therefore answers every later read with the text the first read produced. An implementation need not record a value section 4 converts by the ordinary string conversion and whose conversion can neither run host code nor answer twice over: converting one again visits no node the conversion limit counts, runs no host code and answers what the first conversion answered, so the answer a resolution reads does not change with it. What a resolution spends can change with it — a host whose integers grow without bound converts a large one to text in time that grows with it, work no node of the conversion limit counts — so the exemption is written over the answer alone and leaves an implementation free to record such a value anyway. A value section 4 serializes is not exempt however plainly it is built: its walk is the work the limit bounds, and a resolution that walks it once per placeholder pays for it once per placeholder.
The conversion limit bounds the serialization alone: the walk section 4 asks of a plain object or a plain array. The ordinary string conversion is the value's own type's, and so is what it costs. A type that converts by walking members of its own — a sequence derived from the host's, say — walks them as its conversion does, and no limit of this section bounds the work that walk does, as none bounds the work of writing out the digits a large integer converts to. What either writes is value text all the same: the read limit counts it as a placeholder reads it, and the output limit as a placeholder's result holds it. A payload the host decodes from JSON holds no such type, since what that decoding produces for an object and an array is plain (section 4), so only a value the application built reaches it, and the application bounds what its own types cost.
A resolution may begin while another is running: a host-defined modifier (section 11.3), a reporting handler (section 14.3) and any host code a conversion runs (section 4) all reach the implementation, and any of them may ask it to resolve a message. Each of the four limits belongs to the resolution that reached it, so a resolution begun inside another owns its own output and read budgets, its own nesting count, and its own record of what it has converted. Neither can reach a bound the other owns: an inner resolution that spends its whole output limit leaves the outer free to produce its own, and one whose message nests past its nesting limit says nothing about how deep the outer message may nest. A value both of them read is converted once for each of them, because the record the requirement above asks for belongs to a resolution and not to the implementation. And a report names the id the resolution it came from was given, not the id of the resolution around that one (section 14.3).
That nesting is not section 12's. A message holding a message is one resolution calling another through host code, where section 12's is one message holding a construct, and the nesting limit above counts the second and not the first. Nothing here bounds the first, and the four limits being per-resolution is exactly what leaves it unbounded. What ends a resolution that reaches itself without end is therefore the host's own limit on how deep it will go, not anything this document states. Reaching that limit is a failure inside host code the implementation called, and MUST be contained the way section 14.2 contains every other: the placeholder whose modifier or whose value did not come back resolves to its fallback chain (section 10), and the resolution around it MUST NOT raise. A reporting handler is the third of those callers and has no placeholder waiting on it: what a placeholder resolves to is settled by the condition being reported and not by the handler's answer (section 14.3). So a handler that does not come back leaves its report undelivered and the resolution that was reporting MUST carry on and MUST NOT raise. That holds for a handler that fails any other way too: a channel is where diagnostics go, and a message does not fail to render because one could not be logged.
A host may end the script or the process at that limit rather than signal a failure the implementation can contain. There an implementation MAY stand in for the host's limit with a bound of its own on how many resolutions are in progress at once on one thread of execution — a thread, a goroutine or a fiber, whatever runs on a call stack of its own — documented with the limits it permits. Keeping that count needs to know only which thread of execution asks, and a resolution that reaches itself on one call stack deepens the count as it deepens the stack. A resolution asked for on a thread of execution where none is in progress is never refused, and returns its string as always. The implementation MAY also bound how many resolutions are in progress at once across the process that were each asked for where another was already in progress on the same thread of execution, where one suspended part way keeps its place in the count. What reaches either bound is resolutions asked for while another is in progress on their thread of execution, never the walk of one resolution: a resolution's own walk MUST NOT reach either while its message keeps within the minima of this section. The bound on one thread of execution also counts the reads the paragraphs below count toward it, which are not the walk. A resolution asked for past a bound is refused, and where the bound across the process refuses one, what is refused is the resolution asked for and not one holding a place in the count. The refusal is a failure signalled to the host code that asked for it, as the host signals any other (section 14.2), and that code may handle it as it may any other; one it lets pass is contained as the paragraph above contains the host's limit, so the resolution around it does not raise. A read counted past the bound on one thread of execution is refused differently, as the paragraphs below say. Such a bound is then the host's own limit for the purpose of this section, and this document sets no minimum for it. An implementation whose host ends the script or the process at its limit and that sets no such bound does not meet the requirement above (section 14.2).
An implementation whose host signals its limit as a failure the implementation contains MAY keep the bound on one thread of execution too, documented the same way. It MAY keep it for the reads the paragraphs below count and for nothing else. This document sets no minimum for it there either.
An option's value and a default are text a read produces. A default is an inline default or one the payload declares (section 10), and what a read of it produces is rendered text or the text a payload link converts to. During a resolution each is produced at most once: the text a read produced is recorded, and a later read produces nothing and answers it. Where this section asks for no record of a payload's default, converting it again is no second production: it runs no host code and answers the same text.
A modifier is current on a thread of execution while its call is the innermost call of a modifier on that thread that has not returned. A modifier this document defines and one a host defines are current alike, and a modifier stays current through whatever host code runs beneath its call: a value's conversion method, a getter, a reporting handler, or what a resolution asked for there runs outside any modifier call of its own.
Two kinds of read are the walk of a resolution. The walk takes a placeholder's fallback chain. And while a modifier is current, a read of an option's value or of the default of the placeholder that modifier was called for is the walk's, whatever code beneath the call makes it, unless an option's value or the default of that placeholder is already being produced on that thread of execution. These reads count toward no bound: they cost nesting alone, since one stands inside another only as deep as the message nests (section 12).
Any other read by host code of an option's value or of a default, of an input a host-defined modifier kept (section 11.3) or handed on, is counted where it produces its text. While it produces it, it counts as one more resolution in progress on the thread of execution it is made on, toward the bound the implementation keeps there, as a resolution asked for there would. The bound is one on depth: the place is given back when the read returns. A read of text an earlier read produced produces nothing: it holds no place, is not refused and answers the text recorded.
A counted read past the bound is refused: it produces nothing and answers the empty string. A read of an option's value or of a default whose own production is in progress on the same thread of execution is refused at once the same way, whether or not the implementation keeps a bound, a current modifier's read of its own placeholder's included. A refused read is not recorded: a later read that is not refused produces the text.
A refused read fails nothing: the code that made it is handed the empty string and goes on, and what a modifier then answers is taken as any answer is.
Neither bound reaches what a chain does across threads of execution. A chain leaves a thread of execution where its host code resolves on one it starts, or hands the request to one already running and waits for the answer: the resolution asked for there runs on a call stack of its own, and carries nothing that names the resolution whose host code asked for it on another. The bound on one thread of execution therefore counts each part of a chain that stays on one call stack, and bounds that part as it bounds any resolution that reaches itself, but it does not add one part to the next. The bound across the process adds the parts up, though a part that holds a single resolution adds nothing to it, so a chain that hands off at every step reaches neither bound. Where a chain goes on through such hand-offs without end and no bound the implementation keeps reaches it, as the bound on one thread of execution alone never does while each part stays short of it, what ends it is the host running out of memory or of threads of execution, not a limit on how deep one call stack goes. Where the host signals that as a failure in code the implementation called, it is contained as any other; where the host ends the script or the process instead, the chain is the host code's to bound, as the threads of execution it starts are, and the requirement above does not reach it. The bound across the process stays optional for it: an implementation that keeps only the bound on one thread of execution meets the requirement above.
A report SHOULD identify the unresolved text. A report MUST bound that text's
length and MUST NOT emit line terminators from it, so that nothing it carries
can forge additional log lines. A line terminator there is a member of
line-term (section 6) and nothing else. An implementation MAY escape more,
and documents what it escapes; where a consumer's log reader ends a line at a
further code point too — U+0085, or the C0 controls U+000B, U+000C and U+001C
to U+001E, as some line readers do — the consumer escapes it, since which code
points end a line is decided by what reads the log rather than by the host
that wrote the report. The bound is written over the text a report carries
rather than over where that text came from: under section 5 the text a
report identifies is the placeholder the message spells rather than anything a
placeholder resolved to, and a message is written in a catalogue by hands that
are not the host's, so an implementation is held to the bound whether or not it
carries payload-derived text as well. A cut SHOULD fall on a code-point
boundary in the unit the host measures its strings in, so that the excerpt ends
on a whole character: one that would fall between the two halves of a surrogate
pair falls before the pair instead, and one that would fall inside the bytes of
a UTF-8 sequence falls before the sequence. An excerpt bounded at 120 bytes of
UTF-8 whose 119th byte begins a three-byte sequence therefore ends at its 118th
byte.
14. Security properties and error behavior
14.1 Security properties
A conforming implementation MUST provide all three of the following. They are requirements, not permissions.
Own-property lookup. Placeholder resolution MUST NOT reach inherited
members (section 9.2). Without this, a message containing {{constructor}} or
{{__proto__}} discloses host internals, and in hosts with mutable prototypes a
polluted prototype changes the rendering of messages that never referenced it.
The requirement covers configuration as well as the payload: the modifier registry (sections 11.3, 11.4), the formatting layers of section 11.2 and the reporting channel (section 14.3) MUST be read from their own entries only. Nobody writes configuration onto a prototype, so an inherited props entry, an inherited modifier registration or an inherited reporting channel is somebody else's, and reading it lets a polluted prototype reformat, re-route or hijack a message whose caller configured none of it.
It covers the call's own inputs last of all. This document specifies no host API, so an implementation is free to receive the message, the payload, the props and the locale grouped in a single host container rather than as separate arguments; where it does, that container MUST be read from its own entries too. A container read through its prototype lets a polluted prototype supply the payload a caller passed none of, which is the whole of section 9.2's protection undone one level above the payload.
Data is not syntax. What a resolution reads from its caller MUST NOT be read
as message text. A payload value, a props value, a payload default, a
wrapper's default and a modifier's answer are data: an implementation MUST
emit each as it stands, MUST NOT scan any of them for placeholders, and MUST NOT
remove escape sequences from any of them (sections 5, 7, 11, 12).
Without this a payload writes the message. A value of {{apiKey}} reads a
payload entry the message never named; a value of ; live:DELETED adds an
option to a construct the message wrote, and so selects a branch its author
never offered; a modifier's answer does either through the one extension point a
host has. Each is a payload deciding what the message means, and this property
is the whole of what keeps a translation catalogue from being an execution
surface.
It bounds the work too, which is why the section after it is as short as it is: nothing a value contributes is ever read again, so there is no growth to bound and a resolution terminates because the message is finite.
Data is not configuration either, and this property does not make it so. A
payload entry shaped like a wrapper (section 4.1) is read as one, and the
props it carries join above the caller's own (section 11.2), so an untrusted
value of that shape reconfigures every modifier the placeholder reaches that
reads properties (sections 11.2, 11.3, 11.5) without spelling any syntax at all.
An implementation cannot tell such an entry from one a caller meant; a caller
that passes untrusted data MUST NOT pass it where a wrapper is recognized, and
section 4.1 is where recognition is turned off.
Bounded work. Section 13 MUST bound the work a resolution can be made to do, by a payload and by a message alike.
A payload buys work by being large and being read often, which the output and read limits bound, and by being expensive to describe, which the conversion limit bounds — a message carrying no placeholder at all reaches a conversion and nothing else, so the conversion limit is the only one that holds it. That limit holds one conversion; what keeps a message from buying as many of them as it has placeholders is section 13's requirement that a resolution convert a value once.
A message buys work by nesting, and no limit of section 13 can bound it, because the cost is paid deriving the message and none of those limits has anything to measure until there is output. An implementation MUST derive a message in time bounded by a polynomial in the length of that message. Section 6 note 11 is how: the scan resumes at the very next code point where a placeholder does not derive, so the attempts overlap, and an implementation that records the verdict it reached for a position costs time proportional to the message's length where one that does not costs time growing faster than any polynomial in it — a denial of service an attacker writes in a few hundred characters.
An implementation MUST NOT provide configuration that disables any of these properties.
14.2 Message errors
A message error is a defect in the message: an unknown modifier (11.4), a selection that names a comparison or a plural selection and no options (9.5), or a placeholder nested deeper than the implementation resolves (13).
On a message error an implementation MUST resolve the placeholder to the fallback chain (section 10), MUST NOT raise, and SHOULD report the error.
A value that is present but that no conversion can describe (section 4) is a defect in the payload rather than in the message. It is treated as absent, so the placeholder takes the fallback chain; the implementation MUST NOT raise and SHOULD report the condition. The same holds for a link of the fallback chain that is present and cannot be described.
So is an input a modifier cannot process (sections 11.2, 11.3, 11.5). The value, the props and the locale a modifier is handed are the caller's, and so is a host-defined modifier that raised, so none of it is a defect the message's author repairs; the message can name no property a modifier reads. The placeholder takes the fallback chain; the implementation MUST NOT raise and SHOULD report the failure.
The message itself is not such a link. A message that no conversion can describe is nothing to resolve (section 4), and a message nobody wrote is not a defect in the payload, so stepping past it is not a condition to report. Neither is the id: no step of resolution reads it, and it is a caller's input rather than a payload value, so section 4's reporting SHOULD does not reach it.
What is contained is a failure: what the host signals as one in code the
implementation called — a modifier, a value's conversion, a reporting handler
(sections 4, 11.3, 13) — an exception, say, or a panic the host lets the
implementation recover from. The host interrupting the process, or the thread a
resolution runs on — an interruption the user sent, a request to exit, a thread
ended at its own request — is not a failure of that code. Nor is the host ending
the script it runs, or the process, at a memory limit it enforces, such as PHP's
memory_limit, which a script reaches wherever it allocates. An implementation
lets either pass rather than contain it, and the resolution it interrupts does
not return, which is not the implementation raising. Neither reaches a
resolution that reaches itself without end (section 13): where the host's limit
on how deep it will go is one it ends the script or the process at, its memory
limit among them, section 13's containment still applies, and an implementation
that sets no bound of its own short of that limit, as section 13 describes
one, does not meet it. The one exception is what a chain does once it leaves a
thread of execution, which no one call stack holds: section 13 leaves it to the
host code that runs it.
A report names the condition it describes with a code, and every code
declares an origin: which of a resolution's inputs the defect is in, and so
who repairs it. The origin message is a defect in the message, which the
translator who wrote it repairs; payload is a defect in the payload, the props
or the locale, which the code that supplied them repairs; limit is a bound of
section 13 the resolution reached, which is a defect in neither input, because
the message and the payload together asked for more work than the implementation
permits, so what repairs it is asking for less or permitting more (section 13).
The three are the taxonomy this section states, named: the message error and the
payload defect above are the first two, and section 13's limits are the third.
The vocabulary is eight codes. unknown-modifier is a modifier name that is
neither specified nor registered (section 11.4); missing-options is a
selection that names one of this format's comparison modifiers or plural
selections, one no host replaced, and declares no option (section 9.5);
nesting-limit is a placeholder nested deeper than the implementation resolves
(section 13). Those three declare the origin message. The third is a limit and
still declares message, because how deeply a message nests is a fact about the
message alone (section 12): the payload asked for none of it, and what repairs
it is rewriting the message or choosing an implementation that resolves further.
failed-modifier is a modifier that cannot process its input (sections 11.2,
11.3, 11.5); unserializable-value is a value no conversion can describe
(section 4), an entry or a property the host raises on where it is read among
them (sections 4.1, 9.2, 11.2); missing-locale is a modifier of section 11.2
or 11.5 reached where no locale is available. Those three declare the origin
payload.
output-limit and read-limit are the two bounds of section 13 the message and
the payload reach together, and both declare the origin limit. The conversion
limit is not among them: a value whose serialization reaches it is a value no
conversion can describe (section 4), and is reported as one.
An implementation that reports MUST name the condition with the code this section gives it, and where a report carries an origin it MUST be the one that code declares. The set is closed: this document defines no code beyond these eight, and a code a later version of this format defines declares its origin with it.
Rendering must not fail because one translation is wrong. A single malformed message must not take down the page that contains it.
14.3 Reports
This specification does not prescribe a reporting channel. Reports SHOULD identify the message id and the placeholder. Every condition this document names is met at a placeholder, limits included: a limit is reached while a particular placeholder is being resolved, and that is the one to name (section 13). The one exception is a read of the call's own configuration, below. Section 13's bounds on report content apply to every report, whatever text it carries.
An implementation that reports emits one report for each placeholder that met
the condition, where the walk met it. A message naming an unknown modifier at
three placeholders therefore reports three times. A placeholder is reached at
most once (section 5), and it reads each of its inputs once: its value, the
props its modifier is handed, the answer the modifier gives, and each link of
its fallback chain it reaches (section 10). It meets a condition at most once in
each of them, and any condition met at the placeholder itself rather than in
what it reads — a message error, a limit — at most once, and is reported at most
once for each. The props are one input
however many layers they compose from — the implementation's defaults, the
caller's props, a wrapper's — and, for plural, together with the digits it
reads under number (section 11.5), so a placeholder whose props refuse to be
read under any number of properties reports once. Its value and each link are
inputs of their own, so one whose value and next link no conversion can
describe reports twice. A placeholder that names default reads the chain's
payload link as its value (section 9.3), which is one input and not two. A
placeholder the walk never reaches — one in an option the modifier passed over —
meets no condition and is reported not at all.
What a call configures is no placeholder's input. The modifier registry (sections 11.3, 11.4), the formatting defaults (section 11.2), the reporting channel, and the call's own inputs — the payload, the props, the locale and the message id, from the host container they arrive in where an implementation takes one (section 14.1) — are each read once for the call, before or apart from the walk, and each is one input of the call. One the host raises on where it is read, or an entry of the registry it raises on, is absent: an input it would have held is not passed, and a modifier it would have registered is not registered. The implementation MUST NOT raise for it. It is reported as a value no conversion can describe, at most once for each of those inputs however many of the registry's entries refused, and the report names the message rather than a placeholder, so a message with no placeholder can report it too.
Reports are emitted in the order the walk meets the conditions. Section 5 resolves a message in one walk and section 9 fixes the steps a placeholder takes, so where neither of two reporting placeholders holds the other, the one the message writes first reports first; and where one holds the other, the inner reports while the outer's value is being read — after the outer's selector was resolved and before its result was produced. Within one placeholder the order is section 9's: what its value's conversion reports comes before a message error it meets. An implementation MUST NOT reorder reports by condition, by origin or by severity.
The count is therefore bounded by the message: a resolution emits at most one report per condition for each input a placeholder reads and one for each input of the call's own configuration, a placeholder reads a value, its props, an answer and no more links than section 10 gives its chain, and a message holds fewer placeholders than it holds characters. Nothing a payload supplies multiplies that, which is what a model of repeated passes could not say. Each report's content is bounded separately, by section 13.
The conformance set observes reports through an adapter an implementation supplies for it; this document still prescribes no channel. The same adapter names the levels the implementation satisfies (section 2) and the limits it permits that the set derives cases from — section 13's output, read and nesting limits, and the nodes its conversion limit counts — which is what makes those two statements an implementation makes about itself observable: the set reads the levels to select the fixtures a level requires, and the limits to derive the cases that sit at a boundary, so an implementation that permits more than section 13's minima is exercised at the bounds it documents rather than at those minima. The bound on the text one serialization writes is not among them: the set derives no case from it, and the implementation's documentation is where it is stated (section 13).
Appendix: the reference implementation
@curly-message/parser is the reference implementation of this specification.
The format is specified independently of it. An implementation in any language that satisfies section 2 conforms, whether or not it shares any code with it.
A host library that wants this syntax adapts an implementation to its own calling convention. That adapter belongs to the host, and this document describes neither — the format is indifferent to which host, if any, an implementation is reached through.
License
This specification is published under the MIT License. See LICENSE.