message-format-wg icon indicating copy to clipboard operation
message-format-wg copied to clipboard

[FEEDBACK] add `:number` offset option

Open macchiati opened this issue 1 year ago • 9 comments

I investigated using a mock implementation, and there is nothing really standing in the way of adding an offset option to :number. This could be done as an icu:offset option, but it should be noted that one wouldn't be able to have MF1 compatibility without something like it.

The key to implementing is that:

  • The formatting is done with the offset applied.
  • The matching is done depending on the type of key:
    • for literals the offset is not applied.
    • for plural/ordinal categories the offset is applied.

This only required the addition of a few lines of code.

I compared against the equivalent MF1 pattern. Note that MF1 has the special # symbol to indicate the formatted value with offset applied: that just turned into a regular placeholder. Here are the results.

MF1 Pattern:

	{gender_of_host, select, 
	 female {
	  {num_guests, plural, offset:1 
	   =0 {{host} does not give a party.}
	   =1 {{host} invites {guest} to her party.}
	   =2 {{host} invites {guest} and one other person to her party.}
	   other {{host} invites {guest} and # other people to her party.}}}
	 male {
	  {num_guests, plural, offset:1 
	   =0 {{host} does not give a party.}
	   =1 {{host} invites {guest} to his party.}
	   =2 {{host} invites {guest} and one other person to his party.}
	   other {{host} invites {guest} and # other people to his party.}}}
	 other {
	  {num_guests, plural, offset:1 
	   =0 {{host} does not give a party.}
	   =1 {{host} invites {guest} to their party.}
	   =2 {{host} invites {guest} and one other person to their party.}
	   other {{host} invites {guest} and # other people to their party.}}}}

MF2Pattern:

	.input {$num_guests :number u:offset=1}
	.match {$gender_of_host}{$num_guests}
	 female 0 {{{$host} does not give a party.}}
	 female 1 {{{$host} invites {$guest} to her party.}}
	 female 2 {{{$host} invites {$guest} and one other person to her party.}}
	 female * {{{$host} invites {$guest} and {$num_guests} other people to her party.}}
	 male 0 {{{$host} does not give a party.}}
	 male 1 {{{$host} invites {$guest} to his party.}}
	 male 2 {{{$host} invites {$guest} and one other person to his party.}}
	 male * {{{$host} invites {$guest} and {$num_guests} other people to his party.}}
	 * 0 {{{$host} does not give a party.}}
	 * 1 {{{$host} invites {$guest} to their party.}}
	 * 2 {{{$host} invites {$guest} and one other person to their party.}}
	 * * {{{$host} invites {$guest} and {$num_guests} other people to their party.}}

Test results:

OK with input: {$gender_of_host=female, $guest=Mike, $host=Sarah, $num_guests=0}
	MF1: Sarah does not give a party.
	MF2: Sarah does not give a party.
OK with input: {$gender_of_host=female, $guest=Mike, $host=Sarah, $num_guests=1}
	MF1: Sarah invites Mike to her party.
	MF2: Sarah invites Mike to her party.
OK with input: {$gender_of_host=female, $guest=Mike, $host=Sarah, $num_guests=2}
	MF1: Sarah invites Mike and one other person to her party.
	MF2: Sarah invites Mike and one other person to her party.
OK with input: {$gender_of_host=female, $guest=Mike, $host=Sarah, $num_guests=3}
	MF1: Sarah invites Mike and 2 other people to her party.
	MF2: Sarah invites Mike and 2 other people to her party.

macchiati avatar Mar 03 '24 01:03 macchiati

The key to implementing is that:

  • The formatting is done with the offset applied.

  • The matching is done depending on the type of key:

    • for literals the offset is not applied.
    • for plural/ordinal categories the offset is applied.

I think we can simplify this thanks to MF2's .local declarations. Here's an alternative implementation which I'd call more idiomatic. Note that the selection uses $num_guests, while variants reference $other_guests:

.input {$num_guests :number}
.local $other_guests = {$num_guests :number u:offset=1}
.match {$gender_of_host}{$num_guests}
female 0 {{{$host} does not give a party.}}
female 1 {{{$host} invites {$guest} to her party.}}
female 2 {{{$host} invites {$guest} and one other person to her party.}}
female * {{{$host} invites {$guest} and {$other_guests} other people to her party.}}
male 0 {{{$host} does not give a party.}}
male 1 {{{$host} invites {$guest} to his party.}}
male 2 {{{$host} invites {$guest} and one other person to his party.}}
male * {{{$host} invites {$guest} and {$other_guests} other people to his party.}}
* 0 {{{$host} does not give a party.}}
* 1 {{{$host} invites {$guest} to their party.}}
* 2 {{{$host} invites {$guest} and one other person to their party.}}
* * {{{$host} invites {$guest} and {$other_guests} other people to their party.}}

For extra clarity, the offset operation could be a separate function rather than an option to :number:

.local $other_guests = {$num_guests :u:offset by=1}

stasm avatar Mar 03 '24 10:03 stasm

An offset option seems like a special purpose solution to the problem of working with lists. I'd rather have a set of list/array functions. Why is the guest "Mike" special? Because he's the first item in a list of guests and you wrote code to get the first item and the remaining list count. It's way more powerful to do things like the following (ignoring gender for now):

.local $numGuests = {$guests :list-count}
.local $remainingGuests = {$guests :list-count offset=3}
.match {$numGuests}{$remainingGuests}
0 * {{{$host} does not give a party}}
1 * {{{$host} invites {$guests :list-item index=0}}}
2 * {{{$host} invites {$guests :list startIndex=0 end-index=1 type=and}}}
3 * {{{$host} invites {$guests :list startIndex=0 end-index=2 type=and}}}
* one {{{$host} invites {$guests :list startIndex=0 end-index=2 type=none}} and {$remainingGuests} other guest}}
* * {{{$host} invites {$guests :list startIndex=0 end-index=2 type=none}} and {$remainingGuests} other guests}}

Besides x more items, what other use cases are there for offset?

aphillips avatar Mar 03 '24 15:03 aphillips

It is very important that we be able to easily migrate MF1 messages to MF2 message, including the translated versions AND not change the input parameters at the call site. So we need to have a function option that deals with the inputs as they are.

Also, the approach you describe might be ok IF formatting of lists is completely stable, meaning that the formatting doesn't affect the surrounding message, and the surrounding message doesn't affect the formatting. That is not a given, across the languages that we have, until we have much more structure.

macchiati avatar Mar 03 '24 18:03 macchiati

Re >

I think we can simplify this thanks to MF2's .local declarations.

That doesn't quite work in general. Since {$other_guests} is a number, it has to use plural formatting. So because of that, the general approach would take 3 selectors in an approach like Addison outlined.

I do like your suggestion to make the offset into a function. That could also be broadened.

So here is a modified version of your approach that I think would be functionally equivalent, adding 1 new function.

.input {$num_guests :number}
.local $num_other_guests = {$num_guests :modify-number add=-1}
.match {$gender_of_host}{$num_guests}{$num_other_guests}
female 0 * {{{$host} does not give a party.}}
female 1 *  {{{$host} invites {$guest} to her party.}}
female 2 *  {{{$host} invites {$guest} and one other person to her party.}}
// this might expand to cover one, two, few, many, depending on the language
female * *  {{{$host} invites {$guest} and {$num_other_guests} other people to her party.}}
male 0 *  {{{$host} does not give a party.}}
male 1 *  {{{$host} invites {$guest} to his party.}}
male 2 *  {{{$host} invites {$guest} and one other person to his party.}}
// this might expand to cover one, two, few, many, depending on the language
male * *  {{{$host} invites {$guest} and {$other_guests} other people to his party.}}
* 0 *  {{{$host} does not give a party.}}
* 1 *  {{{$host} invites {$guest} to their party.}}
* 2 *  {{{$host} invites {$guest} and one other person to their party.}}
// this might expand to cover one, two, few, many, depending on the language
* * *  {{{$host} invites {$guest} and {$other_guests} other people to their party.}}

It looks like this could work to convert an MF1 format to MF2. (With adding some more functions.)

To Addison's point about lists, this could also be recast to take a list:

.input {$guests :list} .local $num_guests {$guests :listCount} .local $guest1 {$guests :listExtract index=1} .local $guest2 {$guests :listExtract index=2} .local $num_other_guests = {$num_guests :modify-number add=-1} .match {$gender_of_host}{$num_guests}{$num_other_guests}

Note also: This should have been marked as feedback for during the TP period, not before v45 release.

Note: This may seem like a special case to some, but the specific format of listing the first few guests followed by "and X others" was added in response to some strong developer requests. (The example is somewhat artificial, but the gender of the reader is often very important, and would be needed if "{$host} invites" were replaced by "You invited".}

macchiati avatar Mar 03 '24 20:03 macchiati

We should attempt to deal with offset in the 46/46.1 release. Tagging as 46 for now, but expect this will actually be 46.1.

aphillips avatar Sep 10 '24 23:09 aphillips

To me this looks like it should be an ICU extension, something like {$n :number icu:offset=2}.

eemeli avatar Sep 10 '24 23:09 eemeli

If this is not an ICU extension it would have to be an optional option. I will add this as Agenda+ for next week with a severe timebox.

aphillips avatar Oct 15 '24 17:10 aphillips

In the 2024-10-21 call we agreed to make this a standard feature based on the claim that it is trivial to implement and exists as an MF1 feature. I drew the action to create a PR. The resulting PR should be used to discuss whether to make it optional instead of standard.

aphillips avatar Oct 21 '24 17:10 aphillips

+1!

macchiati avatar Oct 21 '24 17:10 macchiati