Using react-intl with es6 classes
johanneslumpe opened this issue · 42 comments
Are you guys planning on providing a solution for this, like a higher order component for example? Or would you propose a different way of using react-intl without mixins?
How about putting the translation methods into context
, together with the already present props? Then a higher order component could just prepare the context and render a given child. But instead of then having to mix in the react-intl mixin, we could just define the proper contextTypes
and have access to all the functionality we need.
If you are using flux, you can keep the messages
and locales
in a store and pass them down to your components as props
from a higher order component (I took the idea from this article). I moved getIntlMessage
out from the IntlMixin
and made it accept the messages
as first argument. This way you don't need to use context
for react-intl and you can skip the mixin!
Yeah I'm using flux. I'm already keeping my messages + locales inside a store, since I want to be able to change languages on the fly. But I most certainly don't want to wrap each translatable component inside a higher order component. Having stuff on context seems a bit more convenient to me. Of course if there is no other way, then a higher order component will have to do. Also I don't necessarily want to copy the libraries functionality. That's why I asked for support from the devs ;)
In my experience skipping context when using flux is not actually that big deal: instead of adding the mixin, you wrap the component. The getIntlMessage
is also a simple function. This way you can solve the problem with the context and shouldComponentUpdate
(especially if you are changing the language on the fly).
Well the context is nice, because I don't want to have to pass down messages explicitly, if from grandparent to grandchild, if the parent does not need the messages. But your idea of externalizing getIntlMessage
isn't actually a bad idea. Each component can then just define the required context type and be done with it. That could work :) So thanks for that idea!
Did you personally run into any issues with context
and shouldComponentUpdate
? There is still some discussion over a the react repo about how to handle that with context.
This is a good discussion!
I'd like to provide a sans-mixin way of using all the features of React Intl. We're actively working with teams at Yahoo to figure out good patterns for translation fallbacks (for things like missing translations, etc.) Let's keep this discussion going and continue to expiriment with solutions has you two have already.
@ericf I went with what @gpbl suggested and externalized the getIntlMessage
method. I have a smaller higher order component now, which specifies the required contextTypes
and then passes in the messages to the wrapped component, along with a bound getIntlMessage
method.
So in the wrapped component translations work rather normally:
render() {
const getIntlMessage = this.props.getIntlMessage;
const msg = getIntlMessage('some.message.path');
}
For fallbacks my idea was to always provide a complete default set in a TranslationStore, and then upon language change merge the new language with the default set and return that. That way at least the english version of the message will show up. And if it's missing, then the original english version could even decorated with a clear suffix, that this string is missing (might be useful for dev mode).
Another idea would be to return the path identifier, with some added prefix or suffix whenever something is not defined.
My implementation is a bit different. I'm using Fluxible, so I'm cheating when I say I skip context
, since Fluxible does use it :-)
Said I use IntlStore
to save locales and messages, this function returns an high order component:
// utils/connectToIntlStore.js
import React from 'react';
import { FluxibleMixin } from 'fluxible';
function connectToIntlStore(Component) {
const Connection = React.createClass({
mixins: [FluxibleMixin],
render() {
const { messages, locales } = this.getStore('IntlStore').getState();
return <Component messages={messages} locales={locales} {...this.props} />;
}
});
return Connection;
}
export default connectToIntlStore;
This is a wrapped component:
import { Component } from 'react';
import connectToIntlStore from '../utils/connectToIntlStore';
import getIntlMessage from '../utils/getIntlMessage';
class MyComponent extends Component {
render() {
const { messages } = this.props;
return (
<p>
{ getIntlMessage(messages, 'MyComponent.helloWorld') }
</p>
);
}
}
export default connectToIntlStore(MyComponent);
// MyComponent has `messages` and `locales` as props.
Basically, instead of using the react-intl context
, I rely on the fluxible's one. A non-isomorphic approach, using stores as singletons, should work without context
!
utils/getIntlMessage
is the same as in react-intl, but it accepts messages
as first argument. The higher order component could listen to IntlStore
for changing the language on the fly.
I'm still experimenting with this, anyway.
@johanneslumpe I like the idea of the fallback to the english locale – but how would that work? Would a foreign user download two languages, just because a missing string? I'm convinced the best practice is to stop the build when the locale strings don't match.
The issue with contexts and shouldComponentUpdate
is exposed here: I met it in my (non-flux) locale-hot-switch example. It turns out it could be solved using Flux :-)
So you're fetching the messages from your store for each component. I'm fetching them once at the top and then I'm relying on the normal context.
Here's what I use as mixin replacement:
export default (Component) => {
return class Translatable extends React.Component {
static contextTypes = {
messages: React.PropTypes.object
}
constructor(props, context) {
super(props, context);
// we bind `getIntlMessage` so we can just use it normally inside
// `render`, without having to always pass the component instance
this.getIntlMessage = getIntlMessage.bind(null, this);
}
render() {
return <Component getIntlMessage={this.getIntlMessage} {...this.props} {...this.state} />;
}
};
}
And the usage is as follows:
class Comp extends React.Component {
render() {
const getIntlMessage = this.props.getIntlMessage;
return (
<div>
{getIntlMessage('some.path')}
</div>
);
}
}
export default makeTranslatable(Comp);
@gpbl Yeah I've read that thread about the context. I didn't encounter it yet. But if I do, I might have to switch to props, instead of context. But I'm confident that the context issue will be solved in time.
About the fallback: Yeah, a simple version would include both language sets. Another way would be to generate language files on the server, and do the merging prior to serving (potentially cached).
So you're fetching the messages from your store for each component. I'm fetching them once at the top and then I'm relying on the normal context.
Right! Your solution works better for changing the language on the fly: attaching a store listener to each component could be too much :) – on the other side, I am not sure about that getIntlMessage
as prop. Also, which role does the Flux store play in your implementation?
But if I do, I might have to switch to props, instead of context. But I'm confident that the context issue will be solved in time.
What about the PureRenderMixin
? Are you checking also context
in shouldComponentUpdate
, to know if messages
changed?
Another part I couldn't replace with the high order component was the use of this.formatMessage
& co (i needed it to pass a translated string to a component, for rendering the meta tags) – so sometimes I had to use the mixin. The README states they are going to remove those functions from the mixin anyway in the next major version.
Yeah don't use those functions, just go with the components. I'm currently not using the PureRenderMixin
. So I didn't think about that yet.
My TranslationStore
is where I store all the available translations, keyed by locale. So technically a user could load different translation sets and switch between them. My top level component listens to that store and on change, re-renders a component, which - still - implements the IntlMixin
so it can set up the proper context. But that mixin is now confined within a single component, so when we figure out a proper solution without a mixin, it will be easier to replace.
I chose to pass getIntlMessage
as a prop because the component, which has the messages in the context, is the higher order component. And in my version of getIntlMessage
i have to pass the component to use as the first parameter, because the code is the same as the original one. It checks this.props
and this.context
for a messages
prop. And in the wrapped component, there is no way to pass the parent component in, except when receiving it as a prop. But that doesn't make a lot of sense, so I figured that just passing in a bound method is the least problematic version for me right now :D but just like you - I'm experimenting ;)
In my demo app i tried to skip the IntlMixin
at all. So there's a store and a connectToIntlStore utils to create an high-order component and always pass messages
and locales
as props.
Then i got the idea to wrap react-intl components into an high-order component so that they always use the values coming from the store. For example, a <FormatMessage/>
that is directly wrapped in the high order component (source).
...but here i have a problem with react-intl contexts, when a wrapped components get another wrapped component as prop, for example here:
<WrappedFormattedMessage
message="photo.rating"
rating={<WrappedFormattedNumber value={rating} />}
/>
where I get a warning from react:
owner-based and parent-based contexts differ (values: `undefined` vs `en`) for key (locales) while mounting FormattedNumber
maybe @ericf could suggest what's going wrong here?
So I've begun a major refactor to try to address many of the things brought up in this thread and others. I'm still trying to work through the details and flush out an implementation to propose, while also attempting to preserve v1.x backwards compatibility.
Here's the rough set of things I'm trying to address:
- Remove need to use mixin
- Provide new imperative API
- Support message fallbacks
- Add deprecation warnings
- Author components as pure ES6 classes
I first attempted to create a new <Intl>
component that you'd wrap around your app's root component, or nest further around some sub tree which would filling in the intl-related context
. But I ran into @slorber's issue facebook/react#3392 where it's currently impossible to opt-into using parent-based context. To work around this, I kept the <Intl>
component (but don't plan to expose it yet) and created a high-order component (HOC) factory (still want a better name than provideIntl()
, any ideas?)
import Intl from './components/intl';
export default function provideIntl(Component) {
return class extends Intl {
render() {
return React.createElement(Component, this.props);
}
};
}
This approach gets around the current state of React only having owner-based contexts; and it would be used like this:
AppComponent = provideIntl(AppComponent);
React.render(
<AppComponent locale="en-US" messages={{...}} />,
document.getElementById('container')
);
This new HOC Intl
component would replace the job of IntlMixin
— which is to set the intl-related context
. @gpbl I would like to investigate shouldComponentUpdate
and context
further to understand if this will be resolved in a near future version of React where a component will be forced to render when its context
changes. I'm also okay with the idea that a full forceUpdate
might have to happen when trying to hot-swap locales — the problem is making sure all the components know they need to re-render :-/
@johanneslumpe you're right about adding contextTypes
to things. In my current WIP-POC the Intl
HOC I talked about above will create an instance of a new Format
class which takes locale
(singular), messages
, and formats
(for custom format options). This instance has the new imperative API that covers the main things the mixin provided; e.g., format.number()
, format.message()
, etc.
I'm then extrapolating these concepts out to provide support for fallbacks. What we realized is that in order to correctly provide support for falling back to the app's default locale when a translation is missing we need to combine locale
, messages
, and formats
together so that the message can be properly formatted. The good thing is, the new Format
utility class provides an encapsulation of exactly that!
AppComponent = provideIntl(AppComponent);
React.render(
<AppComponent locale="fr-FR" messages={{...}}
fallback={{
locale: 'en-US',
messages: {...}
}} />,
document.getElementById('container')
);
(Still working on the exact API and naming, but you get the idea.)
So now the Intl
HOC can provide two Format
instances, one for the current locale the app is in, and one for the app's default locale that can be used as a fallback. This should provide the low-level infrastructure on which we can build out this declarative, fully-componentized approach to embedding the default strings in the components and extracting them with tools to pass off for translation: #89 (comment)
I've also worked on updating the <FormattedMessage>
component to automatically handle fallbacks, and get rid of the need for getIntlMessage()
(which is still provided on the Format
class.) I'm thinking of adding a new key
prop, which is the same input as what getIntlMessage()
accepts, but it will also check the fallback messages if a translation is missing. This is what I have to handle that:
var format = this.context.format;
var props = this.props;
var message = this.props.message;
var key = this.props.key;
if (typeof message !== 'string') {
if (!key) {
throw new Error(
'A `message` or `key` path to a message must be provided.'
);
}
try {
message = format.getMessage(key);
} catch (e) {
try {
// Fallback.
format = this.context.fallbackFormat;
message = format.getMessage(key);
} catch (e) {
throw new Error(
'Message could not be located at key: ' + key
);
}
}
}
I'm still unsure if adding key
is the right way to go, or if making message
also accept a callback function would be better…thoughts?
One other open question I have is around what to do with the props
the mixin current provides on all of the <Formatted*>
components? Current all of the components accept locales
, messages
, and formats
props. I could wrap all of the <Formatted*>
components with the Intl
HOC to continue providing that functionality if these props
are specified, otherwise it would simply use what's provided via context
.
This is sort of a rant at this point, but since you two are already thinking about this stuff, it makes sense to flush some of this out here first before I open a RFC and/or branch to discuss the details future 😄
@ericf nice start :) First thing: I don't think you can use key
on props
anymore. AFAIK it has been removed since 0.12 (https://facebook.github.io/react/blog/2014/10/16/react-v0.12-rc1.html#breaking-change-key-and-ref-removed-from-this.props) But renaming that to keypath
would be an easy fix.
As for the HOC name, how about just i18n
, internationalize
, or makeI18nAware
?
Instead of using a HOC factory, we could have a component like the FluxComponent
over at https://github.com/acdlite/flummox/ which allows your to define a custom render
function as a prop, which then gets executed by the component. It's similar to the solution provided by sebmarkbage over at facebook/react#3392 (comment)
I think with that we could solve the context issue of providing a context to children of the I18n component. Also messages + locales and fallbacks could be made directly available as params to the custom render function (not sure if we need that, but it would be possible), so they could be manually passed down to basically any component inside that render function.
So each time a component needs direct access to the context
it would just be rendered within a custom render function provided by the new component.
I'd like the fallback to be configurable. Instead of throwing an error, if there is no message and no fallback, I'd like to be able to specify that just the key gets output again. This would make it easy (while in dev-mode) to see where something is missing, taking a note and continue to check, instead of the app breaking. And in addition to that we could log using console.warn
, which specifies more details, like which locale and whether the key is missing on the current locale or on the fallback.
Have no idea about the <Formatted*>
components yet. And I have no opinion about providing a function as message
prop. But if we did that - then we could actually manually take care of language switching (based on a store for example). But that probably would miss the point ;)
First thing: I don't think you can use key on props anymore.
Ah good point!
Instead of using a HOC factory, we could have a component like the FluxComponent over at https://github.com/acdlite/flummox/ which allows your to define a custom render function as a prop, which then gets executed by the component.
So you're thinking that we could go with this style:
<Intl locale="en-US" messages={{...}} render={() => {
return <AppComponent />;
}} />
And then when React switches to parent-based context, the refactor would be to the following?
<Intl locale="en-US" messages={{...}}>
<AppComponent />
</Intl>
I could make the above "work" today using cloneWithProps()
, but that requires a user of React Intl to use React-with-addons — and that's not something I wanted to force, like using FluxComponent
does. (I guess there's a pattern where React libs also have addons?)
Also messages + locales and fallbacks could be made directly available as params to the custom render function (not sure if we need that, but it would be possible), so they could be manually passed down to basically any component inside that render function.
Yeah, that's a cool idea! But maybe we'd just pass the format
and fallbackFormat
instances into the render()
callback function?
I'd like the fallback to be configurable. Instead of throwing an error, if there is no message and no fallback, I'd like to be able to specify that just the key gets output again.
Yeah I'd like to figure out a good way to do that which doesn't complicate the story of using React Intl. I just started (locally) with keeping the same error semantics as there currently are.
The other thing I'm trying to figure out is how to keep the <Formatted*>
components usable without requiring that you wrapped one of its ancestors with <Intl>
to provide the context; e.g., I want this to still work:
<FormattedNumber value={0.95} locales={['en-US', 'en']} style="percent" />
(Note: using the plural locales
here because that's what Intl.NumberFormat
uses.)
Currently, each <Formatted*>
component uses the IntlMixin
, therefore the above is possible without ever adding the mixin somewhere upstream — that's what I want to maintain with this refactor as well. I want the <Intl>
component and the context
it provides to be sugar for keeping uses of the <Formatted*>
components DRY.
I could make the above "work" today using cloneWithProps(), but that requires a user of React Intl to use React-with-addons — and that's not something I wanted to force, like using FluxComponent does. (I guess there's a pattern where React libs also have addons?)
v0.13 introduces the new React.cloneElement()
API so no need for addons.
@PaulLeCam yeah I was hoping that would work, but it doesn't change the ownership of the element being closed, therefore it won't change the context inheritance chain.
I just went down much the same path @ericf went already before if I read his comments correctly.
I created an Intl React component that sets up a context which contains the locales, messages and formats. You use it somewhere on the outside of your application to wrap it, i.e:
<Intl messages={mymessages} locales={mylocales} formats={myformats}>
... everything inside ...
</Intl>
I then created a special wrapper of FormattedMessage which does two things:
- get its i18n info from the context as set up by
Intl
- takes a
messageId
argument which can automatically retrieve the appropriate message frommessages
without having to use getIntlMessage manually.
I ran into the same issue with ownership versus parents for context where I wanted to handle the children of Intl
. React.addons.cloneWithProps
changes the ownership to be consistent with the parentage while React.cloneElement
does not.
Some questions:
- is it indeed still a goal to make an
Intl
element that sets up context eventually in the way I described? - Is this waiting for React 0.14 so that we can avoid depending on React addons? (assuming that parent based context fixes whatever context confusion we have now)
- I saw a mention that you want to get rid of the need for
getIntlMessage
. Does this mean what I think it means and that you can simply pass a messageId toFormattedMessage
? Or do you mean something else? I didn't see a reason to require people messing around withgetIntlMessage
whenFormattedMessage
can do it automatically for you, but I may be missing use cases.
I'm asking also because I may want to start using this approach faster than react-intl does, but I want to make sure react-intl is actually heading in this direction at all.
+1
It's even worse than needing to use React.addons.cloneWithProps to make sure ownership is consistent with parentage. I actually can't get it to work -- I tried my best to replicate what FluxComponent from Flummox does, but I can't get it working. I think to make stuff like this work we need React 0.14 with a proper parent-based context.
The changes @ericf is working on sound good and I'd like to be able to upgrade to a future major release without too much trouble. Given that, I'm wondering what those of you who have been working around mixins-with-es6-classes are currently using or would recommend.
@necolas check this out:
https://gist.github.com/aldendaniels/5d94ecdbff89295f4cd6
Thanks. I'm leaning more towards what @johanneslumpe is doing, rather than using a mixin directly on every component. @johanneslumpe please can you elaborate / share more of the implementation if it's working well for you.
@necolas Sorry for the late reply - wasn't around. I actually haven't had the chance to continue my research on that topic. The current implementation is working in a way that you have TranslationStore
, which always has a default language (English in my case) loaded and then also loads the custom language for the user - if it differs from the default.
I have a single root component, which does use the mixin to set up the context
, but every other component which has to be translatable just uses the HoC to enhance itself.
The application itself has a single top level handler, which listens for changes in the TranslationStore
and then passes down the new data from the store to the component, which implements the mixin. Further down the hierarchy every component then just uses this.props.getIntlMessage
to translate things.
That worked well for me so far. Always loading the default and the custom language set is of course not react-intl
specific, but it gives you the nice ability to fall back to a definitely available translation, which allows us to prevent react-intl
from going haywire and throwing things at us. (It's also nice for translators, because you can augment the missing strings in a custom merge function so people can easily see what's still missing.
Does that help or do you need more details? :)
Hey, thanks for the follow up. Specifically, you have this line in your earlier example:
this.getIntlMessage = getIntlMessage.bind(null, this);
Is this a custom implementation of getIntlMessage
?
@necolas Oh sorry, forgot to mention that. I probably should have included that up that. It's the getIntlMessage
method from the mixin, but refactored into its own file. So I just import it at the top of the HoC file and then bind it to the current instance on construction, so that it is able to find the context/messages.
Thanks. I had it working with the mixin method too. Was just curious if there was some extra stuff going on in there. Quite a nice solution for now.
What I've done to use formatMessage
from IntlMixin
:
import {assign} from 'lodash';
import {IntlMixin from 'react-intl';
export default class Profile extends React.Component {
displayName = 'Profile'
contextTypes = {
router: React.PropTypes.func
}
static propTypes = {
flux: React.PropTypes.object.isRequired
}
_getIntlMessage = IntlMixin.getIntlMessage
_formatMessage = IntlMixin.formatMessage.bind(assign({}, this, IntlMixin))
...
I can use this._getIntlMessage
and this._formatMessage
in my component class without problems.
(note: I use babel stage 0)
@faassen I like your solution about creating a wrapper around FormattedMessage that looks up directly into the context.
@ericf I think it would be great to have it integrated in the lib directly.
Something like <FormattedMessage messageKey="a.b.c"/>
would be great: no need for the mixin anymore, and removes some boilerplate
@slorber this is similar to what I'm doing now, but I don't use contexts – I wrapped the original FormattedMessage
to pass messages
and locales
props to it (in my case, they are coming from an IntlStore
). Now I can write
<FormattedMessage message="some.message" />
https://github.com/gpbl/isomorphic500/blob/master/src/utils/FormattedMessage.js
https://github.com/gpbl/isomorphic500/blob/master/src/utils/connectToIntlStore.js
Here I've explained a bit my approach.
would be great if the right way to use ES6 syntax is described in the README file
To be honest, I don't think there's a right way to use the current version with ES6, if you want to have the full access to the API and not just the components.
I struggled for a while with wrappers and finally decided to create a fork, where I have a Intl class serving as the API passed through context.
As all of our applications use react-router, the first route handler is a IntlApp component, injecting the context along with messages and formats, selected for the user locale. We have the same component for all of our applications.
We don't have any trouble this way with parent/owner context clashes so far.
Hi @AlexJozwicki, do you have an example of you implementation?
I will appreciate it a lot.
Regards.
I struggled for a while with wrappers and finally decided to create a fork, where I have a Intl class serving as the API passed through context.
This is what's being worked on in the v2-dev
branch.
hey @ericf, I'm looking forward to 2.0 🎉
sorry for OT, but is there a timeline for releasing 2.0?
@jakubsikora working on integrating/upgrading it into several Yahoo apps in production before shipping to work out all the kinks. I'm feeling good where it's at so nearing a preview release though.
@ericf in ver 1 there is a method getIntlMessage
, are you going to remove that from 2.0 or replace it and expose something similar. I'm just wondering because currently in some places in my app I get messages like this:
var foo = this.getIntlMessage('foo');
return (
<div>{foo}</div>
);
@jakubsikora this will be one area I'll be looking for feedback on. The current design uses a simple, flat, key --> value hash to store the messages, but one of the bigger features for v2 is automatic translations fallbacks, implemented by this algorithm.
Just for fun, what we've ended up doing is shipping a default language with the application (English in our case) and having that key/value hash live in a Flux store. Then we dynamically load other languages from S3 and put them into the same Flux store in an array.
If a user wants to use a language other than English, we use lodash's merge function to combine one language key/value hash with English. The premise is that English will always be a step ahead and canonical. So if a key is missing in a newly loaded language, it easily falls back to English as a result of merging the two objects.
The premise is that English will always be a step ahead and canonical. So if a key is missing in a newly loaded language, it easily falls back to English as a result of merging the two objects.
@ryan1234 yup, this is our experience too! In v2, this idea is baked in, and if you're using the Babel and ES6 modules, then you'll be able to use the forthcoming babel-plugin-react-intl
to extract the default English messages from your React Component during your build. The Babel plugin currently leaves the source un-modified, it simply visits and collects the data, in the future I can add an option to remove the metadata and default messages leaving just the message ids.