Skip to content

Commit

Permalink
feat(react-intl): fail fast when intl Provider is missing
Browse files Browse the repository at this point in the history
BREAKING CHANGE: This also comes from Dropbox internal developer feedback. `FormattedMessage` has a default English renderer that masks `Provider` setup issues which causes them to not be handled during testing phase.
  • Loading branch information
Long Ho committed Jul 2, 2020
1 parent f2963bf commit 42fa3c1
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 62 deletions.
4 changes: 2 additions & 2 deletions packages/react-intl/examples/Advanced.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import {RawIntlProvider, FormattedMessage, createIntl} from '../';
import {RawIntlProvider, FormattedMessage, createIntl} from '..';
import {parse, MessageFormatElement} from 'intl-messageformat-parser';

interface Props {}
Expand Down Expand Up @@ -63,7 +63,7 @@ const App: React.FC<Props> = () => {
<br />
<FormattedMessage
id="richtext"
values={{num: 99, bold: (...chunks) => <strong>{chunks}</strong>}}
values={{num: 99, bold: chunks => <strong>{chunks}</strong>}}
/>
</p>
</RawIntlProvider>
Expand Down
6 changes: 3 additions & 3 deletions packages/react-intl/examples/Messages.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import {IntlProvider, FormattedMessage} from '../';
import {IntlProvider, FormattedMessage} from '..';

interface Props {}

Expand Down Expand Up @@ -55,12 +55,12 @@ const App: React.FC<Props> = () => {
<br />
<FormattedMessage
id="richtext"
values={{num: 99, bold: (...chunks) => <strong>{chunks}</strong>}}
values={{num: 99, bold: chunks => <strong>{chunks}</strong>}}
/>
<br />
<FormattedMessage
id="richertext"
values={{num: 99, bold: (...chunks) => <strong>{chunks}</strong>}}
values={{num: 99, bold: chunks => <strong>{chunks}</strong>}}
/>
<br />
<FormattedMessage id="unicode" values={{placeholder: 'world'}} />
Expand Down
42 changes: 4 additions & 38 deletions packages/react-intl/src/components/message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,40 +8,11 @@ import * as React from 'react';
import {PrimitiveType, FormatXMLElementFn} from 'intl-messageformat';
import {Context} from './injectIntl';
import {MessageDescriptor} from '../types';
import {formatMessage} from '../formatters/message';
import {
invariantIntlContext,
DEFAULT_INTL_CONFIG,
createFormatters,
} from '../utils';
import {invariantIntlContext} from '../utils';
import * as shallowEquals_ from 'shallow-equal/objects';
const shallowEquals: typeof shallowEquals_ =
(shallowEquals_ as any).default || shallowEquals_;

function defaultFormatMessage<T = React.ReactNode>(
descriptor: MessageDescriptor,
values?: Record<
string,
PrimitiveType | React.ReactElement | FormatXMLElementFn<T, T>
>
): string {
if (process.env.NODE_ENV !== 'production') {
console.error(
'[React Intl] Could not find required `intl` object. <IntlProvider> needs to exist in the component ancestry. Using default message as fallback.'
);
}

return formatMessage(
{
...DEFAULT_INTL_CONFIG,
locale: 'en',
},
createFormatters(),
descriptor,
values as any
);
}

export interface Props<
V extends Record<string, any> = Record<string, React.ReactNode>
> extends MessageDescriptor {
Expand Down Expand Up @@ -73,14 +44,9 @@ class FormattedMessage<
return (
<Context.Consumer>
{(intl): React.ReactNode => {
if (!this.props.defaultMessage) {
invariantIntlContext(intl);
}
invariantIntlContext(intl);

const {
formatMessage = defaultFormatMessage,
textComponent: Text = React.Fragment,
} = intl || {};
const {formatMessage, textComponent: Text = React.Fragment} = intl;
const {
id,
description,
Expand All @@ -101,7 +67,7 @@ class FormattedMessage<
}

if (typeof children === 'function') {
return children(...nodes);
return children(nodes);
}

if (Component) {
Expand Down
17 changes: 7 additions & 10 deletions packages/react-intl/test/unit/components/message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,10 @@ describe('<FormattedMessage>', () => {
);
});

it('should work if <IntlProvider> is missing from ancestry but there is defaultMessage', () => {
const rendered = mount(
<FormattedMessage id="hello" defaultMessage="Hello" />
);

expect(rendered.text()).toBe('Hello');
expect(console.error).toHaveBeenCalledTimes(1);
it('should not work if <IntlProvider> is missing from ancestry', () => {
expect(() =>
mount(<FormattedMessage id="hello" defaultMessage="Hello" />)
).toThrow();
});

it('should work w/ multiple context', function () {
Expand Down Expand Up @@ -181,7 +178,7 @@ describe('<FormattedMessage>', () => {

expect(spy).toHaveBeenCalledTimes(1);

expect(spy.mock.calls[0][0]).toBe(intl.formatMessage(descriptor));
expect(spy.mock.calls[0][0]).toEqual([intl.formatMessage(descriptor)]);

expect(rendered.text()).toBe('Jest');
});
Expand Down Expand Up @@ -228,7 +225,7 @@ describe('<FormattedMessage>', () => {
defaultMessage: 'Hello, <b>{name}<i>!</i></b>',
values: {
name: 'Jest',
b: (...chunks: any[]) => <b>{...chunks}</b>,
b: (chunks: any[]) => <b>{chunks}</b>,
i: (msg: string) => <i>{msg}</i>,
},
},
Expand Down Expand Up @@ -261,7 +258,7 @@ describe('<FormattedMessage>', () => {
values: {
name: <b>Jest</b>,
},
children: (...chunks) => <strong>{chunks}</strong>,
children: chunks => <strong>{chunks}</strong>,
},
providerProps
);
Expand Down
4 changes: 2 additions & 2 deletions packages/react-intl/test/unit/format.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -750,14 +750,14 @@ describe('format API', () => {
state
);
const {locale, messages} = config;
const values = {b: (...chunks) => <b>{chunks}</b>};
const values = {b: chunks => <b>{chunks}</b>};
expect(formatMessage({id: 'richText'}, values)).toMatchSnapshot();
});

it('formats rich text messages w/o wrapRichTextChunksInFragment', () => {
const {locale, messages} = config;
const mf = new IntlMessageFormat(messages.richText, locale);
const values = {b: (...chunks) => <b>{chunks}</b>};
const values = {b: chunks => <b>{chunks}</b>};
expect(formatMessage({id: 'richText'}, values)).toMatchSnapshot();
});

Expand Down
12 changes: 6 additions & 6 deletions website/docs/react-intl/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,7 @@ Hello, Eric!
description="Greeting to welcome the user to the app"
defaultMessage="Hello, <b>Eric</b> {icon}"
values={{
b: (...chunks) => <b>{chunks}</b>,
b: chunks => <b>{chunks}</b>,
icon: <svg />,
}}
/>
Expand All @@ -628,12 +628,12 @@ By allowing embedding XML tag we want to make sure contextual information is not
<FormattedMessage
defaultMessage="To buy a shoe, <a>visit our website</a> and <cta>buy a shoe</cta>"
values={{
a: (...chunks) => (
a: chunks => (
<a class="external_link" target="_blank" href="https://www.shoe.com/">
{chunks}
</a>
),
cta: (...chunks) => <strong class="important">{chunks}</strong>,
cta: chunks => <strong class="important">{chunks}</strong>,
}}
/>
```
Expand All @@ -647,15 +647,15 @@ Since rich text formatting allows embedding `ReactElement`, in function as the c
<FormattedMessage
defaultMessage="To buy a shoe, <a>visit our website</a> and <cta>buy a shoe</cta>"
values={{
a: (...chunks) => (
a: chunks => (
<a class="external_link" target="_blank" href="https://www.shoe.com/">
{chunks}
</a>
),
cta: (...chunks) => <strong class="important">{chunks}</strong>,
cta: chunks => <strong class="important">{chunks}</strong>,
}}
>
{(...chunks) => <span>{chunks}</span>}
{chunks => <span>{chunks}</span>}
</FormattedMessage>
```

Expand Down
2 changes: 1 addition & 1 deletion website/docs/react-intl/upgrade-guide-3.x.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ After
values={{
b: name => <b>{name}</b>,
custom: str => <span style="font-weight: bold;">{str}</span>,
more: (...chunks) => <span>{chunks}</span>,
more: chunks => <span>{chunks}</span>,
}}
/>
```
Expand Down
60 changes: 60 additions & 0 deletions website/docs/react-intl/upgrade-guide-5.x.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
id: upgrade-guide-4x
title: Upgrade Guide (v4 -> v5)
---

## Breaking API Changes

- Rich text formatting callback function is no longer variadic.

Before:

```tsx
new IntlMessageFormat('a<b>strong</b>').format({
b: (...chunks) => <strong>chunks</strong>,
});
```

After:

```tsx
new IntlMessageFormat('a<b>strong</b>').format({
b: (...chunks) => <strong>chunks</strong>,
});
```

- `FormattedMessage` render prop is no longer variadic.

Before:

```tsx
<FormattedMessage defaultMessage="a<b>strong</b>">
{(...chunks) => <b>{chunks}</b>}
</FormattedMessage>
```

After:

```tsx
<FormattedMessage defaultMessage="a<b>strong</b>">
{chunks => <b>{chunks}</b>}
</FormattedMessage>
```

- Using `FormattedMessage` without a `intl` context will fail fast.

## Why are we doing those changes?

### Rich text formatting callback function is no longer variadic

- We received feedback from the community that variadic callback function isn't really ergonomic.
- There's also an issue where React `chunks` do not come with keys, thus causing warning in React during development.
- The `chunks` by themselves are not enough to render duplicate tags, such as `<a>link</a> and another <a>link</a>` where you want to render 2 different `href`s for the `<a>` tag. In this case `a: chunks => <a>{chunks}</a>` isn't enough especially when the contents are the same. In the future we can set another argument that might contain metadata to distinguish between the 2 elements.

### `FormattedMessage` render prop is no longer variadic

- Same reasons as above.

### Using `FormattedMessage` without a `intl` context will fail fast

- This also comes from Dropbox internal developer feedback. `FormattedMessage` has a default English renderer that masks `Provider` setup issues which causes them to not be handled during testing phase.

0 comments on commit 42fa3c1

Please sign in to comment.