Skip to content

Commit f2963bf

Browse files
czabajLong Ho
authored and
Long Ho
committed
feat(intl-messageformat): make FormatXMLElementFn non-variadic
BREAKING CHANGE: This effectively change the signature for formatter function from `(...chunks) => any` to `(chunks) => any`. This solves a couple of issues: 1. We received user feedback that variadic function is not as ergonomic 2. Right now there's not way to distinguish between 2 chunks that have the same tag, e.g `<b>on</b> and <b>on</b>`. The function would receive 2 chunks that are identical. By consoliding to the 1st param we can reserve additional params to provide mode metadata in the future
1 parent 1b5892f commit f2963bf

File tree

4 files changed

+39
-47
lines changed

4 files changed

+39
-47
lines changed

packages/intl-messageformat/src/formatters.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ function mergeLiteral<T>(
8686
}, [] as MessageFormatPart<T>[]);
8787
}
8888

89-
function isFormatXMLElementFn<T>(
89+
export function isFormatXMLElementFn<T>(
9090
el: PrimitiveType | T | FormatXMLElementFn<T>
9191
): el is FormatXMLElementFn<T> {
9292
return typeof el === 'function';
@@ -218,7 +218,7 @@ export function formatToParts<T>(
218218
values,
219219
currentPluralValue
220220
);
221-
let chunks = formatFn(...parts.map(p => p.value));
221+
let chunks = formatFn(parts.map(p => p.value));
222222
if (!Array.isArray(chunks)) {
223223
chunks = [chunks];
224224
}
@@ -291,5 +291,5 @@ Try polyfilling it using "@formatjs/intl-pluralrules"
291291
}
292292

293293
export type FormatXMLElementFn<T, R = string | Array<string | T>> = (
294-
...args: Array<string | T>
294+
parts: Array<string | T>
295295
) => R;

packages/intl-messageformat/tests/index.test.ts

+18-18
Original file line numberDiff line numberDiff line change
@@ -573,8 +573,8 @@ describe('IntlMessageFormat', function () {
573573
it('simple message', function () {
574574
const mf = new IntlMessageFormat('hello <b>world</b>', 'en');
575575
expect(
576-
mf.format<object>({b: str => ({str})})
577-
).toEqual(['hello ', {str: 'world'}]);
576+
mf.format<object>({b: parts => ({parts})})
577+
).toEqual(['hello ', {parts: ['world']}]);
578578
});
579579
it('nested tag message', function () {
580580
const mf = new IntlMessageFormat(
@@ -583,7 +583,7 @@ describe('IntlMessageFormat', function () {
583583
);
584584
expect(
585585
mf.format<object>({
586-
b: (...chunks) => ({chunks}),
586+
b: chunks => ({chunks}),
587587
i: c => ({val: `$$${c}$$`}),
588588
})
589589
).toEqual(['hello ', {chunks: ['world', {val: '$$!$$'}, ' <br/> ']}]);
@@ -595,7 +595,7 @@ describe('IntlMessageFormat', function () {
595595
);
596596
expect(
597597
mf.format<object>({
598-
b: (...chunks) => ['<b>', ...chunks, '</b>'],
598+
b: chunks => ['<b>', ...chunks, '</b>'],
599599
i: c => ({val: `$$${c}$$`}),
600600
})
601601
).toEqual(['hello <b>world', {val: '$$!$$'}, ' <br/> </b>']);
@@ -616,20 +616,20 @@ describe('IntlMessageFormat', function () {
616616
);
617617
expect(
618618
mf.format<object>({
619-
b: str => ({str}),
619+
b: parts => ({parts}),
620620
placeholder: 'gaga',
621-
a: str => ({str}),
621+
a: parts => ({parts}),
622622
})
623-
).toEqual(['hello ', {str: 'world'}, ' ', {str: 'gaga'}]);
623+
).toEqual(['hello ', {parts: ['world']}, ' ', {parts: ['gaga']}]);
624624
});
625625
it('message w/ placeholder & HTML entities', function () {
626626
const mf = new IntlMessageFormat('Hello&lt;<tag>{text}</tag>', 'en');
627627
expect(
628628
mf.format<object>({
629-
tag: str => ({str}),
629+
tag: parts => ({parts}),
630630
text: '<asd>',
631631
})
632-
).toEqual(['Hello&lt;', {str: '<asd>'}]);
632+
).toEqual(['Hello&lt;', {parts: ['<asd>']}]);
633633
});
634634
it('message w/ placeholder & >', function () {
635635
const mf = new IntlMessageFormat(
@@ -638,16 +638,16 @@ describe('IntlMessageFormat', function () {
638638
);
639639
expect(
640640
mf.format<object>({
641-
b: str => ({str}),
641+
b: parts => ({parts}),
642642
token: '<asd>',
643643
placeholder: '>',
644-
a: str => ({str}),
644+
a: parts => ({parts}),
645645
})
646646
).toEqual([
647647
'&lt; hello ',
648-
{str: 'world'},
648+
{parts: ['world']},
649649
' <asd> &lt;&gt; ',
650-
{str: '>'},
650+
{parts: ['>']},
651651
]);
652652
});
653653
it('select message w/ placeholder & >', function () {
@@ -665,16 +665,16 @@ describe('IntlMessageFormat', function () {
665665
})
666666
).toEqual([
667667
'&lt; hello ',
668-
{str: 'world'},
668+
{str: ['world']},
669669
' <asd> &lt;&gt; ',
670-
{str: '>'},
670+
{str: ['>']},
671671
]);
672672
expect(
673673
mf.format<object>({
674674
gender: 'female',
675675
b: str => ({str}),
676676
})
677-
).toEqual({str: 'foo &lt;&gt; bar'});
677+
).toEqual({str: ['foo &lt;&gt; bar']});
678678
});
679679
it('should allow escaping tag as legacy HTML', function () {
680680
const mf = new IntlMessageFormat(
@@ -696,7 +696,7 @@ describe('IntlMessageFormat', function () {
696696
}),
697697
bar: {bar: 1},
698698
})
699-
).toEqual(['hello ', {obj: {bar: 1}}, ' test']);
699+
).toEqual(['hello ', {obj: [{bar: 1}]}, ' test']);
700700
});
701701
it('should handle tag in plural', function () {
702702
const mf = new IntlMessageFormat(
@@ -705,7 +705,7 @@ describe('IntlMessageFormat', function () {
705705
);
706706
expect(
707707
mf.format<string>({
708-
b: (...chunks) => `{}${chunks}{}`,
708+
b: chunks => `{}${chunks}{}`,
709709
count: 1000,
710710
})
711711
).toBe('You have {}1,000{} Messages');

packages/react-intl/src/formatters/message.ts

+9-13
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import * as React from 'react';
88
import {invariant} from '@formatjs/intl-utils';
9-
import {unapplyFormatXMLElementFn} from '../utils';
9+
import {assignUniqueKeysToParts} from '../utils';
1010

1111
import {
1212
Formatters,
@@ -18,6 +18,7 @@ import {
1818
import IntlMessageFormat, {
1919
FormatXMLElementFn,
2020
PrimitiveType,
21+
isFormatXMLElementFn,
2122
} from 'intl-messageformat';
2223
import {MissingTranslationError, MessageFormatError} from '../error';
2324

@@ -73,23 +74,17 @@ function deepMergeFormatsAndSetTimeZone(
7374
};
7475
}
7576

76-
function isFormatXMLElementFn(
77-
input: any
78-
): input is FormatXMLElementFn<React.ReactNode, React.ReactNode> {
79-
return typeof input === 'function';
80-
}
81-
82-
export function unapplyFormatXMLElementFnInValues(
77+
export function assignUniqueKeysToFormatXMLElementFnArgument(
8378
values: Record<
8479
string,
85-
| PrimitiveType
86-
| React.ReactNode
87-
| FormatXMLElementFn<React.ReactNode, React.ReactNode>
80+
PrimitiveType | React.ReactNode | FormatXMLElementFn<React.ReactNode>
8881
>
8982
): typeof values {
9083
return Object.keys(values).reduce((acc: typeof values, k) => {
9184
const v = values[k];
92-
acc[k] = isFormatXMLElementFn(v) ? unapplyFormatXMLElementFn(v) : v;
85+
acc[k] = isFormatXMLElementFn<React.ReactNode>(v)
86+
? assignUniqueKeysToParts(v)
87+
: v;
9388
return acc;
9489
}, {});
9590
}
@@ -163,7 +158,8 @@ export function formatMessage(
163158
if (!values && message && typeof message === 'string') {
164159
return message.replace(/'\{(.*?)\}'/gi, `{$1}`);
165160
}
166-
const patchedValues = values && unapplyFormatXMLElementFnInValues(values);
161+
const patchedValues =
162+
values && assignUniqueKeysToFormatXMLElementFnArgument(values);
167163
formats = deepMergeFormatsAndSetTimeZone(formats, timeZone);
168164
defaultFormats = deepMergeFormatsAndSetTimeZone(defaultFormats, timeZone);
169165

packages/react-intl/src/utils.ts

+9-13
Original file line numberDiff line numberDiff line change
@@ -136,20 +136,16 @@ export function getNamedFormat<T extends keyof CustomFormats>(
136136
}
137137

138138
/**
139-
* Takes a `formatXMLElementFn`, which takes a single React.Node argument, and
140-
* returns a FormatXMLElementFn which takes any number of positional arguments.
141-
* I.e. converts non-variadic FormatXMLElementFn to variadic. Variadic
142-
* FormatXMLElementFn is needed for 'intl-messageformat' package, non-variadic
143-
* simplifies API of 'react-intl' package.
139+
* Takes a `formatXMLElementFn`, and composes it in function, which passes
140+
* argument `parts` through, assigning unique key to each part, to prevent
141+
* "Each child in a list should have a unique "key"" React error.
144142
* @param formatXMLElementFn
145143
*/
146-
export function unapplyFormatXMLElementFn(
147-
formatXMLElementFn: FormatXMLElementFn<React.ReactNode, React.ReactNode>
148-
): (node: React.ReactNode) => React.ReactNode {
149-
return function () {
150-
return formatXMLElementFn(
151-
// eslint-disable-next-line prefer-rest-params
152-
arguments.length === 1 ? arguments[0] : React.Children.toArray(arguments)
153-
);
144+
export function assignUniqueKeysToParts(
145+
formatXMLElementFn: FormatXMLElementFn<React.ReactNode>
146+
): typeof formatXMLElementFn {
147+
return function (parts) {
148+
// eslint-disable-next-line prefer-rest-params
149+
return formatXMLElementFn(React.Children.toArray(parts));
154150
};
155151
}

0 commit comments

Comments
 (0)