Skip to content

Commit

Permalink
feat(intl-messageformat): make FormatXMLElementFn non-variadic
Browse files Browse the repository at this point in the history
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
  • Loading branch information
czabaj authored and Long Ho committed Jul 2, 2020
1 parent 1b5892f commit f2963bf
Show file tree
Hide file tree
Showing 4 changed files with 39 additions and 47 deletions.
6 changes: 3 additions & 3 deletions packages/intl-messageformat/src/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ function mergeLiteral<T>(
}, [] as MessageFormatPart<T>[]);
}

function isFormatXMLElementFn<T>(
export function isFormatXMLElementFn<T>(
el: PrimitiveType | T | FormatXMLElementFn<T>
): el is FormatXMLElementFn<T> {
return typeof el === 'function';
Expand Down Expand Up @@ -218,7 +218,7 @@ export function formatToParts<T>(
values,
currentPluralValue
);
let chunks = formatFn(...parts.map(p => p.value));
let chunks = formatFn(parts.map(p => p.value));
if (!Array.isArray(chunks)) {
chunks = [chunks];
}
Expand Down Expand Up @@ -291,5 +291,5 @@ Try polyfilling it using "@formatjs/intl-pluralrules"
}

export type FormatXMLElementFn<T, R = string | Array<string | T>> = (
...args: Array<string | T>
parts: Array<string | T>
) => R;
36 changes: 18 additions & 18 deletions packages/intl-messageformat/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -573,8 +573,8 @@ describe('IntlMessageFormat', function () {
it('simple message', function () {
const mf = new IntlMessageFormat('hello <b>world</b>', 'en');
expect(
mf.format<object>({b: str => ({str})})
).toEqual(['hello ', {str: 'world'}]);
mf.format<object>({b: parts => ({parts})})
).toEqual(['hello ', {parts: ['world']}]);
});
it('nested tag message', function () {
const mf = new IntlMessageFormat(
Expand All @@ -583,7 +583,7 @@ describe('IntlMessageFormat', function () {
);
expect(
mf.format<object>({
b: (...chunks) => ({chunks}),
b: chunks => ({chunks}),
i: c => ({val: `$$${c}$$`}),
})
).toEqual(['hello ', {chunks: ['world', {val: '$$!$$'}, ' <br/> ']}]);
Expand All @@ -595,7 +595,7 @@ describe('IntlMessageFormat', function () {
);
expect(
mf.format<object>({
b: (...chunks) => ['<b>', ...chunks, '</b>'],
b: chunks => ['<b>', ...chunks, '</b>'],
i: c => ({val: `$$${c}$$`}),
})
).toEqual(['hello <b>world', {val: '$$!$$'}, ' <br/> </b>']);
Expand All @@ -616,20 +616,20 @@ describe('IntlMessageFormat', function () {
);
expect(
mf.format<object>({
b: str => ({str}),
b: parts => ({parts}),
placeholder: 'gaga',
a: str => ({str}),
a: parts => ({parts}),
})
).toEqual(['hello ', {str: 'world'}, ' ', {str: 'gaga'}]);
).toEqual(['hello ', {parts: ['world']}, ' ', {parts: ['gaga']}]);
});
it('message w/ placeholder & HTML entities', function () {
const mf = new IntlMessageFormat('Hello&lt;<tag>{text}</tag>', 'en');
expect(
mf.format<object>({
tag: str => ({str}),
tag: parts => ({parts}),
text: '<asd>',
})
).toEqual(['Hello&lt;', {str: '<asd>'}]);
).toEqual(['Hello&lt;', {parts: ['<asd>']}]);
});
it('message w/ placeholder & >', function () {
const mf = new IntlMessageFormat(
Expand All @@ -638,16 +638,16 @@ describe('IntlMessageFormat', function () {
);
expect(
mf.format<object>({
b: str => ({str}),
b: parts => ({parts}),
token: '<asd>',
placeholder: '>',
a: str => ({str}),
a: parts => ({parts}),
})
).toEqual([
'&lt; hello ',
{str: 'world'},
{parts: ['world']},
' <asd> &lt;&gt; ',
{str: '>'},
{parts: ['>']},
]);
});
it('select message w/ placeholder & >', function () {
Expand All @@ -665,16 +665,16 @@ describe('IntlMessageFormat', function () {
})
).toEqual([
'&lt; hello ',
{str: 'world'},
{str: ['world']},
' <asd> &lt;&gt; ',
{str: '>'},
{str: ['>']},
]);
expect(
mf.format<object>({
gender: 'female',
b: str => ({str}),
})
).toEqual({str: 'foo &lt;&gt; bar'});
).toEqual({str: ['foo &lt;&gt; bar']});
});
it('should allow escaping tag as legacy HTML', function () {
const mf = new IntlMessageFormat(
Expand All @@ -696,7 +696,7 @@ describe('IntlMessageFormat', function () {
}),
bar: {bar: 1},
})
).toEqual(['hello ', {obj: {bar: 1}}, ' test']);
).toEqual(['hello ', {obj: [{bar: 1}]}, ' test']);
});
it('should handle tag in plural', function () {
const mf = new IntlMessageFormat(
Expand All @@ -705,7 +705,7 @@ describe('IntlMessageFormat', function () {
);
expect(
mf.format<string>({
b: (...chunks) => `{}${chunks}{}`,
b: chunks => `{}${chunks}{}`,
count: 1000,
})
).toBe('You have {}1,000{} Messages');
Expand Down
22 changes: 9 additions & 13 deletions packages/react-intl/src/formatters/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

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

import {
Formatters,
Expand All @@ -18,6 +18,7 @@ import {
import IntlMessageFormat, {
FormatXMLElementFn,
PrimitiveType,
isFormatXMLElementFn,
} from 'intl-messageformat';
import {MissingTranslationError, MessageFormatError} from '../error';

Expand Down Expand Up @@ -73,23 +74,17 @@ function deepMergeFormatsAndSetTimeZone(
};
}

function isFormatXMLElementFn(
input: any
): input is FormatXMLElementFn<React.ReactNode, React.ReactNode> {
return typeof input === 'function';
}

export function unapplyFormatXMLElementFnInValues(
export function assignUniqueKeysToFormatXMLElementFnArgument(
values: Record<
string,
| PrimitiveType
| React.ReactNode
| FormatXMLElementFn<React.ReactNode, React.ReactNode>
PrimitiveType | React.ReactNode | FormatXMLElementFn<React.ReactNode>
>
): typeof values {
return Object.keys(values).reduce((acc: typeof values, k) => {
const v = values[k];
acc[k] = isFormatXMLElementFn(v) ? unapplyFormatXMLElementFn(v) : v;
acc[k] = isFormatXMLElementFn<React.ReactNode>(v)
? assignUniqueKeysToParts(v)
: v;
return acc;
}, {});
}
Expand Down Expand Up @@ -163,7 +158,8 @@ export function formatMessage(
if (!values && message && typeof message === 'string') {
return message.replace(/'\{(.*?)\}'/gi, `{$1}`);
}
const patchedValues = values && unapplyFormatXMLElementFnInValues(values);
const patchedValues =
values && assignUniqueKeysToFormatXMLElementFnArgument(values);
formats = deepMergeFormatsAndSetTimeZone(formats, timeZone);
defaultFormats = deepMergeFormatsAndSetTimeZone(defaultFormats, timeZone);

Expand Down
22 changes: 9 additions & 13 deletions packages/react-intl/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,20 +136,16 @@ export function getNamedFormat<T extends keyof CustomFormats>(
}

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

0 comments on commit f2963bf

Please sign in to comment.