-
-
Notifications
You must be signed in to change notification settings - Fork 36
[DESIGN] Effect of selectors on placeholders #755
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
Changes from all commits
dd2d589
7e4231b
976d39d
e6d9555
eee7071
905a20d
a02cd3d
98ff114
191a908
c19bcfa
fc7287e
276d1f8
76740a3
cd1ffa6
d7d095d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,360 @@ | ||||||
# Effect of Selectors on Subsequent Placeholders | ||||||
|
||||||
Status: **Proposed** | ||||||
|
||||||
<details> | ||||||
<summary>Metadata</summary> | ||||||
<dl> | ||||||
<dt>Contributors</dt> | ||||||
<dd>@aphillips</dd> | ||||||
<dt>First proposed</dt> | ||||||
<dd>2024-03-27</dd> | ||||||
<dt>Pull Requests</dt> | ||||||
<dd>#000</dd> | ||||||
</dl> | ||||||
</details> | ||||||
|
||||||
## Objective | ||||||
|
||||||
_What is this proposal trying to achieve?_ | ||||||
|
||||||
Define what effect (if any) the _annotation_ of a _selector_ has on subsequent _placeholders_ | ||||||
that access the same _variable_. | ||||||
|
||||||
## Background | ||||||
|
||||||
_What context is helpful to understand this proposal?_ | ||||||
|
||||||
In MF2, we require that all _selectors_ have an _annotation_. | ||||||
The purpose of this requirement is to help ensure that a _selector_ on a given _operand_ | ||||||
is working with the same value as the _formatter_ eventually used for presentation | ||||||
of that _operand_. | ||||||
This is needed because the format of a value can have an effect on the grammar used | ||||||
in the localized _message_. | ||||||
|
||||||
For example, in English: | ||||||
|
||||||
> You have 1 mile to go. | ||||||
> You have 1.0 miles to go. | ||||||
|
||||||
These messages might be written as: | ||||||
|
||||||
``` | ||||||
.input {$togo :integer} | ||||||
.match {$togo} | ||||||
0 {{You have arrived.}} | ||||||
one {{You have {$togo} mile to go.}} | ||||||
* {{You have {$togo} miles to go.}} | ||||||
|
||||||
.input {$togo :number minimumFractionDigits=1} | ||||||
.match {$togo} | ||||||
0 {{You have arrived.}} | ||||||
one {{Unreachable in an English locale.}} | ||||||
* {{You have {$togo} miles to go.}} | ||||||
``` | ||||||
|
||||||
It is tempting to want to write these as a shorthand, with the _annotation_ in the _selector_: | ||||||
|
||||||
``` | ||||||
.match {$togo :integer} | ||||||
0 {{You have arrived.}} | ||||||
one {{You have {$togo} mile to go.}} | ||||||
* {{You have {$togo} miles to go.}} | ||||||
``` | ||||||
|
||||||
## Use-Cases | ||||||
|
||||||
_What use-cases do we see? Ideally, quote concrete examples._ | ||||||
|
||||||
1. As a user, I want my formatting to match my selector. | ||||||
This is one of the reasons why MF2 requires that selectors be annotated. | ||||||
When I write a selector, the point is to choose the pattern to use as a template | ||||||
for formatting the value being selected on. | ||||||
Mismatches between these are undesirable. | ||||||
|
||||||
``` | ||||||
.match {$num :number minimumFractionDigits=1} | ||||||
one {{This case can never happen in an English locale}} | ||||||
* {{I expect this formats num with one decimal place: {$num}}} | ||||||
``` | ||||||
|
||||||
2. As a user, I want to use the least amount of MF special syntax possible. | ||||||
3. As a user, I don't want to repeat formatting, particularly in large selection matrices. | ||||||
``` | ||||||
.match {$num1 :integer} {$num2 :number minimumFractionDigits=1} | ||||||
0 0 {{You have {$num1 :integer} ({$num2 :number minimumFractionDigits=1}) wildebeest.}} | ||||||
0 one {{You have {$num1 :integer} ({$num2 :number minimumFractionDigits=1}) wildebeest.}} | ||||||
0 * {{You have {$num1 :integer} ({$num2 :number minimumFractionDigits=1}) wildebeest.}} | ||||||
one 0 {{ }} | ||||||
one one {{ }} | ||||||
one * {{ }} | ||||||
// more cases for other locales that use two/few/many | ||||||
* 0 {{ }} | ||||||
* one {{ }} | ||||||
* * {{ }} | ||||||
``` | ||||||
|
||||||
4. As a user (especially as a translator), I don't want to have to modify | ||||||
declarations and selectors to keep them in sync. | ||||||
``` | ||||||
.input {$num :number minimumFractionDigits=1} | ||||||
.match {$num} | ||||||
* {{Shouldn't have to modify the selector}} | ||||||
``` | ||||||
> Note that this is currently provided for by the spec. | ||||||
|
||||||
5. As a user, I want to write multiple selectors using the same variable with different annotations. | ||||||
How do I know which one will format the placeholder later? | ||||||
``` | ||||||
.match {$num :integer} {$num :number minimumFractionDigits=2} | ||||||
* * {{Which selector formats {$num}?}} | ||||||
|
||||||
.match {$num :number minimumFractionDigits=2} {$num :integer} | ||||||
* * {{Which selector formats {$num}?}} | ||||||
``` | ||||||
|
||||||
If both formats are needed in the message (presumably they are or why the selector), | ||||||
how does one reference one or the other? | ||||||
|
||||||
|
||||||
6. As a user I want to use the same operand for both formatting and selection, | ||||||
but use different functions or options for each. | ||||||
I don't want the options used for selection to mess up the formatting. | ||||||
|
||||||
For example, while LDML45 doesn't support selection on dates, | ||||||
it's easy to conceptualize a date selector at odds with the formatter: | ||||||
``` | ||||||
.input {$d :datetime skeleton=yMMMdjm} | ||||||
.match {$d :datetime month=numeric} | ||||||
1 {{Today is {$d} in cold cold {$d :datetime month=long} (trying to select on month)}} | ||||||
* {{Today is {$d}}} | ||||||
``` | ||||||
|
||||||
Note that users can achieve this effect using a `.local`: | ||||||
``` | ||||||
.input {$d :datetime skeleton=yMMMdjm} | ||||||
.local $monthSelect = {$d :datetime month=numeric} | ||||||
.match {$monthSelect} | ||||||
1 {{No problem getting January and formatting {$d}}} | ||||||
* {{...}} | ||||||
``` | ||||||
|
||||||
## Requirements | ||||||
|
||||||
_What properties does the solution have to manifest to enable the use-cases above?_ | ||||||
|
||||||
## Constraints | ||||||
|
||||||
_What prior decisions and existing conditions limit the possible design?_ | ||||||
|
||||||
## Proposed Design | ||||||
|
||||||
_Describe the proposed solution. Consider syntax, formatting, errors, registry, tooling, interchange._ | ||||||
|
||||||
## Alternatives Considered | ||||||
|
||||||
_What other solutions are available?_ | ||||||
_How do they compare against the requirements?_ | ||||||
_What other properties they have?_ | ||||||
|
||||||
### Do nothing | ||||||
|
||||||
In this alternative, selectors are independent of declarations. | ||||||
Selectors also do not affect the resolved value. | ||||||
|
||||||
Examples: | ||||||
``` | ||||||
.input {$n :integer} | ||||||
.match {$n :number minimumFractionDigits=2} | ||||||
* {{Formats '$n' as an integer: {$n}}} | ||||||
|
||||||
.match {$n :integer} | ||||||
* {{If $n==1.2 formats {$n} as 1.2 in en-US}} | ||||||
``` | ||||||
|
||||||
**Pros** | ||||||
- No changes required. | ||||||
- `.local` can be used to solve problems with variations in selection and formatting | ||||||
- Supports multiple selectors on the same operand | ||||||
|
||||||
**Cons** | ||||||
- May require the user to annotate the operand for both formatting and selection. | ||||||
- Can produce a mismatch between formatting and selection, since the operand's formatting | ||||||
isn't visible to the selector. | ||||||
|
||||||
### Allow both local and input declarative selectors with immutability | ||||||
|
||||||
In this alternative, we modify the syntax to allow selectors to | ||||||
annotate an input variable (as with `.input`) | ||||||
or bind a local variable (as with `.local`). | ||||||
Either variable binding is immutable and results in a Duplicate Declaration error | ||||||
if it attempts to annotate a variable previously annotated. | ||||||
|
||||||
Example: | ||||||
``` | ||||||
.match {$input :function} $local = {$input :function} | ||||||
* * {{This annotates {$input} and assigns {$local} a value.}} | ||||||
|
||||||
.match $local1 = {$input :function} $local2 = {$input :function2} | ||||||
* * {{This assigns two locals}} | ||||||
|
||||||
.input {$input :function} | ||||||
.local $local = {$input :function} | ||||||
.match {$input :function} {$local :function} | ||||||
* * {{This produces two duplicate declaration errors.}} | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why? I only see one declaration for each of the two variables. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
``` | ||||||
|
||||||
The ABNF change looks like: | ||||||
```abnf | ||||||
selector = expression / declaration | ||||||
declaration = s variable [s] "=" [s] expression | ||||||
``` | ||||||
|
||||||
**Pros** | ||||||
- Shorthand is consistent with the rest of the syntax | ||||||
- Shorthand version works intuitively with minimal input | ||||||
- Preserves immutability | ||||||
- Produces an error when users inappropriately annotate some items | ||||||
|
||||||
**Cons** | ||||||
- Selectors can't provide additional selection-specific options | ||||||
if the variable name is already in scope | ||||||
- Doesn't allow multiple selection on the same operand, e.g. | ||||||
``` | ||||||
.input {$date :datetime skeleton=yMdjm} | ||||||
.match {$date :datetime field=month} {$date :datetime field=dayOfWeek} | ||||||
* * {{This message produces a Duplicate Declaration error | ||||||
even though selection is separate from formatting.}} | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As with the previous example, I'm confused. I thought that in this alternative, you need the '=' sign for a declaration, and the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I re-titled this option. In this option, we do not have the |
||||||
``` | ||||||
However, this design does allow for a local variable to be easily created | ||||||
for the purpose of selection. | ||||||
|
||||||
### Allow _immutable_ input declarative selectors | ||||||
|
||||||
In this alternative, selectors are treated as declaration-selectors. | ||||||
That is, an annotation in a selector works like a `.input`. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That doesn't seem right to me. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nope. Only the first (well, now second after "do nothing") option provides a local variable binding (which alters the syntax!) |
||||||
This permits `.match` selectors to be a shorthand when no declarations exist. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand what this means. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A selector works like When you use this feature, you don't have to write a .input. That is, these are equivalent, with the first being a shorthand of the second:
|
||||||
The option does not permit local variable declaration. | ||||||
|
||||||
It is not an error to re-declare a variable that is in scope. | ||||||
Instead the selector's annotation replaces what came before. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
(This is another place where the distinction between literal and variable operands is important. You can shadow variables, but changing the value of a literal doesn't make sense.) |
||||||
|
||||||
``` | ||||||
.input {$num :integer} | ||||||
.match {$num :number minimumFractionDigits=1} | ||||||
* {{Formats {$num} like 1.0}} | ||||||
``` | ||||||
|
||||||
**Pros** | ||||||
- Shorthand version works intuitively with minimal typing. | ||||||
|
||||||
**Cons** | ||||||
- Violates immutability that we've established everywhere else | ||||||
|
||||||
### Allow _mutable_ input declarative selectors | ||||||
|
||||||
In this alternative, selectors are treated as declaration-selectors. | ||||||
That is, an annotation in a selector works like a `.input`. | ||||||
However, it is an error for the selector to try to modify a previous declaration | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
(just as it is an error for a declaration to try to modify a previous declaration). | ||||||
This permits `.match` selectors to be a shorthand when no declarations exist. | ||||||
|
||||||
It is also an error for a selector to modify a previous selector. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
(Again, this needs some qualifiers... writing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The example you gave doesn't modify a previous selector: the literal There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If a selector implicitly introduces a name, then what you're referring to as "modifying" is actually "shadowing". If you write, in this scenario:
then the second There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I'm steering clear of "shadowing" because the thing that is or is not affected is the (ill-defined) "resolved value" in the formatting context (which I tend to think of as a |
||||||
This implies that multiple selecton on the same operand is pointless. | ||||||
catamorphism marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
``` | ||||||
.match {$num :integer} | ||||||
* {{Formats {$num} as integer}} | ||||||
|
||||||
.input {$num :integer} | ||||||
.match {$num :number maximumFractionDigits=0} | ||||||
* {{This message produces a Duplicate Declaration error}} | ||||||
|
||||||
.input {$num :integer} {$num :number} | ||||||
* * {{This message produces a Duplicate Declaration error}} | ||||||
``` | ||||||
|
||||||
**Pros** | ||||||
- Shorthand version works intuitively with minimal typing | ||||||
- Preserves immutability | ||||||
- Produces an error when users inappropriately annotate some items | ||||||
|
||||||
**Cons** | ||||||
- Selectors can't provide additional selection-specific options | ||||||
if the value has already been annotated | ||||||
- Doesn't allow multiple selection on the same operand, e.g. | ||||||
``` | ||||||
.input {$date :datetime skeleton=yMdjm} | ||||||
.match {$date :datetime field=month} {$date :datetime field=dayOfWeek} | ||||||
* * {{This message produces a Duplicate Declaration error | ||||||
even though selection is separate from formatting.}} | ||||||
``` | ||||||
|
||||||
aphillips marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
### Match on variables instead of expressions | ||||||
|
||||||
In this alternative, the `.match` syntax is simplified | ||||||
to work on variable references rather than expressions. | ||||||
This requires users to declare any selector using a `.input` or `.local` declaration | ||||||
before writing the `.match`: | ||||||
|
||||||
``` | ||||||
.input {$count :number} | ||||||
.match $count | ||||||
one {{You have {$count} apple}} | ||||||
* {{You have {$count} apples}} | ||||||
|
||||||
.local $empty = {$theList :isEmpty} | ||||||
.match $empty | ||||||
true {{You bought nothing}} | ||||||
* {{You bought {$theList}!}} | ||||||
``` | ||||||
|
||||||
The ABNF change would look like: | ||||||
```diff | ||||||
match-statement = match 1*([s] selector) | ||||||
-selector = expression | ||||||
+selector = variable | ||||||
``` | ||||||
|
||||||
**Pros** | ||||||
- Overall the syntax is simplified. | ||||||
- Preserves immutability. | ||||||
|
||||||
**Cons** | ||||||
- A separate declaration is required for each selector. | ||||||
|
||||||
### Provide a `#`-like Feature | ||||||
|
||||||
(Copy-pasta adapted from @eemeli's proposal in #736) | ||||||
|
||||||
Make the `.match` expression also act as implicit declarations accessed by index position: | ||||||
|
||||||
``` | ||||||
.match {$count :number} | ||||||
one {{You have {$0} apple}} | ||||||
* {{You have {$0} apples}} | ||||||
``` | ||||||
|
||||||
Assigning values to `$0`, `$1`, ... would not conflict with any input values, | ||||||
as numbers are invalid `name-start` characters. | ||||||
That's by design so that we encourage at least _some_ name for each variable; | ||||||
here that's effectively provided by the `.match` expressions. | ||||||
|
||||||
ABNF would be modified: | ||||||
```diff | ||||||
-variable = "$" name | ||||||
+variable = "$" (name / %x30-39) | ||||||
``` | ||||||
|
||||||
...with accompanying spec language making numeric variables resolve to the `.match` selectors in placeholders, | ||||||
and a data model error otherwise. | ||||||
|
||||||
**Pros** | ||||||
- More ergonomic for most `.input` cases | ||||||
- Enables representation of many messages without any declarations | ||||||
|
||||||
**Cons** | ||||||
- Confusing that the operand name can't be used in the pattern? | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does the operand name have to be disallowed from appearing in the pattern? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps not worded correctly. You can use the operand name in the pattern, but it's annotation won't/might not be applied.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, so maybe something like: "The meaning of the operand name in the context of the pattern might not be what the user expects"? |
||||||
Removes some self-documentation from the pattern. | ||||||
- Requires the pattern to change if the selectors are modified. | ||||||
- Limits number of referenceable selectors to 10 (in the current form) | ||||||
aphillips marked this conversation as resolved.
Show resolved
Hide resolved
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.