Skip to content

Commit

Permalink
fix: merge timeZone into formats when formatting message
Browse files Browse the repository at this point in the history
fix #1219
  • Loading branch information
Long Ho committed Sep 27, 2019
1 parent aefb68b commit aea3f56
Show file tree
Hide file tree
Showing 7 changed files with 229 additions and 69 deletions.
11 changes: 8 additions & 3 deletions src/components/html-message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,22 @@ class FormattedHTMLMessage extends FormattedMessage<
invariantIntlContext(intl);
}

const {formatHTMLMessage, textComponent: Text} = intl;
const {formatHTMLMessage, textComponent} = intl;
const {
id,
description,
defaultMessage,
values: rawValues,
// This is bc of TS3.3 doesn't recognize `defaultProps`
tagName: Component = Text || 'span',
children,
} = this.props;

let {tagName: Component} = this.props;

// This is bc of TS3.3 doesn't recognize `defaultProps`
if (!Component) {
Component = textComponent || 'span';
}

let descriptor = {id, description, defaultMessage};
let formattedHTMLMessage = formatHTMLMessage(descriptor, rawValues);

Expand Down
55 changes: 3 additions & 52 deletions src/components/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,66 +52,17 @@ export type OptionalIntlConfig = Omit<
> &
Partial<typeof DEFAULT_INTL_CONFIG>;

function setTimeZoneInOptions(
opts: Record<string, Intl.DateTimeFormatOptions>,
timeZone: string
) {
return Object.keys(opts).reduce(
(all: Record<string, Intl.DateTimeFormatOptions>, k) => {
all[k] = {
timeZone,
...opts[k],
};
return all;
},
{}
);
}

function processIntlConfig<P extends OptionalIntlConfig = OptionalIntlConfig>(
config: P
): OptionalIntlConfig {
let {formats, defaultFormats, timeZone} = config;
if (timeZone) {
if (formats) {
const {date: dateFormats, time: timeFormats} = formats;
if (dateFormats) {
formats = {
...formats,
date: setTimeZoneInOptions(dateFormats, timeZone),
};
}
if (timeFormats) {
formats = {
...formats,
time: setTimeZoneInOptions(timeFormats, timeZone),
};
}
}
if (defaultFormats) {
const {date: dateFormats, time: timeFormats} = defaultFormats;
if (dateFormats) {
defaultFormats = {
...defaultFormats,
date: setTimeZoneInOptions(dateFormats, timeZone),
};
}
if (timeFormats) {
defaultFormats = {
...defaultFormats,
time: setTimeZoneInOptions(timeFormats, timeZone),
};
}
}
}
return {
locale: config.locale,
timeZone,
formats,
timeZone: config.timeZone,
formats: config.formats,
textComponent: config.textComponent,
messages: config.messages,
defaultLocale: config.defaultLocale,
defaultFormats,
defaultFormats: config.defaultFormats,
onError: config.onError,
};
}
Expand Down
68 changes: 66 additions & 2 deletions src/formatters/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,70 @@ import * as React from 'react';
import * as invariant_ from 'invariant';
const invariant: typeof invariant_ = (invariant_ as any).default || invariant_;

import {Formatters, IntlConfig, MessageDescriptor} from '../types';
import {
Formatters,
IntlConfig,
MessageDescriptor,
CustomFormats,
} from '../types';

import {createError, escape} from '../utils';
import {FormatXMLElementFn, PrimitiveType} from 'intl-messageformat';
import IntlMessageFormat, {
FormatXMLElementFn,
PrimitiveType,
} from 'intl-messageformat';

function setTimeZoneInOptions(
opts: Record<string, Intl.DateTimeFormatOptions>,
timeZone: string
) {
return Object.keys(opts).reduce(
(all: Record<string, Intl.DateTimeFormatOptions>, k) => {
all[k] = {
timeZone,
...opts[k],
};
return all;
},
{}
);
}

function deepMergeOptions(
opts1: Record<string, Intl.DateTimeFormatOptions>,
opts2: Record<string, Intl.DateTimeFormatOptions>
) {
const keys = Object.keys({...opts1, ...opts2});
return keys.reduce((all: Record<string, Intl.DateTimeFormatOptions>, k) => {
all[k] = {
...(opts1[k] || {}),
...(opts2[k] || {}),
};
return all;
}, {});
}

function deepMergeFormatsAndSetTimeZone(
f1: CustomFormats,
timeZone?: string
): CustomFormats {
if (!timeZone) {
return {};
}
const mfFormats = IntlMessageFormat.formats;
return {
...mfFormats,
...f1,
date: deepMergeOptions(
setTimeZoneInOptions(mfFormats.date, timeZone),
setTimeZoneInOptions(f1.date || {}, timeZone)
),
time: deepMergeOptions(
setTimeZoneInOptions(mfFormats.time, timeZone),
setTimeZoneInOptions(f1.time || {}, timeZone)
),
};
}

export function formatMessage(
{
Expand Down Expand Up @@ -46,6 +106,7 @@ export function formatMessage(
defaultLocale,
defaultFormats,
onError,
timeZone,
}: Pick<
IntlConfig,
| 'locale'
Expand All @@ -54,6 +115,7 @@ export function formatMessage(
| 'defaultLocale'
| 'defaultFormats'
| 'onError'
| 'timeZone'
>,
state: Formatters,
messageDescriptor: MessageDescriptor = {id: ''},
Expand All @@ -68,6 +130,8 @@ export function formatMessage(
invariant(id, '[React Intl] An `id` must be provided to format a message.');

const message = messages && messages[id];
formats = deepMergeFormatsAndSetTimeZone(formats, timeZone);
defaultFormats = deepMergeFormatsAndSetTimeZone(defaultFormats, timeZone);

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

Expand Down
20 changes: 20 additions & 0 deletions test/unit/components/date.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,26 @@ describe('<FormattedDateParts>', () => {
);
});

it('renders a string date', () => {
const date = new Date();

mountPartsWithProvider({value: date + '', children}, intl);

expect(children.mock.calls[0][0]).toEqual(
intl.formatDateToParts(date)
);
});

it('renders date 0 if value is ""', () => {
const date = new Date(0);

mountPartsWithProvider({value: '', children}, intl);

expect(children.mock.calls[0][0]).toEqual(
intl.formatDateToParts(date)
);
});

it('accepts `format` prop', () => {
intl = createIntl({
locale: 'en',
Expand Down
43 changes: 43 additions & 0 deletions test/unit/components/html-message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';
import {mountFormattedComponentWithProvider} from '../testUtils';
import FormattedHTMLMessage from '../../../src/components/html-message';
import {createIntl} from '../../../src/components/provider';
import {mount} from 'enzyme'

const mountWithProvider = mountFormattedComponentWithProvider(
FormattedHTMLMessage
Expand All @@ -23,6 +24,48 @@ describe('<FormattedHTMLMessage>', () => {
expect(FormattedHTMLMessage.displayName).toBeA('string');
});

it('should throw if intl is not provided', function () {
expect(() => mount(<FormattedHTMLMessage id="foo"/>)).toThrow(/Could not find required `intl` object./)
})

it('should use textComponent if tagName is ""', function () {
intl = createIntl({
locale: 'en',
defaultLocale: 'en-US',
textComponent: 'p',
});
const descriptor = {
id: 'hello',
defaultMessage: 'Hello, <b>World</b>!',
tagName: ''
}

const rendered = mountWithProvider(descriptor, intl).find('p');

expect(rendered.prop('dangerouslySetInnerHTML')).toEqual({
__html: intl.formatHTMLMessage(descriptor),
});
})

it('should use span if textComponent & tagName is undefined', function () {
intl = createIntl({
locale: 'en',
defaultLocale: 'en-US',
textComponent: undefined,
});
const descriptor = {
id: 'hello',
defaultMessage: 'Hello, <b>World</b>!',
tagName: undefined
}

const rendered = mountWithProvider(descriptor, intl).find('span');

expect(rendered.prop('dangerouslySetInnerHTML')).toEqual({
__html: intl.formatHTMLMessage(descriptor),
});
})

it('renders a formatted HTML message in a <span>', () => {
const descriptor = {
id: 'hello',
Expand Down
81 changes: 69 additions & 12 deletions test/unit/components/message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,21 @@ describe('<FormattedMessage>', () => {
expect(rendered.text()).toBe(intl.formatMessage(descriptor));
});

it('should render out raw array if tagName is not specified', () => {
const descriptor = {
id: 'hello',
defaultMessage: 'Hello, World!',
tagName: ''
};

const rendered = mountWithProvider(
descriptor,
{...providerProps, textComponent: undefined}
);

expect(rendered.text()).toBe(intl.formatMessage(descriptor));
});

it('supports function-as-child pattern', () => {
const descriptor = {
id: 'hello',
Expand Down Expand Up @@ -244,6 +259,44 @@ describe('<FormattedMessage>', () => {
expect(nameNode.text()).toBe('Jest');
});
});
it('should use timeZone from Provider', function() {
const rendered = mountWithProvider(
{
id: 'hello',
values: {
ts: new Date(0),
},
},
{
...providerProps,
messages: {
hello: 'Hello, {ts, date, short} - {ts, time, short}',
},
timeZone: 'Asia/Tokyo',
}
);

expect(rendered.text()).toBe('Hello, 1/1/70 - 9:00 AM');
});

it('should use timeZone from Provider for defaultMessage', function() {
const rendered = mountWithProvider(
{
id: 'hello',
defaultMessage: 'Hello, {ts, date, short} - {ts, time, short}',
values: {
ts: new Date(0),
},
},
{
...providerProps,
timeZone: 'Asia/Tokyo',
}
);

expect(rendered.text()).toBe('Hello, 1/1/70 - 9:00 AM');
});

it('should merge timeZone into formats', function() {
const rendered = mountWithProvider(
{
Expand All @@ -259,18 +312,20 @@ describe('<FormattedMessage>', () => {
},
formats: {
time: {
short: {hour: 'numeric', minute: 'numeric', second: 'numeric'},
},
date: {short: {year: '2-digit', month: 'numeric', day: 'numeric'}},
} as CustomFormats,
timeZone: 'Europe/London',
short: {
second: 'numeric',
timeZoneName: 'long'
}
}
},
timeZone: 'Asia/Tokyo',
}
);

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

it('should merge timeZone into formats', function() {
it('should merge timeZone into defaultFormats', function() {
const rendered = mountWithProvider(
{
id: 'hello',
Expand All @@ -283,15 +338,17 @@ describe('<FormattedMessage>', () => {
...providerProps,
defaultFormats: {
time: {
short: {hour: 'numeric', minute: 'numeric', second: 'numeric'},
},
date: {short: {year: '2-digit', month: 'numeric', day: 'numeric'}},
} as CustomFormats,
short: {
second: 'numeric',
timeZoneName: 'long'
}
}
},
timeZone: 'Asia/Tokyo',
}
);

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

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

0 comments on commit aea3f56

Please sign in to comment.