Skip to content

Commit aea3f56

Browse files
author
Long Ho
committed
fix: merge timeZone into formats when formatting message
fix #1219
1 parent aefb68b commit aea3f56

File tree

7 files changed

+229
-69
lines changed

7 files changed

+229
-69
lines changed

src/components/html-message.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,22 @@ class FormattedHTMLMessage extends FormattedMessage<
2626
invariantIntlContext(intl);
2727
}
2828

29-
const {formatHTMLMessage, textComponent: Text} = intl;
29+
const {formatHTMLMessage, textComponent} = intl;
3030
const {
3131
id,
3232
description,
3333
defaultMessage,
3434
values: rawValues,
35-
// This is bc of TS3.3 doesn't recognize `defaultProps`
36-
tagName: Component = Text || 'span',
3735
children,
3836
} = this.props;
3937

38+
let {tagName: Component} = this.props;
39+
40+
// This is bc of TS3.3 doesn't recognize `defaultProps`
41+
if (!Component) {
42+
Component = textComponent || 'span';
43+
}
44+
4045
let descriptor = {id, description, defaultMessage};
4146
let formattedHTMLMessage = formatHTMLMessage(descriptor, rawValues);
4247

src/components/provider.tsx

Lines changed: 3 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -52,66 +52,17 @@ export type OptionalIntlConfig = Omit<
5252
> &
5353
Partial<typeof DEFAULT_INTL_CONFIG>;
5454

55-
function setTimeZoneInOptions(
56-
opts: Record<string, Intl.DateTimeFormatOptions>,
57-
timeZone: string
58-
) {
59-
return Object.keys(opts).reduce(
60-
(all: Record<string, Intl.DateTimeFormatOptions>, k) => {
61-
all[k] = {
62-
timeZone,
63-
...opts[k],
64-
};
65-
return all;
66-
},
67-
{}
68-
);
69-
}
70-
7155
function processIntlConfig<P extends OptionalIntlConfig = OptionalIntlConfig>(
7256
config: P
7357
): OptionalIntlConfig {
74-
let {formats, defaultFormats, timeZone} = config;
75-
if (timeZone) {
76-
if (formats) {
77-
const {date: dateFormats, time: timeFormats} = formats;
78-
if (dateFormats) {
79-
formats = {
80-
...formats,
81-
date: setTimeZoneInOptions(dateFormats, timeZone),
82-
};
83-
}
84-
if (timeFormats) {
85-
formats = {
86-
...formats,
87-
time: setTimeZoneInOptions(timeFormats, timeZone),
88-
};
89-
}
90-
}
91-
if (defaultFormats) {
92-
const {date: dateFormats, time: timeFormats} = defaultFormats;
93-
if (dateFormats) {
94-
defaultFormats = {
95-
...defaultFormats,
96-
date: setTimeZoneInOptions(dateFormats, timeZone),
97-
};
98-
}
99-
if (timeFormats) {
100-
defaultFormats = {
101-
...defaultFormats,
102-
time: setTimeZoneInOptions(timeFormats, timeZone),
103-
};
104-
}
105-
}
106-
}
10758
return {
10859
locale: config.locale,
109-
timeZone,
110-
formats,
60+
timeZone: config.timeZone,
61+
formats: config.formats,
11162
textComponent: config.textComponent,
11263
messages: config.messages,
11364
defaultLocale: config.defaultLocale,
114-
defaultFormats,
65+
defaultFormats: config.defaultFormats,
11566
onError: config.onError,
11667
};
11768
}

src/formatters/message.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,70 @@ import * as React from 'react';
1212
import * as invariant_ from 'invariant';
1313
const invariant: typeof invariant_ = (invariant_ as any).default || invariant_;
1414

15-
import {Formatters, IntlConfig, MessageDescriptor} from '../types';
15+
import {
16+
Formatters,
17+
IntlConfig,
18+
MessageDescriptor,
19+
CustomFormats,
20+
} from '../types';
1621

1722
import {createError, escape} from '../utils';
18-
import {FormatXMLElementFn, PrimitiveType} from 'intl-messageformat';
23+
import IntlMessageFormat, {
24+
FormatXMLElementFn,
25+
PrimitiveType,
26+
} from 'intl-messageformat';
27+
28+
function setTimeZoneInOptions(
29+
opts: Record<string, Intl.DateTimeFormatOptions>,
30+
timeZone: string
31+
) {
32+
return Object.keys(opts).reduce(
33+
(all: Record<string, Intl.DateTimeFormatOptions>, k) => {
34+
all[k] = {
35+
timeZone,
36+
...opts[k],
37+
};
38+
return all;
39+
},
40+
{}
41+
);
42+
}
43+
44+
function deepMergeOptions(
45+
opts1: Record<string, Intl.DateTimeFormatOptions>,
46+
opts2: Record<string, Intl.DateTimeFormatOptions>
47+
) {
48+
const keys = Object.keys({...opts1, ...opts2});
49+
return keys.reduce((all: Record<string, Intl.DateTimeFormatOptions>, k) => {
50+
all[k] = {
51+
...(opts1[k] || {}),
52+
...(opts2[k] || {}),
53+
};
54+
return all;
55+
}, {});
56+
}
57+
58+
function deepMergeFormatsAndSetTimeZone(
59+
f1: CustomFormats,
60+
timeZone?: string
61+
): CustomFormats {
62+
if (!timeZone) {
63+
return {};
64+
}
65+
const mfFormats = IntlMessageFormat.formats;
66+
return {
67+
...mfFormats,
68+
...f1,
69+
date: deepMergeOptions(
70+
setTimeZoneInOptions(mfFormats.date, timeZone),
71+
setTimeZoneInOptions(f1.date || {}, timeZone)
72+
),
73+
time: deepMergeOptions(
74+
setTimeZoneInOptions(mfFormats.time, timeZone),
75+
setTimeZoneInOptions(f1.time || {}, timeZone)
76+
),
77+
};
78+
}
1979

2080
export function formatMessage(
2181
{
@@ -46,6 +106,7 @@ export function formatMessage(
46106
defaultLocale,
47107
defaultFormats,
48108
onError,
109+
timeZone,
49110
}: Pick<
50111
IntlConfig,
51112
| 'locale'
@@ -54,6 +115,7 @@ export function formatMessage(
54115
| 'defaultLocale'
55116
| 'defaultFormats'
56117
| 'onError'
118+
| 'timeZone'
57119
>,
58120
state: Formatters,
59121
messageDescriptor: MessageDescriptor = {id: ''},
@@ -68,6 +130,8 @@ export function formatMessage(
68130
invariant(id, '[React Intl] An `id` must be provided to format a message.');
69131

70132
const message = messages && messages[id];
133+
formats = deepMergeFormatsAndSetTimeZone(formats, timeZone);
134+
defaultFormats = deepMergeFormatsAndSetTimeZone(defaultFormats, timeZone);
71135

72136
let formattedMessageParts: Array<string | object> = [];
73137

test/unit/components/date.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,26 @@ describe('<FormattedDateParts>', () => {
182182
);
183183
});
184184

185+
it('renders a string date', () => {
186+
const date = new Date();
187+
188+
mountPartsWithProvider({value: date + '', children}, intl);
189+
190+
expect(children.mock.calls[0][0]).toEqual(
191+
intl.formatDateToParts(date)
192+
);
193+
});
194+
195+
it('renders date 0 if value is ""', () => {
196+
const date = new Date(0);
197+
198+
mountPartsWithProvider({value: '', children}, intl);
199+
200+
expect(children.mock.calls[0][0]).toEqual(
201+
intl.formatDateToParts(date)
202+
);
203+
});
204+
185205
it('accepts `format` prop', () => {
186206
intl = createIntl({
187207
locale: 'en',

test/unit/components/html-message.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as React from 'react';
22
import {mountFormattedComponentWithProvider} from '../testUtils';
33
import FormattedHTMLMessage from '../../../src/components/html-message';
44
import {createIntl} from '../../../src/components/provider';
5+
import {mount} from 'enzyme'
56

67
const mountWithProvider = mountFormattedComponentWithProvider(
78
FormattedHTMLMessage
@@ -23,6 +24,48 @@ describe('<FormattedHTMLMessage>', () => {
2324
expect(FormattedHTMLMessage.displayName).toBeA('string');
2425
});
2526

27+
it('should throw if intl is not provided', function () {
28+
expect(() => mount(<FormattedHTMLMessage id="foo"/>)).toThrow(/Could not find required `intl` object./)
29+
})
30+
31+
it('should use textComponent if tagName is ""', function () {
32+
intl = createIntl({
33+
locale: 'en',
34+
defaultLocale: 'en-US',
35+
textComponent: 'p',
36+
});
37+
const descriptor = {
38+
id: 'hello',
39+
defaultMessage: 'Hello, <b>World</b>!',
40+
tagName: ''
41+
}
42+
43+
const rendered = mountWithProvider(descriptor, intl).find('p');
44+
45+
expect(rendered.prop('dangerouslySetInnerHTML')).toEqual({
46+
__html: intl.formatHTMLMessage(descriptor),
47+
});
48+
})
49+
50+
it('should use span if textComponent & tagName is undefined', function () {
51+
intl = createIntl({
52+
locale: 'en',
53+
defaultLocale: 'en-US',
54+
textComponent: undefined,
55+
});
56+
const descriptor = {
57+
id: 'hello',
58+
defaultMessage: 'Hello, <b>World</b>!',
59+
tagName: undefined
60+
}
61+
62+
const rendered = mountWithProvider(descriptor, intl).find('span');
63+
64+
expect(rendered.prop('dangerouslySetInnerHTML')).toEqual({
65+
__html: intl.formatHTMLMessage(descriptor),
66+
});
67+
})
68+
2669
it('renders a formatted HTML message in a <span>', () => {
2770
const descriptor = {
2871
id: 'hello',

test/unit/components/message.tsx

Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,21 @@ describe('<FormattedMessage>', () => {
156156
expect(rendered.text()).toBe(intl.formatMessage(descriptor));
157157
});
158158

159+
it('should render out raw array if tagName is not specified', () => {
160+
const descriptor = {
161+
id: 'hello',
162+
defaultMessage: 'Hello, World!',
163+
tagName: ''
164+
};
165+
166+
const rendered = mountWithProvider(
167+
descriptor,
168+
{...providerProps, textComponent: undefined}
169+
);
170+
171+
expect(rendered.text()).toBe(intl.formatMessage(descriptor));
172+
});
173+
159174
it('supports function-as-child pattern', () => {
160175
const descriptor = {
161176
id: 'hello',
@@ -244,6 +259,44 @@ describe('<FormattedMessage>', () => {
244259
expect(nameNode.text()).toBe('Jest');
245260
});
246261
});
262+
it('should use timeZone from Provider', function() {
263+
const rendered = mountWithProvider(
264+
{
265+
id: 'hello',
266+
values: {
267+
ts: new Date(0),
268+
},
269+
},
270+
{
271+
...providerProps,
272+
messages: {
273+
hello: 'Hello, {ts, date, short} - {ts, time, short}',
274+
},
275+
timeZone: 'Asia/Tokyo',
276+
}
277+
);
278+
279+
expect(rendered.text()).toBe('Hello, 1/1/70 - 9:00 AM');
280+
});
281+
282+
it('should use timeZone from Provider for defaultMessage', function() {
283+
const rendered = mountWithProvider(
284+
{
285+
id: 'hello',
286+
defaultMessage: 'Hello, {ts, date, short} - {ts, time, short}',
287+
values: {
288+
ts: new Date(0),
289+
},
290+
},
291+
{
292+
...providerProps,
293+
timeZone: 'Asia/Tokyo',
294+
}
295+
);
296+
297+
expect(rendered.text()).toBe('Hello, 1/1/70 - 9:00 AM');
298+
});
299+
247300
it('should merge timeZone into formats', function() {
248301
const rendered = mountWithProvider(
249302
{
@@ -259,18 +312,20 @@ describe('<FormattedMessage>', () => {
259312
},
260313
formats: {
261314
time: {
262-
short: {hour: 'numeric', minute: 'numeric', second: 'numeric'},
263-
},
264-
date: {short: {year: '2-digit', month: 'numeric', day: 'numeric'}},
265-
} as CustomFormats,
266-
timeZone: 'Europe/London',
315+
short: {
316+
second: 'numeric',
317+
timeZoneName: 'long'
318+
}
319+
}
320+
},
321+
timeZone: 'Asia/Tokyo',
267322
}
268323
);
269324

270-
expect(rendered.text()).toBe('Hello, 1/1/70 - 1:00:00 AM');
325+
expect(rendered.text()).toBe('Hello, 1/1/70 - 9:00:00 AM Japan Standard Time');
271326
});
272327

273-
it('should merge timeZone into formats', function() {
328+
it('should merge timeZone into defaultFormats', function() {
274329
const rendered = mountWithProvider(
275330
{
276331
id: 'hello',
@@ -283,15 +338,17 @@ describe('<FormattedMessage>', () => {
283338
...providerProps,
284339
defaultFormats: {
285340
time: {
286-
short: {hour: 'numeric', minute: 'numeric', second: 'numeric'},
287-
},
288-
date: {short: {year: '2-digit', month: 'numeric', day: 'numeric'}},
289-
} as CustomFormats,
341+
short: {
342+
second: 'numeric',
343+
timeZoneName: 'long'
344+
}
345+
}
346+
},
290347
timeZone: 'Asia/Tokyo',
291348
}
292349
);
293350

294-
expect(rendered.text()).toBe('Hello, 1/1/70 - 9:00:00 AM');
351+
expect(rendered.text()).toBe('Hello, 1/1/70 - 9:00:00 AM Japan Standard Time');
295352
});
296353

297354
it('should re-render when `values` are different', () => {

0 commit comments

Comments
 (0)