Skip to content
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

Add getFragment method to ReactLocalization #595

Merged
merged 19 commits into from Oct 27, 2022

Conversation

Vinnl
Copy link
Contributor

@Vinnl Vinnl commented Jul 9, 2022

Fixes #544.

It's similar to getString, but returns a React Fragment instead
of a string. The main added benefit here is that it allows the
caller to pass in elems analogous to the similarly-named prop on
<Localized>.

We're mostly using getString in the Firefox Relay codebase, but have to switch to <Localized> every time we want to use an element in the translation string, which can be confusing to new contributors.

I do understand that generally it's best to be aligned on a new feature before submitting a PR, which I didn't do, so I want to emphasise that it's perfectly fine to let me know that this feature isn't desirable for one reason or another, or that I should redo everything if I want to get this in. It didn't seem like a feature that was too hard to implement, so I decided to just go ahead and give it a shot, with the worst case being a little wasted effort :)

It's similar to getString(), but returns a React Fragment instead
of a string. The main added benefit here is that it allows the
caller to pass in `elems` analogous to the similarly-named prop on
<Localized>.
@eemeli
Copy link
Member

eemeli commented Aug 3, 2022

Apologies for the delay; I've been travelling and currently fluent.js reviews pretty much fall on me.

In general, I think I'd be fine with adding getFragment(), as it makes behaviour that's now only available via Localized usable from Localization as well.

I took a quick look at this PR; do I understand right that effectively you've combined code from getString() in Localization with the elems and message attribute handling of Localized in order to create getFragment()? My main issue with this PR is that it ends up duplicating quite a bit of code, and I'd much rather have a single internal implementation that's called from two different places for that.

@Vinnl
Copy link
Contributor Author

Vinnl commented Aug 3, 2022

No worries @eemeli, hope you enjoyed your travels :)

Yes, you are correct about the approach I took. I understand the desire for a single internal implementation, but I'm hesitant about doing that myself - I find that introducing layers of abstraction ideally is informed by an idea of which lines of code are likely to change together in the future, which is hard to do for someone like me who's unfamiliar with the codebase and its history. The wrong abstractions are often worse than no abstraction, and I don't want to be responsible for that :)

Possibly I could give it a shot if you'd have some pointers about the abstractions you'd like to see?

@eemeli
Copy link
Member

eemeli commented Aug 3, 2022

@Vinnl I'd be happy to have you poke at this. :)

My gut feeling would be to have it so that Localized calls getFragment() internally. If there's a reason why that wouldn't be a good idea, you'll probably find it on the way. 😇

@Vinnl
Copy link
Contributor Author

Vinnl commented Sep 1, 2022

Thanks @eemeli, that made sense. There was a bunch of extra code in <Localized> dealing with its children and setting attributes. I've now added a getElement function that replicates that functionality, redefined getFragment to just call that, and redefined <Localized> to use it as well. That should cut down on a lot of similar code - let me know what you think.

(And apologies for the delayed response - holidays and other work and all that :) )

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.

I like this! I do think we need to bikeshed the API a bit, though. See inline comments.

fluent-react/src/localization.ts Outdated Show resolved Hide resolved
Comment on lines 96 to 100
args?: {
vars?: Record<string, FluentVariable>,
elems?: Record<string, ReactElement>,
attrs?: Record<string, boolean>;
},
Copy link
Member

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 about this. In <Localized/> passing each of these in as props makes sense, but here it feels like we're putting different things into the same basket -- esp. as this is so close to the shape of the getString() args argument.

My first instinct would be to separate vars from the other two, but maybe calling this something other than args could also help?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh heh, good eye. I'd actually argue that renaming getStrings second argument to vars would be more appropriate, since the Fluent docs also call them variables, but I can understand that changing the name of an existing API might not be desirable.

I could name it something like props? Because they're basically analogous to <Localized>'s props, although it could also be confusing, given that they're not React props. Or how about params?

Copy link
Member

Choose a reason for hiding this comment

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

I'm ok with changing the internal name of the getSrting() argument to vars, because it only impacts the documentation.

props and params aren't really better, and both come with their own sets of baggage.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fluent-react/src/localization.ts Outdated Show resolved Hide resolved
fluent-react/src/localization.ts Outdated Show resolved Hide resolved
fluent-react/src/localization.ts Outdated Show resolved Hide resolved
// Check if the component to render is a valid element -- if not, then
// it's either null or a simple fallback string. No need to localize the
// attributes.
if (!isValidElement(componentToRender)) {
Copy link
Member

Choose a reason for hiding this comment

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

I would really rather prefer requiring componentToRender to be a valid element when using this method. For <Localized/> we do need to continue supporting use with no child, but would it be possible to check for that and handle it there, rather than needing to do so here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, I'm not sure what the best way to do that is - there'd be quite a bit of duplication in initialising the bundle and handling its various error cases. Personally I wouldn't be too worried about doing too much error handling. What I could do is update the type definition of getElement to only allow ReactChild | ReactFragment? Then we're technically requiring it to be a valid element in the public API.

Or, hmm, what I could also do is update <Localized> to check for non-element children and, if found, wrap them in a fragment and pass that to getElement? That would cause the latter to do some unnecessary work, but nothing too egregious I thinkg - what do you think?

Copy link
Member

Choose a reason for hiding this comment

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

How about calling getString() rather than getElement() from a Localized with non-element children, and wrapping the result in a Fragment? As far as I can tell, that should replicate the current behaviour.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah yeah, that's clever: 59671cc

I should note that that does change the behaviour a little bit: if you do not provide a child, it will now render the string ID, rather than null. Which makes sense to me and aligns it with the existing behaviour of getString(), but I can also imagine that that's not desirable.

One way to work around that might be to set the fallback string to some unique string, and return null instead of a fragment if getString returns the fallback string, but that feels pretty hacky :/ Alternatively, although still a change in behaviour but closer to the previous behaviour, would be to set the fallback string to the empty string, resulting in it returning a fragment containing an empty string instead of null. That's technically different from what it used to be, but should have the same result for the user.

fluent-react/src/localization.ts Outdated Show resolved Hide resolved
fluent-react/src/localization.ts Outdated Show resolved Hide resolved
fluent-react/src/localization.ts Outdated Show resolved Hide resolved
@Vinnl Vinnl requested a review from eemeli September 9, 2022 07:04
@@ -106,7 +106,7 @@ export class ReactLocalization {
if (Array.isArray(component)) {
if (component.length > 1) {
throw new Error(
"<Localized/> expected to receive a single React node child"
"Expected to receive a single React node element to inject a localized string into."
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"Expected to receive a single React node element to inject a localized string into."
"Expected to receive a single React element to localize"

Adding more words doesn't really help here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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.

See replies above for requested changes, and a couple minor new inline comments.

It's a thin wrapper, so not worth it for now to spend additional
API surface area on, as per
projectfluent#595 (comment)
It's equivalent to the `vars` prop on <Localiszed> and the
respective parameter for getElement, holding Fluent variables.
@Vinnl Vinnl requested a review from eemeli September 12, 2022 15:15
@Vinnl
Copy link
Contributor Author

Vinnl commented Oct 20, 2022

@eemeli Just wondering if you have time to look at my latest changes - I think I've addressed all your comments :)

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.

@Vinnl Sorry for the delay here, finally managed to take a look. A few small changes, and then I think we're good?

fluent-react/src/localized.ts Show resolved Hide resolved
fluent-react/src/localized.ts Outdated Show resolved Hide resolved
fluent-react/src/localization.ts Outdated Show resolved Hide resolved
fluent-react/src/localization.ts Outdated Show resolved Hide resolved
@Vinnl Vinnl requested a review from eemeli October 27, 2022 09:34
@Vinnl
Copy link
Contributor Author

Vinnl commented Oct 27, 2022

Thanks for getting back on it @eemeli, applied your suggestions!

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.

I reverted the Children.only() addition and applied a little code styling. Let's finally merge this! 🎉

@eemeli eemeli merged commit e99614e into projectfluent:master Oct 27, 2022
@Vinnl
Copy link
Contributor Author

Vinnl commented Oct 28, 2022

Thank you so much @eemeli!

@Vinnl
Copy link
Contributor Author

Vinnl commented Nov 15, 2022

Hi @eemeli, sorry to bug you again about this, but I was wondering if we could get a release of @fluent/react with this in it? :)

Vinnl added a commit to mozilla/fx-private-relay that referenced this pull request Apr 3, 2023
Vinnl added a commit to mozilla/fx-private-relay that referenced this pull request Apr 3, 2023
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.

withLocalization's getString API is missing the elems parameter
2 participants