Skip to content

Use keywords in syntax #287

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 4 additions & 10 deletions spec/message.ebnf
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
Message ::= Plain | Pattern | Preamble Variant+
Message ::= Declaration* ( Pattern | Selector Variant+ )

/* Preamble */
Preamble ::= Selector+
Selector ::= (Variable '=')? '{' Expression '}'
Declaration ::= 'let' WhiteSpace Variable '=' '{' Expression '}'
Selector ::= 'match' ( '{' Expression '}' )+

/* Variants and Patterns */
Variant ::= VariantKey* Pattern
Variant ::= 'when' ( WhiteSpace VariantKey )+ Pattern
VariantKey ::= Literal | Nmtoken | '*'
Pattern ::= '[' (Text | Placeholder)* ']' /* ws: explicit */

Expand All @@ -23,12 +23,6 @@ Markup ::= MarkupStart Option*

<?TOKENS?>

/* Plain */
Plain ::= PlainStart (PlainChar* PlainEnd)? /* ws: explicit */
PlainChar ::= AnyChar - ('{' | '}')
PlainStart ::= PlainChar - ('[' | '$' | WhiteSpace)
PlainEnd ::= PlainChar - WhiteSpace

/* Text */
Text ::= (TextChar | TextEscape)+
TextChar ::= AnyChar - ('[' | ']' | '{' | '}' | Esc)
Expand Down
159 changes: 69 additions & 90 deletions spec/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
1. [Complex Messages](#complex-messages)
1. [Productions](#productions)
1. [Message](#message)
1. [Plain](#plain)
1. [Preamble](#preamble)
1. [Variable Declarations](#variable-declarations)
1. [Selectors](#selectors)
1. [Variants](#variants)
1. [Patterns](#patterns)
1. [Placeholders](#placeholders)
Expand Down Expand Up @@ -66,9 +66,6 @@ The design goals of the syntax specification are as follows:
`.properties`, YAML, XML, inlined as string literals in programming languages, etc.
This includes a future _MessageResource_ specification.

1. Simple messages that do not use any placeholders or selectors should (as far as possible)
be represented in the syntax with no additional characters than their actual contents.

### Design Restrictions

The syntax specification takes into account the following design restrictions:
Expand All @@ -77,52 +74,36 @@ The syntax specification takes into account the following design restrictions:
It should be possible to define a message entirely on a single line with no ambiguity,
as well as to format it over multiple lines for clarity.

1. The syntax should not use nor reserve any keywords in any natural language,
such as `if`, `match`, or `let`.

1. The syntax should define as few special characters and sigils as possible.

## Overview & Examples

### Simple Messages

A simple message without any variables does not need any syntax:
All messages, including simple ones, need `[…]` delimiters:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think {} brackets are better than [] ones because of the need to quote inside the literal.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack. The wording here is just being consistent with the rest of the doc in its current state. I know that #255 is still very much open.


Hello, world!
[Hello, world!]

The same message defined in a `.properties` file:

```properties
app.greetings.hello = Hello, world!
app.greetings.hello = [Hello, world!]
```

The same message defined inline in JavaScript:

```js
let hello = new MessageFormat('Hello, world!')
let hello = new MessageFormat('[Hello, world!]')
hello.format()
```

### Simple Placeholders

A message with an interpolated variable needs to be interpreted as a pattern,
which uses `[…]` delimiters:
Messages may contain placeholders within `{…}` delimiters,
such as variables that are expected to be passed in as format paramters:

[Hello, {$userName}!]

The same message defined in a `.properties` file:

```properties
app.greetings.hello = [Hello, {$userName}!]
```

The same message defined inline in JavaScript:

```js
let hello = new MessageFormat('[Hello, {$userName}!]')
hello.format({ userName: 'Anne' })
```

### Formatting Functions

A message with an interpolated `$date` variable formatted with the `:datetime` function:
Expand Down Expand Up @@ -152,73 +133,73 @@ which the runtime can use to construct a document tree structure for a UI framew

A message with a single selector:

{$count :number}
1 [You have one notification.]
* [You have {$count} notifications.]
match {$count :number}
when 1 [You have one notification.]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really don't care for the use of when proposed here. I don't see what value it adds. All of the selectors are clearly part of the match statement. The when is just extra noise which actually obscures the values.

Note that I still favor delimiting the selectors and the literal. The selector delimiter functions the same way that when does--identifying that there is an set of values in the match.

The single-line example (below, line 143) could then be like:

match {:platform} [windows] {Settings} [*] {Preferences}

Or (without delims):

match {:platform} windows {Settings} * {Preferences}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see two benefits to when:

  1. It allows us to add more keywords in the future that come after the last variant, e.g. meta or attribute. Usually, programming languages solve this by putting the body of the statement inside a curly-brace-delimited block, but I think we said we didn't want to risk people forgetting the closing brace.

  2. (This one is more subjective) It creates a visual correspondence between the match and each variant, in particular in case of multiline messages.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that I still favor delimiting the selectors and the literal. The selector delimiter functions the same way that when does--identifying that there is an set of values in the match.

Ack here as well. I'm not opposed to delimiting variant keys, but it's true that I'm still hoping that we can use square brackets [...] for delimiting patterns (#255).

Technically, I think just relying on order here is good enough: match <selector list> <variant list> <pattern value> <variant list> <pattern value> is OK from the parser's point of view, unless we anticipate some other productions to follow all variants in the future (see point 1 in my comment just above).

when * [You have {$count} notifications.]

A message with a single selector which is an invocation of
a custom function `:platform`, formatted on a single line:

{:platform} windows [Settings] * [Preferences]
match {:platform} when windows [Settings] when * [Preferences]

A message with a single selector and a custom `:hasCase` function
which allows the message to query for presence of grammatical cases required for each variant:

{$userName :hasCase}
vocative [Hello, {$userName :person case=vocative}!]
accusative [Please welcome {$userName :person case=accusative}!]
* [Hello!]
match {$userName :hasCase}
when vocative [Hello, {$userName :person case=vocative}!]
when accusative [Please welcome {$userName :person case=accusative}!]
when * [Hello!]

A message with 2 selectors:

{$photoCount :number} {$userGender :equals}
1 masculine [{$userName} added a new photo to his album.]
1 feminine [{$userName} added a new photo to her album.]
1 * [{$userName} added a new photo to their album.]
* masculine [{$userName} added {$photoCount} photos to his album.]
* feminine [{$userName} added {$photoCount} photos to her album.]
* * [{$userName} added {$photoCount} photos to their album.]
match {$photoCount :number} {$userGender :equals}
when 1 masculine [{$userName} added a new photo to his album.]
when 1 feminine [{$userName} added a new photo to her album.]
when 1 * [{$userName} added a new photo to their album.]
when * masculine [{$userName} added {$photoCount} photos to his album.]
when * feminine [{$userName} added {$photoCount} photos to her album.]
when * * [{$userName} added {$photoCount} photos to their album.]

### Local Variables

A message defining a local variable `$whom` which is then used twice inside the pattern:

$whom = {$monster :noun case=accusative}
let $whom = {$monster :noun case=accusative}
[You see {$quality :adjective article=indefinite accord=$whom} {$whom}!]

A message defining two local variables:
`$itemAcc` and `$countInt`, and using `$countInt` as a selector:

$countInt = {$count :number maximumFractionDigits=0}
$itemAcc = {$item :noun count=$count case=accusative}
one [You bought {$color :adjective article=indefinite accord=$itemAcc} {$itemAcc}.]
* [You bought {$countInt} {$color :adjective accord=$itemAcc} {$itemAcc}.]
let $countInt = {$count :number maximumFractionDigits=0}
let $itemAcc = {$item :noun count=$count case=accusative}
match {$countInt}
when one [You bought {$color :adjective article=indefinite accord=$itemAcc} {$itemAcc}.]
when * [You bought {$countInt} {$color :adjective accord=$itemAcc} {$itemAcc}.]

### Complex Messages

A complex message with 2 selectors and 3 local variable definitions:

{$host :gender}
{$guestOther :number}
let $hostName = {$host :person firstName=long}
let $guestName = {$guest :person firstName=long}
let $guestsOther = {$guestCount :number offset=1}

$hostName = {$host :person firstName=long}
$guestName = {$guest :person firstName=long}
$guestsOther = {$guestCount :number offset=1}
match {$host :gender} {$guestOther :number}

female 0 [{$hostName} does not give a party.]
female 1 [{$hostName} invites {$guestName} to her party.]
female 2 [{$hostName} invites {$guestName} and one other person to her party.]
female * [{$hostName} invites {$guestName} and {$guestsOther} other people to her party.]
when female 0 [{$hostName} does not give a party.]
when female 1 [{$hostName} invites {$guestName} to her party.]
when female 2 [{$hostName} invites {$guestName} and one other person to her party.]
when female * [{$hostName} invites {$guestName} and {$guestsOther} other people to her party.]

male 0 [{$hostName} does not give a party.]
male 1 [{$hostName} invites {$guestName} to his party.]
male 2 [{$hostName} invites {$guestName} and one other person to his party.]
male * [{$hostName} invites {$guestName} and {$guestsOther} other people to his party.]
when male 0 [{$hostName} does not give a party.]
when male 1 [{$hostName} invites {$guestName} to his party.]
when male 2 [{$hostName} invites {$guestName} and one other person to his party.]
when male * [{$hostName} invites {$guestName} and {$guestsOther} other people to his party.]

* 0 [{$hostName} does not give a party.]
* 1 [{$hostName} invites {$guestName} to their party.]
* 2 [{$hostName} invites {$guestName} and one other person to their party.]
* * [{$hostName} invites {$guestName} and {$guestsOther} other people to their party.]
when * 0 [{$hostName} does not give a party.]
when * 1 [{$hostName} invites {$guestName} to their party.]
when * 2 [{$hostName} invites {$guestName} and one other person to their party.]
when * * [{$hostName} invites {$guestName} and {$guestsOther} other people to their party.]

## Productions

Expand All @@ -229,63 +210,61 @@ if it meets additional semantic requirements about its structure, defined below.

### Message

A single message is either a plain message, a single pattern, or has a preamble
A single message is either a single pattern, or has a `match` statement
followed by one or more variants which represent the translatable body of the message.

```ebnf
Message ::= Plain | Pattern | Preamble Variant+
Message ::= Declaration* ( Pattern | Selector Variant+ )
```

### Plain
### Variable Declarations

A plain message only contains translatable content;
placeholders or their delimiters are not allowed inside a plain message.
Plain messages must not start with one of the syntax characters `[`, `{` or `$`,
as those would indicate that the message has a more complex structure.
Any whitespace at the beginning or end of a plain message is ignored.
A plain message cannot represent an empty string;
for that, use an empty pattern `[]` instead.
A variable declaration is an expression binding a variable identifier
within the scope of the message to the value of an expression.
This local variable may then be used in other expressions within the same message.

```ebnf
Plain ::= PlainStart (PlainChar* PlainEnd)? /* ws: explicit */
PlainChar ::= AnyChar - ('{' | '}')
PlainStart ::= PlainChar - ('[' | '$' | WhiteSpace)
PlainEnd ::= PlainChar - WhiteSpace
Declaration ::= 'let' WhiteSpace Variable '=' '{' Expression '}'
Copy link
Collaborator

@stasm stasm Jun 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let is clear enough, but I think there's an opportunity here to use a keyword that directly relates to the name that we choose for these let bindings (#248). This would be self-explanatory and easier to learn and search for. For example:

local $foo = {$count :number}   // ...and call them "locals"
alias $foo = {$count :number}   // ...and call them "aliases"
macro $foo = {$count :number}   // ...and call them "macros"
decl $foo = {$count :number}    // ...and call them "declarations"
expr $foo = {$count :number}    // ...and call them "named expressions"

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think let is fine, as it says "immutability" (if you come from JS, if not, then "tough luck")

From your list I also like macro and alias, as they are suggest "things you declare for convenience, something shorter, that you use to not repeat again and again something long"

The rest (decl and expr) are too generic.

And I think that macro and alias are more familiar than let, for a lot more people than JS devs.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this PR is now closed, let's use #289 to continue this topic.

```

### Preamble
### Selectors

The preamble is where selectors and local variables can be defined.
A selector is an expression which will be used to choose one of the variants during formatting.
A selector can be optionally bound to a local variable, which may then be used in other expressions.
A selector is a statement containing one or more expressions
which will be used to choose one of the variants during formatting.

```ebnf
Preamble ::= Selector+
Selector ::= (Variable '=')? '{' Expression '}'
Selector ::= 'match' ( '{' Expression '}' )+
```

Examples:

```
$frac = {$count: number minFractionDigits=2}
1 [One apple]
* [{$frac} apples]
match {$count :plural}
when 1 [One apple]
when * [{$count} apples]
```

```
let $frac = {$count: number minFractionDigits=2}
match {$frac}
when 1 [One apple]
when * [{$frac} apples]
```

### Variants

A variant is a keyed pattern.
The keys are used to match against the selectors defined in the preamble.
The keys are used to match against the selectors defined in the `match` statement.
The key `*` is a "catch-all" key, matching all selector values.

```ebnf
Variant ::= VariantKey* Pattern
Variant ::= 'when' ( WhiteSpace VariantKey )+ Pattern
VariantKey ::= Literal | Nmtoken | '*'
```

A well-formed message is considered valid if the following requirements are satisfied:

- The number of keys on each variant must be fewer or equal to the number of selectors defined in the preamble.
- The number of keys on each variant must be equal to the number of selectors.
- At least one variant's keys must all be equal to the catch-all key (`*`).

### Patterns
Expand Down