Skip to content

Commit 7b18367

Browse files
thchiaryandrewjohnson
authored andcommitted
Allow React components as dynamic data arguments (#100)
* Allow react data in components, update demo with NavLink example. * Update types
1 parent c8358d9 commit 7b18367

File tree

7 files changed

+118
-19
lines changed

7 files changed

+118
-19
lines changed

demo/src/Main.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ class Main extends React.Component<any, any> {
4646
trackStyle={{borderRadius: 2}}
4747
onToggle={() => this.props.onToggleClick()}
4848
/>
49+
50+
<Translate
51+
id="homeLink"
52+
data={{link: <NavLink to="/"><Translate id="here">here</Translate></NavLink>}}
53+
>
54+
{'Click ${ here } to go home'}
55+
</Translate>
4956
</header>
5057

5158
<main>
@@ -57,10 +64,6 @@ class Main extends React.Component<any, any> {
5764
<Route exact path="/movies" component={Movies} />
5865
<Route exact path="/books" component={Books} />
5966
</main>
60-
61-
{/* <h1>
62-
<Translate id="title">Title</Translate>
63-
</h1> */}
6467
</div>
6568
);
6669
}

demo/src/translations/global.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,15 @@
88
null,
99
"Bonjour ${name}!",
1010
"Hola ${name}!"
11+
],
12+
"homeLink": [
13+
"Click ${ link } to go home.",
14+
"Cliquez ${ link } pour rentrer à la page d'accueil",
15+
"Haga clic ${ link } para ir a la página de inicio"
16+
],
17+
"here": [
18+
"here",
19+
"ici",
20+
"aquí"
1121
]
1222
}

src/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export interface LocalizedElementMap {
9797
}
9898

9999
export interface TranslatePlaceholderData {
100-
[key: string]: string | number;
100+
[key: string]: string | number | React.ReactNode;
101101
}
102102

103103
export type TranslateChildFunction = (context: LocalizeContextProps) => any;

src/localize.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export type LocalizedElementMap = {
8080
};
8181

8282
export type TranslatePlaceholderData = {
83-
[string]: string | number
83+
[string]: string | number | React.Node
8484
};
8585

8686
export type TranslateValue = string | string[];

src/utils.js

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,30 @@ export const getLocalizedElement = (
4242
translationId: options.translationId,
4343
languageCode: options.languageCode
4444
};
45-
const translatedValue = templater(localizedString, placeholderData);
45+
const translatedValueOrArray = templater(localizedString, placeholderData);
46+
47+
// if result of templater is string, do the usual stuff
48+
if (typeof translatedValueOrArray === 'string') {
49+
return renderInnerHtml === true && hasHtmlTags(translatedValueOrArray)
50+
? React.createElement('span', {
51+
dangerouslySetInnerHTML: { __html: translatedValueOrArray }
52+
})
53+
: translatedValueOrArray;
54+
}
55+
56+
// at this point we know we have react components;
57+
// check if there are HTMLTags in the translation (not allowed)
58+
for (let portion of translatedValueOrArray) {
59+
if (typeof portion === 'string' && hasHtmlTags(portion)) {
60+
warning(
61+
'HTML tags in the translation string are not supported when passing React components as arguments to the translation.'
62+
);
63+
return '';
64+
}
65+
}
4666

47-
return renderInnerHtml === true && hasHtmlTags(translatedValue)
48-
? React.createElement('span', {
49-
dangerouslySetInnerHTML: { __html: translatedValue }
50-
})
51-
: translatedValue;
67+
// return as Element
68+
return React.createElement('span', null, ...translatedValueOrArray);
5269
};
5370

5471
export const hasHtmlTags = (value: string): boolean => {
@@ -63,13 +80,39 @@ export const hasHtmlTags = (value: string): boolean => {
6380
* @param {object} data The data that should be inserted in template
6481
* @return {string} The template string with the data merged in
6582
*/
66-
export const templater = (strings: string, data: Object = {}): string => {
67-
for (let prop in data) {
68-
const pattern = '\\${\\s*' + prop + '\\s*}';
69-
const regex = new RegExp(pattern, 'gmi');
70-
strings = strings.replace(regex, data[prop]);
83+
export const templater = (strings: string, data: Object = {}): string | string[] => {
84+
if (!strings) return '';
85+
86+
// ${**}
87+
// brackets to include it in the result of .split()
88+
const genericPlaceholderPattern = '(\\${\\s*[^\\s]+\\s*})';
89+
90+
// split: from 'Hey ${name}' -> ['Hey', '${name}']
91+
// filter: clean empty strings
92+
// map: replace ${prop} with data[prop]
93+
let splitStrings = strings
94+
.split(new RegExp(genericPlaceholderPattern, 'gmi'))
95+
.filter(str => !!str)
96+
.map(templatePortion => {
97+
let matched;
98+
for (let prop in data) {
99+
if (matched) break;
100+
const pattern = '\\${\\s*' + prop + '\\s*}';
101+
const regex = new RegExp(pattern, 'gmi');
102+
if (regex.test(templatePortion)) matched = data[prop];
103+
}
104+
return matched || templatePortion;
105+
});
106+
107+
// if there is a React element, return as array
108+
if (splitStrings.some(portion => React.isValidElement(portion))) {
109+
return splitStrings;
71110
}
72-
return strings;
111+
112+
// otherwise concatenate all portions into the translated value
113+
return splitStrings.reduce((translated, portion) => {
114+
return translated + `${portion}`;
115+
}, '');
73116
};
74117

75118
export const getIndexForLanguageCode = (

tests/Translate.test.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ describe('<Translate />', () => {
2525
bye: ['Goodbye', 'Goodbye FR'],
2626
missing: ['Missing'],
2727
html: ['Hey <a href="http://google.com">google</a>', ''],
28+
htmlPlaceholder: ['Translation with <strong>html</strong> and placeholder: ${ comp }.'],
2829
multiline: [null, ''],
2930
placeholder: ['Hey ${name}!', '']
3031
},
@@ -105,6 +106,27 @@ describe('<Translate />', () => {
105106
);
106107
});
107108

109+
it('should render React', () => {
110+
const Comp = ({name}) => <strong>{name}</strong>;
111+
const Translate = getTranslateWithContext();
112+
const wrapper = mount(
113+
<Translate id='placeholder' data={{ name: <Comp name='ReactJS' /> }} />
114+
);
115+
116+
expect(wrapper.find(Comp).length).toBe(1);
117+
expect(wrapper.text()).toContain('ReactJS');
118+
})
119+
120+
it('should render empty string if passing React placeholder data to translation with html', () => {
121+
const Comp = ({name}) => <strong>{name}</strong>;
122+
const Translate = getTranslateWithContext();
123+
const wrapper = mount(
124+
<Translate id='htmlPlaceholder' data={{comp: <Comp name='ReactJS' />}} />
125+
);
126+
127+
expect(wrapper.text()).toBe('');
128+
})
129+
108130
it('should just pass through string when renderToStaticMarkup not set', () => {
109131
const Translate = getTranslateWithContext({
110132
...initialState,

tests/utils.test.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import Enzyme, { shallow } from 'enzyme';
1+
import React from 'react'
2+
import Enzyme, { mount, shallow } from 'enzyme';
23
import Adapter from 'enzyme-adapter-react-16';
34
import * as utils from 'utils';
45

@@ -61,6 +62,17 @@ describe('locale utils', () => {
6162
});
6263
expect(result).toEqual('Hello Ted');
6364
});
65+
66+
it('should handle React in data', () => {
67+
const Comp = () => <div>ReactJS</div>
68+
const translations = { test: 'Hello ${ comp } data' }
69+
const result = utils.getLocalizedElement({
70+
translationId: 'test',
71+
translations,
72+
data: { comp: <Comp /> }
73+
});
74+
expect(mount(result).text()).toContain('ReactJS');
75+
})
6476
});
6577

6678
describe('hasHtmlTags', () => {
@@ -86,6 +98,15 @@ describe('locale utils', () => {
8698
const result = utils.templater(before);
8799
expect(result).toEqual(before);
88100
});
101+
102+
it('should return an array if React components are passed in data', () => {
103+
const Comp = () => <div>Test</div>;
104+
const data = { comp: <Comp />};
105+
const before = 'Hello this is a ${ comp } translation';
106+
const after = ['Hello this is a ', <Comp /> , ' translation'];
107+
const result = utils.templater(before, data);
108+
expect(result).toEqual(after);
109+
})
89110
});
90111

91112
describe('getIndexForLanguageCode', () => {

0 commit comments

Comments
 (0)