Skip to content

Commit

Permalink
feat: add formatList & FormattedList (#1494)
Browse files Browse the repository at this point in the history
  • Loading branch information
longlho authored Nov 1, 2019
1 parent a098369 commit f5eacbf
Show file tree
Hide file tree
Showing 13 changed files with 198 additions and 10 deletions.
27 changes: 27 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ There are a few API layers that React Intl provides and is built on. When using
- [Number Formatting APIs](#number-formatting-apis)
- [`formatNumber`](#formatnumber)
- [`formatPlural`](#formatplural)
- [List Formatting APIs](#list-formatting-apis)
- [`formatList`](#formatlist)
- [String Formatting APIs](#string-formatting-apis)
- [Message Syntax](#message-syntax)
- [Message Descriptor](#message-descriptor)
Expand Down Expand Up @@ -393,6 +395,31 @@ formatPlural(4, {style: 'ordinal'}); // "other"

**Note:** This function should only be used in apps that only need to support one language. If your app supports multiple languages use [`formatMessage`](#formatmessage) instead.

### List Formatting APIs

**This is currently stage 3 so [polyfill](https://www.npmjs.com/package/@formatjs/intl-listformat) would be required.**

#### `formatList`

```ts
type ListFormatOptions = {
type?: 'disjunction' | 'conjunction' | 'unit';
style?: 'long' | 'short' | 'narrow';
};

function formatPlural(
elements: (string | React.ReactNode)[],
options?: Intl.ListFormatOptions
): string | React.ReactNode[];
```

This function allows you to join list of things together in an i18n-safe way. For example:

```tsx
formatList(['Me', 'myself', 'I'], {type: 'conjunction'}); // Me, myself and I
formatList(['5 hours', '3 minues'], {type: 'unit'}); // 5 hours, 3 minutes
```

### String Formatting APIs

React Intl provides two functions to format strings/messages:
Expand Down
35 changes: 35 additions & 0 deletions docs/Components.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ React Intl has a set of React components that provide a declarative way to setup
- [`FormattedNumber`](#formattednumber)
- [`FormattedNumberParts`](#formattednumberparts)
- [`FormattedPlural`](#formattedplural)
- [List Formatting Components](#list-formatting-components)
- [`FormattedList`](#formattedlist)
- [String Formatting Components](#string-formatting-components)
- [Message Syntax](#message-syntax)
- [Message Descriptor](#message-descriptor)
Expand Down Expand Up @@ -488,6 +490,39 @@ By default `<FormattedPlural>` will select a [plural category](http://www.unicod
messages
```

## List Formatting Components

### `FormattedList`

This component uses [`formatList`](API.md#formatlist) API and [Intl.ListFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ListFormat). Its props corresponds to `Intl.ListFormatOptions`.

**Props:**

```tsx
props: ListFormatOptions &
{
children: (chunksOrString: string | React.ReactElement[]) => ReactElement,
};
```

**Example:**

```tsx
<FormattedList type="conjunction" value={['Me', 'myself', 'I']} />
```

```html
Me, myself and I
```

```tsx
<FormattedList type="conjunction" value={['Me', <b>myself</b>, 'I']} />
```

```html
Me, <b>myself</b> and I
```

## String Formatting Components

React Intl provides two components to format strings:
Expand Down
14 changes: 11 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"types": "./dist/index.d.ts",
"sideEffects": false,
"dependencies": {
"@formatjs/intl-listformat": "^1.2.1",
"@formatjs/intl-relativetimeformat": "^4.2.1",
"@formatjs/intl-unified-numberformat": "^2.1.0",
"@types/hoist-non-react-statics": "^3.3.1",
Expand Down Expand Up @@ -87,7 +88,7 @@
"react": "^16.11.0",
"react-dom": "^16.11.0",
"rimraf": "^3.0.0",
"rollup": "^1.25.1",
"rollup": "^1.25.2",
"rollup-plugin-babel": "^4.3.3",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-node-resolve": "^5.2.0",
Expand Down
15 changes: 12 additions & 3 deletions src/components/createFormattedComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
import * as React from 'react';
import {invariantIntlContext} from '../utils';
import {IntlShape, FormatDateOptions, FormatNumberOptions} from '../types';
import {
IntlShape,
FormatDateOptions,
FormatNumberOptions,
FormatListOptions,
} from '../types';
import {Context} from './injectIntl';

enum DisplayName {
formatDate = 'FormattedDate',
formatTime = 'FormattedTime',
formatNumber = 'FormattedNumber',
formatList = 'FormattedList',
}

enum DisplayNameParts {
formatDate = 'FormattedDateParts',
formatTime = 'FormattedTimeParts',
formatNumber = 'FormattedNumberParts',
formatList = 'FormattedListParts',
}

type Formatter = {
formatDate: FormatDateOptions;
formatTime: FormatDateOptions;
formatNumber: FormatNumberOptions;
formatList: FormatListOptions;
};

export const FormattedNumberParts: React.FC<
Expand All @@ -39,7 +47,7 @@ export const FormattedNumberParts: React.FC<
FormattedNumberParts.displayName = 'FormattedNumberParts';

export function createFormattedDateTimePartsComponent<
Name extends keyof Formatter
Name extends 'formatDate' | 'formatTime'
>(name: Name) {
type FormatFn = IntlShape[Name];
type Props = Formatter[Name] & {
Expand Down Expand Up @@ -80,7 +88,8 @@ export function createFormattedComponent<Name extends keyof Formatter>(
{intl => {
invariantIntlContext(intl);
const {value, children, ...formatProps} = props;
const formattedValue = intl[name](value as any, formatProps);
// TODO: fix TS type definition for localeMatcher upstream
const formattedValue = intl[name](value as any, formatProps as any);

if (typeof children === 'function') {
return children(formattedValue as any);
Expand Down
2 changes: 2 additions & 0 deletions src/components/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
import {formatPlural} from '../formatters/plural';
import {formatMessage, formatHTMLMessage} from '../formatters/message';
import * as shallowEquals_ from 'shallow-equal/objects';
import {formatList} from '../formatters/list';
const shallowEquals: typeof shallowEquals_ =
(shallowEquals_ as any).default || shallowEquals_;

Expand Down Expand Up @@ -177,5 +178,6 @@ export function createIntl(
),
formatMessage: formatMessage.bind(null, resolvedConfig, formatters),
formatHTMLMessage: formatHTMLMessage.bind(null, resolvedConfig, formatters),
formatList: formatList.bind(null, resolvedConfig, formatters.getListFormat),
};
}
66 changes: 66 additions & 0 deletions src/formatters/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import * as React from 'react';
import {IntlConfig, Formatters, IntlFormatters} from '../types';
import {filterProps, createError} from '../utils';
import IntlListFormat, {IntlListFormatOptions} from '@formatjs/intl-listformat';

const LIST_FORMAT_OPTIONS: Array<keyof IntlListFormatOptions> = [
'localeMatcher',
'type',
'style',
];

const now = Date.now();

function generateToken(i: number) {
return `${now}_${i}_${now}`;
}

export function formatList(
{locale, onError}: Pick<IntlConfig, 'locale' | 'onError'>,
getListFormat: Formatters['getListFormat'],
values: Parameters<IntlFormatters['formatList']>[0],
options: Parameters<IntlFormatters['formatList']>[1] = {}
) {
const ListFormat: typeof IntlListFormat = (Intl as any).ListFormat;
if (!ListFormat) {
onError(
createError(`Intl.ListFormat is not available in this environment.
Try polyfilling it using "@formatjs/intl-listformat"
`)
);
}
let filteredOptions = filterProps(options, LIST_FORMAT_OPTIONS);

try {
const richValues: Record<string, React.ReactNode> = {};
const serializedValues = values.map((v, i) => {
if (typeof v === 'object') {
const id = generateToken(i);
richValues[id] = v;
return id;
}
return String(v);
});
if (!Object.keys(richValues).length) {
return getListFormat(locale, filteredOptions).format(serializedValues);
}
const parts = getListFormat(locale, filteredOptions).formatToParts(
serializedValues
);
return parts.reduce((all: Array<string | React.ReactNode>, el) => {
const val = el.value;
if (richValues[val]) {
all.push(richValues[val]);
} else if (typeof all[all.length - 1] === 'string') {
all[all.length - 1] += val;
} else {
all.push(val);
}
return all;
}, []);
} catch (e) {
onError(createError('Error formatting list.', e));
}

return values;
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export {default as IntlProvider, createIntl} from './components/provider';
export const FormattedDate = createFormattedComponent('formatDate');
export const FormattedTime = createFormattedComponent('formatTime');
export const FormattedNumber = createFormattedComponent('formatNumber');
export const FormattedList = createFormattedComponent('formatList');
export const FormattedDateParts = createFormattedDateTimePartsComponent(
'formatDate'
);
Expand Down
11 changes: 11 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import IntlRelativeTimeFormat, {
} from '@formatjs/intl-relativetimeformat';
import {MessageFormatElement} from 'intl-messageformat-parser';
import {UnifiedNumberFormatOptions} from '@formatjs/intl-unified-numberformat';
import IntlListFormat, {IntlListFormatOptions} from '@formatjs/intl-listformat';

export interface IntlConfig {
locale: string;
Expand Down Expand Up @@ -55,6 +56,8 @@ export type FormatPluralOptions = Exclude<
> &
CustomFormatConfig;

export type FormatListOptions = Exclude<IntlListFormatOptions, 'localeMatcher'>;

export interface IntlFormatters {
formatDate(
value: Parameters<Intl.DateTimeFormat['format']>[0] | string,
Expand Down Expand Up @@ -104,6 +107,10 @@ export interface IntlFormatters {
descriptor: MessageDescriptor,
values?: Record<string, PrimitiveType>
): string;
formatList(
values: Array<string | React.ReactNode>,
opts?: FormatListOptions
): string | Array<string | React.ReactNode>;
}

export interface Formatters {
Expand All @@ -122,6 +129,9 @@ export interface Formatters {
getPluralRules(
...args: ConstructorParameters<typeof Intl.PluralRules>
): Intl.PluralRules;
getListFormat(
...args: ConstructorParameters<typeof IntlListFormat>
): IntlListFormat;
}

export interface IntlShape extends IntlConfig, IntlFormatters {
Expand All @@ -134,6 +144,7 @@ export interface IntlCache {
message: Record<string, IntlMessageFormat>;
relativeTime: Record<string, IntlRelativeTimeFormat>;
pluralRules: Record<string, Intl.PluralRules>;
list: Record<string, IntlListFormat>;
}

export interface MessageDescriptor {
Expand Down
3 changes: 3 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export function createIntlCache(): IntlCache {
message: {},
relativeTime: {},
pluralRules: {},
list: {},
};
}

Expand All @@ -112,6 +113,7 @@ export function createIntlCache(): IntlCache {
*/
export function createFormatters(cache: IntlCache = createIntlCache()) {
const RelativeTimeFormat = (Intl as any).RelativeTimeFormat;
const ListFormat = (Intl as any).ListFormat;
return {
getDateTimeFormat: memoizeIntlConstructor(
Intl.DateTimeFormat,
Expand All @@ -124,6 +126,7 @@ export function createFormatters(cache: IntlCache = createIntlCache()) {
cache.relativeTime
),
getPluralRules: memoizeIntlConstructor(Intl.PluralRules, cache.pluralRules),
getListFormat: memoizeIntlConstructor(ListFormat, cache.list),
};
}

Expand Down
1 change: 1 addition & 0 deletions test/setup.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {configure} from 'enzyme';
import '@formatjs/intl-pluralrules/polyfill-locales';
import '@formatjs/intl-relativetimeformat/polyfill-locales';
import '@formatjs/intl-listformat/polyfill-locales';
import * as Adapter from 'enzyme-adapter-react-16';

configure({adapter: new Adapter()});
Expand Down
2 changes: 1 addition & 1 deletion test/unit/components/relative.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ describe('<FormattedRelativeTime>', () => {
expect(rendered.text()).toBe('0');
expect(console.error).toHaveBeenCalledWith(
expect.stringMatching(
/Error formatting relative time.\nRangeError: Invalid unit argument for (.*) 'invalid'/
/Error formatting relative time.\nRangeError: Invalid unit(.*)invalid/
)
);
expect(console.error).toHaveBeenCalledTimes(1);
Expand Down
Loading

0 comments on commit f5eacbf

Please sign in to comment.