1 | const I = require('immutable');
|
2 | const {expect, assert} = require('chai');
|
3 |
|
4 | import {extractMessages as extract, InputError} from '../src/extract';
|
5 |
|
6 |
|
7 | describe('extraction', function() {
|
8 | describe('of strings', function() {
|
9 | it('extracts a string', function() {
|
10 | let messages = extract('i18n("foo")');
|
11 |
|
12 | expect(messages).to.eql(['foo']);
|
13 | });
|
14 |
|
15 | it('extracts multiple strings', function() {
|
16 | let messages = extract(`
|
17 | let foo = i18n("foo foo foo");
|
18 | let bar = i18n("foo, bar, foo");
|
19 | let baz = \`\${i18n('this is silly')}\`;
|
20 | `);
|
21 |
|
22 | expect(messages).to.eql([
|
23 | 'foo foo foo',
|
24 | 'foo, bar, foo',
|
25 | 'this is silly'
|
26 | ]);
|
27 | });
|
28 | });
|
29 |
|
30 | describe('of jsx', function() {
|
31 | it('extracts simple strings', function() {
|
32 | let messages = extract(`
|
33 | React.createClass({
|
34 | render() {
|
35 | return <div>
|
36 | <I18N>O, hai.</I18N>
|
37 | <I18N>You look nice today!</I18N>
|
38 | </div>;
|
39 | }
|
40 | })
|
41 | `);
|
42 |
|
43 | expect(messages).to.eql([
|
44 | 'O, hai.',
|
45 | 'You look nice today!'
|
46 | ]);
|
47 | });
|
48 |
|
49 | it('extracts strings with expressions', function() {
|
50 | let messages = extract(`
|
51 | React.createClass({
|
52 | render() {
|
53 | let name = this.props.name;
|
54 | return <div>
|
55 | <I18N>O, hai, {name}.</I18N>
|
56 | <I18N>You look nice today, {this.props.subject}!</I18N>
|
57 | </div>;
|
58 | }
|
59 | })
|
60 | `);
|
61 |
|
62 | expect(messages).to.eql([
|
63 | 'O, hai, {name}.',
|
64 | 'You look nice today, {this.props.subject}!'
|
65 | ]);
|
66 | });
|
67 |
|
68 | it('extracts strings with nested components', function() {
|
69 | let messages = extract(`
|
70 | React.createClass({
|
71 | render() {
|
72 | let name = this.props.name;
|
73 | return <div>
|
74 | <I18N>O, hai, <span>{name}</span>.</I18N>
|
75 | <I18N>You look <em>nice</em> today, <strong>{this.props.subject}</strong>!</I18N>
|
76 | </div>;
|
77 | }
|
78 | })
|
79 | `);
|
80 |
|
81 | expect(messages).to.eql([
|
82 | 'O, hai, <span>{name}</span>.',
|
83 | 'You look <em>nice</em> today, <strong>{this.props.subject}</strong>!'
|
84 | ]);
|
85 | });
|
86 |
|
87 | it('extracts strings with nested components with attributes', function() {
|
88 | let messages = extract(`
|
89 | React.createClass({
|
90 | render() {
|
91 | let name = this.props.name;
|
92 | return <div>
|
93 | <I18N>O, hai, <span title="boop">{name}</span>.</I18N>
|
94 | <I18N>You look <a href="#nice">nice</a> today, <strong>{this.props.subject}</strong>!</I18N>
|
95 | </div>;
|
96 | }
|
97 | })
|
98 | `);
|
99 |
|
100 | expect(messages).to.eql([
|
101 | 'O, hai, <span title="boop">{name}</span>.',
|
102 | 'You look <a href="#nice">nice</a> today, <strong>{this.props.subject}</strong>!'
|
103 | ]);
|
104 | });
|
105 |
|
106 | it('extracts strings with nested components with i18n-id attributes', function() {
|
107 | let messages = extract(`
|
108 | React.createClass({
|
109 | render() {
|
110 | let name = this.props.name;
|
111 | return <div>
|
112 | <I18N><span i18n-id="step-2" className="step-text">Step 2: </span>Add your organization to Idealist</I18N>
|
113 | </div>;
|
114 | }
|
115 | })
|
116 | `);
|
117 |
|
118 | expect(messages).to.eql([
|
119 | '<span:step-2>Step 2: </span:step-2>Add your organization to Idealist'
|
120 | ]);
|
121 | });
|
122 |
|
123 | it('extracts strings with nested components with namespaced i18n-id', function() {
|
124 | let messages = extract(`
|
125 | React.createClass({
|
126 | render() {
|
127 | let name = this.props.name;
|
128 | return <div>
|
129 | <I18N><span:step-2 className="step-text">Step 2: </span:step-2>Add your organization to Idealist</I18N>
|
130 | </div>;
|
131 | }
|
132 | })
|
133 | `);
|
134 |
|
135 | expect(messages).to.eql([
|
136 | '<span:step-2>Step 2: </span:step-2>Add your organization to Idealist'
|
137 | ]);
|
138 | });
|
139 |
|
140 | it('extracts strings with nested components with no children', function() {
|
141 | let messages = extract(`
|
142 | React.createClass({
|
143 | render() {
|
144 | let name = this.props.name;
|
145 | return <div>
|
146 | <I18N>Line, <br title="boop"/>Break.</I18N>
|
147 | <I18N>React <Components/>, am I right?</I18N>
|
148 | </div>;
|
149 | }
|
150 | })
|
151 | `);
|
152 |
|
153 | expect(messages).to.eql([
|
154 | 'Line, <br title="boop" />Break.',
|
155 | 'React <Components />, am I right?'
|
156 | ]);
|
157 | });
|
158 |
|
159 | it('does not assume an i18n-id is present when there are unsafe attributes', function() {
|
160 | let messages = extract(`
|
161 | <li><I18N><span i18n-id="stat" className="stat"><ReactIntl.FormattedNumber value={dailyVisitors}/></span>daily visitors</I18N></li>
|
162 | `);
|
163 |
|
164 | expect(messages).to.eql([
|
165 | '<span:stat><ReactIntl.FormattedNumber /></span:stat>daily visitors'
|
166 | ]);
|
167 | });
|
168 |
|
169 | it('deals correctly with whitespace', function() {
|
170 | let messages = extract(`<p id="are-we-eligible" className="in-form-link">
|
171 | <I18N>
|
172 | <a href="/info/Help/Organizations#Eligibility">Are we eligible?</a>
|
173 | </I18N>
|
174 | </p>`);
|
175 |
|
176 | expect(messages).to.eql([
|
177 | '<a href="/info/Help/Organizations#Eligibility">Are we eligible?</a>'
|
178 | ]);
|
179 | });
|
180 |
|
181 | describe('of various strings', function() {
|
182 | var extractions = {
|
183 | 'Hello': [],
|
184 | '<I18N>Hello</I18N>': ['Hello'],
|
185 | 'i18n("world")': ['world'],
|
186 | '<I18N><a href="foo">tag with only safe attributes</a></I18N>': ['<a href="foo">tag with only safe attributes</a>'],
|
187 | '<I18N><a:link href="foo" target="_blank">tag with unsafe attributes</a:link></I18N>': ['<a:link href="foo">tag with unsafe attributes</a:link>'],
|
188 | '<I18N><a href="foo" target="_blank" i18n-id="link">tag with unsafe attributes</a></I18N>': ['<a:link href="foo">tag with unsafe attributes</a:link>'],
|
189 | '<I18N><SelfClosing i18n-id="foo" attr="attr" /></I18N>': ['<SelfClosing:foo />'],
|
190 | '<I18N><SelfClosing /></I18N>': ['<SelfClosing />'],
|
191 | '<I18N><SelfClosing:a /><SelfClosing:b /></I18N>': ['<SelfClosing:a /><SelfClosing:b />'],
|
192 | '<I18N><Member.Name /></I18N>': ['<Member.Name />'],
|
193 | '<I18N><a><b><i>Deeply nested</i> nested <i>nested</i> nested</b> tags</a></I18N>': ['<a><b><i>Deeply nested</i> nested <i>nested</i> nested</b> tags</a>'],
|
194 | '<I18N>Cat: {hat}</I18N>': ['Cat: {hat}'],
|
195 | '<I18N>And now {a.member.expression}</I18N>': ['And now {a.member.expression}'],
|
196 | 'var {nested, ...rested} = i18n("hatters"); <I18N>Cat: {nested}</I18N>': ['hatters', 'Cat: {nested}'],
|
197 | '<p><I18N>1: {same.name.different.message}</I18N> <I18N>2: {same.name.different.message}</I18N></p>': ['1: {same.name.different.message}', '2: {same.name.different.message}'],
|
198 | '<I18N><Pluralize:count on={count}><Match when="zero">You have no items</Match><Match when="one">You have one item</Match><Match when="other">You have {count} items</Match></Pluralize:count></I18N>': [
|
199 | '<Pluralize:count><Match when="zero">You have no items</Match><Match when="one">You have one item</Match><Match when="other">You have {count} items</Match></Pluralize:count>'],
|
200 | };
|
201 |
|
202 | it('extracts expected strings', function() {
|
203 | Object.keys(extractions).forEach(input => {
|
204 | try { extract(input); } catch(e) { console.error(e); }
|
205 | assert(I.is(I.fromJS(extractions[input]), I.fromJS(extract(input))),
|
206 | `
|
207 | Incorrect extraction for input
|
208 | ${input}
|
209 | Expected
|
210 | ${extractions[input]}
|
211 | but got
|
212 | ${extract(input)}
|
213 | `);
|
214 | });
|
215 | });
|
216 | });
|
217 | });
|
218 |
|
219 | describe('errors and warnings', function() {
|
220 | it('throws an error when an element has sanitized attributes but no i18n-id', function() {
|
221 | expect(() => extract('<I18N>O, hai, <span className="boop">{name}</span>.</I18N>')).to.throw(InputError);
|
222 | });
|
223 |
|
224 | it('does not require i18n-id on unique components', function() {
|
225 | expect(() => extract('<I18N>O, hai, <Component beep="boop">{name}</Component>.</I18N>')).to.not.throw(InputError);
|
226 | });
|
227 |
|
228 | it('requires i18n-id on duplicated components', function() {
|
229 | expect(() => extract('<I18N>O, hai, <C beep="boop">{name}</C>, <C beep="boöp">{game}</C>.</I18N>')).to.throw(InputError);
|
230 | });
|
231 |
|
232 | it('warns on unextractable messages', function() {
|
233 | var shouldNotBeExtractable = [
|
234 | '<I18N>Nested <I18N>message markers.</I18N></I18N>',
|
235 | 'i18n("Not" + "just a string" + "literal")',
|
236 | 'i18n()',
|
237 | 'i18n("Too many", "arguments")',
|
238 | '<I18N><a target="_blank">Unsafe attributes but no id.</a></I18N>',
|
239 | '<I18N><Doubled/>two of the same Component type without ids<Doubled/></I18N>',
|
240 | '<I18N><Doubled:doubled/>two of the same Component type with the same ids<Doubled:doubled/></I18N>',
|
241 | '<I18N>{"string literal"}</I18N>',
|
242 | '<I18N>{arbitrary.expression()}</I18N>',
|
243 | '<I18N>{("non"+"simple").memberExpression}</I18N>',
|
244 | '<I18N>{computed["memberExpression"]}</I18N>'
|
245 | ];
|
246 |
|
247 | shouldNotBeExtractable.forEach(msg => {
|
248 | expect(() => extract(msg)).to.throw;
|
249 | });
|
250 | });
|
251 | });
|
252 | });
|