Skip to content

Commit 61d3c1b

Browse files
authored
feat: Allow formatting embedded XML (#1379)
## Enhanced `FormattedMessage` & `formatMessage` rich text formatting In v2, in order to do rich text formatting (embedding a `ReactElement`), you had to do this: ```tsx <FormattedMessage defaultMessage="To buy a shoe, { link } and { cta }" values={{ link: ( <a class="external_link" target="_blank" href="https://www.shoe.com/"> visit our website </a> ), cta: <strong class="important">eat a shoe</strong>, }} /> ``` Now you can do: ```tsx <FormattedMessage defaultMessage="To buy a shoe, <a>visit our website</a> and <cta>eat a shoe</cta>" values={{ link: msg => ( <a class="external_link" target="_blank" href="https://www.shoe.com/"> {msg} </a> ), cta: msg => <strong class="important">{msg}</strong>, }} /> ``` The change solves several issues: 1. Contextual information was lost when you need to style part of the string: In this example above, `link` effectively is a blackbox placeholder to a translator. It can be a person, an animal, or a timestamp. Conveying contextual information via `description` & `placeholder` variable is often not enough since the variable can get sufficiently complicated. 2. This brings feature-parity with other translation libs, such as [fluent](https://projectfluent.org/) by Mozilla (using Overlays). However, in cases where we allow placeholders to be a ReactElement will have to be rewritten to 1 of the 2 syntax down below: ### Before ```tsx <FormattedMessage defaultMessage="Hello, {name}" values={{ name: <b>John</b>, }} /> ``` ### After ```tsx <FormattedMessage defaultMessage="Hello, <b>John</b>" values={{ b: name => <b>{name}</b>, }} /> ``` OR (NOT RECOMMENDED) ```tsx <FormattedMessage defaultMessage="Hello, <name/>" values={{ name: () => <b>{John}</b>, }} /> ```
1 parent aefc81f commit 61d3c1b

12 files changed

+182
-101
lines changed

docs/Components.md

+24-5
Original file line numberDiff line numberDiff line change
@@ -446,15 +446,16 @@ By default `<FormattedMessage>` will render the formatted string into a `<span>`
446446

447447
#### Rich Text Formatting
448448

449-
`<FormattedMessage>` also supports rich-text formatting by passing React elements to the `values` prop. In the message you need to use a simple argument (e.g., `{name}`); here's an example:
449+
`<FormattedMessage>` also supports rich-text formatting by specifying a XML tag in the message & resolving that tag in the `values` prop. Here's an example:
450450

451-
```js
451+
```tsx
452452
<FormattedMessage
453453
id="app.greeting"
454454
description="Greeting to welcome the user to the app"
455-
defaultMessage="Hello, {name}!"
455+
defaultMessage="Hello, <b>Eric</b> <icon/>"
456456
values={{
457-
name: <b>Eric</b>,
457+
b: msg => <b>{msg}</b>,
458+
icon: () => <svg />,
458459
}}
459460
/>
460461
```
@@ -463,7 +464,25 @@ By default `<FormattedMessage>` will render the formatted string into a `<span>`
463464
<span>Hello, <b>Eric</b>!</span>
464465
```
465466

466-
This allows messages to still be defined as a plain string _without_ HTML — making it easier for it to be translated. At runtime, React will also optimize this by only re-rendering the variable parts of the message when they change. In the above example, if the user changed their name, React would only need to update the contents of the `<b>` element.
467+
By allowing embedding XML tag we want to make sure contextual information is not lost when you need to style part of the string. In a more complicated example like:
468+
469+
```tsx
470+
<FormattedMessage
471+
defaultMessage="To buy a shoe, <a>visit our website</a> and <cta>buy a shoe</cta>"
472+
values={{
473+
link: msg => (
474+
<a class="external_link" target="_blank" href="https://www.shoe.com/">
475+
{msg}
476+
</a>
477+
),
478+
cta: msg => <strong class="important">{msg}</strong>,
479+
}}
480+
/>
481+
```
482+
483+
All the rich text gets translated together which yields higher quality output. This brings feature-parity with other translation libs as well, such as [fluent](https://projectfluent.org/) by Mozilla (using `overlays` concept).
484+
485+
Extending this also allows users to potentially utilizing other rich text format, like [Markdown](https://daringfireball.net/projects/markdown/).
467486

468487
### `FormattedHTMLMessage`
469488

docs/Getting-Started.md

+12-8
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
- [The `react-intl` Package](#the-react-intl-package)
99
- [Module Bundlers](#module-bundlers)
1010
- [The React Intl Module](#the-react-intl-module)
11-
- [`Intl` APIs requirements](#intl-apis-requirements)
12-
- [Intl in browser](#intl-in-browser)
13-
- [Intl in Node.js](#intl-in-nodejs)
14-
- [Intl in React Native](#intl-in-react-native)
11+
- [Runtime Requirements](#runtime-requirements)
12+
- [Browser](#browser)
13+
- [Node.js](#nodejs)
14+
- [React Native](#react-native)
1515
- [Creating an I18n Context](#creating-an-i18n-context)
1616
- [Formatting Data](#formatting-data)
1717
- [Core Concepts](#core-concepts)
@@ -90,7 +90,7 @@ Whether you use the ES6, CommonJS, or UMD version of React Intl, they all provid
9090

9191
**Note:** When using the UMD version of React Intl _without_ a module system, it will expect `react` to exist on the global variable: **`React`**, and put the above named exports on the global variable: **`ReactIntl`**.
9292

93-
### `Intl` APIs requirements
93+
### Runtime Requirements
9494

9595
React Intl relies on these `Intl` APIs:
9696

@@ -109,11 +109,11 @@ import '@formatjs/intl-relativetimeformat/polyfill';
109109
import '@formatjs/intl-relativetimeformat/dist/locale-data/de'; // Add locale data for de
110110
```
111111

112-
#### Intl in browser
112+
#### Browser
113113

114114
We officially support IE11 along with modern browsers (Chrome/FF/Edge/Safari).
115115

116-
#### Intl in Node.js
116+
#### Node.js
117117

118118
When using React Intl in Node.js, your `node` binary has to either:
119119

@@ -125,13 +125,17 @@ When using React Intl in Node.js, your `node` binary has to either:
125125

126126
If your `node` version is missing any of the `Intl` APIs above, you'd have to polyfill them accordingly.
127127

128-
#### Intl in React Native
128+
We also rely on `DOMParser` to format rich text, thus for Node will need to polyfill using [xmldom](https://github.com/jindw/xmldom).
129+
130+
#### React Native
129131

130132
If you're using `react-intl` in React Native, make sure your runtime has built-in `Intl` support (similar to [JSC International variant](https://github.com/react-native-community/jsc-android-buildscripts#international-variant)). See these issues for more details:
131133

132134
- https://github.com/formatjs/react-intl/issues/1356
133135
- https://github.com/formatjs/react-intl/issues/992
134136

137+
We also rely on `DOMParser` to format rich text, thus for JSC will need to polyfill using [xmldom](https://github.com/jindw/xmldom).
138+
135139
### Creating an I18n Context
136140

137141
Now with React Intl and its locale data loaded an i18n context can be created for your React app.

docs/Upgrade-Guide.md

+70-20
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
- [Migrate to using native Intl APIs](#migrate-to-using-native-intl-apis)
1010
- [TypeScript Support](#typescript-support)
1111
- [FormattedRelativeTime](#formattedrelativetime)
12-
- [`formatMessage` now supports `ReactElement`](#formatmessage-now-supports-reactelement)
12+
- [Enhanced `FormattedMessage` & `formatMessage` rich text formatting](#enhanced-formattedmessage--formatmessage-rich-text-formatting)
13+
- [Before](#before)
14+
- [After](#after)
1315
- [ESM Build](#esm-build)
1416
- [Jest](#jest)
1517
- [webpack babel-loader](#webpack-babel-loader)
@@ -29,6 +31,8 @@
2931
<IntlProvider textComponent="span" />
3032
```
3133

34+
- Rich text formatting enhancement in `FormattedMessage` & `formatMessage`
35+
3236
## Use React 16.3 and upwards
3337

3438
React Intl v3 supports the new context API, fixing all kinds of tree update problems :tada:
@@ -237,32 +241,78 @@ const {value, unit} = selectUnit(Date.now() - 48 * 3600 * 1000);
237241
<FormattedRelativeTime value={value} unit={unit} />;
238242
```
239243

240-
## `formatMessage` now supports `ReactElement`
244+
## Enhanced `FormattedMessage` & `formatMessage` rich text formatting
241245

242-
The imperative API `formatMessage` now supports `ReactElement` in values and will resolve type correctly. This change should be backwards-compatible since for regular non-`ReactElement` values it will still return a `string`, but for rich text like the example down below, it will return a `Array<string, React.ReactElement>`:
246+
In v2, in order to do rich text formatting (embedding a `ReactElement`), you had to do this:
243247

244-
```ts
245-
const messages = defineMessages({
246-
greeting: {
247-
id: 'app.greeting',
248-
defaultMessage: 'Hello, {name}!',
249-
description: 'Greeting to welcome the user to the app',
250-
},
251-
});
248+
```tsx
249+
<FormattedMessage
250+
defaultMessage="To buy a shoe, { link } and { cta }"
251+
values={{
252+
link: (
253+
<a class="external_link" target="_blank" href="https://www.shoe.com/">
254+
visit our website
255+
</a>
256+
),
257+
cta: <strong class="important">eat a shoe</strong>,
258+
}}
259+
/>
260+
```
261+
262+
Now you can do:
263+
264+
```tsx
265+
<FormattedMessage
266+
defaultMessage="To buy a shoe, <a>visit our website</a> and <cta>eat a shoe</cta>"
267+
values={{
268+
link: msg => (
269+
<a class="external_link" target="_blank" href="https://www.shoe.com/">
270+
{msg}
271+
</a>
272+
),
273+
cta: msg => <strong class="important">{msg}</strong>,
274+
}}
275+
/>
276+
```
277+
278+
The change solves several issues:
252279

253-
formatMessage(messages.greeting, {name: 'Eric'}); // "Hello, Eric!"
280+
1. Contextual information was lost when you need to style part of the string: In this example above, `link` effectively is a blackbox placeholder to a translator. It can be a person, an animal, or a timestamp. Conveying contextual information via `description` & `placeholder` variable is often not enough since the variable can get sufficiently complicated.
281+
2. This brings feature-parity with other translation libs, such as [fluent](https://projectfluent.org/) by Mozilla (using Overlays).
282+
283+
However, in cases where we allow placeholders to be a ReactElement will have to be rewritten to 1 of the 2 syntax down below:
284+
285+
### Before
286+
287+
```tsx
288+
<FormattedMessage
289+
defaultMessage="Hello, {name}"
290+
values={{
291+
name: <b>John</b>,
292+
}}
293+
/>
254294
```
255295

296+
### After
297+
256298
```tsx
257-
const messages = defineMessages({
258-
greeting: {
259-
id: 'app.greeting',
260-
defaultMessage: 'Hello, {name}!',
261-
description: 'Greeting to welcome the user to the app',
262-
},
263-
});
299+
<FormattedMessage
300+
defaultMessage="Hello, <b>John</b>"
301+
values={{
302+
b: name => <b>{name}</b>,
303+
}}
304+
/>
305+
```
264306

265-
formatMessage(messages.greeting, {name: <b>Eric</b>}); // ['Hello, ', <b>Eric</b>, '!']
307+
OR (NOT RECOMMENDED)
308+
309+
```tsx
310+
<FormattedMessage
311+
defaultMessage="Hello, <name/>"
312+
values={{
313+
name: () => <b>{John}</b>,
314+
}}
315+
/>
266316
```
267317

268318
## ESM Build

package-lock.json

+14-14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@
4343
"hoist-non-react-statics": "^3.3.0",
4444
"intl-format-cache": "^4.1.2",
4545
"intl-locales-supported": "^1.4.2",
46-
"intl-messageformat": "^5.1.2",
47-
"intl-messageformat-parser": "^2.1.2",
46+
"intl-messageformat": "^5.2.0",
47+
"intl-messageformat-parser": "^2.1.3",
4848
"invariant": "^2.1.1",
4949
"react": "^16.3.0",
5050
"shallow-equal": "^1.1.0"
@@ -63,11 +63,11 @@
6363
"@types/enzyme": "^3.10.3",
6464
"@types/jest": "^24.0.13",
6565
"@types/prop-types": "^15.7.1",
66-
"@types/react-dom": "^16.8.4",
66+
"@types/react-dom": "^16.8.5",
6767
"@typescript-eslint/eslint-plugin": "^1.13.0",
6868
"@typescript-eslint/parser": "^1.13.0",
6969
"babel-jest": "^24.8.0",
70-
"babel-plugin-react-intl": "^4.1.2",
70+
"babel-plugin-react-intl": "^4.1.3",
7171
"babel-plugin-transform-member-expression-literals": "^6.9.4",
7272
"babel-plugin-transform-property-literals": "^6.9.4",
7373
"babel-plugin-transform-react-remove-prop-types": "^0.4.18",

src/components/html-message.tsx

+2-4
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,9 @@
77
import * as React from 'react';
88
import withIntl from './injectIntl';
99
import {BaseFormattedMessage} from './message';
10-
import {MessageFormatPrimitiveValue} from '../types';
10+
import {PrimitiveType} from 'intl-messageformat/core';
1111

12-
class FormattedHTMLMessage extends BaseFormattedMessage<
13-
MessageFormatPrimitiveValue
14-
> {
12+
class FormattedHTMLMessage extends BaseFormattedMessage<PrimitiveType> {
1513
static defaultProps = {
1614
...BaseFormattedMessage.defaultProps,
1715
tagName: 'span' as 'span',

src/components/message.tsx

+6-9
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,7 @@
66

77
import * as React from 'react';
88
import withIntl from './injectIntl';
9-
import {
10-
MessageDescriptor,
11-
IntlShape,
12-
MessageFormatPrimitiveValue,
13-
} from '../types';
9+
import {MessageDescriptor, IntlShape} from '../types';
1410
const shallowEquals = require('shallow-equal/objects');
1511

1612
import {formatMessage as baseFormatMessage} from '../format';
@@ -19,10 +15,11 @@ import {
1915
DEFAULT_INTL_CONFIG,
2016
createDefaultFormatters,
2117
} from '../utils';
18+
import {PrimitiveType, FormatXMLElementFn} from 'intl-messageformat/core';
2219

2320
const defaultFormatMessage = (
2421
descriptor: MessageDescriptor,
25-
values?: Record<string, MessageFormatPrimitiveValue | React.ReactElement>
22+
values?: Record<string, PrimitiveType | FormatXMLElementFn>
2623
) => {
2724
if (process.env.NODE_ENV !== 'production') {
2825
console.error(
@@ -50,9 +47,9 @@ export interface Props<V extends React.ReactNode = React.ReactNode>
5047
}
5148

5249
export class BaseFormattedMessage<
53-
V extends MessageFormatPrimitiveValue | React.ReactElement =
54-
| MessageFormatPrimitiveValue
55-
| React.ReactElement
50+
V extends PrimitiveType | FormatXMLElementFn =
51+
| PrimitiveType
52+
| FormatXMLElementFn
5653
> extends React.Component<Props<V>> {
5754
static defaultProps = {
5855
values: {},

0 commit comments

Comments
 (0)