Skip to content

fix: make the data-model data#457

Merged
eemeli merged 3 commits intomessageformat:mainfrom
fatbrain:fix/data-model-as-data
Sep 2, 2025
Merged

fix: make the data-model data#457
eemeli merged 3 commits intomessageformat:mainfrom
fatbrain:fix/data-model-as-data

Conversation

@fatbrain
Copy link
Contributor

Remove the use of Map from data-model, to enable using pre-parsed messages. Removing the need for parsing while running my application.

@linux-foundation-easycla
Copy link

linux-foundation-easycla bot commented Aug 31, 2025

CLA Missing ID CLA Not Signed

Remove the use of `Map` from data-model, to enable using pre-parsed
messages.
@fatbrain fatbrain force-pushed the fix/data-model-as-data branch from 34822b7 to 0b72b88 Compare August 31, 2025 20:29
@eemeli
Copy link
Member

eemeli commented Sep 1, 2025

Could you explain a bit more how the current approach is making things difficult for you, preferably with some code examples? It seems like this might be best considered first via an issue, rather than jumping directly into an implementation.

@fatbrain
Copy link
Contributor Author

fatbrain commented Sep 1, 2025

I wasn't sure how to best approach this, it's a learning experience for me. I will make a mental note, and next time you'll see an issue before implementation.

I parse and validate my messages as part of my build pipeline and collect and output the result as data in JSON files.

Example message hi.mf2:

.input { $name :string }
.input { $when :datetime dateStyle=short timeStyle=short}
{{Hi {$name}! - {$when}}}

Gets turned into hi.json:

{
  "type": "message",
  "declarations": [
    {
      "type": "input",
      "name": "name",
      "value": {
        "type": "expression",
        "arg": { "type": "variable", "name": "name" },
        "functionRef": { "type": "function", "name": "string" }
      }
    },
    {
      "type": "input",
      "name": "when",
      "value": {
        "type": "expression",
        "arg": { "type": "variable", "name": "when" },
        "functionRef": {
          "type": "function",
          "name": "datetime",
          "options": {
            "dateStyle": { "type": "literal", "value": "short" },
            "timeStyle": { "type": "literal", "value": "short" }
          }
        }
      }
    }
  ],
  "pattern": [
    "Hi ",
    { "type": "expression", "arg": { "type": "variable", "name": "name" } },
    "! - ",
    { "type": "expression", "arg": { "type": "variable", "name": "when" } }
  ]
}

Then I use this in my application (pseudo-code):

import hi from './hi.json';

const msg = new MessageFormat('en', hi);
const greeting = msg.format({ name: 'Client', when: '1930-05-31T17:35:00' });

The problem I run into is as part of new MessageFormat(...), validate is called (which calls visit) on the pre-parsed message and visit goes through the options of the functionRef (using options_.values(), which is part of Map interface) but since my pre-parsed message is JSON, it does not have the functionRef.options property as a Map, but rather a JavaScript object.

Hopefully my explanation added the missing context / meaning.

Cheers,
Jonas

@eemeli
Copy link
Member

eemeli commented Sep 1, 2025

Splitting the parsing like that isn't something that's been considered very deeply here, as the original design of the library is meant to act as a polyfill for a native-code Intl.MessageFormat implementation, where we can expect that parsing a syntax string is comparable to parsing JSON -- but your point is valid, esp. as we consider workflows where a non-MF2 syntax is parsed into a JSON structure in a back-end.

Using Objects rather than Maps would also match the spec's message.json definition of the data model, rather than its TypeScript description, so it ought to be supported at least for input.

The question then becomes, is there a reason to support both Maps and Objects, or only Objects? At the very least, we should make sure that prototype pollution is not an issue.

@fatbrain
Copy link
Contributor Author

fatbrain commented Sep 1, 2025

If prototype pollution is handled, I don't see a reason to also support Map. But then again I've not considered mf2 in my reasoning. Is the Intl.MessageFormat suppose to also support MessageData, if so, I don't think I've seen Map being part of any of the other Intl.XXX interfaces?

If I wanted to add a short helper function in order to safely get a own-property from an object, you have any suggestion where to put it, or you think it's something that can be handled inline for each case?

Something like:

const getOwn = <T extends object>(object: T | undefined, name: keyof T) =>
  object && Object.hasOwn(object, name) ? object[name] : undefined;

Any and all input greatly appreciated, and thanks for such quick responses.

Copy link
Member

@eemeli eemeli left a comment

Choose a reason for hiding this comment

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

After a bit more consideration, I think this is indeed a good change. I've added some comments inline; most significant is making sure that when replacing a .get() we don't look into the prototype chain unless we're sure to have created the object ourselves, or if anything being present there would be an error in any case.

Co-authored-by: Eemeli Aro <eemeli@gmail.com>
@eemeli
Copy link
Member

eemeli commented Sep 2, 2025

@fatbrain Looks like a Prettier formatter run is needed?

@eemeli eemeli merged commit 87b434c into messageformat:main Sep 2, 2025
9 of 10 checks passed
@fatbrain fatbrain deleted the fix/data-model-as-data branch September 2, 2025 16:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants