-
Notifications
You must be signed in to change notification settings - Fork 79
FluentBundle.formatPattern #380
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
FluentBundle.formatPattern #380
Conversation
Patterns can resolve to FluentNone if a cyclic reference is detected.
This PR could be viewed as a fix to #358. |
The performance looks good, with a significant win in
|
With this design, how deep does the definition go? Specifically, could/should a pattern be considered an opaque blob, or is there some benefit in defining e.g. expressions as well? |
I'd like to document all of the runtime AST, including expressions. I would not, however, expose additional methods like |
I would prefer patterns being left opaque, or at least documented as internal to the implementation. The output of |
Would you say that the goal of (I can see how the When I say documented and public, I don't mean part of the Fluent specification. I only propose that the shape of the runtime AST be documented as part of |
My intent is to get the compiler to a state where tools that are built on top of Maybe another way of expressing this would be to ask whether anything else except for |
I second this. While I'm not strictly opposed to Stas' approach, I think that specifying the AST as public and stable is a major effort for very symbolic gain. |
This sounds great, thank you for clarifying.
I see what you mean. If all of the runtime AST it is public, than the criteria for creating drop-in replacements widen.
Thanks for rephrasing. In the
I'd like for it to be private too, but the reality and past experience is that there's no true private for data which is meant to be serializable to JSON. We've seen people use it for use-caes which should really use Would it be enough to slightly obfuscate the internals of patterns to hint to consumers that they're not supposed to access them? Something like |
I think the question about what's public and what's private can be broken down into two questions:
|
I'm not sure how this will work. At some place we need to expose a stable public API, and while working on Rust and Gecko implementations I have two strong cases where I have to explicitly decide on what I expose. In Rust, it's a bit more flexible, I can create a type called What I have right now is:
Those two are relatively simple to encode and carry around, and even can be easily serialized if needed. I know Additionally, if you make the p.s. The number of places where this conversation is being held is a bit overwhelming for me. |
I would say that
This would be perfect for me.
As I see it, this binding only applies if the opaque output of the bundle is meant to be consumed by anything other than the bundle itself. I'm facing pretty much the same issue with |
Both of these describe a formatted message. The
I don't think this is true. Higher-level wrappers will continue to use
The |
// a FluentType instance without incurring the cost of creating one. | ||
function resolveExpression(scope, expr) { | ||
// A special case for VariantKeys. | ||
// XXX Should not go through bundle._transform. |
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.
I filed #387 for this.
* | ||
* @returns {Iterator} | ||
*/ | ||
get messages() { |
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.
I'll remove addMessages
in a fix to #328.
@@ -34,7 +34,7 @@ suite('Reference bombs', function() { | |||
// https://bugzil.la/1307126 | |||
test.skip('does not expand all placeables', function() { |
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.
I filed #388 to unskip this test.
if (Array.isArray(expr)) { | ||
return Pattern(scope, expr); | ||
} | ||
|
||
switch (expr.type) { |
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.
I filed #386 to consider renaming fields to indicate that they're not part of the public API.
* If formatting fails, it will return a partially resolved entity. | ||
* | ||
* In both cases, an error is being added to the errors array. | ||
* If the message doesn't have a value, return `null`. |
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.
This is how it worked before, too. The comment was wrong.
test('returns undefined for terms and missing messages', function() { | ||
assert.strictEqual(bundle.getMessage('-bar'), undefined); | ||
assert.strictEqual(bundle.getMessage('baz'), undefined); | ||
assert.strictEqual(bundle.getMessage('-baz'), undefined); |
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.
I filed #389 to switch to using strictEqual
in all tests.
if (typeof message.value === "string") { | ||
return this._transform(message.value); | ||
} | ||
throw new TypeError("Invalid Pattern type"); |
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.
I'm not really sure what's the right thing to do here. Should we guard against unexpected types of patterns
? This could be helpful when someone forgets to check if message.value
is not null
before calling formatPattern
. If I don't throw here, resolveComplexPattern
likely will, anyways, but with a more cryptic error.
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.
I don't think we should throw, but rather push something to errors
, and return undefined
.
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.
My rationale for throwing here is that this isn't a formatting error. It's not caused by a bug in the translation which resulted in an incompletely formatted output. As such, I don't think it fits into errors
either. This is an error in how the API has been used by the consumer. If the consumer takes care to always only pass Patterns
to formatPattern
, this error will never happen.
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.
I guess your last comment also addresses your initial concern if developers should guard their calls with try catch
?
Also, we're not really protecting this API by checking if the input is an Array
against being fed garbage, [{foopy:true}]
is still bad ;-)
If Pattern
was a subclass of Array
, we might have a slightly stronger assertion?
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.
I guess your last comment also addresses your initial concern if developers should guard their calls with try catch ?
Oh! Yes, I think so! :)
Let me try to rephrase this to make sure I understand your point: If I'm fine expecting developers to use a try catch
, I should also be fine expecting them to check if (message.value)
before calling formatPattern
. Is this what you meant?
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.
I'm not sure what you're trying to achieve. If this is just about message.value
being null unexpectedly, I'd consider null
to be actually an OK return value.
I read this to be about creating a defined error scenario when people pass in garbage like Math.random
or so. I wouldn't expect good users of the API to try-catch
to proof them for API abuse.
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.
OK, thanks for clarifying. I'd like to consider adding null
as an OK return value in a follow-up. I'd prefer if this method only returned strings, so that it's not necessary to check for non-null output after every formatPattern
.
For now, I'll keep the throwing on non-string, non-array pattern in order to help developers find buggy callsites faster.
@Pike This is now ready for your review. The PR changes the I left a few comments above. I have a few ideas for direct follow-ups, but I didn't want to make this PR even bigger. I filed new issues for each of the planned follow-ups. My biggest question is: should |
@zbraniecki Would you mind taking a look at the changes to |
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.
I've got a couple of comments, surprisingly enough, the react one bothers me most.
I only glanced at fluent-dom, leaving that one to zibi for real.
I'd also like to push out the optional parameter definition for formatPattern
.
if (typeof message.value === "string") { | ||
return this._transform(message.value); | ||
} | ||
throw new TypeError("Invalid Pattern type"); |
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.
I don't think we should throw, but rather push something to errors
, and return undefined
.
@Pike I've updated the PR to address your feedback. I also added two tests to I kept |
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.
Thanks, lgtm. Still need @zbraniecki to look at the changes in fluent-dom
, aka Fluent in gecko in spe.
I think that I'm tackling that in projectfluent/fluent-rs#119 and trying to find a way to massage this API into Rust and Gecko. |
I created a release branch called release-fluent-zero-thirteen and re-targeted this PR to it. I hope to be able to land this PR there soon and move on to working on the follow-up for the rest of the week. |
Rather than hide the runtime representation of messages, let's document it and commit to only changing it in major version releases.
FluentResource
now derives fromArray
rather than aSet
. I've noticed a perf win related to this change.Messages and terms are now always stored as a
{value, attributes:{}}
object. The value maybe null or may be a pattern (more on that below).attributes
is anObject.create(null)
whose keys are attribute names and values are patterns.Patterns represented either as primitive strings (when they don't have any placeable) or as arrays of primitive strings or expressions.
Expressions are stored as
{type, ...}
objects. The exact fields depend on the expression.The mindset of formatting shifts from formatting a message or formatting an attribute to simply: formatting a pattern. To make it explicit, I renamed
FluentBundle.format
to.formatPattern
.To format a message with attributes, something like the following should be used:
This pattern supports all possible methods of iterating over the keys of an object, as well as formatting only a set of known attributes instead of all of them.
I still need to figure out how to document or even formally describe the now-public data representation of messages. Perhaps this could be left as a follow up and taken care of together with or after Migrate to TypeScript? #376, if we decide to do it.