@transifex/express 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +8 -0
- package/README.md +472 -0
- package/jest.config.js +194 -0
- package/package.json +44 -0
- package/src/index.js +180 -0
- package/tests/express.test.js +303 -0
package/.eslintrc.json
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
# Transifex Native integration for Express JS
|
|
2
|
+
|
|
3
|
+
## Quick start
|
|
4
|
+
|
|
5
|
+
Install the necessary express packages:
|
|
6
|
+
|
|
7
|
+
```shell
|
|
8
|
+
npm install --save express cookie-parser body-parser ...
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
And the Transifex Native integration:
|
|
12
|
+
|
|
13
|
+
```shell
|
|
14
|
+
npm install --save @transifex/native @transifex/express
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Create an express app and attach the necessary middleware:
|
|
18
|
+
|
|
19
|
+
```javascript
|
|
20
|
+
const express = require('express');
|
|
21
|
+
const cookieParser = require('cookie-parser');
|
|
22
|
+
const bodyParser = require('body-parser');
|
|
23
|
+
|
|
24
|
+
const app = express();
|
|
25
|
+
app.use(cookieParser());
|
|
26
|
+
app.use(bodyParser.urlencoded({ extended: false }));
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Import the Transifex Native libraries and set up:
|
|
30
|
+
|
|
31
|
+
```javascript
|
|
32
|
+
const { TxExpress } = require('@transifex/express');
|
|
33
|
+
|
|
34
|
+
const txExpress = new TxExpress({ token: '...' });
|
|
35
|
+
app.use(txExpress.middleware());
|
|
36
|
+
app.post('/i18n', txExpress.setLocale());
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
> All options passed to the `TxExpress`'s constructor that are not handled by it
|
|
40
|
+
> will be passed on to `tx.init` internally. If you have already initialized the
|
|
41
|
+
> `tx` object, you do not have to supply these options.
|
|
42
|
+
>
|
|
43
|
+
> ```javascript
|
|
44
|
+
> const txExpress = new TxExpress({
|
|
45
|
+
> // TxExpress options
|
|
46
|
+
> daemon: true,
|
|
47
|
+
> ttl: 2 * 60,
|
|
48
|
+
>
|
|
49
|
+
> // tx options
|
|
50
|
+
> token: '...',
|
|
51
|
+
> filterTags: 'mytags',
|
|
52
|
+
> });
|
|
53
|
+
>
|
|
54
|
+
> // is equivalent to
|
|
55
|
+
>
|
|
56
|
+
> const { tx } from '@transifex/native';
|
|
57
|
+
> tx.init({ token: '...', filterTags: 'mytags' })
|
|
58
|
+
> const txExpress = new TxExpress({ daemon: true, ttl: 2 * 60 });
|
|
59
|
+
> ```
|
|
60
|
+
|
|
61
|
+
Finally, fetch available languages and translations and start the server:
|
|
62
|
+
|
|
63
|
+
```javascript
|
|
64
|
+
txExpress.fetch().then(() => {
|
|
65
|
+
app.listen(3000, () => {
|
|
66
|
+
console.log('App listening on port 3000');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### `txExpress.middleware()` middleware
|
|
72
|
+
|
|
73
|
+
```javascript
|
|
74
|
+
app.use(txExpress.middleware());
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The middleware will make sure that you have a `req.t` function to translate the
|
|
78
|
+
argument to the user's selected language.
|
|
79
|
+
|
|
80
|
+
```javascript
|
|
81
|
+
app.get('/', (req, res) => {
|
|
82
|
+
res.send(req.t('Hello world!'));
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The `t`-function has the same interface as `@transifex/native`'s `t`-function.
|
|
87
|
+
So, you can pass all extra arguments, like this:
|
|
88
|
+
|
|
89
|
+
```javascript
|
|
90
|
+
app.get('/', (req, res) => {
|
|
91
|
+
res.send(req.t('Hello world!', { _context: 'foo', _tags: 'bar' }));
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The middleware will also make sure that any templates that are rendered by
|
|
96
|
+
Express will have a `t`-function and a `tx` object in their context. The
|
|
97
|
+
`t`-function will take care of translation (in the same way as `req.t` does)
|
|
98
|
+
and the `tx` object holds a list of available languages and the currently
|
|
99
|
+
selected language code (`tx.languages` and `tx.currentLocale` respectively).
|
|
100
|
+
Using this, you can do:
|
|
101
|
+
|
|
102
|
+
```javascript
|
|
103
|
+
// index.js
|
|
104
|
+
app.set('views', './views');
|
|
105
|
+
app.set('view engine', 'pug');
|
|
106
|
+
|
|
107
|
+
app.get('/', (req, res) => {
|
|
108
|
+
res.render('index.pug');
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
```pug
|
|
114
|
+
// views/index.pug
|
|
115
|
+
html
|
|
116
|
+
body
|
|
117
|
+
form(method='POST' action='/i18n')
|
|
118
|
+
select(name='locale')
|
|
119
|
+
each locale in tx.languages
|
|
120
|
+
option(
|
|
121
|
+
value=locale.code
|
|
122
|
+
selected=locale.code === tx.currentLocale
|
|
123
|
+
)= locale.name
|
|
124
|
+
input(type='submit' value="Change language")
|
|
125
|
+
p= t('Hello World!')
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
This will render a language-select dropdown (with the list of languages
|
|
129
|
+
dynamically fetched by Transifex Native) and a translated string.
|
|
130
|
+
|
|
131
|
+
This (having `t` and `tx` available in the template's context) works regardless
|
|
132
|
+
of which template engine is being used.
|
|
133
|
+
|
|
134
|
+
### Escaping strings
|
|
135
|
+
|
|
136
|
+
Normally, interpolating strings in HTML that is to be rendered by a browser can
|
|
137
|
+
make your application vulnerable to XSS attacks. For this purpose, the
|
|
138
|
+
`t`-function in the express integration (both `req.t` and the `t`-function that
|
|
139
|
+
is available to the template's context) return the escaped version of the
|
|
140
|
+
rendered string. If you are confident that your string is safe to use inside
|
|
141
|
+
HTML or that your template engine takes care of escaping for you, then you must
|
|
142
|
+
use `ut` (available both as `req.ut` and as the `ut` function in your
|
|
143
|
+
templates). Also, be careful of double escaping:
|
|
144
|
+
|
|
145
|
+
```javascript
|
|
146
|
+
// index.js
|
|
147
|
+
|
|
148
|
+
app.get('/', (req, res) => {
|
|
149
|
+
// This will send 'hello <world>' and it will appear as 'hello <world>'
|
|
150
|
+
// in the browser
|
|
151
|
+
res.send(req.t('hello <world>'));
|
|
152
|
+
|
|
153
|
+
// This will send 'hello <world>' and it is dangerous to show in the browser
|
|
154
|
+
res.send(req.ut('hello <world>'));
|
|
155
|
+
})
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```pug
|
|
159
|
+
// views/index.pug
|
|
160
|
+
|
|
161
|
+
// These will send 'hello &lt;world&gt;' and they will appear as
|
|
162
|
+
// 'hello <world>' in the browser
|
|
163
|
+
p #{t('hello <world>')}
|
|
164
|
+
p= t('hello <world>')
|
|
165
|
+
|
|
166
|
+
// These will send 'hello <world>' and they will appear as
|
|
167
|
+
// 'hello <world>' in the browser
|
|
168
|
+
p #{ut('hello <world>')}
|
|
169
|
+
p= ut('hello <world>')
|
|
170
|
+
|
|
171
|
+
// These will send 'hello <world>' and they will appear as
|
|
172
|
+
// 'hello <world>' in the browser
|
|
173
|
+
p !{t('hello <world>')}
|
|
174
|
+
p!= t('hello <world>')
|
|
175
|
+
|
|
176
|
+
// These will send 'hello <world>' and they are dangerous to show in the
|
|
177
|
+
// browser
|
|
178
|
+
p !{ut('hello <world>')}
|
|
179
|
+
p!= ut('hello <world>')
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### `txExpress.setLocale()` handler
|
|
183
|
+
|
|
184
|
+
```javascript
|
|
185
|
+
app.post('/i18n', txExpress.setLocale());
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The `txExpress.setLocale()` endpoint handler (mapped to `/i18n` in the example)
|
|
189
|
+
is used by the user to change their selected language. The form to make this
|
|
190
|
+
happen could look like this:
|
|
191
|
+
|
|
192
|
+
```html
|
|
193
|
+
<form method="POST" action="/i18n">
|
|
194
|
+
<input type="hidden" name="next" value="/current_url" />
|
|
195
|
+
<select name="locale">
|
|
196
|
+
<option value="en">English</option>
|
|
197
|
+
<option value="el">Greek</option>
|
|
198
|
+
<option value="fr">French</option>
|
|
199
|
+
</select>
|
|
200
|
+
<input type="submit" value="change language" />
|
|
201
|
+
</form>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The value of `next` will determine where the user will be redirected to after
|
|
205
|
+
the form is submitted. If `next` is missing, then the user will be redirected
|
|
206
|
+
to the value of `req.headers.referer` which is the page where the form
|
|
207
|
+
originated from.
|
|
208
|
+
|
|
209
|
+
If you make an AJAX POST request with a JSON Content-Type to this endpoint with
|
|
210
|
+
a `locale` field, the server will respond with a `{"status": "success"}` reply,
|
|
211
|
+
after having changed the user's selected language (it will be up to you to
|
|
212
|
+
reload the page if you want to).
|
|
213
|
+
|
|
214
|
+
## Modes
|
|
215
|
+
|
|
216
|
+
The user's selected language can be saved and retrieved with a number of
|
|
217
|
+
available modes:
|
|
218
|
+
|
|
219
|
+
### Cookie (default)
|
|
220
|
+
|
|
221
|
+
This saves the selected language on a cookie named after the value of 'options.name'.
|
|
222
|
+
|
|
223
|
+
```javascript
|
|
224
|
+
const { TxExpress, CookieMode } = require('@transifex/express');
|
|
225
|
+
const txExpress = new TxExpress({
|
|
226
|
+
mode: CookieMode({ name: 'my-tx-cookie' }),
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
It must be used alongside `cookie-parser`:
|
|
231
|
+
|
|
232
|
+
```javascript
|
|
233
|
+
const express = require('express');
|
|
234
|
+
const bodyParser = require('body-parser');
|
|
235
|
+
const app = express();
|
|
236
|
+
app.use(bodyParser.urlencoded({ extended: false }));
|
|
237
|
+
|
|
238
|
+
const cookieParser = require('cookie-parser');
|
|
239
|
+
app.use(cookieParser());
|
|
240
|
+
|
|
241
|
+
const { TxExpress, CookieMode } = require('@transifex/express');
|
|
242
|
+
const txExpress = new TxExpress({
|
|
243
|
+
token: '...',
|
|
244
|
+
mode: CookieMode({ name: 'my-tx-cookie' }),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
app.use(txExpress.middleware());
|
|
248
|
+
app.post('/i18n', txExpress.setLocale());
|
|
249
|
+
app.get('/', (req, res) => { res.send(req.t('Hello world!')); });
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Also accepts the `cookieOptions` option which will be forwarded to `req.cookie()`.
|
|
253
|
+
|
|
254
|
+
### Signed cookie
|
|
255
|
+
|
|
256
|
+
This saves the selected language on a signed cookie named after the value of
|
|
257
|
+
'options.name'.
|
|
258
|
+
|
|
259
|
+
```javascript
|
|
260
|
+
const { TxExpress, SignedCookieMode } = require('@transifex/express');
|
|
261
|
+
const txExpress = new TxExpress({
|
|
262
|
+
mode: SignedCookieMode({ name: 'my-tx-cookie' }),
|
|
263
|
+
});
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
It must be used alongside `cookie-parser` which needs to be supplied with a secret:
|
|
267
|
+
|
|
268
|
+
```javascript
|
|
269
|
+
const express = require('express');
|
|
270
|
+
const bodyParser = require('body-parser');
|
|
271
|
+
const app = express();
|
|
272
|
+
app.use(bodyParser.urlencoded({ extended: false }));
|
|
273
|
+
|
|
274
|
+
const cookieParser = require('cookie-parser');
|
|
275
|
+
app.use(cookieParser('mysecret'));
|
|
276
|
+
|
|
277
|
+
const { TxExpress, SignedCookieMode } = require('@transifex/express');
|
|
278
|
+
const txExpress = new TxExpress({
|
|
279
|
+
token: '...',
|
|
280
|
+
mode: SignedCookieMode({ name: 'my-tx-cookie' }),
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
app.use(txExpress.middleware());
|
|
284
|
+
app.post('/i18n', txExpress.setLocale());
|
|
285
|
+
app.get('/', (req, res) => { res.send(req.t('Hello world!')); });
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Also accepts the `cookieOptions` option which will be forwarded to `req.cookie()`.
|
|
289
|
+
|
|
290
|
+
### Session
|
|
291
|
+
|
|
292
|
+
This saves the selected language on a session variable named after the value of
|
|
293
|
+
'options.name'.
|
|
294
|
+
|
|
295
|
+
```javascript
|
|
296
|
+
const { TxExpress, SessionMode } = require('@transifex/express');
|
|
297
|
+
const txExpress = new TxExpress({
|
|
298
|
+
mode: SessionMode({ name: 'my-tx-cookie' }),
|
|
299
|
+
});
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
It must be used alongside `express-session` or `cookie-session`:
|
|
303
|
+
|
|
304
|
+
```javascript
|
|
305
|
+
const express = require('express');
|
|
306
|
+
const bodyParser = require('body-parser');
|
|
307
|
+
const app = express();
|
|
308
|
+
app.use(bodyParser.urlencoded({ extended: false }));
|
|
309
|
+
|
|
310
|
+
const session = require('express-session');
|
|
311
|
+
// or
|
|
312
|
+
const cookieSession = require('cookie-session');
|
|
313
|
+
|
|
314
|
+
app.use(session({ secret: 'mysecret', ... }));
|
|
315
|
+
// or
|
|
316
|
+
app.use(cookieSession({ keys: ['mysecret'], ... }));
|
|
317
|
+
|
|
318
|
+
const { TxExpress, SessionMode } = require('@transifex/express');
|
|
319
|
+
|
|
320
|
+
const txExpress = new TxExpress({
|
|
321
|
+
token: '...',
|
|
322
|
+
mode: SessionMode({ name: 'my-tx-cookie' }),
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
app.use(txExpress.middleware());
|
|
326
|
+
app.post('/i18n', txExpress.setLocale());
|
|
327
|
+
app.get('/', (req, res) => { res.send(req.t('Hello world!')); });
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Custom modes
|
|
331
|
+
|
|
332
|
+
The values for the `mode` options are objects that implement the
|
|
333
|
+
`setLocale(req, res, locale)` and `getLocale(req, res)` functions. You can
|
|
334
|
+
easily implement your own. A sample implementation could look like this:
|
|
335
|
+
|
|
336
|
+
```javascript
|
|
337
|
+
const myMode = {
|
|
338
|
+
userLocales: {}, // User ID to selected locale map
|
|
339
|
+
setLocale(req, res, locale) {
|
|
340
|
+
this.userLocales[req.cookies.userId] = locale;
|
|
341
|
+
},
|
|
342
|
+
getLocale(req, res) {
|
|
343
|
+
return this.userLocales[req.cookies.userId];
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const txExpress = new TxExpress({ mode: myMode });
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
## Extracting strings with `txjs-cli`
|
|
351
|
+
|
|
352
|
+
The `txjs-cli` program from the `@transifex/cli` package will manage to extract
|
|
353
|
+
invocations of the `req.t` function in your source code, as well as invocations
|
|
354
|
+
of the `t` function in '.pug' and '.ejs' templates.
|
|
355
|
+
|
|
356
|
+
```shell
|
|
357
|
+
➜ npm install @transifex/cli
|
|
358
|
+
|
|
359
|
+
➜ npx txjs-cli push views -v
|
|
360
|
+
|
|
361
|
+
Parsing all files to detect translatable content...
|
|
362
|
+
✓ Processed 2 file(s) and found 2 translatable phrases.
|
|
363
|
+
✓ Content detected in 2 file(s).
|
|
364
|
+
/views/index.ejs
|
|
365
|
+
└─ This string originated from a EJS file
|
|
366
|
+
└─ occurrences: ["/views/index.ejs"]
|
|
367
|
+
/views/index.pug
|
|
368
|
+
└─ This string originated from a PUG file
|
|
369
|
+
└─ occurrences: ["/views/index.pug"]
|
|
370
|
+
|
|
371
|
+
Uploading content to Transifex... Success
|
|
372
|
+
✓ Successfully pushed strings to Transifex:
|
|
373
|
+
Created strings: 2
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
It is easy to enhance support for express template engines in `txjs-cli`,
|
|
377
|
+
especially if the template engine in question works by converting a template to
|
|
378
|
+
javascript code that can be then fed to the normal extraction process. In fact,
|
|
379
|
+
this in the only piece of code that was needed in order to extend support to
|
|
380
|
+
.pug and .ejs templates:
|
|
381
|
+
|
|
382
|
+
```javascript
|
|
383
|
+
// transifex-javascript/packages/cli/src/api/extract.js
|
|
384
|
+
|
|
385
|
+
function extractPhrases(file, relativeFile, options = {}) {
|
|
386
|
+
|
|
387
|
+
// ...
|
|
388
|
+
|
|
389
|
+
let source = fs.readFileSync(file, 'utf8');
|
|
390
|
+
|
|
391
|
+
if (path.extname(file) === '.pug') {
|
|
392
|
+
source = pug.compileClient(source);
|
|
393
|
+
} else if (path.extname(file) === '.ejs') {
|
|
394
|
+
const template = new ejs.Template(source);
|
|
395
|
+
template.generateSource();
|
|
396
|
+
source = template.source;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ...
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
So, if your template engine of choice is not supported by `txjs-cli` yet,
|
|
404
|
+
please consider contributing a pull request 😉.
|
|
405
|
+
|
|
406
|
+
## API
|
|
407
|
+
|
|
408
|
+
### TxExpress
|
|
409
|
+
|
|
410
|
+
```javascript
|
|
411
|
+
new TxExpress({
|
|
412
|
+
|
|
413
|
+
// How to save the selected language. Must implement the `setLocale(req, res,
|
|
414
|
+
// locale)` and `getLocale(req, res)` methods. Builtin modes: `CookieMode`,
|
|
415
|
+
// `SignedCookieMode`, `SessionMode`.
|
|
416
|
+
mode: Object,
|
|
417
|
+
|
|
418
|
+
// Whether to fall back to the request's 'Accept-Language' header (set by the
|
|
419
|
+
// browser) if the selected language isn't set, default: true
|
|
420
|
+
fallBackToAcceptLanguage: Boolean
|
|
421
|
+
|
|
422
|
+
// The locale to fall back to if both the mode and the 'Accept-Language'
|
|
423
|
+
// header fail to produce a result, default: 'en'
|
|
424
|
+
sourceLocale: String,
|
|
425
|
+
|
|
426
|
+
// If the server should periodically refetch translations from Transifex,
|
|
427
|
+
// default: true
|
|
428
|
+
daemon: Boolean,
|
|
429
|
+
|
|
430
|
+
// If daemon is true, how often to refetch translations in seconds, default:
|
|
431
|
+
// 10 minutes
|
|
432
|
+
ttl: Integer,
|
|
433
|
+
|
|
434
|
+
// How to display log messages; a straightforward option would be
|
|
435
|
+
// `console.log`, default: noop
|
|
436
|
+
logging: Function
|
|
437
|
+
})
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### CookieMode
|
|
441
|
+
|
|
442
|
+
```javascript
|
|
443
|
+
CookieMode({
|
|
444
|
+
// The name of the cookie to be used
|
|
445
|
+
name: String,
|
|
446
|
+
|
|
447
|
+
// Extra options passed to the `req.cookie()` function
|
|
448
|
+
cookieOptions: Object,
|
|
449
|
+
});
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### SignedCookieMode
|
|
453
|
+
|
|
454
|
+
```javascript
|
|
455
|
+
SignedCookieMode({
|
|
456
|
+
// The name of the cookie to be used
|
|
457
|
+
name: String,
|
|
458
|
+
|
|
459
|
+
// Extra options passed to the `req.cookie()` function; the `signed: true`
|
|
460
|
+
// option will always be set
|
|
461
|
+
cookieOptions: Object,
|
|
462
|
+
});
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### SessionMode
|
|
466
|
+
|
|
467
|
+
```javascript
|
|
468
|
+
SessionMode({
|
|
469
|
+
// The name of the session field to be used
|
|
470
|
+
name: String,
|
|
471
|
+
});
|
|
472
|
+
```
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* For a detailed explanation regarding each configuration property, visit:
|
|
3
|
+
* https://jestjs.io/docs/configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
// All imported modules in your tests should be mocked automatically
|
|
8
|
+
// automock: false,
|
|
9
|
+
|
|
10
|
+
// Stop running tests after `n` failures
|
|
11
|
+
// bail: 0,
|
|
12
|
+
|
|
13
|
+
// The directory where Jest should store its cached dependency information
|
|
14
|
+
// cacheDirectory: "/tmp/jest_rs",
|
|
15
|
+
|
|
16
|
+
// Automatically clear mock calls, instances and results before every test
|
|
17
|
+
clearMocks: true,
|
|
18
|
+
|
|
19
|
+
// Indicates whether the coverage information should be collected while executing the test
|
|
20
|
+
collectCoverage: true,
|
|
21
|
+
|
|
22
|
+
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
|
23
|
+
// collectCoverageFrom: undefined,
|
|
24
|
+
|
|
25
|
+
// The directory where Jest should output its coverage files
|
|
26
|
+
coverageDirectory: "coverage",
|
|
27
|
+
|
|
28
|
+
// An array of regexp pattern strings used to skip coverage collection
|
|
29
|
+
// coveragePathIgnorePatterns: [
|
|
30
|
+
// "/node_modules/"
|
|
31
|
+
// ],
|
|
32
|
+
|
|
33
|
+
// Indicates which provider should be used to instrument code for coverage
|
|
34
|
+
coverageProvider: "v8",
|
|
35
|
+
|
|
36
|
+
// A list of reporter names that Jest uses when writing coverage reports
|
|
37
|
+
// coverageReporters: [
|
|
38
|
+
// "json",
|
|
39
|
+
// "text",
|
|
40
|
+
// "lcov",
|
|
41
|
+
// "clover"
|
|
42
|
+
// ],
|
|
43
|
+
|
|
44
|
+
// An object that configures minimum threshold enforcement for coverage results
|
|
45
|
+
// coverageThreshold: undefined,
|
|
46
|
+
|
|
47
|
+
// A path to a custom dependency extractor
|
|
48
|
+
// dependencyExtractor: undefined,
|
|
49
|
+
|
|
50
|
+
// Make calling deprecated APIs throw helpful error messages
|
|
51
|
+
// errorOnDeprecated: false,
|
|
52
|
+
|
|
53
|
+
// Force coverage collection from ignored files using an array of glob patterns
|
|
54
|
+
// forceCoverageMatch: [],
|
|
55
|
+
|
|
56
|
+
// A path to a module which exports an async function that is triggered once before all test suites
|
|
57
|
+
// globalSetup: undefined,
|
|
58
|
+
|
|
59
|
+
// A path to a module which exports an async function that is triggered once after all test suites
|
|
60
|
+
// globalTeardown: undefined,
|
|
61
|
+
|
|
62
|
+
// A set of global variables that need to be available in all test environments
|
|
63
|
+
// globals: {},
|
|
64
|
+
|
|
65
|
+
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
|
|
66
|
+
// maxWorkers: "50%",
|
|
67
|
+
|
|
68
|
+
// An array of directory names to be searched recursively up from the requiring module's location
|
|
69
|
+
// moduleDirectories: [
|
|
70
|
+
// "node_modules"
|
|
71
|
+
// ],
|
|
72
|
+
|
|
73
|
+
// An array of file extensions your modules use
|
|
74
|
+
// moduleFileExtensions: [
|
|
75
|
+
// "js",
|
|
76
|
+
// "jsx",
|
|
77
|
+
// "ts",
|
|
78
|
+
// "tsx",
|
|
79
|
+
// "json",
|
|
80
|
+
// "node"
|
|
81
|
+
// ],
|
|
82
|
+
|
|
83
|
+
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
|
84
|
+
// moduleNameMapper: {},
|
|
85
|
+
|
|
86
|
+
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
|
87
|
+
// modulePathIgnorePatterns: [],
|
|
88
|
+
|
|
89
|
+
// Activates notifications for test results
|
|
90
|
+
// notify: false,
|
|
91
|
+
|
|
92
|
+
// An enum that specifies notification mode. Requires { notify: true }
|
|
93
|
+
// notifyMode: "failure-change",
|
|
94
|
+
|
|
95
|
+
// A preset that is used as a base for Jest's configuration
|
|
96
|
+
// preset: undefined,
|
|
97
|
+
|
|
98
|
+
// Run tests from one or more projects
|
|
99
|
+
// projects: undefined,
|
|
100
|
+
|
|
101
|
+
// Use this configuration option to add custom reporters to Jest
|
|
102
|
+
// reporters: undefined,
|
|
103
|
+
|
|
104
|
+
// Automatically reset mock state before every test
|
|
105
|
+
// resetMocks: false,
|
|
106
|
+
|
|
107
|
+
// Reset the module registry before running each individual test
|
|
108
|
+
// resetModules: false,
|
|
109
|
+
|
|
110
|
+
// A path to a custom resolver
|
|
111
|
+
// resolver: undefined,
|
|
112
|
+
|
|
113
|
+
// Automatically restore mock state and implementation before every test
|
|
114
|
+
// restoreMocks: false,
|
|
115
|
+
|
|
116
|
+
// The root directory that Jest should scan for tests and modules within
|
|
117
|
+
// rootDir: undefined,
|
|
118
|
+
|
|
119
|
+
// A list of paths to directories that Jest should use to search for files in
|
|
120
|
+
// roots: [
|
|
121
|
+
// "<rootDir>"
|
|
122
|
+
// ],
|
|
123
|
+
|
|
124
|
+
// Allows you to use a custom runner instead of Jest's default test runner
|
|
125
|
+
// runner: "jest-runner",
|
|
126
|
+
|
|
127
|
+
// The paths to modules that run some code to configure or set up the testing environment before each test
|
|
128
|
+
// setupFiles: [],
|
|
129
|
+
|
|
130
|
+
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
|
131
|
+
// setupFilesAfterEnv: [],
|
|
132
|
+
|
|
133
|
+
// The number of seconds after which a test is considered as slow and reported as such in the results.
|
|
134
|
+
// slowTestThreshold: 5,
|
|
135
|
+
|
|
136
|
+
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
|
137
|
+
// snapshotSerializers: [],
|
|
138
|
+
|
|
139
|
+
// The test environment that will be used for testing
|
|
140
|
+
// testEnvironment: "jest-environment-node",
|
|
141
|
+
|
|
142
|
+
// Options that will be passed to the testEnvironment
|
|
143
|
+
// testEnvironmentOptions: {},
|
|
144
|
+
|
|
145
|
+
// Adds a location field to test results
|
|
146
|
+
// testLocationInResults: false,
|
|
147
|
+
|
|
148
|
+
// The glob patterns Jest uses to detect test files
|
|
149
|
+
// testMatch: [
|
|
150
|
+
// "**/__tests__/**/*.[jt]s?(x)",
|
|
151
|
+
// "**/?(*.)+(spec|test).[tj]s?(x)"
|
|
152
|
+
// ],
|
|
153
|
+
|
|
154
|
+
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
|
155
|
+
// testPathIgnorePatterns: [
|
|
156
|
+
// "/node_modules/"
|
|
157
|
+
// ],
|
|
158
|
+
|
|
159
|
+
// The regexp pattern or array of patterns that Jest uses to detect test files
|
|
160
|
+
// testRegex: [],
|
|
161
|
+
|
|
162
|
+
// This option allows the use of a custom results processor
|
|
163
|
+
// testResultsProcessor: undefined,
|
|
164
|
+
|
|
165
|
+
// This option allows use of a custom test runner
|
|
166
|
+
// testRunner: "jest-circus/runner",
|
|
167
|
+
|
|
168
|
+
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
|
|
169
|
+
// testURL: "http://localhost",
|
|
170
|
+
|
|
171
|
+
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
|
|
172
|
+
// timers: "real",
|
|
173
|
+
|
|
174
|
+
// A map from regular expressions to paths to transformers
|
|
175
|
+
// transform: undefined,
|
|
176
|
+
|
|
177
|
+
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
|
178
|
+
// transformIgnorePatterns: [
|
|
179
|
+
// "/node_modules/",
|
|
180
|
+
// "\\.pnp\\.[^\\/]+$"
|
|
181
|
+
// ],
|
|
182
|
+
|
|
183
|
+
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
|
184
|
+
// unmockedModulePathPatterns: undefined,
|
|
185
|
+
|
|
186
|
+
// Indicates whether each individual test should be reported during the run
|
|
187
|
+
// verbose: undefined,
|
|
188
|
+
|
|
189
|
+
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
|
190
|
+
// watchPathIgnorePatterns: [],
|
|
191
|
+
|
|
192
|
+
// Whether to use watchman for file crawling
|
|
193
|
+
// watchman: true,
|
|
194
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@transifex/express",
|
|
3
|
+
"version": "2.4.1",
|
|
4
|
+
"description": "Transifex Native for Express JS",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"transifex",
|
|
7
|
+
"i18n",
|
|
8
|
+
"l10n",
|
|
9
|
+
"localization"
|
|
10
|
+
],
|
|
11
|
+
"author": "Transifex",
|
|
12
|
+
"homepage": "https://github.com/transifex/transifex-javascript/tree/master/packages/express",
|
|
13
|
+
"license": "Apache-2.0",
|
|
14
|
+
"main": "src/index.js",
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"repository": "git://github.com/transifex/transifex-javascript.git",
|
|
19
|
+
"scripts": {
|
|
20
|
+
"lint": "eslint src/",
|
|
21
|
+
"build": "",
|
|
22
|
+
"test": "jest",
|
|
23
|
+
"publish-npm": "npm publish"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/transifex/transifex-javascript/issues"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=12.0.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@transifex/native": "^2.4.1",
|
|
33
|
+
"eslint": "^8.10.0",
|
|
34
|
+
"eslint-config-airbnb-base": "^15.0.0",
|
|
35
|
+
"eslint-plugin-import": "^2.25.4",
|
|
36
|
+
"jest": "^27.5.1"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"type-is": "^1.6.18"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@transifex/native": "^2.4.1"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
const { tx, escape } = require('@transifex/native');
|
|
2
|
+
const typeis = require('type-is');
|
|
3
|
+
|
|
4
|
+
function CookieMode({ name = 'tx-locale', cookieOptions } = {}) {
|
|
5
|
+
return {
|
|
6
|
+
setLocale(req, res, locale) {
|
|
7
|
+
res.cookie(name, locale, cookieOptions);
|
|
8
|
+
},
|
|
9
|
+
getLocale(req) {
|
|
10
|
+
return req.cookies[name];
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function SignedCookieMode({ name = 'tx-locale', cookieOptions } = {}) {
|
|
16
|
+
return {
|
|
17
|
+
setLocale(req, res, locale) {
|
|
18
|
+
res.cookie(name, locale, { ...cookieOptions, signed: true });
|
|
19
|
+
},
|
|
20
|
+
getLocale(req) {
|
|
21
|
+
return req.signedCookies[name];
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function SessionMode({ name = 'tx-locale' } = {}) {
|
|
27
|
+
return {
|
|
28
|
+
setLocale(req, res, locale) {
|
|
29
|
+
req.session[name] = locale;
|
|
30
|
+
},
|
|
31
|
+
getLocale(req) {
|
|
32
|
+
return req.session[name];
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function noop() {}
|
|
38
|
+
|
|
39
|
+
class TxExpress {
|
|
40
|
+
constructor(options = {}) {
|
|
41
|
+
// https://www.kbairak.net/programming/python/2020/09/16/global-singleton-vs-instance-for-libraries.html
|
|
42
|
+
this.mode = CookieMode();
|
|
43
|
+
this.fallBackToAcceptLanguage = true;
|
|
44
|
+
this.sourceLocale = 'en';
|
|
45
|
+
this.daemon = true;
|
|
46
|
+
this.ttl = 10 * 60;
|
|
47
|
+
this.logging = noop;
|
|
48
|
+
this.setup(options);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setup({
|
|
52
|
+
mode,
|
|
53
|
+
fallBackToAcceptLanguage,
|
|
54
|
+
sourceLocale,
|
|
55
|
+
daemon,
|
|
56
|
+
ttl,
|
|
57
|
+
logging,
|
|
58
|
+
...txOptions
|
|
59
|
+
} = {}) {
|
|
60
|
+
if (txOptions) {
|
|
61
|
+
tx.init(txOptions);
|
|
62
|
+
}
|
|
63
|
+
if (mode) {
|
|
64
|
+
this.mode = mode;
|
|
65
|
+
}
|
|
66
|
+
if (fallBackToAcceptLanguage !== undefined) {
|
|
67
|
+
this.fallBackToAcceptLanguage = fallBackToAcceptLanguage;
|
|
68
|
+
}
|
|
69
|
+
if (sourceLocale) {
|
|
70
|
+
this.sourceLocale = sourceLocale;
|
|
71
|
+
}
|
|
72
|
+
if (daemon !== undefined) {
|
|
73
|
+
this.daemon = daemon;
|
|
74
|
+
}
|
|
75
|
+
if (ttl) {
|
|
76
|
+
this.ttl = ttl;
|
|
77
|
+
}
|
|
78
|
+
if (logging) {
|
|
79
|
+
this.logging = logging;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
middleware() {
|
|
84
|
+
return (req, res, next) => {
|
|
85
|
+
let locale = this.mode.getLocale(req, res);
|
|
86
|
+
|
|
87
|
+
if (
|
|
88
|
+
!locale
|
|
89
|
+
&& this.fallBackToAcceptLanguage
|
|
90
|
+
&& req.headers['accept-language']
|
|
91
|
+
) {
|
|
92
|
+
// The header looks like: 'da, en-gb;q=0.8, en;'
|
|
93
|
+
// Locales without a 'q' value will be considered first, the rest will
|
|
94
|
+
// be sorted based on their q value. After sorting, we will use the
|
|
95
|
+
// first locale that is supported by Transifex Native.
|
|
96
|
+
const locales = req.headers['accept-language']
|
|
97
|
+
.split(',')
|
|
98
|
+
.map((section) => section.trim())
|
|
99
|
+
.map((section) => section.split(';'));
|
|
100
|
+
const localesWithoutQ = locales
|
|
101
|
+
.filter(([, q]) => !q)
|
|
102
|
+
.map(([code]) => code);
|
|
103
|
+
const localesWithQ = locales.filter(([, q]) => !!q)
|
|
104
|
+
.sort(([, left], [, right]) => (
|
|
105
|
+
parseFloat(right.substring(2)) - parseFloat(left.substring(2))
|
|
106
|
+
))
|
|
107
|
+
.map(([code]) => code);
|
|
108
|
+
const finalLocales = localesWithoutQ.concat(localesWithQ);
|
|
109
|
+
|
|
110
|
+
for (let i = 0; i < finalLocales.length; i++) {
|
|
111
|
+
const current = finalLocales[i];
|
|
112
|
+
if (tx.locales.indexOf(current) !== -1) {
|
|
113
|
+
locale = current;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!locale) { locale = this.sourceLocale; }
|
|
120
|
+
|
|
121
|
+
const ut = (...args) => tx.translateLocale(locale, ...args);
|
|
122
|
+
const t = (...args) => escape(ut(...args));
|
|
123
|
+
|
|
124
|
+
req.ut = ut;
|
|
125
|
+
req.t = t;
|
|
126
|
+
|
|
127
|
+
const oldRender = res.render.bind(res);
|
|
128
|
+
res.render = (view, locals, ...args) => {
|
|
129
|
+
const actualLocals = locals || {};
|
|
130
|
+
Object.assign(actualLocals, {
|
|
131
|
+
ut,
|
|
132
|
+
t,
|
|
133
|
+
tx: { languages: tx.languages, currentLocale: locale },
|
|
134
|
+
});
|
|
135
|
+
return oldRender(view, actualLocals, ...args);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
next();
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
setLocale() {
|
|
143
|
+
return (req, res) => {
|
|
144
|
+
const locale = req.body.locale || this.sourceLocale;
|
|
145
|
+
|
|
146
|
+
this.mode.setLocale(req, res, locale);
|
|
147
|
+
if (typeis(req, ['json'])) {
|
|
148
|
+
res.json({ status: 'success' });
|
|
149
|
+
} else {
|
|
150
|
+
res.redirect(req.body.next || req.headers.referer);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async fetch() {
|
|
156
|
+
const _fetch = async () => {
|
|
157
|
+
await tx.getLocales();
|
|
158
|
+
for (let i = 0; i < tx.locales.length; i++) {
|
|
159
|
+
const locale = tx.locales[i];
|
|
160
|
+
/* eslint-disable no-await-in-loop */
|
|
161
|
+
await tx.fetchTranslations(locale);
|
|
162
|
+
/* eslint-enable */
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
this.logging('Transifex Native: fetching translations');
|
|
167
|
+
await _fetch();
|
|
168
|
+
this.logging('Transifex Native: done');
|
|
169
|
+
if (this.daemon) {
|
|
170
|
+
setInterval(_fetch, this.ttl * 1000);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = {
|
|
176
|
+
TxExpress,
|
|
177
|
+
CookieMode,
|
|
178
|
+
SignedCookieMode,
|
|
179
|
+
SessionMode,
|
|
180
|
+
};
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/* globals jest test expect */
|
|
2
|
+
|
|
3
|
+
const typeis = require('type-is');
|
|
4
|
+
|
|
5
|
+
const { tx } = require('@transifex/native');
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
TxExpress,
|
|
9
|
+
CookieMode,
|
|
10
|
+
SignedCookieMode,
|
|
11
|
+
SessionMode,
|
|
12
|
+
} = require('../src');
|
|
13
|
+
|
|
14
|
+
jest.mock('type-is');
|
|
15
|
+
|
|
16
|
+
test('TxExpress default values', () => {
|
|
17
|
+
const txExpress = new TxExpress();
|
|
18
|
+
|
|
19
|
+
// Ideally this should work but the functions are not considered equal and we
|
|
20
|
+
// have no data members to compare
|
|
21
|
+
// expect(txExpress.mode).toEqual(CookieMode());
|
|
22
|
+
|
|
23
|
+
expect(txExpress.fallBackToAcceptLanguage).toBe(true);
|
|
24
|
+
expect(txExpress.sourceLocale).toBe('en');
|
|
25
|
+
expect(txExpress.daemon).toBe(true);
|
|
26
|
+
expect(txExpress.ttl).toBe(10 * 60);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('TxExpress custom values', () => {
|
|
30
|
+
const customOptions = {
|
|
31
|
+
mode: 'other',
|
|
32
|
+
fallBackToAcceptLanguage: false,
|
|
33
|
+
sourceLocale: 'other',
|
|
34
|
+
daemon: false,
|
|
35
|
+
ttl: 3,
|
|
36
|
+
logging: 'other',
|
|
37
|
+
};
|
|
38
|
+
let txExpress = new TxExpress(customOptions);
|
|
39
|
+
expect(txExpress).toEqual(customOptions);
|
|
40
|
+
|
|
41
|
+
txExpress = new TxExpress();
|
|
42
|
+
txExpress.setup(customOptions);
|
|
43
|
+
expect(txExpress).toEqual(customOptions);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('Middleware with cookies', () => {
|
|
47
|
+
const languages = [{ code: 'en', name: 'English' }, { code: 'fr', name: 'French' }];
|
|
48
|
+
tx.languages = languages;
|
|
49
|
+
tx.cache.update('fr', { foo: 'translation' });
|
|
50
|
+
|
|
51
|
+
const txExpress = new TxExpress({ mode: CookieMode({ name: 'tx-locale' }) });
|
|
52
|
+
const req = {
|
|
53
|
+
cookies: { 'tx-locale': 'fr' },
|
|
54
|
+
headers: {},
|
|
55
|
+
};
|
|
56
|
+
const res = {
|
|
57
|
+
render(view, locals) {
|
|
58
|
+
return [locals.t('foo', { _key: 'foo' }), locals.tx];
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
let nextCalled = false;
|
|
62
|
+
function next() { nextCalled = true; }
|
|
63
|
+
|
|
64
|
+
txExpress.middleware()(req, res, next);
|
|
65
|
+
|
|
66
|
+
expect(req.t('foo', { _key: 'foo' })).toBe('translation');
|
|
67
|
+
expect(res.render()).toEqual(['translation', { languages, currentLocale: 'fr' }]);
|
|
68
|
+
expect(nextCalled).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('Middleware with signed cookies', () => {
|
|
72
|
+
const languages = [{ code: 'en', name: 'English' }, { code: 'fr', name: 'French' }];
|
|
73
|
+
tx.languages = languages;
|
|
74
|
+
tx.cache.update('fr', { foo: 'translation' });
|
|
75
|
+
|
|
76
|
+
const txExpress = new TxExpress({ mode: SignedCookieMode({ name: 'tx-locale' }) });
|
|
77
|
+
const req = {
|
|
78
|
+
signedCookies: { 'tx-locale': 'fr' },
|
|
79
|
+
headers: {},
|
|
80
|
+
};
|
|
81
|
+
const res = {
|
|
82
|
+
render(view, locals) {
|
|
83
|
+
return [locals.t('foo', { _key: 'foo' }), locals.tx];
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
let nextCalled = false;
|
|
87
|
+
function next() { nextCalled = true; }
|
|
88
|
+
|
|
89
|
+
txExpress.middleware()(req, res, next);
|
|
90
|
+
|
|
91
|
+
expect(req.t('foo', { _key: 'foo' })).toBe('translation');
|
|
92
|
+
expect(res.render()).toEqual(['translation', { languages, currentLocale: 'fr' }]);
|
|
93
|
+
expect(nextCalled).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('Middleware with sessions', () => {
|
|
97
|
+
const languages = [{ code: 'en', name: 'English' }, { code: 'fr', name: 'French' }];
|
|
98
|
+
tx.languages = languages;
|
|
99
|
+
tx.cache.update('fr', { foo: 'translation' });
|
|
100
|
+
|
|
101
|
+
const txExpress = new TxExpress({ mode: SessionMode({ name: 'tx-locale' }) });
|
|
102
|
+
const req = {
|
|
103
|
+
session: { 'tx-locale': 'fr' },
|
|
104
|
+
headers: {},
|
|
105
|
+
};
|
|
106
|
+
const res = {
|
|
107
|
+
render(view, locals) {
|
|
108
|
+
return [locals.t('foo', { _key: 'foo' }), locals.tx];
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
let nextCalled = false;
|
|
112
|
+
function next() { nextCalled = true; }
|
|
113
|
+
|
|
114
|
+
txExpress.middleware()(req, res, next);
|
|
115
|
+
|
|
116
|
+
expect(req.t('foo', { _key: 'foo' })).toBe('translation');
|
|
117
|
+
expect(res.render()).toEqual(['translation', { languages, currentLocale: 'fr' }]);
|
|
118
|
+
expect(nextCalled).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('Middleware falls back to header', () => {
|
|
122
|
+
const languages = [{ code: 'en', name: 'English' }, { code: 'fr', name: 'French' }];
|
|
123
|
+
tx.languages = languages;
|
|
124
|
+
tx.locales = ['en', 'fr'];
|
|
125
|
+
tx.cache.update('fr', { foo: 'translation' });
|
|
126
|
+
|
|
127
|
+
const txExpress = new TxExpress({ mode: CookieMode({ name: 'tx-locale' }) });
|
|
128
|
+
const req = {
|
|
129
|
+
cookies: {},
|
|
130
|
+
headers: { 'accept-language': 'de, en;q=0.6, fr;q=0.8' },
|
|
131
|
+
};
|
|
132
|
+
const res = {
|
|
133
|
+
render(view, locals) {
|
|
134
|
+
return [locals.t('foo', { _key: 'foo' }), locals.tx];
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
let nextCalled = false;
|
|
138
|
+
function next() { nextCalled = true; }
|
|
139
|
+
|
|
140
|
+
txExpress.middleware()(req, res, next);
|
|
141
|
+
|
|
142
|
+
expect(req.t('foo', { _key: 'foo' })).toBe('translation');
|
|
143
|
+
expect(res.render()).toEqual(['translation', { languages, currentLocale: 'fr' }]);
|
|
144
|
+
expect(nextCalled).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('setLocale with cookie and form with next', () => {
|
|
148
|
+
typeis.mockReturnValue(false);
|
|
149
|
+
|
|
150
|
+
const txExpress = new TxExpress({ mode: CookieMode({ name: 'tx-locale' }) });
|
|
151
|
+
const req = {
|
|
152
|
+
body: { locale: 'fr', next: 'next' },
|
|
153
|
+
};
|
|
154
|
+
let redirectTo = '';
|
|
155
|
+
const res = {
|
|
156
|
+
cookies: {},
|
|
157
|
+
cookie(key, value) { this.cookies[key] = value; },
|
|
158
|
+
redirect(path) { redirectTo = path; },
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
txExpress.setLocale()(req, res);
|
|
162
|
+
|
|
163
|
+
expect(res.cookies).toEqual({ 'tx-locale': 'fr' });
|
|
164
|
+
expect(redirectTo).toBe('next');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('setLocale with cookie and form without next', () => {
|
|
168
|
+
typeis.mockReturnValue(false);
|
|
169
|
+
|
|
170
|
+
const txExpress = new TxExpress({ mode: CookieMode({ name: 'tx-locale' }) });
|
|
171
|
+
const req = {
|
|
172
|
+
body: { locale: 'fr' },
|
|
173
|
+
headers: { referer: 'referer' },
|
|
174
|
+
};
|
|
175
|
+
let redirectTo = '';
|
|
176
|
+
const res = {
|
|
177
|
+
cookies: {},
|
|
178
|
+
cookie(key, value) { this.cookies[key] = value; },
|
|
179
|
+
redirect(path) { redirectTo = path; },
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
txExpress.setLocale()(req, res);
|
|
183
|
+
|
|
184
|
+
expect(res.cookies).toEqual({ 'tx-locale': 'fr' });
|
|
185
|
+
expect(redirectTo).toBe('referer');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('setLocale with cookie and cookie options and form with next', () => {
|
|
189
|
+
typeis.mockReturnValue(false);
|
|
190
|
+
|
|
191
|
+
const txExpress = new TxExpress({
|
|
192
|
+
mode: CookieMode({ name: 'tx-locale', cookieOptions: { some: 'option' } }),
|
|
193
|
+
});
|
|
194
|
+
const req = {
|
|
195
|
+
body: { locale: 'fr', next: 'next' },
|
|
196
|
+
};
|
|
197
|
+
let cookieOptions = {};
|
|
198
|
+
let redirectTo = '';
|
|
199
|
+
const res = {
|
|
200
|
+
cookies: {},
|
|
201
|
+
cookie(key, value, options) {
|
|
202
|
+
this.cookies[key] = value;
|
|
203
|
+
cookieOptions = options;
|
|
204
|
+
},
|
|
205
|
+
redirect(path) { redirectTo = path; },
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
txExpress.setLocale()(req, res);
|
|
209
|
+
|
|
210
|
+
expect(res.cookies).toEqual({ 'tx-locale': 'fr' });
|
|
211
|
+
expect(cookieOptions).toEqual({ some: 'option' });
|
|
212
|
+
expect(redirectTo).toBe('next');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('setLocale with cookie and json', () => {
|
|
216
|
+
typeis.mockReturnValue(true);
|
|
217
|
+
|
|
218
|
+
const txExpress = new TxExpress({ mode: CookieMode({ name: 'tx-locale' }) });
|
|
219
|
+
const req = {
|
|
220
|
+
body: { locale: 'fr' },
|
|
221
|
+
};
|
|
222
|
+
let jsonResponse = {};
|
|
223
|
+
const res = {
|
|
224
|
+
cookies: {},
|
|
225
|
+
cookie(key, value) { this.cookies[key] = value; },
|
|
226
|
+
json(data) { jsonResponse = data; },
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
txExpress.setLocale()(req, res);
|
|
230
|
+
|
|
231
|
+
expect(res.cookies).toEqual({ 'tx-locale': 'fr' });
|
|
232
|
+
expect(jsonResponse).toEqual({ status: 'success' });
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('setLocale with signed cookie and form with next', () => {
|
|
236
|
+
typeis.mockReturnValue(false);
|
|
237
|
+
|
|
238
|
+
const txExpress = new TxExpress({ mode: SignedCookieMode({ name: 'tx-locale' }) });
|
|
239
|
+
const req = {
|
|
240
|
+
body: { locale: 'fr', next: 'next' },
|
|
241
|
+
};
|
|
242
|
+
let cookieOptions = {};
|
|
243
|
+
let redirectTo = '';
|
|
244
|
+
const res = {
|
|
245
|
+
cookies: {},
|
|
246
|
+
cookie(key, value, options) {
|
|
247
|
+
this.cookies[key] = value;
|
|
248
|
+
cookieOptions = options;
|
|
249
|
+
},
|
|
250
|
+
redirect(path) { redirectTo = path; },
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
txExpress.setLocale()(req, res);
|
|
254
|
+
|
|
255
|
+
expect(res.cookies).toEqual({ 'tx-locale': 'fr' });
|
|
256
|
+
expect(cookieOptions).toEqual({ signed: true });
|
|
257
|
+
expect(redirectTo).toBe('next');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('setLocale with session and form with next', () => {
|
|
261
|
+
typeis.mockReturnValue(false);
|
|
262
|
+
|
|
263
|
+
const txExpress = new TxExpress({ mode: SessionMode({ name: 'tx-locale' }) });
|
|
264
|
+
const req = {
|
|
265
|
+
session: {},
|
|
266
|
+
body: { locale: 'fr', next: 'next' },
|
|
267
|
+
};
|
|
268
|
+
let redirectTo = '';
|
|
269
|
+
const res = {
|
|
270
|
+
redirect(path) { redirectTo = path; },
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
txExpress.setLocale()(req, res);
|
|
274
|
+
|
|
275
|
+
expect(req.session).toEqual({ 'tx-locale': 'fr' });
|
|
276
|
+
expect(redirectTo).toBe('next');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test('t vs ut', () => {
|
|
280
|
+
const languages = [{ code: 'en', name: 'English' }, { code: 'fr', name: 'French' }];
|
|
281
|
+
tx.languages = languages;
|
|
282
|
+
tx.cache.update('fr', { foo: 'A <rich>string</rich>' });
|
|
283
|
+
|
|
284
|
+
const txExpress = new TxExpress();
|
|
285
|
+
const req = {
|
|
286
|
+
cookies: { 'tx-locale': 'fr' },
|
|
287
|
+
headers: {},
|
|
288
|
+
};
|
|
289
|
+
const res = {
|
|
290
|
+
render(view, locals) {
|
|
291
|
+
return [locals.t('foo', { _key: 'foo' }), locals.ut('foo', { _key: 'foo' })];
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
txExpress.middleware()(req, res, () => {});
|
|
296
|
+
|
|
297
|
+
expect(req.t('foo', { _key: 'foo' })).toBe('A <rich>string</rich>');
|
|
298
|
+
expect(req.ut('foo', { _key: 'foo' })).toBe('A <rich>string</rich>');
|
|
299
|
+
expect(res.render()).toEqual([
|
|
300
|
+
'A <rich>string</rich>',
|
|
301
|
+
'A <rich>string</rich>',
|
|
302
|
+
]);
|
|
303
|
+
});
|