formatjs/formatjs

[RFC] React Intl v2

ericf opened this issue · 193 comments

ericf commented

UPDATED: 2015-11-11

React Intl v2 has been in development for several months and the current release is v2.0.0-beta-1 — the first v2 beta release — v2 has been promoted from preview release to beta because we feel it's now feature complete and ready to move forward towards a release candidate once the test suite has been filled out and the docs have been updated.

With v1 being out almost a year we've received tons of great feedback from everyone using React Intl and with these changes we're addressing 20+ issues that have been raised — they're label: fixed-by-v2. While 20-some issues doesn't seem like a lot, many of these are very long discussions fleshing out better ways to approach i18n in React and web apps in general. With v2 we're rethinking what it means to internationalize software…

The Big Idea

ECMA 402 has the following definition of internationalization:

"Internationalization of software means designing it such that it supports or can be easily adapted to support the needs of users speaking different languages and having different cultural expectations [...]"

The usual implementation looks something like this:

  1. Extract strings from source code
  2. Put all strings in a single, large, strings file
  3. Use identifiers in source code to reference strings

This ends up leading to an unpleasant dev experience and problems:

  • Jumping between source and strings files
  • Cruft accumulation, hard to cull strings once added
  • Missing context for translators on how strings are used in the UI

There's a great discussion in #89 on the problems listed above and how to resolve them. With v2, we've rethought this premise of needing to define strings external to the React Components where they're used…

Implementation of Internationalization with React Intl v2:

  1. Declare default messages/strings in source code (React Components)
  2. Use tooling to extract them at build-time

This new approach leads to a more pleasant dev experience and the following benefits:

  • Strings are colocated with where they're used in the UI
  • Delete a file, its strings are cleaned up too (no obsolete translations)
  • Provides a place to describe the context to translators

Default Message Declaration and Extraction

React Intl v2 has a new message descriptor concept which is used to define your app's default messages/strings:

  • id: A unique, stable identifier for the message
  • description: Context for the translator about how it's used in the UI
  • defaultMessage: The default message (probably in English)

Declaring Default Messages

The <FormattedMessage> component's props now map to a message descriptor, plus any values to format the message with:

<FormattedMessage
    id="greeting"
    description="Welcome greeting to the user"
    defaultMessage="Hello, {name}! How are you today?"
    values={{name: this.props.name}}
/>

defineMessages() can also be used to pre-declare messages which can then be formatted now or later:

const messages = defineMessages(
    greeting: {
        id: 'greeting',
        description: 'Welcome greeting to the user',
        defaultMessage: 'Hello, {name}! How are you today?',
    }
});

<FormattedMessage {...messages.greeting} values={{name: this.props.name}} />

Extracting Default Messages

Now that your app's default messages can be declared and defined inside the React components where they're used, you'll need a way to extract them.

The extraction works via a Babel plugin: babel-plugin-react-intl. This plugin will visit all of your JavaScript (ES6) modules looking for ones which import either: FormattedMessage, FormattedHTMLMessage, or defineMessage from "react-intl". When it finds one of these being used, it will extract the default message descriptors into a JSON file and leave the source untouched.

Using the greeting example above, the Babel plugin will create a JSON file with the following contents:

[
    {
        "id": "greeting",
        "description": "Welcome greeting to the user",
        "defaultMessage": "Hello, {name}! How are you today?"
    }
]

With the extracted message descriptors you can now aggregate and process them however you'd like to prepare them for your translators.

Providing Translations to React Intl

Once all of your app's default messages have been translated, you can provide them to React Intl via the new <IntlProvider> component (which you'd normally wrap around your entire app):

const esMessages = {
    "greeting": "¡Hola, {name}! ¿Cómo estás hoy?"
};

ReactDOM.render(
    <IntlProvider locale="es" messages={esMessages}>
        <App />
    </IntlProvider>,
    document.getElementById('container')
);

Note: In v2 things have been simplified to use a flat messages object. Please let us know if you think this would be problematic. (See: #193 (comment))

Try: The Translations example in the repo to see how all this works (be sure to check the contents of the build/ dir after you run the build.)

Automatic Translation Fallbacks

Another great benefit to come out of this approach is automatic fallback to the default message if a translation is missing or something goes wrong when formatting the translated message. A major pain-point we faced at Yahoo which every app experienced was not wanting to wait for new translations to be finished before deploying, or placeholders like {name} getting translated to {nombre} accidentally.

Message formatting in v2 now follows this algorithm:

  1. Try to format the translated message
  2. If that fails, try to format the default message
  3. If either the translated or default message was formatted, return it.
  4. Otherwise, fallback to the unformatted message or its id.

Other Major Changes

For v2, React Intl has been completely re-thought re-written here are the highlights of the major changes:

Simpler Model for Single-Language Apps

React Intl is useful for all apps, even those which only need to support one language. In v2 we've created a simpler model developer's building single-language apps to integrate React Intl. The message formatting features in React Intl are the most complex and are most useful for multi-language apps, but all apps will use pluralization.

In v2 the lower-level pluralization features that message formatting are built on are now exposed as a first-class feature. This allows a single-language app to have pluralization support without the complexity of messages:

<p>
    Hello <b>{name}</b>, you have {' '}
    <FormattedNumber value={unreadCount} /> {' '}
    <FormattedPlural value={unreadCount}
        one="message"
        other="messages"
    />.
</p>

You can think of <FormattedPlural> like a switch statement on its value, with: zero, one, two, few, and many props as cases and other as the default case. This matches the standard ICU Pluralization rules.

Note: Both cardinal and ordinal formatting are supported via the style prop, cardinal is the default.

Try: The Hello World example in the repo to see <FormattedPlural> in action.

Components

<IntlProvider>

This is the new top-level component which your app's root component should be a child of. It replaces the adding the mixin to your app's root component and provides React Intl's API to decedents via React's component context. It takes the following props to configure the intl API and context (all optional):

  • locale: The user's current locale (now singular, defaults to "en")
  • formats: Object of custom named format options for the current locale
  • messages: {id: 'translation'} collection of translated messages for the current locale
  • defaultLocale: The app's default locale used in message formatting fallbacks (defaults to "en")
  • defaultFormats: Object of custom named format options for the defaultLocale
  • initialNow: A reference time for "now" used on initial render of <FormattedRelative> components.

For this component to work the context needs to be setup properly. In React 0.14 context switched to parent-based from owner-based, so <IntlProvider> must be your app's parent/ancestor in React 0.14. React Intl v2 will not support React 0.13.

<IntlProvider>
    <App />
</IntlProvider>

Note: How there's no defaultMessages prop, that's because it's assumed the default message descriptors live co-located to where the messages are being formatted.

See: The Providing Translations to React Intl section above for how it's used.

Function-As-Child Support

There have been many discussions around customizing the rendering of the <Formatted*> components around styling, supporting extras props, and changing the <span> elements that they return. We think of <Fromatted*> components as representations of text.

Our guidance thus far has been to wrap them and style the wrapper. Thinking forward to a single React Intl for both React [Web] and React Native, we want to be more flexible. Also, issues come when rendering a <span> inside an SVG tree, and requires a <tspan>. To remedy this, in v2 all <Formatted*> components support function-as-child, which receives a React node type value. Which enables the following:

let now = Date.now();

<FormattedDate value={now}>
    {(formattedNow) => (
        <time dateTime={now} className="fancy-date">{formattedNow}</time>
    )}
</FormattedDate>

Of course you can always do the following instead, and its valid (and recommended for this example):

let now = Date.now();

<time dateTime={now} className="fancy-date">
    <FormattedDate value={now} />
</time>

The above will yield an inner <span>, and that's okay here. But sometimes it's not okay, e.g. when rending an <option> you should use the function-as-child pattern because you don't want the extra <span> since it'll be rendered as literal text:

let num = 10000;

<FormattedNumber value={num}>
    {(formattedNum) => (
        <option value={num}>{formattedNum}</option>
    )}
</FormattedNumber>

This pattern can work well for targeted use-cases, but sometimes you just want to call an API to format some data and get a string back, e.g., when rending formatted messages in title or aria attributes; this is where using the new API might be a better choice…

New API, No More Mixin

The IntlMixin is gone! And there's a new API to replace it.

The API works very similar to the one provided by the old mixin, but it now live's on this.context.intl and is created by the <IntlProvider> component and can be passed to your custom components via props by wrapping custom components with injectIntl(). It contains all of the config values passed as props to <IntlProvider> plus the following format*(), all of which return strings:

  • formatDate(value, [options])
  • formatTime(value, [options])
  • formatRelative(value, [options])
  • formatNumber(value, [options])
  • formatPlural(value, [options])
  • formatMessage(messageDescriptor, [values])
  • formatHTMLMessage(messageDescriptor, [values])

These functions are all bound to the props and state of the <IntlProvider> and are used under the hood by the <Formatted*> components. This means the formatMessage() function implements the automatic translation fallback algorithm (explained above).

Accessing the API via injectIntl()

This function is used to wrap a component and will inject the intl context object created by the <IntlProvider> as a prop on the wrapped component. Using the HOC factory function alleviates the need for context to be a part of the public API.

When you need to use React Intl's API in your component, you can wrap with with injectIntl() (e.g. when you need to format data that will be used in an ARIA attribute and you can't the a <Formatted*> component). To make sure its of the correct object-shape, React Intl v2 has an intlShape module export. Here's how you access and use the API:

import React, {Component, PropTypes} from 'react';
import {defineMessages, injectIntl, intlShape, FormattedMessage} from 'react-intl';

const messages = defineMessages({
    label: {
        id: 'send_button.label',
        defaultMessage: 'Send',
    },
    tooltip: {
        id: 'send_button.tooltip',
        defaultMessage: 'Send the message'
    }
});

class SendButton extends Component {
    render() {
        const {formatMessage} = this.props.intl;

        return (
            <button 
                onClick={this.props.onClick}
                title={formatMessage(messages.tooltip)}
            >
                <FormattedMessage {...messages.label} />
            </button>
        );
    }
}

SendButton.propTypes = {
    intl   : intlShape.isRequired,
    onClick: PropTypes.func.isRequired,
};

export default injectIntl(SendButton);

Stabilized "now" Time and "ticking" Relative Times

<IntlProvider> uses an initialNow prop to stabilize the reference time when formatting relative times during the initial render. This prop should be set when rendering a universal/isomorphic React app on the server and client so the initial client render will match the server's checksum.

On the server, Date.now() should be captured before calling ReactDOM.renderToString() and passed to <IntlProvider>. This "now" value needs to be serialized to the client so it can also pass the same value to <IntlProvider> when it calls React.render().

Relatives times formatted via <FormattedRelative> will now "tick" and stay up to date over time. The <FormattedRelative> has an initialNow prop to match and override the same prop on <IntlProvider>. It also has a new updateInterval prop which accepts a number of milliseconds for the maximum speed at which relative times should be updated (defaults to 10 seconds).

Special care has been taken in the scheduling algorithm to display accurate information while reducing unnecessary re-renders. The algorithm will update the relative time at its next "interesting" moment; e.g., "1 minute ago" to "2 minutes ago" will use a delay of 60 seconds even if updateInterval is set to 1 second.

See: #186

Locale Data as Modules

React Intl requires that locale data be loaded and added to the library in order to support a locale. Previously this was done via modules which caused side-effects by automatically adding data to React Intl when they were loaded. This anti-pattern has been replaced with modules that export the locale data, and a new public addLocaleData() function which registers the locale data with the library.

This new approach will make it much simpler for developers whose apps only support a couple locales and they just want to bundle the locale data for those locales with React Intl and their app code. Doing would look this like:

import {addLocaleData} from 'react-intl';
import en from 'react-intl/locale-data/en';
import es from 'react-intl/locale-data/es';
import fr from 'react-intl/locale-data/fr';

addLocaleData(en);
addLocaleData(es);
addLocaleData(fr);

Now when this file is bundled, it will include React Intl with en, es, and fr locale data.

Note: The dist/locale-data/ has UMD files which expose the data at: ReactIntlLocaleData.<lang>. Previously the locale data files would automatically call addLocaleData() on the ReactIntl global. This decouples the loading of the locale data files from the loading of the library and allows them to be loaded async.

<script async src="/path/to/react-intl/dist/react-intl.min.js"></script>
<script async src="/path/to/react-intl/dist/locale-data/fr.js"></script>
<script>
    window.addEventListener('load', function () {
        ReactIntl.addLocaleData(ReactIntlLocaleData.fr);
    });
</script>

Todos

This is just a preview release so there's still more work to do until the v2 final release, but we've already begun integrating this code into Yahoo apps that use React Intl.

  • Finish unit tests
  • Add perf tests to determine if shouldComponentUpdate() is needed
  • Create 1.0 -> 2.0 Upgrade Guide
  • Update docs and examples on http://formatjs.io/ website
  • Only support only React 0.14+? Yes
  • Improve build, try to get source maps working on .min.js files
  • Remove all TODO comments in code

Testing and Feedback

We'd love for you to try out this early version of React Intl v2 and give us feedback, it'll be much appreciated!

$ npm install react-intl@next

IMO, a large part of the pain is in maintaining the stable string identifiers. What happens if two components use the same identifier but a different message? Do you need to "namespace" the identifiers, eg: "mycomponent-greeting"?

Is it not possible to get rid of the identifier, and instead use something like defaultMessage+description as the identifier?

g-p-g commented

By flat messages do you mean it no longer supports {a: {b: message}} ? Are you dropping that for performance reasons? To me it's a lot better to support that as it helps understanding where the message is used, i.e. something like {pageX: {featureY: text}} describes that text is used in some page X at a place where feature X is present.

I feel that including a description and a flat key is a step backward as it's very verbose and I'm not aware of tools supporting that (I'm guessing yahoo has something for that, but what about everyone else?). At that point I would prefer to follow @jbrantly suggestion of not having manually defined identifiers. Also, the generated file could be very similar to .pot files, where you use msgid for the original message and msgstr for the translated one.

ericf commented

@jbrantly @g-p-g Using the default message as the identifier was my original thought too, but I was persuaded away from that idea by @tinganho's comment; and instead went for stable, user-defined ids. The namespacing issue is still real, and it would be great to figure out a way to automate this to avoid id conflicts, but it's very easy to catch duplicate ids statically at build time and throw an error. If your app is split into multiple packages, I'd recommend namepsacing the package's message ids with the package name, or using nested <IntlProvider> elements to only expose the package to its own messages.

@g-p-g you're still free to use dots (.) in your identifiers and it should have no observable difference if you think of the messages collection as opaque. "pageX.featureY" is completely fine to have as an id in the flat messages collection and yields the same result as looking up the value in a nested messages collection.

Also, the description field in the message descriptor is completely optional, you can leave it out, or only use it when you need to disambiguate the meaning of a default string for a translator. I feel the tooling to aggregate and process the JSON files with the extracted message descriptors will be app-specific and is easy to write. At Yahoo we're aggregating these descriptors into a collection and converting the data into a different format for our translation system.

@ericf Those updates are HUGE! A lot of pain points addressed - I like this very much. Not sure when I get around to play with it but I feels like a major, major improvement - good work 👍

This looks pretty great to me! I'll probably pull down the RC for my next project. +1 on killing the mixin!

g-p-g commented

@ericf sure you can leave the dots, but the typical applications (crowdin/oneskyapp) will not handle that in the same way as nested objects when displaying. v2 looks very close to how gettext works when you consider the features default translation text, text in the source, some context for the text; except it's more verbose to achieve the same.

I'm not that excited for so much breakage in this part, the other parts (IntlProvider, no more mixins) look good so thank you and everyone else that have been building react-intl.

ericf commented

sure you can leave the dots, but the typical applications (crowdin/oneskyapp) will not handle that in the same way as nested objects when displaying.

@g-p-g can you explain more how the identifiers are interpreted by these translation vendors? My goal with the message descriptors is provide and extract enough information that can be formatted for translations services, and once the translations are returned, they can be put into the runtime's format.

I'm totally on-board with trying to cut down the verbosity of message declaration via the tooling. This is a preview release so I could get feedback like that and keep iterating on these ideas to hone v2 into something that works for a lot of people. I will think about ways to automatically create or transform the message ids to provide greater context for translations and namespacing to avoid collisions.

It's worth noting that converting from nested to flat is a pretty simple function to write, so you can do something like

module.exports = require('react-intl').flattenTranslations({
  blah: {
    blah: {
      blah: {
      }
    }
  }
});

This all looks pretty sensible to me, the use-case we have for mapping server error codes into translations should fit quite nicely as a map of code: defineMessage().

The extraction process doesn't work very well for those of us that wrap the FormattedMessage component in our own component instead of using it directly.

We do this to reduce the exposure of using react-intl in case we want to move to a different localization strategy in the future. Otherwise you end up with FormattedMessage spread throughout the entire project that would need to be fixed. We also use it to make the formatters easier to use in our particular project.

We can definitely still do things the old way which isn't that big of a problem.

STRML commented

This is a great set of changes for v2 and I am very much looking forward to working with this project in the next few weeks as we translate BitMEX into Chinese.

Just a quick note on parent/owner-based context: 0.14 switched to parent-based context from owner-based context, not the other way around. Because it's parent-based, the function hack is no longer necessary as <IntlProvider> is the parent of <App> and doesn't have to execute the function to become its owner.

great update that can solve a lot of the problems i face with building a large scale multi-language app! thanks! One of the biggest change for me here is to self-define message ids. To me I think that can be automated as part of the build script. Coming from python, these message ids are simply the default message string. I have not faced much issues with using them as message ids.

ericf commented

The extraction process doesn't work very well for those of us that wrap the FormattedMessage component in our own component instead of using it directly.

@bunkat would you import and use defineMessage()? If so, then you could define your messages using that, and then spread the descriptors into your custom JSX elements.

ericf commented

I've been working on updates to babel-plugin-react-intl to support generating message ids.

I'm been adding these options to the plugin:

  • enforceDescriptions: Will throw an error if a message is declared without a description for translators.
  • generateMessageIds: Generates the message descriptor's id field by creating a SHA1 hash of the defaultMessage and description (if a description is declared) fields.
  • removeExtractedData: Removes the description and defaultMessage from the React components to save k-weight.

This allows you to either specify ids, or have them generated for you. I have all this working locally, I'm testing it out in some apps and also bikeshedding on the option names.

I'm also working on adding a defineMessages() (plural) function so a hash can be provided to define multiple messages at once with local identifiers; e.g.:

const messages = defineMessages({
    button_label: {defaultMessage: 'Send'},
    tooltip: {defaultMessage: 'Send the message.'},
});

@ericf I don't think using defineMessage() adds anything to our particular scenario, though I can see that others might. We prefer to keep our messages centralized and just pass ids around in the code which still works perfectly fine in v2 (which is a huge improvement by the way!). If we moved to defineMessage() they would still be in a single file so using the extraction process doesn't buy us anything.

@bunkat That's an interesting idea - do you have some code to generate variables containing IDs, or do you just use the strings?

eg.

var translations = require('./translation-ids');
<FormattedMessage id={translations.home.title} />

vs

<FormattedMessage id="home.title" />

We do something similar to the former. I guess the difference is that we treat the messages file as the source of truth instead of the code. We generate IDs from the messages file which then becomes the only thing that the code uses. We handle the default message scenario by just merging our localized message file with the en-us message file before sending it to the client. This way the client gets the minimal set of localized strings needed for the UI.

We used to provide default messages in the code, but it was harder for us to maintain. For our particular product we have a very large number of duplicate strings and were always getting slightly different versions used throughout the code. Also, using an existing string becomes easier than creating new ones which is what we want.

ericf commented

@bunkat one of the primary reasons we've gone with co-location is to manage cruft. We have some apps with 4000+ different strings making it really hard to find a string in the single large strings file, and hard to clean-up. Some of these strings aren't even being used because they are from some A/B test that was discarded.

A product might have an iOS, Android, and web app that could share strings, and in this case there's an argument to have them all use a shared collection that's external to the source code of all the client apps. But we haven't seen this actually happen in practice, and teams are managing their own translations with their own strings baked into their client app code.

The good thing with v2 is that both strategies will be supported! We are actually leveraging this in apps I've been migrating over to v2 because there are externalized and co-located strings during the transition phase.

STRML commented

A decorator that merges the contextTypes into the component would be very helpful. What do the error messages look like if you use a <FormattedMessage> in a component but the context is not available?

ericf commented

@STRML if you don't wrap your app with <IntlProvider> and try to use <FormattedMessage> you'll get the standard React error about contextTypes not having the required intl prop. I can look to making the <Formatted*> component treat intl as an optional contextType and see if that allows me to control the error in the component.

On a related note, I've been thinking about also creating an HOC factory function that'll inject intl as a prop into components which want to use the format*() functions from the API. This would then alleviate the need for people from having to use the still-undocument context feature of React.

STRML commented

Ah okay. I misunderstood. As long as I use <FormattedMessage> et al, I don't have to add contextTypes manually to my elements. But if I want to use format*() in render(), I need it. And I'll need that for any text attributes, correct?

ericf commented

@STRML that's exactly right! I just opened PR #168 to hopefully make this cleaner by using injectIntl(MyComponent) which is a High Order Component (HOC) factory function. This alleviates needing to worry about context, as it'll inject the intl object as a prop to MyComponent.

ericf commented

@jbrantly @g-p-g @geekyme I've refactored babel-plugin-react-intl to support generating ids.
See: formatjs/babel-plugin-react-intl#4 it implements what I was talking about above: #162 (comment)

Are you going to bump the version any time soon, to include changes like defineMessages etc ?

ericf commented

I've been thinking more about the message id generation feature and I'm on the fence for whether or not it's a footgun. I would like to hear more details about how the translations systems/services that people use would deal with generated ids.

Current ID Generation Algorithm:

  1. Parse and "pretty print" defaultMessage to normalize it and remove insignificant whitespace.
  2. NLP tokenize and stem description and sort tokens (if a description was provided to give context to the translator).
  3. Generate SHA1 from 1 + 2 (above).

Potential Issues:

  • Any change the the id generation algorithm could yield all new ids.
  • A new message/string id could mean a re-translations for some systems, even when message is the same.
  • (Probably others which is why I'd like to hear about people's translation systems/services.)

If you're interested in having your app's message ids generated, could you comment on how your translation system/service would deal with changes in ids, especially when the message stays the same (which could happen if the description is updated or if the id generation algorithm changes/improves)?

/cc @jbrantly @g-p-g @geekyme @bunkat

g-p-g commented

@ericf thanks for looking into it and asking for feedback, I haven't been able to track this and other issues more closely.

I'm more used to gettext / .po files where you have msgid as the message itself. I'm guessing you're familiar with po files and that you believe it's not a good fit for react-intl, and that's why you're trying to apply some algorithm to generate ids. Or maybe the problem you're facing is closer to the generation of .mo files (po files compiled with msgfmt).

The impression I got is that some of the changes described here are moving to a model closer to gettext -> po -> msgfmt -> mo. In that model you don't have to worry about IDs, and the po files include some context to indicate the place(s) the message is coming from. Let me know if this impression is wrong, so I can try seeing this from a different angle.

I'm new to react-intl, so it's likely you discussed this before. If it's not too much work, you could point me to issues/documents so I can better understand 1) the reasons for using a model very different from that of gettext mentioned above, 2) and the reason to not use the exact same model mentioned above?

If people are just skimming this, please understand that I'm not proposing to change the message syntax. I'm not saying to move away from ICU. This is only about the flow used by systems that use gettext.

ericf commented

@g-p-g There's a good summary by @tinganho here: #89 (comment), #89 (comment), #89 (comment). The tl;dr of @tinganho's point is that generating message ids would lead the the problems above.

STRML commented

For the use case in #89, I think the id generation method as proposed by @ericf makes sense and is similar to gettext + msgctext, where description is more or less equivalent to msgctext. Perhaps we could also use the owning component's displayName if the description is not present?

ericf commented

Perhaps we could also use the owning component's displayName if the description is not present?

@STRML the defineMessages() API/hook means that messages can be defined at the module level, outside of any component class declaration. That said, another approach would be to use the filename/file path in (or as) the generated id. The problem there is similar in that the ids are not stable and susceptible to refactoring/moving-around files.

STRML commented

True. And this is a problem I have run into with react-localstorage as well; there simply isn't a good way to even get a unique repeatable ID of a component and its context on the page, even if it doesn't move. If it does, you've blown the ID and you have to regenerate.

In that case, it appears to me that an optional description in the style of msgctext is the best solution. If the text or context of that message changes, the message has changed and should be retranslated.

gettext solves the message id changing issue with fuzzy entries which translation tools also support. The basic idea is that you can usually easily tell with tooling that msgid1 changed to msgid2.

For instance, if the old translation file had "Hello world" as an id and the new translation no longer had "Hello world" but had a new entry "Hello there world" you can apply heuristics to suggest that "Hello world" probably changed to "Hello there world".

awesome, just like issue #35 wanted

could have had this a long time ago with more open communications

ericf commented

Update: v2.0.0-pr-2

I've released v2.0.0-pr-2 and updated this issue's description at the top.

I've also released babel-plugin-react-intl@1.0.0-beta-4 with some improvements. While there was lots of discussion in this great about generating message ids, I've held back on this for now because of the problems listed above.

Awesome work done here!
I have one question though:
When using a message that contains an apostrophe it won't work.

const messages = defineMessages({
        gobutton: {
            id: 'home.gobutton',
            defaultMessage: 'Got it, let's begin',          
        }
});

PS: Nvmd, this will of course work just fine:

const messages = defineMessages({
        gobutton: {
            id: 'home.gobutton',
            defaultMessage: 'Got it, let\'s begin',         
        }
});
g-p-g commented

@LorbusChris well, you need to escape the string just like you would do for any other string in javascript.

g-p-g commented

@ericf nice one with defineMessages. I feel that with this relatively simple modification the previous design is much closer to the current one, which I like because it no longer passes the impression that it's moving to a gettext-like implementation. Since there's also a helper to flatten nested objects I don't have any concerns for now.

@g-p-g Thanks, Im still getting to know js 👍

@ericf I use transifex.com

Changing my message keys will require translation again. I currently use gettext so my default message string is my message key

g-p-g commented

@geekyme if you're using gettext and generating .po files how is that affected by this new version? What parts of react-intl are you using? I'm somewhat interested in that because I'm not using gettext with react-intl but it's a pretty common method to use.

gettext generates ids based on the string i supply.

gettext("Hello world, I am {name}") will generate in the translation table as

msgid "Hello world, I am {name}"
msgstr

Then I will upload to transifex for my guys to fill in msgstr for the particular language they are interested in.

Now this new version will affect this because "Hello world, I am {name}" will no longer be generated as the id. It will be a SHA1 hash. New message ids will be interpreted as new strings added to the database, therefore I will have to retranslate my text.

Potential Issues:

Any change the the id generation algorithm could yield all new ids.
A new message/string id could mean a re-translations for some systems, even when message is the same.
(Probably others which is why I'd like to hear about people's translation systems/services.)
If you're interested in having your app's message ids generated, could you comment on how your translation system/service would deal with changes in ids, especially when the message stays the same (which could happen if the description is updated or if the id generation algorithm changes/improves)?

By the way I do not use react-intl to generate .mo/.po files, this is done using PyBabel. However I wish I can do all this without any awkward tooling to force python and javascript together :) Generating message ids is important to me as it can save me tons of time writing message ids for a large number of languages each time I add a new string.

g-p-g commented

@geekyme I understand that but I'm still missing how you use it with react-intl.

It seems that you call .po a "translation table", is that correct? Where is the equivalent step of msgfmt that would generate .mo files? If you're using gettext & associated tools, what would happen is that the .mo file is loaded with the translations to be used. The typical .mo file is a binary one, which supposedly can change without the .po file changing (i.e. let's say the format for .mo changes, but not its translated texts). My impression is that the equivalent for gettext + react-intl would be to load something akin to .mo, or what else are you loading?

If it's easier and you have a simple example using gettext and react-intl, just point to it please.

Ah yes.

I mark my strings with "gettext(....string here....)" and then I use jsxgettext (https://github.com/zaach/jsxgettext) to crawl my files and spit out the .po files for different languages.

I then upload to transifex and allow people to translate. Afterwards I download the files back from transifex using a command and apply po2json to convert my .po files to json format.

The resultant json files is then loaded into my application using require("zh-TW.json") and fed into the intlData.messages (see FormattedMessage here http://formatjs.io/react/).

Sent from my iPhone

On 27 Sep 2015, at 1:25 AM, Guilherme Polo notifications@github.com wrote:

@geekyme I understand that but I'm still missing how you use it with react-intl.

It seems that you call .po a "translation table", is that correct? Where is the equivalent step of msgfmt that would generate .mo files? If you're using gettext & associated tools, what would happen is that the .mo file is loaded with the translations to be used. The typical .mo file is a binary one, which supposedly can change without the .po file changing (i.e. let's say the format for .mo changes, but not its translated texts). My impression is that the equivalent for gettext + react-intl would be to load something akin to .mo, or what else are you loading?

If it's easier and you have a simple example using gettext and react-intl, just point to it please.


Reply to this email directly or view it on GitHub.

Been using this preview... Thank you so much for the best I18n solution I've used to date! :-D

I've got a use case/question. What about HTML5 input placeholders? Should one inject the context manually and call the methods directly for that?

ericf commented

@iammerrick Once I update the docs on the website for v2 I'll definitely explain how to handle that, title and aria attributes are the same. My recommendation would be to use the formatMessage() method to handle this as it'll always return a string; here's an example.

@ericf you told before to use babel-plugin-react-intl to extract translations from the components. But what about Actions and Stores on the Flux case? Will not be great if we can have the plugins extracting from these artifacts types too?

ericf commented

@pablolmiranda There's nothing specific about extracting from React components, the Babel plugin works on any file that Babel processes.

The plugin looks will extract messages from modules which import either FormattedMessage, FormattedHTMLMessage, or defineMessages from 'react-intl'. If one of these imports is detected then the plugin looks for defineMessages() being used as a call expression, and <FormattedMessage> and <FormattedHTMLMessage> being used in an JSX opening element.

Today in fact we prototyped an idea of having a Notifications store which listens to various actions created throughout the app, creates React Element descriptors via <FormattedMessage> in the action handler and sticks them on its state queue to be rendered as a "toast"-style notification. This decoupled the component firing the flux action from knowing that notifications even exist, and allowed us to co-locate and format the internationalized message we want to display where we're handling the action. This pattern is possible with React 0.14 because of parent-based context.

So far it's been wonderful but we have had trouble keeping id's unique, what do you think of warning when an id is used twice?, To leverage a key multiple times people could make their own components that call into Format.js once... Thoughts?

Is there any way to make reusable messages?
Let's say, that I have an application that has 78 cancel buttons with text Cancel.
Due the unique id constraint, I would have to declare 78 different ids for same word, instead of just using <FormattedMessage id="cancel-button" defaultMessage="Cancel" /> in every one of them.

@vesaklip that's the idea with defineMessages - you declare the message in one place and require() it into your components.

Hey all.

Just wanted to put my two cents in the conversation with a couple of observations/suggestions.

A little pre history. I'm using react-intl v2 in combination with fluxible in an isomorphic app. It works flawless. Although, what I'm doing should go to production soon I decided to use react-intl, because the code base is not so big and I can swap it easily. Anyway this is what I wanted to share :

Plugins for other tools might be needed.

I'm using TypeScript with Webpack. What looks like a blocker for me is the babel plugin for capturing the localisation keys from inside the code. I guess it will need additional configuring, since my code-base is TypeScript and it will have hard times ( or at least I don't want to add another dependency to my build that runs babel ). Anyway I just don't use that functionality and add all my localisation keys manually to my localisation files, but then again I end up having No. 2 as a problem.

What If I want to stay old-school and use just localisation files.

The method formatMessage ( not the React Component ) takes as a first argument an object with the message and it's defaultMessage and what I need is formatMessageID, because since I'm not using define message ( or at least is pointless in my case ) I don't have an object with id and defaultMessage.

Workflow with language packages.

So as far as I understood and checked the example repo and the comments in this issue

Implementation of Internationalization with React Intl v2:
Declare default messages/strings in source code (React Components)
Use tooling to extract them at build-time

So the second point of the workflow is more interesting and I was wondering how will it work in a bit multi-team environment. So far I got this in my head :

  1. Tool extracts code to the language files and stores them in /locales/en.json
  2. Team-member takes en.json and send it for translation
  3. Team-member copies translated en.json, de.json files back to /locales
  4. Code is committed and passed to CI.

If this is the case then we should be careful, because translators might receive every time different en.json ( since it's machine-generated ) and will need additional time to scan for new identifiers that they need to copy to de.json.

Am I missing something on that topic?

Well that's it. Other than that the idea of no-more jumping between localisation ( properties ) file and the code is astonishing and I really like it. I can't wait for the final 2.0 release.

ericf commented

Update: v2.0.0-pr-3

I've released v2.0.0-pr-3 and updated this issue's description at the top.

Note: This release removes support for React 0.13.

There's also a great discussion happening in #186 to determine how to make formatted relative times (e.g., "1 min ago") tick so that they stay updated and present accurate information — join in on that discussion if you're interested.

You can replace injectIntl() with an annotation (everyone does it nowadays)

I've implemented v2 in my application
https://github.com/halt-hammerzeit/webapp

Advanced features like the Babel plugin and Locale Data as Modules are yet to be figured out and implemented.

ericf commented

@halt-hammerzeit are you talking about the ECMAScript decorators proposal? If so, I don't feel comfortable with encouraging their usage until it has stabilized and has at least reached Stage 3 status.

https://github.com/tc39/ecma262/blob/master/README.md

ericf commented

@drinchev, thanks for the great questions and feedback. Here's my replies inline…

I'm using TypeScript with Webpack. What looks like a blocker for me is the babel plugin for capturing the localisation keys from inside the code. I guess it will need additional configuring, since my code-base is TypeScript and it will have hard times ( or at least I don't want to add another dependency to my build that runs babel ).

I only have plans to officially support the Babel plugin for extracting the default messages from source so they can be prepared for translation.

It looks like you could have the TypeScript compiler output ES6 via the --target ES6 option, and then you could use Babel to transpile to ES3/5. It would be adding to your toolchain, but could open up your code base to more static analysis and tooling outside of what TypeScript provides.

What If I want to stay old-school and use just localisation files.

The method formatMessage ( not the React Component ) takes as a first argument an object with the message and it's defaultMessage and what I need is formatMessageID, because since I'm not using define message ( or at least is pointless in my case ) I don't have an object with id and defaultMessage.

This is supported, I made sure the design works without requiring the Babel plugin tooling. The primary reason for this is that at Yahoo apps use translation files that are external from the .js files, so I needed to make sure both approaches could be used at the same time while strings are migrated back into the .js files. In some apps, developers have been liking the new approach so much that the whole code base will be converted to the new approach in a matter of a couple weeks.

If you're using external translation files, presumably you still have ids, so this is valid and will work perfectly:

<FormattedMessage id="greeting" values={{name: "Eric"}} />
// or
this.props.intl.formatMessage({id: 'greeting'}, {name: 'Eric'});

You don't have to use defineMessages(), it's simply a noop function that currently only serves as a hook for the Babel plugin to find your default messages.

If this is the case then we should be careful, because translators might receive every time different en.json ( since it's machine-generated ) and will need additional time to scan for new identifiers that they need to copy to de.json.

I've backpedalled on the idea of the Babel plugin generating the message ids, so the message ids will be written by the team, not the tools. The Babel plugin will however help make sure the default messages are "stable" by stripping any insignificant whitespace from the message and "pretty-printing" it. This will protect against refactoring changes or indentation when using multiline ES6 template literals from triggering retranslation.

This approach sounds great. I hope that the separation of concerns in react 0.14 will also allow the use of this library w react-native

bardt commented

Working with plural forms is still not clear. How should FormattedPlural be used to be then extracted via babel plugin? What is the l10n messages format for plural forms?

Tried this example:

<FormattedPlural value={documents.length}
    id="documents_count"
    zero="No documents"
    one="{value} document"
    other="{value} documents"
    />

{value} is not injected in runtime and these strings are not extracted.

(oops, wrong thread)

ericf commented

This approach sounds great. I hope that the separation of concerns in react 0.14 will also allow the use of this library w react-native

@mschipperheyn This will likely become a reality with React 0.15. Right now there doesn't appear to be a way to return a React element from a render() function that's a primitive (like a <span>) that works with React web and native.

ericf commented

@bardt <FormattedPlural> is intended to be use in apps that only need to support one language but still need plural formatting. It's a lower-level component that's simpler to use than <FormattedMessage>. Message extraction only works with messages, which can contain plural arguments.

bardt commented

@ericf thank you! But I think, my confusion is a sign of lack of documentation for such case, especially for one who start from v2 and this thread.

ericf commented

@bardt yeah the docs are still a Todo listed at the top of the thread, so I expect to receive these kinds of good questions until I have a change to flesh out the updated docs.

Just checking: is it now possible to configure react-intl to not throw an exception when a message key is not found but instead just print the message key?

@mschipperheyn read the part in the original post about automatic translations fallbacks. ;)

@ericf sorry if this is the wrong place, but I couldn't find any other information:

After upgrading to React 0.14 I ran into some errors with react-into 1.2.1 and found the info here that it's not compatible with the latest React version. To my surprise I noticed that my code seems to work fine with 1.2.0 and I was wondering what part is actually broken and if it is safe to run React 0.14 / react-intl 1.2.0 for now until v2 is ready and battle-tested.

Thanks in advance!

ericf commented

Update: v2.0.0-beta-1

I've released v2.0.0-beta-1 and updated this issue's description at the top.

React Intl v2 has been promoted from preview release to beta because we feel it's now feature complete and ready to move forward towards a release candidate once the test suite has been filled out and the docs have been updated. Lots of new features, improvements, and bug fixes in this release so be sure to check out the release notes.

👍

If there's any typescript enthusiast, I've updated the DefinitelyTyped file for v2.0.0.

It's right here: https://github.com/cdroulers/DefinitelyTyped/tree/master/react-intl in react-intl.d.ts. There's a test file react-intl-tests.tsx which shows how to use it. It would be great to get feedback on this so I can create a pull request to the master branch!

@cdroulers I'm checking it right now. How do you handle intlShape?

So far I was breaking the typecheck by doing inside the component :

static propTypes : React.ValidationMap<any> = {
        intl : intlShape.isRequired,
    };

But it looks like you didn't include this in the definition files.

@drinchev Forgot about that! I'll get it fixed soon and create the PR for the feedback to be done there!

@cdroulers sorry just forgot another thing

defineMessage should be declared something like :

interface Messages {
    [key: string]: FormattedMessage.MessageDescriptor
}

function defineMessages<T extends Messages>( messages: T ) : T;

Anyway you can create pull request in DefinitelyTyped we can continue conversation there :)

@drinchev Here is the pull request!
DefinitelyTyped/DefinitelyTyped#6542

I've made both changes you told me. I'm really not sure about intlShape though!

What about hot swapping languages? is it possible?

@enahum It's absolutely possible, although since locale data comes from Context, it's very easily foiled by shouldComponentUpdate.

Hello!, where can I get support? I want to format a number with FormattedNumber so it shows "." in thousands and "," in decimals.

Thank you and sorry if this is not the support forum.

@dallonf @enahum The solution here might be to keep the messages in your store (in case you use a flux-like architecture) and create your own wrapper functions which do not depend on the messages being passed down via context. That would allow you to switch your language by dispatching an action with the new translation set.

@titanve You should open a separate issue on the repo for this :) But for what it's worth: make sure that you have loaded the correct locale and you also pass in the correct locale identifier to your IntlProvider.

@johanneslumpe ok very good!

ericf commented

@enahum hot-swapping locales will be possible, but as @dallonf points out, context and shouldComponentUpdate() has semantics that's different than passing everything through props:

If a context value provided by a component changes, descendants that use that value won't update if an intermediate parent returns false from shouldComponentUpdate. See issue facebook/react#2517 for more details.

http://facebook.github.io/react/docs/context.html#known-limitations

If every component throughout the hierarchy that implements shouldComponentUpdate(), also is wrapped by the injectIntl() HOC, then everything should work as desired since the props.intl objet will have changed when <IntlProvider>'s props changes. Hopefully context worm hole updates will be implemented in a future version of React to make this automatic.

@ericf, @dallonf Thx, I´m reviewing that, If i come up with some nice implementation I´ll let you guys know.

ericf commented

@titanve please don't use this thread for support. You've already created another issue specific to your support questions, use that.

@ericf sorry!
@johanneslumpe can you check the issue for my question? @ericf can you help me on my issue?

baer commented

@ericf I have been pouring over the spec and there is a lot that is awesome in here. Thank you for all of your great (and much needed) work on Globalization - especially being an advocate on TC39! All of these tools are really well set up to internationalize an app but I'm having a little trouble understanding how this would work to internationalize a standalone components. The apps that I work on are composed of many many independently developed components so I want to get a handle on this before I get too deep.

To make this concrete, lets say I want to make a stand alone Greeting component and deploy it to NPM. I don't have access to the app's root so would I still wrap this in an IntlProvider? It seems like that could have a name collision with the root level provider. This is especially true if there were children like in a List component or a Forms library.

I could use defineMessages to pass messages in as props but all the tooling still takes the locale, formats, defaultLocale etc from the IntlProvider via context.

var React = require('react');
var ReactDOM = require('react-dom');

var Greeting = (props) => {
  return (
    <h1>
      Hello there, {props.name}!
    </h1>
  );
};

export default Greeting;
ericf commented

@baer great question! We have apps at Yahoo that are architected in this way too. I've been thinking about this problem some, but I don't have any concrete guidance, yet. However, I'm actually working with internal teams this week to figure out a solution and guidance for this problem.

In your app, does each team that works on a sub-tree of the app translate their own strings? Or are the strings translated in aggregate at the level of the main app?

One thing I've been thinking about is using multiple <IntlProvider>s, one at the root, and one wrapping each sub-tree's root, but nested within the main app. Something like this:

// subtree.js

import React from 'react';
import {FormattedMessage} from 'react-intl';

export default const SubTree = () => (
    <FormattedMessage
        id="subtree.greeting"
        defaultMessage="Hello!"
    />
);

// app.js

import React from 'react';
import {injectIntl, IntlProvider} from 'react-intl';
import SubTree from './subtree';

export default const App = injectIntl(({intl}) => (
    <IntlProvider {...intl}>
        <SubTree />
    </IntlProvider>
));

// client.js

import React from 'react';
import ReactDOM from 'react-dom';
import {injectIntl, IntlProvider} from 'react-intl';
import App from './app';

ReactDOM.render(
    <IntlProvider
        locale={window.AppState.locale}
        messages={window.AppState.messages}
    >
        <App />
    </IntlProvider>,
    document.getElementById('container')
);

This propagates the locale, messages, and any other configuration from the outer <IntlProvider> to the nested <IntlProvider>s that wrap each sub-tree. You could imagine messages containing everything for the entire app, or some subset for the particular subtree that's being wrapped. Maybe the messages for each subtree come from a store?

One thing I still need to figure out with this approach is how to deal with formatted relative times' initial "now" reference time. See #191

baer commented

@ericf Thanks for such a detailed response! Glad to hear that you're thinking about the problem. What I am running into would probably be well solved by having multiple cascading IntlProviders. I quite like that idea actually. In our code (www.walmart.com) we have dozens of independently developed React components that are all pulled into apps via npm. Since many of these components are open source we can't control the environment / specific i18n solution of the app the components are contained in. For us we are trying to hit four main criteria:

  1. Useful by itself - If there is no Intl data provided, the component should work
  2. Useful in an app that uses any arbitrary Intl solution - A component should have a mechanism for passing in Intl data so it can be used with external i18n tooling.
  3. Easily Tested - This can use the same tooling as above
  4. It should be really clean when you used the preferred Intl solution - If there is an <IntlProvider> HoC it should all just work beautifully.

Below is an idea of how that might look:

Greeting Component - This is Deployed on NPM and managed by the common-components team

import React from "react";
import ReactDOM from"react-dom";

import { FormattedMessage } from "intl-format";
import IntlDataInjector from "intl-data-injector";

const localMessages = defineMessages(
  greeting: {
    id: 'greeting',
    description: 'Welcome greeting to the user',
    defaultMessage: 'Hello, {name}!',
  }
});

var Greeting = (props) => {
  // This probably should go into the IntlDataInjector
  const messages = getMessagesFromContext() || localMessages;
  return (
    <h1>
      <FormattedMessage {...messages.greeting} values={{name: props.name}} />
    </h1>
  );
};

export const IntlWrappedGreeting = (props) => {
  return (
    <IntlDataInjector {...props}>
      <Greeting/>
    </IntlDataInjector>
  );
};

IntlDataInjector.js - A new or adapted version of IntlProvider that passes Intl data passed in from props if available. If it is not available it proxies Intl data available on context. If neither is available it passes an empty Intl object so format* will just use the default.

export default const IntlDataInjector = (props) => {
  if (intlProps(props)) {
    // set context with Intl data from props
  } else if () {
    // pass through the existing Intl data available on context
  } else {
    // set context.Intl to an empty object to prevent errors
  }

  return React.Children.map(props.children, (child) => {
    React.cloneElement(child, _.omit(props, {locale}))
  });
}

My Account Page - This is maintained by the Account team and is deployed to production

import React from "react";
import ReactDOM from "react-dom";
import {injectIntl, IntlProvider} from "react-intl";

import Greeting from "react-greeting";

ReactDOM.render(
  <IntlProvider
      locale={window.AppState.locale}
      messages={window.AppState.messages}
  >
    <div className="my-account-styling">
      <Greeting />
    </div>
  </IntlProvider>,
  document.getElementById("container")
);

Some Generic App - The component should be useful in an app with no Intl concerns. Internal tooling, rapid prototyping and other general purpose projects should still work

import React from "react";
import ReactDOM from "react-dom";

import Greeting from "react-greeting";

ReactDOM.render(
  <div className="my-app-styling">
    <Greeting />
  </div>,
  document.getElementById("container")
);
ericf commented

I think it's useful to have two categories of shared components:

  1. Highly shareable UI building blocks (e.g. <Button>) — these components shouldn't do any i18n/l10n stuff, in fact they shouldn't have any UI strings. Instead, they should take props and use the node PropTypes so elements, or strings can be passed in. These are the most likely packages coming from public npm.
  2. App-level nouns (e.g., <ChatThread>) — these components represents the main UI parts of the app, they can do i18n and use React Intl for formatting dates and numbers, and even define UI strings. These are likely internal, private packages.

Useful by itself - If there is no Intl data provided, the component should work

For this second category of components (the App-level nouns), the contract can be that the user must make sure an <IntlProvider> exists somewhere in their ancestry. Alternatively, the package could export a root component which accepts the intl props and passes them down to an internal <IntlProvider> that wraps the component. I think either approach would satisfy making the package usable on its own.

Another thing to note is that React Intl will fallback to en by default, and will use the defaultMessages if translations are missing or not passed-in.

Useful in an app that uses any arbitrary Intl solution - A component should have a mechanism for passing in Intl data so it can be used with external i18n tooling.

I'm not sure what you mean by this requirement. Can you explain more?

baer commented

That's a great way to break it down but I think the definition of app-level nouns may be too narrow. To me there are really the following three classes:

  • Building Block - Atomic and should contain no strings I agree 100% with what you said
  • App - This is the repo that contains the <body> tag and contains the top most parent component
  • Widget (app-level nouns) - I agree with your definition with one exception - they are not always internal or private packages. A great example of this would be the React Date Range component. This component does include some of it's own strings and may have some built in error states. Form validation is another example of this type of public internationalizable widget. This is the type of component that I am referring to above.

In a typical app, we extract lots of widgets into separate repos some public and some private. In this way we have been able to prevent monolithic apps and share common UI across the company. For example, the Account team provides a login widget that contains a non-customizable component to drop into the various UIs across the org. Below is an example of a toy Product page that needs i18n.

Product Page - This is maintained by the Product team and is deployed to production

import React from "react";
import ReactDOM from "react-dom";
import {IntlProvider} from "react-intl";

// Developed by the common-components team and is deployed to public GitHub/npm
import Greeting from "react-greeting";
// Developed by the account team and is deployed to private npme (not in the same repo)
import LogIn from "react-login";
// Developed by the homepage team and is deployed to private npme (not in the same repo)
import Footer from "react-footer";

ReactDOM.render(
  <IntlProvider
      locale={window.AppState.locale}
      messages={window.AppState.messages}
  >
    <nav>
      <Greeting/>
      <LogIn/>
    </nav>
    <div className="all-my-custom-product-page-code">
      Look at this great thing!
      ...
    </div>
    <Footer/>
  </IntlProvider>,
  document.getElementById("container")
);

With this as the goal I will try to clarify some of the points and answer the questions you posed.

the contract can be that the user must make sure an exists somewhere in their ancestry. Alternatively, the package could export a root component which accepts the intl props and passes them down to an internal that wraps the component.

The goal of the IntlDataInjector.js pseudocode above was to not force the choice. It is doing exactly what you suggest but will take either props or <IntlProvider> in the ancestry.

Useful in an app that uses any arbitrary Intl solution - A component should have a mechanism for passing in Intl data so it can be used with external i18n tooling.

All I meant here was that for OSS we would prefer not to place hard constraints on the consumer unless it is absolutely necessary. In that spirit a widget should be able to take in Intl data both from an <IntlProvider> in the ancestry OR from props which can be controlled by whatever tooling the consumer chooses whether it is Format.js or something else entirely.


I think at the heart of what I am suggesting is that the dev ergonomics for deploying a standalone widget there are two things that are needed:

  1. Many <IntlProvider>s in a hierarchy - probably just proxying higher order intl data if it exists
  2. A utility that can consume Intl data as either props OR from a higher order <IntlProvider>.
ericf commented

@baer I was roping Widgets into the Building Blocks category. Even if they provide their own strings, they'll need to provide PropTypes.node props so all strings can be localized and/or rich-text (<b>, etc.) If they displaying numbers and dates they'll either need to localize the values or also allow PropType.node or some way to delegate formatting to the user of the widget.

The goal of the IntlDataInjector.js pseudocode above was to not force the choice. It is doing exactly what you suggest but will take either props or in the ancestry.

I'm not sure I totally follow why you couldn't always use <IntlProvider> and spread the props you want to propagate and then declare any overrides like message={widgetMessages}?

baer commented

If a Widget or Building Block contains an <IntlProvider> then there are potentially n <IntlProvider>s in any given react tree. Will that work properly?

baer commented

So if each widget/building-block has it's own <IntlProvider> then the effective React tree will look like what is below. In this case, how are the <IntlProvider>s supposed to interact? In this case there are two things that I am not understanding:

  1. How does the App (not widget/building-block) developer control the locale or the widgets?
  2. Is it possible for the App (not widget/building-block) developer to pass translations into the widgets? This would be needed if the App developer wanted to only use a single ajax call to fetch a language file rather than either 1 per widget or bundling all languages.
import {FormattedMessage} from "intl-format";
import {IntlProvider} from "react-intl";
import React from "react";
import ReactDOM from "react-dom";

ReactDOM.render(
  <IntlProvider
      locale={window.AppState.locale}
      messages={window.AppState.messages}
  >
    <nav>
      // Independently Developed Greeting component (not in the same repo)
      <IntlProvider>
        <h1>
          <FormattedMessage {...messages.greeting} values={{name: props.name}} />
        </h1>
      </IntlProvider>

      // Independently Developed Login component (not in the same repo)
      <IntlProvider>
        <div class="input-group">
          <span class="input-group-addon" id="basic-addon3">
            <FormattedMessage {...messages.login} />
          </span>
          <input type="text" class="form-control" placeholder="Username">
        </div>
      </IntlProvider>
    </nav>
    <div className="all-my-custom-product-page-code">
      Look at this great thing!
      ...
    </div>

    // Independently Developed Login component (not in the same repo)
    <IntlProvider>
      <Footer>
        <div class="footer">
          <FormattedMessage {...messages.copyright} />
        </div>
      </Footer>
    </IntlProvider>
  </IntlProvider>,
  document.getElementById("container")
);

This is an amazing library! Thank you very much for all of your hard work.

ericf commented

@baer good questions, here's some more thoughts on this as I've been working through this problem for Yahoo apps… tl;dr maybe a is the right answer?

How does the App (not widget/building-block) developer control the locale or the widgets?

<IntlProvider> is the way to control the i18n configuration via its props.

Is it possible for the App (not widget/building-block) developer to pass translations into the widgets? This would be needed if the App developer wanted to only use a single ajax call to fetch a language file rather than either 1 per widget or bundling all languages.

<IntlProvider> is also the answer to this too, you control which strings are available to the subtree by passing them them to the messages prop. Even with Widgets providing chunks of the UI existing in separate packages, you could have one aggregated messages object you pass in to a single <IntlProvider> at the root. The one potential issue with this collision of message ids if they are not namespaced based on some schema that developers all coordinate around.

This got me thinking about how to mitigate the message id collisions, and shadowing feels like the answer. Maybe a simpler model would be to have a <MessagesProvider> which simply exists as a descendent of <IntlProvider> and shadows context.intl.messages in its subtree.


I see several types of npm packages providing Widgets that render chunks of the UI for an app:

Packages that…

  1. don't use React Intl
    1. don't do any i18n
    2. use another i18n lib(s)
  2. use React Intl internally
    1. require being wrapped with <IntlProvider>
    2. can be used standalone

1.i packages need to have "slots" to accept localized data and strings. For these components it's best if their slots are PropType.node so that React Intl elements can be passed-in as props. If this is not the case, then you'll have to use the imperative API to format to string values and pass those as props.

1.ii packages will likely need to be configured with a locale value, in this case you can access the locale via this.context.intl.locale or using the injectIntl() HOC and access it via this.props.intl.locale. Then you can pass this along to the component exported by the package.

I feel that the first group are likely to be packages available in the open source ecosystem, and I think they'll mostly be UI building blocks. I'd even consider the calendar example a UI building block since it's not specific or unique to any one app. If you're building an app where the date picker is a core "noun" of the UI, then you're likely building it yourself and not using some open source package. In terms of integration with React Intl I think these components will be straight forward.

Open source components are generic by definition and usually won't have enough context to be able to define strings, therefore they provide slots that your app can will with <FormattedMessage> elements.

2.i packages should compose nicely since the app root will already contain or be wrapped by an <IntlProvider>. The interesting challenge with these components avoiding message id namespace collisions as mentioned above. This is where a <MessageProvider> component could come in handy. I think this should be a fairly easy component to develop and would alleviate most of the use cases I can think of for having nested <IntlProvider>s instances.

2.ii packages are probably the most difficult to build and integrate. My thinking is that they could provide a component which renders its own <IntlProvider>. At a minimum, the component would need to have a locale prop and probably also need a messages prop. If the component renders relative times, then it probably should take an initialNow value as well. The benefit would be that these packages can provide a component that doesn't require the user to wrap it with an <IntlProvider>, but you'd still need the user to make sure the Intl.js polyfill is loaded in runtimes that need it, and the React Intl locale data is loaded. I can't think of a good use case for this category of Widget package.

ericf commented

@baer I won't have time until after the weekend to work on this, but I quickly whipped what I'm thinking might work for the <MessagesProvider> idea here: https://gist.github.com/c7296c5bd064df02c16e

baer commented

Hey @ericf thanks for so much good feedback. I'm super appreciative of all the work you guys have been doing. I spun up a new react-intl-component and react-intl-app project as a playground for ideas. The app has both a component at a sub-tree that needs react-intl and a dependency on the react-intl-component that should be able to take intl data via either context or props. As of now the component throws a hard exception if <IntlProvider> isn't found in it's tree making it useful only to a project that uses react-intl already as you pointed out. I'm pretty sure this playground covers all the cases above.

I tried to distill them to the simplest possible config that was still viable so it should be pretty easy to grock. I'm happy to make you a collaborator if that would be useful.


So to respond to the groups you outlined - I think you're mostly correct about those groups though you should not underestimate NPMe - package composition via components can come from anywhere.

For group 1 I'm not sure what you can provide for them. Maybe I misunderstood what you mean.

2.i - These should compose pretty well though you're right about the message collision. It would be really slick to do shadow messages but I think that it's not a lot to ask for name spacing via the build.

2.ii - This is the type of composition that interests me most. One way you could handle these would be to use the <IntlProvider> as a general purpose i18n API you can wrap a component in. In the case where the component is used without a <IntlProvider> in it's ancestry it would just pass props and provide the equivalent of <IntlProvider locale="en">. If an Intl object (messages, locale, now) is passed into the component via props the <IntlProvider> would pass the non-intl props and return the component with the equivalent of <IntlProvider {...this.props.intl}>. If there IS a <IntlProvider> in the ancestry it would just act as if it were a component in the app and take the parent context (this last part could be problematic).

I'm happy to PR this and see if we can figure something out.

ericf commented

Update: 1.0 -> 2.0 Upgrade Guide

I've created an Upgrade Guide to help people transition their apps from React Intl 1.0 to 2.0.

If there's things I've missed, or areas where the Upgrade Guide can be improved please open an issue, or better yet a PR 😄

Awesome work, many thanks!!!! 👍

@ericf @baer I'm having the same problem at the moment, I was thinking along the same lines of having each component that uses ReactIntl always return it's markup inside an IntlProvider with defaults & this way when it's used in an application that is wrapped in IntlProvider it will use the settings from the parent.

Something like:

<IntlProvider locale="en" formats={..} messages={..}>// Outer IntlProvider sets messages & formats for all children
  <Component1>
    <Component2> // If this component is used outside of this app... the IntlProvider in the child will still be ok with defaults
      <IntlProvider defaultLocale="en" defaultFormats={..}> // These defaults are only used when this component is standalone
        // more code here
        <FormattedNumber format="myCustomFormat" />
      </IntlProvider>
    </Component2>
  </Component1>
</IntlProvider>
ericf commented

@baer @robhuzzey I've thought about the nested <IntlProvider> more today, and I've come up with a clean solution that should work well: PR #217; let's discuss this more in that thread. The actual code changes ended up being small!

@ericf the nested example looks really interesting! I'm asking myself though why messages are stored in state. shouldnt they rather be props? (i.e a change of locale (state) in HOC will trigger a change in the messages prop of the components). Its also how I understood this: https://facebook.github.io/react/docs/interactivity-and-dynamic-uis.html

ericf commented

@LorbusChris I'm not sure I follow. Currently both locale and messages are props on <IntlProvider> which then sticks them on context.intl so they don't have to be threaded through the component tree at every level.

nvmd, I just got confused by this for a moment.

ReactDOM.render(
    <IntlProvider
        locale={window.AppState.locale}
        messages={window.AppState.messages}

its the window.AppState put through from the express app on the server side.
Keep up the good work & Thanks!