Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ICU: Fixes macro to support count prop and expressions better #939

Merged
merged 18 commits into from
Sep 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 61 additions & 38 deletions icu.macro.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ function ICUMacro({ references, state, babel }) {
// assert we have the react-i18next Trans component imported
addNeededImports(state, babel);

// transform Plural
Plural.forEach(referencePath => {
// transform Plural and SelectOrdinal
[...Plural, ...SelectOrdinal].forEach(referencePath => {
if (referencePath.parentPath.type === 'JSXOpeningElement') {
pluralAsJSX(
referencePath.parentPath,
Expand All @@ -27,23 +27,6 @@ function ICUMacro({ references, state, babel }) {
}
});

// transform SelectOrdinal
SelectOrdinal.forEach(referencePath => {
if (referencePath.parentPath.type === 'JSXOpeningElement') {
// selectordinal is a form of plural
pluralAsJSX(
referencePath.parentPath,
{
attributes: referencePath.parentPath.get('attributes'),
children: referencePath.parentPath.parentPath.get('children'),
},
babel,
);
} else {
// throw a helpful error message or something :)
}
});

// transform Select
Select.forEach(referencePath => {
if (referencePath.parentPath.type === 'JSXOpeningElement') {
Expand Down Expand Up @@ -85,39 +68,54 @@ function pluralAsJSX(parentPath, { attributes }, babel) {
// plural or selectordinal
const nodeName = parentPath.node.name.name.toLocaleLowerCase();

let componentStartIndex = 0;
// will need to merge count attribute with existing values attribute in some cases
const existingValuesAttribute = findAttribute('values', attributes);
const existingValues = existingValuesAttribute
? existingValuesAttribute.node.value.expression.properties
: [];

let componentStartIndex = 0;
const extracted = attributes.reduce(
(mem, attr) => {
if (attr.node.name.name === 'i18nKey') {
// copy the i18nKey
mem.attributesToCopy.push(attr.node);
} else if (attr.node.name.name === 'count') {
// take the count for element
mem.values.push(toObjectProperty(attr.node.value.expression.name));
mem.defaults = `{${attr.node.value.expression.name}, ${nodeName}, ${mem.defaults}`;
let exprName = attr.node.value.expression.name;
if (!exprName) {
exprName = 'count';
}
if (exprName === 'count') {
// if the prop expression name is also "count", copy it instead: <Plural count={count} --> <Trans count={count}
mem.attributesToCopy.push(attr.node);
} else {
mem.values.unshift(toObjectProperty(exprName));
}
mem.defaults = `{${exprName}, ${nodeName}, ${mem.defaults}`;
} else if (attr.node.name.name === 'values') {
// skip the values attribute, as it has already been processed into mem from existingValues
} else if (attr.node.value.type === 'StringLiteral') {
// take any string node as plural option
let pluralForm = attr.node.name.name;
if (pluralForm.indexOf('$') === 0) pluralForm = pluralForm.replace('$', '=');
mem.defaults = `${mem.defaults} ${pluralForm} {${attr.node.value.value}}`;
} else if (attr.node.value.type === 'JSXExpressionContainer') {
// convert any Trans component to plural option extracting any values and components
const children = attr.node.value.expression.children;
const children = attr.node.value.expression.children || [];
const thisTrans = processTrans(children, babel, componentStartIndex);

let pluralForm = attr.node.name.name;
if (pluralForm.indexOf('$') === 0) pluralForm = pluralForm.replace('$', '=');

mem.defaults = `${mem.defaults} ${pluralForm} {${thisTrans.defaults}}`;
mem.components = mem.components.concat(thisTrans.components);
mem.values = mem.values.concat(thisTrans.values);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was causing problems with duplicate keys in values, and I'm not sure I understand the use case and all the test cases I needed are passing, so I just removed it.
If we need to support this, then I can add code to dedupe these from values.


componentStartIndex += thisTrans.components.length;
}
return mem;
},
{ attributesToCopy: [], values: [], components: [], defaults: '' },
{ attributesToCopy: [], values: existingValues, components: [], defaults: '' },
);

// replace the node with the new Trans
Expand All @@ -129,6 +127,12 @@ function selectAsJSX(parentPath, { attributes }, babel) {
const toObjectProperty = (name, value) =>
t.objectProperty(t.identifier(name), t.identifier(name), false, !value);

// will need to merge switch attribute with existing values attribute
const existingValuesAttribute = findAttribute('values', attributes);
const existingValues = existingValuesAttribute
? existingValuesAttribute.node.value.expression.properties
: [];

let componentStartIndex = 0;

const extracted = attributes.reduce(
Expand All @@ -137,26 +141,33 @@ function selectAsJSX(parentPath, { attributes }, babel) {
// copy the i18nKey
mem.attributesToCopy.push(attr.node);
} else if (attr.node.name.name === 'switch') {
// take the switch for plural element
mem.values.push(toObjectProperty(attr.node.value.expression.name));
mem.defaults = `{${attr.node.value.expression.name}, select, ${mem.defaults}`;
// take the switch for select element
let exprName = attr.node.value.expression.name;
if (!exprName) {
exprName = 'selectKey';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't use switch because it is a reserved word. I'm open to alternate names for this.

It is used when switch is an expression.

<Select
  switch={'fe'+'male'} // very contrived example
  ...
/>
// This transforms to:
<Trans values={{ selectKey: 'fe'+'male' }} defaults="{selectKey, select, ....} />

I don't think this will be used very often, but it was the same logic as Plural and count so I included it for completeness.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks ok for me

mem.values.unshift(t.objectProperty(t.identifier(exprName), attr.node.value.expression));
} else {
mem.values.unshift(toObjectProperty(exprName));
}
mem.defaults = `{${exprName}, select, ${mem.defaults}`;
} else if (attr.node.name.name === 'values') {
// skip the values attribute, as it has already been processed into mem as existingValues
} else if (attr.node.value.type === 'StringLiteral') {
// take any string node as select option
mem.defaults = `${mem.defaults} ${attr.node.name.name} {${attr.node.value.value}}`;
} else if (attr.node.value.type === 'JSXExpressionContainer') {
// convert any Trans component to select option extracting any values and components
const children = attr.node.value.expression.children;
const children = attr.node.value.expression.children || [];
const thisTrans = processTrans(children, babel, componentStartIndex);

mem.defaults = `${mem.defaults} ${attr.node.name.name} {${thisTrans.defaults}}`;
mem.components = mem.components.concat(thisTrans.components);
mem.values = mem.values.concat(thisTrans.values);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see comment above for similar code


componentStartIndex += thisTrans.components.length;
}
return mem;
},
{ attributesToCopy: [], values: [], components: [], defaults: '' },
{ attributesToCopy: [], values: existingValues, components: [], defaults: '' },
);

// replace the node with the new Trans
Expand Down Expand Up @@ -260,15 +271,26 @@ function processTrans(children, babel, componentStartIndex = 0) {
return res;
}

// eslint-disable-next-line no-control-regex
const leadingNewLineAndWhitespace = new RegExp('^\n\\s+', 'g');
// eslint-disable-next-line no-control-regex
const trailingNewLineAndWhitespace = new RegExp('\n\\s+$', 'g');
function trimIndent(text) {
const newText = text
.replace(leadingNewLineAndWhitespace, '')
.replace(trailingNewLineAndWhitespace, '');
return newText;
}

function mergeChildren(children, babel, componentStartIndex = 0) {
const t = babel.types;
let componentFoundIndex = componentStartIndex;

return children.reduce((mem, child) => {
const ele = child.node ? child.node : child;

// add text
if (t.isJSXText(ele) && ele.value) mem += ele.value;
// add text, but trim indentation whitespace
if (t.isJSXText(ele) && ele.value) mem += trimIndent(ele.value);
// add ?!? forgot
if (ele.expression && ele.expression.value) mem += ele.expression.value;
// add `{ val }`
Expand Down Expand Up @@ -329,14 +351,15 @@ function getComponents(children, babel) {

if (t.isJSXElement(ele)) {
const clone = t.clone(ele);
clone.children = clone.children.reduce((mem, child) => {
const ele = child.node ? child.node : child;
clone.children = clone.children.reduce((clonedMem, clonedChild) => {
const clonedEle = clonedChild.node ? clonedChild.node : clonedChild;

// clean out invalid definitions by replacing `{ catchDate, date, short }` with `{ catchDate }`
if (ele.expression && ele.expression.expressions)
ele.expression.expressions = [ele.expression.expressions[0]];
if (clonedEle.expression && clonedEle.expression.expressions)
clonedEle.expression.expressions = [clonedEle.expression.expressions[0]];

mem.push(child);
clonedMem.push(clonedChild);
return clonedMem;
}, []);

mem.push(ele);
Expand Down
178 changes: 178 additions & 0 deletions test/__snapshots__/icu.macro.spec.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -316,3 +316,181 @@ const x = <Trans i18nKey=\\"testKey\\" defaults=\\"{position, selectordinal, on
}} />;
"
`;

exports[`macros 19. macros: 19. macros 1`] = `
"
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Plural, Select, SelectOrdinal, Trans } from '../icu.macro'
const Link = ({to, children}) => (<a href={to}>{children}</a>)

export default function TestPage({count = 1}) {
const [t] = useTranslation()
const catchDate = Date.now()
const completion = 0.75
const gender = Math.random() < 0.5 ? 'female' : 'male'
return (
<>
{t('sample.text', 'Some sample text with {word} {gender} {count, number} {catchDate, date} {completion, number, percent}', {word: 'interpolation', gender, count, catchDate, completion})}
<Plural i18nKey=\\"plural\\"
count={count}
values={{linkPath: \\"/item/\\" + count}}
$0={<Trans><Link to='/cart'>Your cart</Link> is <strong>empty</strong>.</Trans>}
one={<Trans>You have <strong># item</strong> in <Link to='/cart'>your cart</Link>.</Trans>}
other={<Trans>You have <strong># items</strong> in <Link to='/cart'>your cart</Link>.</Trans>}
/>
<Select
i18nKey=\\"select\\"
switch={gender}
female={<Trans>These are <Link to='/items'>her items</Link></Trans>}
male={<Trans>These are <Link to='/items'>his items</Link></Trans>}
other={<Trans>These are <Link to='/items'>their items</Link></Trans>}
/>
<SelectOrdinal i18nKey=\\"ordinal\\"
count={itemIndex+1}
values={{linkPath: \\"/item/\\" + itemIndex}}
one={<Trans>Your <Link to={linkPath}><strong>#st</strong> item</Link></Trans>}
two={<Trans>Your <Link to={linkPath}><strong>#nd</strong> item</Link></Trans>}
few={<Trans>Your <Link to={linkPath}><strong>#rd</strong> item</Link></Trans>}
other={<Trans>Your <Link to={linkPath}><strong>#th</strong> item</Link></Trans>}
/>
<Trans i18nKey=\\"percent\\" defaults=\\"You&apos;ve completed <Link to='/tasks'>{completion, number, percent} of your tasks</Link>.\\"/>
<Trans i18nKey=\\"date\\" defaults=\\"Caught on <Link to='/dest'>{ catchDate, date, short }</Link>!\\"/>
<SelectOrdinal
i18nKey=\\"ordinal.prettier\\"
count={count}
values={{ linkPath: \`/item/\${count}\`, type: 'item', prop }}
one={
<Trans>
Your{' '}
<Link to={linkPath}>
<strong>#st</strong> {type}
</Link>
</Trans>
}
two={
<Trans>
Your{' '}
<Link to={linkPath}>
<strong>#nd</strong> {type}
</Link>
</Trans>
}
few={
<Trans>
Your{' '}
<Link to={linkPath}>
<strong>#rd</strong> {type}
</Link>
</Trans>
}
other={
<Trans>
Your{' '}
<Link to={linkPath}>
<strong>#th</strong> {type}
</Link>
</Trans>
}
/>
<Select
i18nKey=\\"select.expr.prettier\\"
switch={\`\${gender}Person\`}
values={{ linkPath: \`/users/\${number}\`, type: 'bugs' }}
malePerson={
<Trans>
<Link to={linkPath}>
<strong>He</strong>
</Link>{' '}
avoids {type}.
</Trans>
}
femalePerson={
<Trans>
<Link to={linkPath}>
<strong>She</strong>
</Link>{' '}
avoids {type}.
</Trans>
}
other={
<Trans>
<Link to={linkPath}>
<strong>They</strong>
</Link>{' '}
avoid {type}.
</Trans>
}
/>
</>
)
}

↓ ↓ ↓ ↓ ↓ ↓

import React from 'react';
import { useTranslation, Trans } from 'react-i18next';

const Link = ({
to,
children
}) => <a href={to}>{children}</a>;

export default function TestPage({
count = 1
}) {
const [t] = useTranslation();
const catchDate = Date.now();
const completion = 0.75;
const gender = Math.random() < 0.5 ? 'female' : 'male';
return <>
{t('sample.text', 'Some sample text with {word} {gender} {count, number} {catchDate, date} {completion, number, percent}', {
word: 'interpolation',
gender,
count,
catchDate,
completion
})}
<Trans i18nKey=\\"plural\\" count={count} defaults=\\"{count, plural, =0 {<0>Your cart</0> is <1>empty</1>.} one {You have <2># item</2> in <3>your cart</3>.} other {You have <4># items</4> in <5>your cart</5>.}}\\" components={[<Link to='/cart'>Your cart</Link>, <strong>empty</strong>, <strong># item</strong>, <Link to='/cart'>your cart</Link>, <strong># items</strong>, <Link to='/cart'>your cart</Link>]} values={{
linkPath: \\"/item/\\" + count
}} />
<Trans i18nKey=\\"select\\" defaults=\\"{gender, select, female {These are <0>her items</0>} male {These are <1>his items</1>} other {These are <2>their items</2>}}\\" components={[<Link to='/items'>her items</Link>, <Link to='/items'>his items</Link>, <Link to='/items'>their items</Link>]} values={{
gender
}} />
<Trans i18nKey=\\"ordinal\\" count={itemIndex + 1} defaults=\\"{count, selectordinal, one {Your <0><0>#st</0> item</0>} two {Your <1><0>#nd</0> item</1>} few {Your <2><0>#rd</0> item</2>} other {Your <3><0>#th</0> item</3>}}\\" components={[<Link to={linkPath}><strong>#st</strong> item</Link>, <Link to={linkPath}><strong>#nd</strong> item</Link>, <Link to={linkPath}><strong>#rd</strong> item</Link>, <Link to={linkPath}><strong>#th</strong> item</Link>]} values={{
linkPath: \\"/item/\\" + itemIndex
}} />
<Trans i18nKey=\\"percent\\" defaults=\\"You've completed <0>{completion, number, percent} of your tasks</0>.\\" components={[<Link to='/tasks'>{(completion)} of your tasks</Link>]} values={{
completion
}} />
<Trans i18nKey=\\"date\\" defaults=\\"Caught on <0>{catchDate, date, short}</0>!\\" components={[<Link to='/dest'>{(catchDate)}</Link>]} values={{
catchDate
}} />
<Trans i18nKey=\\"ordinal.prettier\\" count={count} defaults=\\"{count, selectordinal, one {Your <0><0>#st</0> {type}</0>} two {Your <1><0>#nd</0> {type}</1>} few {Your <2><0>#rd</0> {type}</2>} other {Your <3><0>#th</0> {type}</3>}}\\" components={[<Link to={linkPath}>
<strong>#st</strong> {type}
</Link>, <Link to={linkPath}>
<strong>#nd</strong> {type}
</Link>, <Link to={linkPath}>
<strong>#rd</strong> {type}
</Link>, <Link to={linkPath}>
<strong>#th</strong> {type}
</Link>]} values={{
linkPath: \`/item/\${count}\`,
type: 'item',
prop
}} />
<Trans i18nKey=\\"select.expr.prettier\\" defaults=\\"{selectKey, select, malePerson {<0><0>He</0></0> avoids {type}.} femalePerson {<1><0>She</0></1> avoids {type}.} other {<2><0>They</0></2> avoid {type}.}}\\" components={[<Link to={linkPath}>
<strong>He</strong>
</Link>, <Link to={linkPath}>
<strong>She</strong>
</Link>, <Link to={linkPath}>
<strong>They</strong>
</Link>]} values={{
selectKey: \`\${gender}Person\`,
linkPath: \`/users/\${number}\`,
type: 'bugs'
}} />
</>;
}
"
`;
Loading