Skip to content

Commit

Permalink
ICU: Fixes macro to support count prop and expressions better
Browse files Browse the repository at this point in the history
Fixes for #937
Allows count prop to use a variable also named count
Add support for expressions in the count and switch props
Handles the values prop better
Allows macros to be self-closing without children
Trims newlines and whitespace around code formatted with prettier or other formatting tools
  • Loading branch information
redbugz committed Sep 10, 2019
1 parent b9b8de6 commit 5b97c81
Show file tree
Hide file tree
Showing 3 changed files with 348 additions and 38 deletions.
99 changes: 61 additions & 38 deletions icu.macro.js
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);

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';
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);

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
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'
}} />
</>;
}
"
`;

0 comments on commit 5b97c81

Please sign in to comment.