tc39 / proposal-intl-messageformat

TC39 Proposal for Intl.MessageFormat
https://tc39.es/proposal-intl-messageformat
MIT License
111 stars 9 forks source link

What is the use case of resolvedOptions().functions ? #54

Open FrankYFTang opened 7 months ago

FrankYFTang commented 7 months ago

Why do we need to return the functions in the resolvedOptions() object? What is the use case for providing such information. The caller should have it already, right? Why do we need to force the implementation to return a copy of that ?

eemeli commented 7 months ago

Two reasons:

  1. I've understood that we have an expectation that the object returned by resolvedOptions() can be fed back into the corresponding constructor's options argument to get a second formatter with the same options. The functions are an important part of that.
  2. This is the only interface providing access to the implementations for the built-in functions. This allows e.g. for a formatter for custom date-like values to be built on top of the built-in :datetime, rather than needing to be completely custom-built.
FrankYFTang commented 7 months ago

I've understood that we have an expectation that the object returned by resolvedOptions() can be fed back into the corresponding constructor's options argument to get a second formatter with the same options. The functions are an important part of that.

ok, that I can see the motivation.

2. This is the only interface providing access to the implementations for the built-in functions. This allows e.g. for a formatter for custom date-like values to be built on top of the built-in :datetime, rather than needing to be completely custom-built.

ok, could you write some sample code to demostrate what this would look like for such use case

eemeli commented 7 months ago

could you write some sample code to demostrate what this would look like for such use case

As an example, here's a sketch of what a Decimal128 formatter could look like:

const { number } = new Intl.MessageFormat('').resolvedOptions().functions;

/** @param {Decimal128} value */
function decimal(ctx, options, value) {
  const str = String(value);
  const dotPos = str.indexOf('.');
  const fd = dotPos === -1 ? 0 : str.length - dotPos - 1;
  const defaults = { minimumFractionDigits: fd, maximumFractionDigits: fd };
  return number(ctx, Object.assign(defaults, options), Number(value));
}

const mf = new Intl.MessageFormat(..., locale, { functions: { decimal } });

With the above, a function :decimal would be made available in messages, with the same options as :number, but with support for Decimal128 values, and with different defaults for the minimumFractionDigits and maximumFractionDigits options.

Without access to the built-in :number function implementation, the custom function implementation gets quite a bit more complicated, as it needs to handle formatting to a string, formatting to parts, selection, and use as a operand and option values. Here's the :number polyfill implementation, to give you some idea.

FrankYFTang commented 7 months ago

so ... basically you are using const { number } = new Intl.MessageFormat('').resolvedOptions().functions; const { string } = new Intl.MessageFormat('').resolvedOptions().functions; const { datetime } = new Intl.MessageFormat('').resolvedOptions().functions;

to get back the function for the built-in type "number", "string" and "datetime" right? That looks so strange to me. Why not just add 3 functions to the spec as Properties of the Intl.MessageFormat Constructor get Intl.MessageFormat.numberFunction() get Intl.MessageFormat.stringFunction() get Intl.MessageFormat.datetimeFunction()

I think it is a bad programing style to ask the developer to construct a unused (new Intl.MessageFormat('')) just to call the resolvedOptions().functions.number to access that function. There are no reason this Intl.MessageFormat object should be created here.

eemeli commented 7 months ago

If taking that approach is viable, then we probably should make the built-in functions directly callable properties:

const mv = Intl.MessageFormat.number({ locales: ['en'] }, { minimumFractionDigits: 2 }, 42);
mv.format(); // '42.00'

Is there any reason this might be a bad idea? The full set of MF2 built-in functions we'll need to support is:

:number
:integer
:datetime
:date
:time
:string
FrankYFTang commented 6 months ago

no, I do not think that is a good idea. What you need to think is if your have a message format which is needed to be called several times in a webpage, (say to format 100 items, each contains two varables (price and weight) in 100 rows, all following the same format) how could you consstruct a Intl.MessageFormat that internally you do not need to create the Intl.NumberFormat 100 times but only once. All these 100 invokations will share the same setting and only differ in the value, right? If you have two number variables in that message you may need to create two Intl.NumberFormat (since they may not share the same setting in the same message) but not 200 of them. If you pass in 42 to access that function, then you cannot share what it create internally across the 100 invokations

eemeli commented 6 months ago

Could you help me connect the dots here? I don't see the connection between concerns about the internal memoization of NumberFormat instances with the public accessibility of the :number implementation.

sffc commented 4 months ago

2024-07-18 TG2 notes: https://github.com/tc39/ecma402/blob/main/meetings/notes-2024-07-18.md#what-is-the-use-case-of-resolvedoptionsfunctions--54