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:
- Extract strings from source code
- Put all strings in a single, large, strings file
- 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:
- Declare default messages/strings in source code (React Components)
- 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: https://github.com/yahoo/react-intl/issues/193#issuecomment-152019363)
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:
- Try to format the translated message
- If that fails, try to format the default message
- If either the translated or default message was formatted, return it.
- 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 case
s 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.
- [x] Finish unit tests
- [x] Add perf tests to determine if
shouldComponentUpdate()
is needed
- [x] Create 1.0 -> 2.0 Upgrade Guide
- [x] Update docs and examples on http://formatjs.io/ website
- [x] Only support only React 0.14+? Yes
- [x] Improve build, try to get source maps working on
.min.js
files
- [x] 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
discussion