Skip to content

Commit 10bcbe2

Browse files
authored
feat: add formatDisplayName and FormattedDisplayName (#1567)
* feat: add formatDisplayName and FormattedDisplayName Fixes #1547 * add docs
1 parent fb21186 commit 10bcbe2

17 files changed

+302
-8
lines changed

docs/API.md

+43
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ There are a few API layers that React Intl provides and is built on. When using
2828
- [Message Formatting Fallbacks](#message-formatting-fallbacks)
2929
- [`formatMessage`](#formatmessage)
3030
- [`formatHTMLMessage`](#formathtmlmessage)
31+
- [Localized Display Name APIs](#localized-display-name-apis)
32+
- [`formatDisplayName`](#formatdisplayname)
3133
- [React Intl Components](#react-intl-components)
3234

3335
<!-- tocstop -->
@@ -560,6 +562,47 @@ function formatHTMLMessage(
560562

561563
**Note:** This API is provided to format legacy string message that contain HTML, but is not recommended, use [`<FormattedMessage>`](./Components.md#formattedmessage) instead.
562564

565+
### Localized Display Name APIs
566+
567+
**This uses stage 3 [`Intl.DisplayNames`][displaynames-repo] API so [polyfill][displaynames-polyfill] is required.**
568+
569+
[displaynames-repo]: https://github.com/tc39/proposal-intl-displaynames
570+
[displaynames-polyfill]: https://www.npmjs.com/package/@formatjs/intl-displaynames
571+
572+
This API provides _localized_ display names for languages, scripts, regions, and currencies.
573+
574+
#### `formatDisplayName`
575+
576+
```ts
577+
type FormatDisplayNameOptions = {
578+
style?: 'narrow' | 'short' | 'long';
579+
type?: 'language' | 'region' | 'script' | 'currency';
580+
fallback?: 'code' | 'none';
581+
};
582+
583+
function formatDisplayName(
584+
value: string | number | object,
585+
options?: FormatDisplayNameOptions
586+
): string | undefined;
587+
```
588+
589+
Usage examples:
590+
591+
```ts
592+
// When locale is `en`
593+
formatDisplayName('zh-Hans-SG'); //=> Simplified Chinese (Singapore)
594+
// When locale is `zh`
595+
formatDisplayName('zh-Hans-SG'); //=> 简体中文(新加坡)
596+
597+
// When locale is `en`...
598+
// ISO-15924 four letters script code to localized display name
599+
formatDisplayName('Deva', {type: 'script'}); //=> Devanagari
600+
// ISO-4217 currency code to localized display name
601+
formatDisplayName('CNY', {type: 'currency'}); //=> Chinese Yuan
602+
// ISO-3166 two letters region code to localized display name
603+
formatDisplayName('UN', {type: 'region'}); //=> United Nations
604+
```
605+
563606
## React Intl Components
564607

565608
The React components provided by React Intl allow for a declarative, idiomatic-React way of providing internationalization configuration and format dates, numbers, and strings/messages in your app.

docs/Components.md

+42
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ React Intl has a set of React components that provide a declarative way to setup
3030
- [Caveats](#caveats)
3131
- [`FormattedHTMLMessage`](#formattedhtmlmessage)
3232
- [Using React-Intl with React Native](#using-react-intl-with-react-native)
33+
- [Localized Display Name Components](#localized-display-name-components)
34+
- [`FormattedDisplayName`](#formatteddisplayname)
3335

3436
<!-- tocstop -->
3537

@@ -710,3 +712,43 @@ This component uses the [`formatHTMLMessage`](API.md#formathtmlmessage) API and
710712
Historically, it was required to provide a `textComponent` for React-Intl to work on React Native, because Fragments didn't exist at the time and React Native would break trying to render a `span` (the default `textComponent` in React-Intl V2).
711713

712714
Starting with [React Native v0.52](https://github.com/react-native-community/releases/blob/master/CHANGELOG.md#0520---2018-01-07), which uses [React v16.2+](https://reactjs.org/blog/2017/11/28/react-v16.2.0-fragment-support.html), Fragments are supported. And since React-Intl V3's default `textComponent` is `<React.Fragment>`, such requirement no longer exists.
715+
716+
## Localized Display Name Components
717+
718+
### `FormattedDisplayName`
719+
720+
This component uses [`formatDisplayName`][formatdisplayname] and [`Intl.DisplayName`][intl-displayname]
721+
has `props` that correspond to `DisplayNameOptions`. You might need a [polyfill][displaynames-polyfill].
722+
723+
[formatdisplayname]: API.md#formatdisplayname
724+
[intl-displayname]: https://github.com/tc39/proposal-intl-displaynames
725+
[displaynames-polyfill]: https://www.npmjs.com/package/@formatjs/intl-displaynames
726+
727+
**Props:**
728+
729+
```ts
730+
props: FormatDisplayNameOptions &
731+
{
732+
value: string | number | object,
733+
};
734+
```
735+
736+
**Example:**
737+
738+
When the locale is `en`:
739+
740+
```tsx
741+
<FormattedDisplayName type="language" value="zh-Hans-SG" />
742+
```
743+
744+
```html
745+
Simplified Chinese (Singapore)
746+
```
747+
748+
```tsx
749+
<FormattedDisplayName type="currency" value="JPY" />
750+
```
751+
752+
```html
753+
Japanese Yen
754+
```

docs/Getting-Started.md

+16
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ React Intl relies on these `Intl` APIs:
5151
- [Intl.DateTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat): Available on IE11+
5252
- [Intl.PluralRules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/PluralRules): This can be polyfilled using [this package](https://www.npmjs.com/package/@formatjs/intl-pluralrules).
5353
- [Intl.RelativeTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RelativeTimeFormat): This can be polyfilled using [this package](https://www.npmjs.com/package/@formatjs/intl-relativetimeformat).
54+
- (Optional) [Intl.DisplayNames][displaynames-spec]: Required if you use [`formatDisplayName`](API.md#formatdisplayname)
55+
or [`FormattedDisplayName`](Components.md#formatteddisplayname). This can be polyfilled using [this package][displaynames-polyfill].
56+
57+
[displaynames-spec]: https://tc39.es/proposal-intl-displaynames/
58+
[displaynames-polyfill]: https://www.npmjs.com/package/@formatjs/intl-displaynames
5459

5560
If you need to support older browsers, we recommend you do the following:
5661

@@ -74,6 +79,16 @@ if (!Intl.RelativeTimeFormat) {
7479
}
7580
```
7681

82+
5. If you need `Intl.DisplayNames`, include this [polyfill][displaynames-polyfill] in your build along
83+
with individual CLDR data for each locale you support.
84+
85+
```js
86+
if (!Intl.DisplayNames) {
87+
require('@formatjs/intl-displaynames/polyfill');
88+
require('@formatjs/intl-displaynames/dist/locale-data/de'); // Add locale data for de
89+
}
90+
```
91+
7792
### Browser
7893

7994
We officially support IE11 along with 2 most recent versions of Edge, Chrome & Firefox.
@@ -118,6 +133,7 @@ If you cannot use the Intl variant of JSC (e.g on iOS), follow the instructions
118133
FormatJS also provides types & polyfill for the following Stage 3 Intl APIs:
119134

120135
- Unified NumberFormat: [polyfill](https://www.npmjs.com/package/@formatjs/intl-unified-numberformat) & [spec](https://github.com/tc39/proposal-unified-intl-numberformat)
136+
- DisplayNames: [polyfill][displaynames-polyfill] & [spec][displaynames-spec]
121137

122138
## The `react-intl` Package
123139

package-lock.json

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

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@
131131
"types": "./lib/react-intl.d.ts",
132132
"sideEffects": false,
133133
"dependencies": {
134+
"@formatjs/intl-displaynames": "^1.2.0",
134135
"@formatjs/intl-listformat": "^1.3.7",
135136
"@formatjs/intl-relativetimeformat": "^4.5.7",
136137
"@formatjs/intl-unified-numberformat": "^3.0.4",

src/components/createFormattedComponent.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
FormatDateOptions,
66
FormatNumberOptions,
77
FormatListOptions,
8+
FormatDisplayNameOptions,
89
} from '../types';
910
import {Context} from './injectIntl';
1011

@@ -13,6 +14,9 @@ enum DisplayName {
1314
formatTime = 'FormattedTime',
1415
formatNumber = 'FormattedNumber',
1516
formatList = 'FormattedList',
17+
// Note that this DisplayName is the locale display name, not to be confused with
18+
// the name of the enum, which is for React component display name in dev tools.
19+
formatDisplayName = 'FormattedDisplayName',
1620
}
1721

1822
enum DisplayNameParts {
@@ -27,6 +31,7 @@ type Formatter = {
2731
formatTime: FormatDateOptions;
2832
formatNumber: FormatNumberOptions;
2933
formatList: FormatListOptions;
34+
formatDisplayName: FormatDisplayNameOptions;
3035
};
3136

3237
export const FormattedNumberParts: React.FC<Formatter['formatNumber'] & {

src/components/provider.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {formatPlural} from '../formatters/plural';
2727
import {formatMessage, formatHTMLMessage} from '../formatters/message';
2828
import * as shallowEquals_ from 'shallow-equal/objects';
2929
import {formatList} from '../formatters/list';
30+
import {formatDisplayName} from '../formatters/displayName';
3031
const shallowEquals: typeof shallowEquals_ =
3132
(shallowEquals_ as any).default || shallowEquals_;
3233

@@ -146,6 +147,11 @@ export function createIntl(
146147
formatMessage: formatMessage.bind(null, resolvedConfig, formatters),
147148
formatHTMLMessage: formatHTMLMessage.bind(null, resolvedConfig, formatters),
148149
formatList: formatList.bind(null, resolvedConfig, formatters.getListFormat),
150+
formatDisplayName: formatDisplayName.bind(
151+
null,
152+
resolvedConfig,
153+
formatters.getDisplayNames
154+
),
149155
};
150156
}
151157

src/formatters/displayName.ts

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import {IntlConfig, Formatters, IntlFormatters} from '../types';
2+
import {filterProps, createError} from '../utils';
3+
import {
4+
DisplayNamesOptions,
5+
DisplayNames as IntlDisplayNames,
6+
} from '@formatjs/intl-displaynames';
7+
8+
const DISPLAY_NAMES_OPTONS: Array<keyof DisplayNamesOptions> = [
9+
'localeMatcher',
10+
'style',
11+
'type',
12+
'fallback',
13+
];
14+
15+
export function formatDisplayName(
16+
{locale, onError}: Pick<IntlConfig, 'locale' | 'onError'>,
17+
getDisplayNames: Formatters['getDisplayNames'],
18+
value: Parameters<IntlFormatters['formatDisplayName']>[0],
19+
options: Parameters<IntlFormatters['formatDisplayName']>[1] = {}
20+
): string | undefined {
21+
const DisplayNames: typeof IntlDisplayNames = (Intl as any).DisplayNames;
22+
if (!DisplayNames) {
23+
onError(
24+
createError(`Intl.DisplayNames is not available in this environment.
25+
Try polyfilling it using "@formatjs/intl-displaynames"
26+
`)
27+
);
28+
}
29+
const filteredOptions = filterProps(options, DISPLAY_NAMES_OPTONS);
30+
try {
31+
return getDisplayNames(locale, filteredOptions).of(value);
32+
} catch (e) {
33+
onError(createError('Error formatting display name.', e));
34+
}
35+
}

src/formatters/message.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export function formatMessage(
127127

128128
// `id` is a required field of a Message Descriptor.
129129
invariant(!!id, '[React Intl] An `id` must be provided to format a message.');
130-
const message = messages && messages[String(id)]
130+
const message = messages && messages[String(id)];
131131
formats = deepMergeFormatsAndSetTimeZone(formats, timeZone);
132132
defaultFormats = deepMergeFormatsAndSetTimeZone(defaultFormats, timeZone);
133133

src/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
import {CustomFormatConfig} from './types';
1414
import {UnifiedNumberFormatOptions} from '@formatjs/intl-unified-numberformat';
1515
import {IntlListFormatOptions} from '@formatjs/intl-listformat';
16+
import {DisplayNamesOptions} from '@formatjs/intl-displaynames/lib';
1617
export {
1718
default as injectIntl,
1819
Provider as RawIntlProvider,
@@ -38,6 +39,9 @@ export const FormattedNumber: React.FC<UnifiedNumberFormatOptions &
3839
export const FormattedList: React.FC<IntlListFormatOptions & {
3940
value: React.ReactNode[];
4041
}> = createFormattedComponent('formatList');
42+
export const FormattedDisplayName: React.FC<DisplayNamesOptions & {
43+
value: string | number | object;
44+
}> = createFormattedComponent('formatDisplayName');
4145
export const FormattedDateParts = createFormattedDateTimePartsComponent(
4246
'formatDate'
4347
);

src/types.ts

+15-4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import IntlRelativeTimeFormat, {
1515
import {MessageFormatElement} from 'intl-messageformat-parser';
1616
import {UnifiedNumberFormatOptions} from '@formatjs/intl-unified-numberformat';
1717
import IntlListFormat, {IntlListFormatOptions} from '@formatjs/intl-listformat';
18+
import {DisplayNames, DisplayNamesOptions} from '@formatjs/intl-displaynames';
1819

1920
export interface IntlConfig {
2021
locale: string;
@@ -58,6 +59,11 @@ export type FormatPluralOptions = Exclude<
5859

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

62+
export type FormatDisplayNameOptions = Exclude<
63+
DisplayNamesOptions,
64+
'localeMatcher'
65+
>;
66+
6167
export interface IntlFormatters {
6268
formatDate(
6369
value: Parameters<Intl.DateTimeFormat['format']>[0] | string,
@@ -107,14 +113,15 @@ export interface IntlFormatters {
107113
descriptor: MessageDescriptor,
108114
values?: Record<string, PrimitiveType>
109115
): React.ReactNode;
110-
formatList(
111-
values: Array<string>,
112-
opts?: FormatListOptions
113-
): string;
116+
formatList(values: Array<string>, opts?: FormatListOptions): string;
114117
formatList(
115118
values: Array<string | React.ReactNode>,
116119
opts?: FormatListOptions
117120
): React.ReactNode;
121+
formatDisplayName(
122+
value: Parameters<DisplayNames['of']>[0],
123+
opts?: FormatDisplayNameOptions
124+
): string | undefined;
118125
}
119126

120127
export interface Formatters {
@@ -136,6 +143,9 @@ export interface Formatters {
136143
getListFormat(
137144
...args: ConstructorParameters<typeof IntlListFormat>
138145
): IntlListFormat;
146+
getDisplayNames(
147+
...args: ConstructorParameters<typeof DisplayNames>
148+
): DisplayNames;
139149
}
140150

141151
export interface IntlShape extends IntlConfig, IntlFormatters {
@@ -149,6 +159,7 @@ export interface IntlCache {
149159
relativeTime: Record<string, IntlRelativeTimeFormat>;
150160
pluralRules: Record<string, Intl.PluralRules>;
151161
list: Record<string, IntlListFormat>;
162+
displayNames: Record<string, DisplayNames>;
152163
}
153164

154165
export interface MessageDescriptor {

src/utils.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {IntlConfig, IntlCache, CustomFormats, Formatters} from './types';
1313
import * as React from 'react';
1414
import IntlMessageFormat from 'intl-messageformat';
1515
import memoizeIntlConstructor from 'intl-format-cache';
16-
import {invariant} from '@formatjs/intl-utils'
16+
import {invariant} from '@formatjs/intl-utils';
1717
import {IntlRelativeTimeFormatOptions} from '@formatjs/intl-relativetimeformat';
1818

1919
const ESCAPED_CHARS: Record<number, string> = {
@@ -97,6 +97,7 @@ export function createIntlCache(): IntlCache {
9797
relativeTime: {},
9898
pluralRules: {},
9999
list: {},
100+
displayNames: {},
100101
};
101102
}
102103

@@ -109,6 +110,7 @@ export function createFormatters(
109110
): Formatters {
110111
const RelativeTimeFormat = (Intl as any).RelativeTimeFormat;
111112
const ListFormat = (Intl as any).ListFormat;
113+
const DisplayNames = (Intl as any).DisplayNames;
112114
return {
113115
getDateTimeFormat: memoizeIntlConstructor(
114116
Intl.DateTimeFormat,
@@ -122,6 +124,7 @@ export function createFormatters(
122124
),
123125
getPluralRules: memoizeIntlConstructor(Intl.PluralRules, cache.pluralRules),
124126
getListFormat: memoizeIntlConstructor(ListFormat, cache.list),
127+
getDisplayNames: memoizeIntlConstructor(DisplayNames, cache.displayNames),
125128
};
126129
}
127130

test/setup.js

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {configure} from 'enzyme';
22
import '@formatjs/intl-pluralrules/polyfill-locales';
33
import '@formatjs/intl-relativetimeformat/polyfill-locales';
44
import '@formatjs/intl-listformat/polyfill-locales';
5+
import '@formatjs/intl-displaynames/polyfill-locales';
56
import * as Adapter from 'enzyme-adapter-react-16';
67

78
configure({adapter: new Adapter()});

0 commit comments

Comments
 (0)