Skip to content

Commit

Permalink
feat(react-i18n): interpolate JSX tags inside translations (#22)
Browse files Browse the repository at this point in the history
Add the ability to interpolate JSX inside translations.
  • Loading branch information
Geoffrey authored and flepretre committed Nov 28, 2019
1 parent 3ed7f5d commit 1e00a17
Show file tree
Hide file tree
Showing 6 changed files with 376 additions and 20 deletions.
67 changes: 53 additions & 14 deletions packages/react-i18n/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,11 @@ export default const MyComponent = ({ nbExample, t }) => {
};
```

* **i18nKeys**: key from the dictionary (required)
* **number**: amount used for plural forms
* **data**: object containing key/values used for interpolation in the translation
* **general**: use general plural form if truthy
- **i18nKeys**: key from the dictionary (required)
- **number**: amount used for plural forms
- **data**: object containing key/values used for interpolation in the translation
- **general**: use general plural form if truthy
- **renderers**: object containing the renderers to use to interpolate JSX (for more information see `JSX interpolation section`)

### i18n HTML component

Expand All @@ -94,11 +95,12 @@ export default const MyComponent = ({ nbExample, t }) => {
};
```

* **i18nKeys**: key from the dictionary (required)
* **number**: amount used for plural forms
* **data**: object containing key/values used for interpolation in the translation
* **general**: use general plural form if truthy
* **element**: HTML element, or React element used for rendering. (default value: `span`)
- **i18nKeys**: key from the dictionary (required)
- **number**: amount used for plural forms
- **data**: object containing key/values used for interpolation in the translation
- **general**: use general plural form if truthy
- **element**: HTML element, or React element used for rendering. (default value: `span`)
- **renderers**: object containing the renderers to use to interpolate JSX (for more information see `JSX interpolation section`)

Note that **number** and **data** can be used together.

Expand Down Expand Up @@ -129,11 +131,12 @@ const MyComponent = ({ nbExample, t }) => {
export default translate(MyComponent);
```

* **t**: translation function, params are:
* **key**: key from the dictionary (required)
* **data**: object containing key/values used for interpolation in the translation
* **number**: amount used for plural forms
* **general**: use general plural form if truthy
- **t**: translation function, params are:
- **key**: key from the dictionary (required)
- **data**: object containing key/values used for interpolation in the translation
- **number**: amount used for plural forms
- **general**: use general plural form if truthy
- **renderers**: object containing the renderers to use to interpolate JSX (for more information see `JSX interpolation section`)

Note that **number** and **data** can be used together.

Expand Down Expand Up @@ -205,3 +208,39 @@ This is the configuration of plural form for keys:
The variable used in translation template string has to be `%(number)d`, and is automatically provided by the translate function.

To use general form, you need to set 4th parameter of the translate function to `true`

### JSX Interpolation

It is possible to interpolate JSX components inside translation, to do so you have to give `renderers` parameter or props.
For example if you have in your translation : `foo <LinkToHome>bar</LinkToHome>` you should have a `LinkToHome` renderer.

```jsx harmony
import React from 'react';
import { useTranslate } from '@m6web/react-i18n';

const renderers = {
LinkToHome: ({ children }) => <a href="/">{children}</a>,
};

const MyComponent = () => {
const t = useTranslate();

return (
<div class="foo">
<p>{t('foo.example', undefined, undefined, undefined, renderers)}</p>
</div>
);
};
```

In this example, the `<LinkToHome>` inside your translation will be rendered by the component given in `renderers`.

For the moment only the children props are used by the renderer.
```jsx harmony
// Do
<Link>Home</Link>
<Link /> or <Link/>

// Don't
<Link href="/home">Home</Link>
```
5 changes: 3 additions & 2 deletions packages/react-i18n/src/components/i18nElement.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Context } from '../context/i18n.context';

export const HtmlTrans = ({ i18nKey, data, number, general, element: Element, ...props }) => (
export const HtmlTrans = ({ i18nKey, data, number, general, element: Element, renderers, ...props }) => (
<Context.Consumer>
{t => <Element {...props} dangerouslySetInnerHTML={{ __html: t(i18nKey, data, number, general) }} />}
{t => <Element {...props} dangerouslySetInnerHTML={{ __html: t(i18nKey, data, number, general, renderers) }} />}
</Context.Consumer>
);

Expand All @@ -20,4 +20,5 @@ HtmlTrans.propTypes = {
data: PropTypes.object,
number: PropTypes.number,
general: PropTypes.bool,
renderers: PropTypes.object,
};
5 changes: 3 additions & 2 deletions packages/react-i18n/src/components/i18nString.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Context } from './../context/i18n.context';

export const Trans = ({ i18nKey, data, number, general }) => (
<Context.Consumer>{t => t(i18nKey, data, number, general)}</Context.Consumer>
export const Trans = ({ i18nKey, data, number, general, renderers }) => (
<Context.Consumer>{t => t(i18nKey, data, number, general, renderers)}</Context.Consumer>
);

Trans.defaultProps = {
Expand All @@ -16,4 +16,5 @@ Trans.propTypes = {
data: PropTypes.object,
number: PropTypes.number,
general: PropTypes.bool,
renderers: PropTypes.object,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`i18n translate function with JSX should not try to interpolate basic html tag 1`] = `
<div>
&lt;em&gt;Hello&lt;/em&gt; &lt;strong&gt;Moto&lt;/strong&gt; !
</div>
`;

exports[`i18n translate function with JSX should render a complex JSX structure inside the translation 1`] = `
<div>
<Bold>
<strong>
<Italic>
<em>
Hell
</em>
</Italic>
o
</strong>
</Bold>
<LineBreak>
<br />
</LineBreak>
Moto
<LineBreak>
<br />
</LineBreak>
!
</div>
`;

exports[`i18n translate function with JSX should render the JSX component and sibling JSX component present inside the translation 1`] = `
<div>
<Italic>
<em>
Hello
</em>
</Italic>
<Bold>
<strong>
Moto
</strong>
</Bold>
!
</div>
`;

exports[`i18n translate function with JSX should render the JSX component present inside the translation 1`] = `
<div>
Hello
<Bold>
<strong>
Moto
</strong>
</Bold>
!
</div>
`;

exports[`i18n translate function with JSX should render the nested JSX component present inside the translation 1`] = `
<div>
Hello
<Bold>
<strong>
<Italic>
<em>
Moto
</em>
</Italic>
</strong>
</Bold>
!
</div>
`;

exports[`i18n translate function with JSX should render the short (without children) JSX component inside the translation 1`] = `
<div>
Hello
<LineBreak>
<br />
</LineBreak>
Moto
<LineBreak>
<br />
</LineBreak>
!
</div>
`;

exports[`i18n translate function with JSX should works with badly formatted JSX 1`] = `
<div>
&lt;Bold&gt;Toto&lt;/Italic&gt;
</div>
`;
132 changes: 132 additions & 0 deletions packages/react-i18n/src/utils/__tests__/i18n.utils.spec.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/* eslint-disable react/prop-types */

import React from 'react';
import { mount } from 'enzyme';
import { translate, buildList } from '../i18n.utils';

describe('i18n translate function', () => {
Expand Down Expand Up @@ -159,6 +163,134 @@ describe('i18n translate function', () => {
);
});
});

describe('with JSX', () => {
const Bold = ({ children }) => <strong>{children}</strong>;
const Italic = ({ children }) => <em>{children}</em>;
const LineBreak = () => <br />;

it('should render the JSX component present inside the translation', () => {
const lang = {
foo: {
bar: 'Hello <Bold>Moto</Bold> !',
},
};
const renderers = { Bold };
const t = translate(lang);

const result = t('foo.bar', undefined, undefined, false, renderers);
const wrapper = mount(<div>{result}</div>);

expect(wrapper).toMatchSnapshot();
});

it('should render the nested JSX component present inside the translation', () => {
const lang = {
foo: {
bar: 'Hello <Bold><Italic>Moto</Italic></Bold> !',
},
};
const renderers = { Bold, Italic };
const t = translate(lang);

const result = t('foo.bar', undefined, undefined, false, renderers);
const wrapper = mount(<div>{result}</div>);

expect(wrapper).toMatchSnapshot();
});

it('should render the JSX component and sibling JSX component present inside the translation', () => {
const lang = {
foo: {
bar: '<Italic>Hello</Italic> <Bold>Moto</Bold> !',
},
};
const renderers = { Bold, Italic };
const t = translate(lang);

const result = t('foo.bar', undefined, undefined, false, renderers);
const wrapper = mount(<div>{result}</div>);

expect(wrapper).toMatchSnapshot();
});

it('should render the short (without children) JSX component inside the translation', () => {
const lang = {
foo: {
bar: 'Hello<LineBreak />Moto<LineBreak/> !',
},
};
const renderers = { LineBreak };
const t = translate(lang);

const result = t('foo.bar', undefined, undefined, false, renderers);
const wrapper = mount(<div>{result}</div>);

expect(wrapper).toMatchSnapshot();
});

it('should render a complex JSX structure inside the translation', () => {
const lang = {
foo: {
bar: '<Bold><Italic>Hell</Italic>o</Bold><LineBreak />Moto<LineBreak/> !',
},
};
const renderers = { LineBreak, Bold, Italic };
const t = translate(lang);

const result = t('foo.bar', undefined, undefined, false, renderers);
const wrapper = mount(<div>{result}</div>);

expect(wrapper).toMatchSnapshot();
});

/* eslint-disable no-console */
it('should throw an error if renderer not provided', () => {
console.warn = jest.fn();
const lang = {
foo: {
bar: 'Hello <Bold><Italic>Moto</Italic></Bold> !',
},
};
const renderers = { Bold };
const t = translate(lang);
t('foo.bar', undefined, undefined, false, renderers);

expect(console.warn).toHaveBeenCalledWith('No renderer provided for component "Italic"');
});
/* eslint-enable no-console */

it('should not try to interpolate basic html tag', () => {
const lang = {
foo: {
bar: '<em>Hello</em> <strong>Moto</strong> !',
},
};
const renderers = {};
const t = translate(lang);

const result = t('foo.bar', undefined, undefined, false, renderers);
const wrapper = mount(<div>{result}</div>);

expect(wrapper).toMatchSnapshot();
});

it('should works with badly formatted JSX', () => {
const lang = {
foo: {
bar: '<Bold>Toto</Italic>'
}
}

const renderers = {Bold, Italic}
const t = translate(lang)

const result = t('foo.bar', undefined, undefined, undefined, renderers)
const wrapper = mount(<div>{result}</div>)

expect(wrapper).toMatchSnapshot()
});
});
});

describe('i18n listBuilder function', () => {
Expand Down
Loading

0 comments on commit 1e00a17

Please sign in to comment.