@depup/i18n 0.15.3-depup.0

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/i18n.js ADDED
@@ -0,0 +1,1404 @@
1
+ /**
2
+ * @author Created by Marcus Spiegel <spiegel@uscreen.de> on 2011-03-25.
3
+ * @link https://github.com/mashpie/i18n-node
4
+ * @license http://opensource.org/licenses/MIT
5
+ */
6
+
7
+ 'use strict'
8
+
9
+ // dependencies
10
+ const printf = require('fast-printf').printf
11
+ const pkgVersion = require('./package.json').version
12
+ const fs = require('fs')
13
+ const url = require('url')
14
+ const path = require('path')
15
+ const debug = require('debug')('i18n:debug')
16
+ const warn = require('debug')('i18n:warn')
17
+ const error = require('debug')('i18n:error')
18
+ const Mustache = require('mustache')
19
+ const Messageformat = require('@messageformat/core')
20
+ const MakePlural = require('make-plural')
21
+ const parseInterval = require('math-interval-parser').default
22
+
23
+ // utils
24
+ const escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
25
+
26
+ /**
27
+ * create constructor function
28
+ */
29
+ const i18n = function I18n(_OPTS = false) {
30
+ const MessageformatInstanceForLocale = {}
31
+ const PluralsForLocale = {}
32
+ let locales = {}
33
+ const api = {
34
+ __: '__',
35
+ __n: '__n',
36
+ __l: '__l',
37
+ __h: '__h',
38
+ __mf: '__mf',
39
+ getLocale: 'getLocale',
40
+ setLocale: 'setLocale',
41
+ getCatalog: 'getCatalog',
42
+ getLocales: 'getLocales',
43
+ addLocale: 'addLocale',
44
+ removeLocale: 'removeLocale'
45
+ }
46
+ const mustacheConfig = {
47
+ tags: ['{{', '}}'],
48
+ disable: false
49
+ }
50
+
51
+ let mustacheRegex
52
+ const pathsep = path.sep // ---> means win support will be available in node 0.8.x and above
53
+ let autoReload
54
+ let cookiename
55
+ let languageHeaderName
56
+ let defaultLocale
57
+ let retryInDefaultLocale
58
+ let directory
59
+ let directoryPermissions
60
+ let extension
61
+ let fallbacks
62
+ let indent
63
+ let logDebugFn
64
+ let logErrorFn
65
+ let logWarnFn
66
+ let preserveLegacyCase
67
+ let objectNotation
68
+ let prefix
69
+ let queryParameter
70
+ let register
71
+ let updateFiles
72
+ let syncFiles
73
+ let missingKeyFn
74
+ let parser
75
+
76
+ // public exports
77
+ const i18n = {}
78
+
79
+ i18n.version = pkgVersion
80
+
81
+ i18n.configure = function i18nConfigure(opt) {
82
+ // reset locales
83
+ locales = {}
84
+
85
+ // Provide custom API method aliases if desired
86
+ // This needs to be processed before the first call to applyAPItoObject()
87
+ if (opt.api && typeof opt.api === 'object') {
88
+ for (const method in opt.api) {
89
+ if (Object.prototype.hasOwnProperty.call(opt.api, method)) {
90
+ const alias = opt.api[method]
91
+ if (typeof api[method] !== 'undefined') {
92
+ api[method] = alias
93
+ }
94
+ }
95
+ }
96
+ }
97
+
98
+ // you may register i18n in global scope, up to you
99
+ if (typeof opt.register === 'object') {
100
+ register = opt.register
101
+ // or give an array objects to register to
102
+ if (Array.isArray(opt.register)) {
103
+ register = opt.register
104
+ register.forEach(applyAPItoObject)
105
+ } else {
106
+ applyAPItoObject(opt.register)
107
+ }
108
+ }
109
+
110
+ // sets a custom cookie name to parse locale settings from
111
+ cookiename = typeof opt.cookie === 'string' ? opt.cookie : null
112
+
113
+ // set the custom header name to extract the language locale
114
+ languageHeaderName =
115
+ typeof opt.header === 'string' ? opt.header : 'accept-language'
116
+
117
+ // query-string parameter to be watched - @todo: add test & doc
118
+ queryParameter =
119
+ typeof opt.queryParameter === 'string' ? opt.queryParameter : null
120
+
121
+ // where to store json files
122
+ directory =
123
+ typeof opt.directory === 'string'
124
+ ? opt.directory
125
+ : path.join(__dirname, 'locales')
126
+
127
+ // permissions when creating new directories
128
+ directoryPermissions =
129
+ typeof opt.directoryPermissions === 'string'
130
+ ? parseInt(opt.directoryPermissions, 8)
131
+ : null
132
+
133
+ // write new locale information to disk
134
+ updateFiles = typeof opt.updateFiles === 'boolean' ? opt.updateFiles : true
135
+
136
+ // sync locale information accros all files
137
+ syncFiles = typeof opt.syncFiles === 'boolean' ? opt.syncFiles : false
138
+
139
+ // what to use as the indentation unit (ex: "\t", " ")
140
+ indent = typeof opt.indent === 'string' ? opt.indent : '\t'
141
+
142
+ // json files prefix
143
+ prefix = typeof opt.prefix === 'string' ? opt.prefix : ''
144
+
145
+ // where to store json files
146
+ extension = typeof opt.extension === 'string' ? opt.extension : '.json'
147
+
148
+ // setting defaultLocale
149
+ defaultLocale =
150
+ typeof opt.defaultLocale === 'string' ? opt.defaultLocale : 'en'
151
+
152
+ // allow to retry in default locale, useful for production
153
+ retryInDefaultLocale =
154
+ typeof opt.retryInDefaultLocale === 'boolean'
155
+ ? opt.retryInDefaultLocale
156
+ : false
157
+
158
+ // auto reload locale files when changed
159
+ autoReload = typeof opt.autoReload === 'boolean' ? opt.autoReload : false
160
+
161
+ // enable object notation?
162
+ objectNotation =
163
+ typeof opt.objectNotation !== 'undefined' ? opt.objectNotation : false
164
+ if (objectNotation === true) objectNotation = '.'
165
+
166
+ // read language fallback map
167
+ fallbacks = typeof opt.fallbacks === 'object' ? opt.fallbacks : {}
168
+
169
+ // setting custom logger functions
170
+ logDebugFn = typeof opt.logDebugFn === 'function' ? opt.logDebugFn : debug
171
+ logWarnFn = typeof opt.logWarnFn === 'function' ? opt.logWarnFn : warn
172
+ logErrorFn = typeof opt.logErrorFn === 'function' ? opt.logErrorFn : error
173
+
174
+ preserveLegacyCase =
175
+ typeof opt.preserveLegacyCase === 'boolean'
176
+ ? opt.preserveLegacyCase
177
+ : true
178
+
179
+ // setting custom missing key function
180
+ missingKeyFn =
181
+ typeof opt.missingKeyFn === 'function' ? opt.missingKeyFn : missingKey
182
+
183
+ parser =
184
+ typeof opt.parser === 'object' &&
185
+ typeof opt.parser.parse === 'function' &&
186
+ typeof opt.parser.stringify === 'function'
187
+ ? opt.parser
188
+ : JSON
189
+
190
+ // when missing locales we try to guess that from directory
191
+ opt.locales = opt.staticCatalog
192
+ ? Object.keys(opt.staticCatalog)
193
+ : opt.locales || guessLocales(directory)
194
+
195
+ // some options should be disabled when using staticCatalog
196
+ if (opt.staticCatalog) {
197
+ updateFiles = false
198
+ autoReload = false
199
+ syncFiles = false
200
+ }
201
+
202
+ // customize mustache parsing
203
+ if (opt.mustacheConfig) {
204
+ if (Array.isArray(opt.mustacheConfig.tags)) {
205
+ mustacheConfig.tags = opt.mustacheConfig.tags
206
+ }
207
+ if (opt.mustacheConfig.disable === true) {
208
+ mustacheConfig.disable = true
209
+ }
210
+ }
211
+
212
+ const [start, end] = mustacheConfig.tags
213
+ mustacheRegex = new RegExp(escapeRegExp(start) + '.*' + escapeRegExp(end))
214
+
215
+ // implicitly read all locales
216
+ if (Array.isArray(opt.locales)) {
217
+ if (opt.staticCatalog) {
218
+ locales = opt.staticCatalog
219
+ } else {
220
+ opt.locales.forEach(read)
221
+ }
222
+
223
+ // auto reload locale files when changed
224
+ if (autoReload) {
225
+ // watch changes of locale files (it's called twice because fs.watch is still unstable)
226
+ fs.watch(directory, (event, filename) => {
227
+ const localeFromFile = guessLocaleFromFile(filename)
228
+
229
+ if (localeFromFile && opt.locales.indexOf(localeFromFile) > -1) {
230
+ logDebug('Auto reloading locale file "' + filename + '".')
231
+ read(localeFromFile)
232
+ }
233
+ })
234
+ }
235
+ }
236
+ }
237
+
238
+ i18n.init = function i18nInit(request, response, next) {
239
+ if (typeof request === 'object') {
240
+ // guess requested language/locale
241
+ guessLanguage(request)
242
+
243
+ // bind api to req
244
+ applyAPItoObject(request)
245
+
246
+ // looks double but will ensure schema on api refactor
247
+ i18n.setLocale(request, request.locale)
248
+ } else {
249
+ return logError(
250
+ 'i18n.init must be called with one parameter minimum, ie. i18n.init(req)'
251
+ )
252
+ }
253
+
254
+ if (typeof response === 'object') {
255
+ applyAPItoObject(response)
256
+
257
+ // and set that locale to response too
258
+ i18n.setLocale(response, request.locale)
259
+ }
260
+
261
+ // head over to next callback when bound as middleware
262
+ if (typeof next === 'function') {
263
+ return next()
264
+ }
265
+ }
266
+
267
+ i18n.__ = function i18nTranslate(phrase) {
268
+ let msg
269
+ const argv = parseArgv(arguments)
270
+ const namedValues = argv[0]
271
+ const args = argv[1]
272
+
273
+ // called like __({phrase: "Hello", locale: "en"})
274
+ if (typeof phrase === 'object') {
275
+ if (
276
+ typeof phrase.locale === 'string' &&
277
+ typeof phrase.phrase === 'string'
278
+ ) {
279
+ msg = translate(phrase.locale, phrase.phrase)
280
+ }
281
+ }
282
+ // called like __("Hello")
283
+ else {
284
+ // get translated message with locale from scope (deprecated) or object
285
+ msg = translate(getLocaleFromObject(this), phrase)
286
+ }
287
+
288
+ // postprocess to get compatible to plurals
289
+ if (typeof msg === 'object' && msg.one) {
290
+ msg = msg.one
291
+ }
292
+
293
+ // in case there is no 'one' but an 'other' rule
294
+ if (typeof msg === 'object' && msg.other) {
295
+ msg = msg.other
296
+ }
297
+
298
+ // head over to postProcessing
299
+ return postProcess(msg, namedValues, args)
300
+ }
301
+
302
+ i18n.__mf = function i18nMessageformat(phrase) {
303
+ let msg, mf, f
304
+ let targetLocale = defaultLocale
305
+ const argv = parseArgv(arguments)
306
+ const namedValues = argv[0]
307
+ const args = argv[1]
308
+
309
+ // called like __({phrase: "Hello", locale: "en"})
310
+ if (typeof phrase === 'object') {
311
+ if (
312
+ typeof phrase.locale === 'string' &&
313
+ typeof phrase.phrase === 'string'
314
+ ) {
315
+ msg = phrase.phrase
316
+ targetLocale = phrase.locale
317
+ }
318
+ }
319
+ // called like __("Hello")
320
+ else {
321
+ // get translated message with locale from scope (deprecated) or object
322
+ msg = phrase
323
+ targetLocale = getLocaleFromObject(this)
324
+ }
325
+
326
+ msg = translate(targetLocale, msg)
327
+ // --- end get msg
328
+
329
+ // now head over to Messageformat
330
+ // and try to cache instance
331
+ if (MessageformatInstanceForLocale[targetLocale]) {
332
+ mf = MessageformatInstanceForLocale[targetLocale]
333
+ } else {
334
+ mf = new Messageformat(targetLocale)
335
+
336
+ mf.compiledFunctions = {}
337
+ MessageformatInstanceForLocale[targetLocale] = mf
338
+ }
339
+
340
+ // let's try to cache that function
341
+ if (mf.compiledFunctions[msg]) {
342
+ f = mf.compiledFunctions[msg]
343
+ } else {
344
+ f = mf.compile(msg)
345
+ mf.compiledFunctions[msg] = f
346
+ }
347
+
348
+ return postProcess(f(namedValues), namedValues, args)
349
+ }
350
+
351
+ i18n.__l = function i18nTranslationList(phrase) {
352
+ const translations = []
353
+ Object.keys(locales)
354
+ .sort()
355
+ .forEach((l) => {
356
+ translations.push(i18n.__({ phrase: phrase, locale: l }))
357
+ })
358
+ return translations
359
+ }
360
+
361
+ i18n.__h = function i18nTranslationHash(phrase) {
362
+ const translations = []
363
+ Object.keys(locales)
364
+ .sort()
365
+ .forEach((l) => {
366
+ const hash = {}
367
+ hash[l] = i18n.__({ phrase: phrase, locale: l })
368
+ translations.push(hash)
369
+ })
370
+ return translations
371
+ }
372
+
373
+ i18n.__n = function i18nTranslatePlural(singular, plural, count) {
374
+ let msg
375
+ let namedValues
376
+ let targetLocale
377
+ let args = []
378
+
379
+ // Accept an object with named values as the last parameter
380
+ if (argsEndWithNamedObject(arguments)) {
381
+ namedValues = arguments[arguments.length - 1]
382
+ args =
383
+ arguments.length >= 5
384
+ ? Array.prototype.slice.call(arguments, 3, -1)
385
+ : []
386
+ } else {
387
+ namedValues = {}
388
+ args =
389
+ arguments.length >= 4 ? Array.prototype.slice.call(arguments, 3) : []
390
+ }
391
+
392
+ // called like __n({singular: "%s cat", plural: "%s cats", locale: "en"}, 3)
393
+ if (typeof singular === 'object') {
394
+ if (
395
+ typeof singular.locale === 'string' &&
396
+ typeof singular.singular === 'string' &&
397
+ typeof singular.plural === 'string'
398
+ ) {
399
+ targetLocale = singular.locale
400
+ msg = translate(singular.locale, singular.singular, singular.plural)
401
+ }
402
+ args.unshift(count)
403
+
404
+ // some template engines pass all values as strings -> so we try to convert them to numbers
405
+ if (typeof plural === 'number' || Number(plural) + '' === plural) {
406
+ count = plural
407
+ }
408
+
409
+ // called like __n({singular: "%s cat", plural: "%s cats", locale: "en", count: 3})
410
+ if (
411
+ typeof singular.count === 'number' ||
412
+ typeof singular.count === 'string'
413
+ ) {
414
+ count = singular.count
415
+ args.unshift(plural)
416
+ }
417
+ } else {
418
+ // called like __n('cat', 3)
419
+ if (typeof plural === 'number' || Number(plural) + '' === plural) {
420
+ count = plural
421
+
422
+ // we add same string as default
423
+ // which efectivly copies the key to the plural.value
424
+ // this is for initialization of new empty translations
425
+ plural = singular
426
+
427
+ args.unshift(count)
428
+ args.unshift(plural)
429
+ }
430
+ // called like __n('%s cat', '%s cats', 3)
431
+ // get translated message with locale from scope (deprecated) or object
432
+ msg = translate(getLocaleFromObject(this), singular, plural)
433
+ targetLocale = getLocaleFromObject(this)
434
+ }
435
+
436
+ if (count === null) count = namedValues.count
437
+
438
+ // enforce number
439
+ count = Number(count)
440
+
441
+ // find the correct plural rule for given locale
442
+ if (typeof msg === 'object') {
443
+ let p
444
+ // create a new Plural for locale
445
+ // and try to cache instance
446
+ if (PluralsForLocale[targetLocale]) {
447
+ p = PluralsForLocale[targetLocale]
448
+ } else {
449
+ // split locales with a region code
450
+ const lc = targetLocale
451
+ .toLowerCase()
452
+ .split(/[_-\s]+/)
453
+ .filter((el) => true && el)
454
+ // take the first part of locale, fallback to full locale
455
+ p = MakePlural[lc[0] || targetLocale]
456
+ PluralsForLocale[targetLocale] = p
457
+ }
458
+
459
+ // fallback to 'other' on case of missing translations
460
+ msg = msg[p(count)] || msg.other
461
+ }
462
+
463
+ // head over to postProcessing
464
+ return postProcess(msg, namedValues, args, count)
465
+ }
466
+
467
+ i18n.setLocale = function i18nSetLocale(object, locale, skipImplicitObjects) {
468
+ // when given an array of objects => setLocale on each
469
+ if (Array.isArray(object) && typeof locale === 'string') {
470
+ for (let i = object.length - 1; i >= 0; i--) {
471
+ i18n.setLocale(object[i], locale, true)
472
+ }
473
+ return i18n.getLocale(object[0])
474
+ }
475
+
476
+ // defaults to called like i18n.setLocale(req, 'en')
477
+ let targetObject = object
478
+ let targetLocale = locale
479
+
480
+ // called like req.setLocale('en') or i18n.setLocale('en')
481
+ if (locale === undefined && typeof object === 'string') {
482
+ targetObject = this
483
+ targetLocale = object
484
+ }
485
+
486
+ // consider a fallback
487
+ if (!locales[targetLocale]) {
488
+ targetLocale = getFallback(targetLocale, fallbacks) || targetLocale
489
+ }
490
+
491
+ // now set locale on object
492
+ targetObject.locale = locales[targetLocale] ? targetLocale : defaultLocale
493
+
494
+ // consider any extra registered objects
495
+ if (typeof register === 'object') {
496
+ if (Array.isArray(register) && !skipImplicitObjects) {
497
+ register.forEach((r) => {
498
+ r.locale = targetObject.locale
499
+ })
500
+ } else {
501
+ register.locale = targetObject.locale
502
+ }
503
+ }
504
+
505
+ // consider res
506
+ if (targetObject.res && !skipImplicitObjects) {
507
+ // escape recursion
508
+ // @see - https://github.com/balderdashy/sails/pull/3631
509
+ // - https://github.com/mashpie/i18n-node/pull/218
510
+ if (targetObject.res.locals) {
511
+ i18n.setLocale(targetObject.res, targetObject.locale, true)
512
+ i18n.setLocale(targetObject.res.locals, targetObject.locale, true)
513
+ } else {
514
+ i18n.setLocale(targetObject.res, targetObject.locale)
515
+ }
516
+ }
517
+
518
+ // consider locals
519
+ if (targetObject.locals && !skipImplicitObjects) {
520
+ // escape recursion
521
+ // @see - https://github.com/balderdashy/sails/pull/3631
522
+ // - https://github.com/mashpie/i18n-node/pull/218
523
+ if (targetObject.locals.res) {
524
+ i18n.setLocale(targetObject.locals, targetObject.locale, true)
525
+ i18n.setLocale(targetObject.locals.res, targetObject.locale, true)
526
+ } else {
527
+ i18n.setLocale(targetObject.locals, targetObject.locale)
528
+ }
529
+ }
530
+
531
+ return i18n.getLocale(targetObject)
532
+ }
533
+
534
+ i18n.getLocale = function i18nGetLocale(request) {
535
+ // called like i18n.getLocale(req)
536
+ if (request && request.locale) {
537
+ return request.locale
538
+ }
539
+
540
+ // called like req.getLocale()
541
+ return this.locale || defaultLocale
542
+ }
543
+
544
+ i18n.getCatalog = function i18nGetCatalog(object, locale) {
545
+ let targetLocale
546
+
547
+ // called like i18n.getCatalog(req)
548
+ if (
549
+ typeof object === 'object' &&
550
+ typeof object.locale === 'string' &&
551
+ locale === undefined
552
+ ) {
553
+ targetLocale = object.locale
554
+ }
555
+
556
+ // called like i18n.getCatalog(req, 'en')
557
+ if (
558
+ !targetLocale &&
559
+ typeof object === 'object' &&
560
+ typeof locale === 'string'
561
+ ) {
562
+ targetLocale = locale
563
+ }
564
+
565
+ // called like req.getCatalog('en')
566
+ if (!targetLocale && locale === undefined && typeof object === 'string') {
567
+ targetLocale = object
568
+ }
569
+
570
+ // called like req.getCatalog()
571
+ if (
572
+ !targetLocale &&
573
+ object === undefined &&
574
+ locale === undefined &&
575
+ typeof this.locale === 'string'
576
+ ) {
577
+ if (register && register.global) {
578
+ targetLocale = ''
579
+ } else {
580
+ targetLocale = this.locale
581
+ }
582
+ }
583
+
584
+ // called like i18n.getCatalog()
585
+ if (targetLocale === undefined || targetLocale === '') {
586
+ return locales
587
+ }
588
+
589
+ if (!locales[targetLocale]) {
590
+ targetLocale = getFallback(targetLocale, fallbacks) || targetLocale
591
+ }
592
+
593
+ if (locales[targetLocale]) {
594
+ return locales[targetLocale]
595
+ } else {
596
+ logWarn('No catalog found for "' + targetLocale + '"')
597
+ return false
598
+ }
599
+ }
600
+
601
+ i18n.getLocales = function i18nGetLocales() {
602
+ return Object.keys(locales)
603
+ }
604
+
605
+ i18n.addLocale = function i18nAddLocale(locale) {
606
+ read(locale)
607
+ }
608
+
609
+ i18n.removeLocale = function i18nRemoveLocale(locale) {
610
+ delete locales[locale]
611
+ }
612
+
613
+ // ===================
614
+ // = private methods =
615
+ // ===================
616
+
617
+ const postProcess = (msg, namedValues, args, count) => {
618
+ // test for parsable interval string
619
+ if (/\|/.test(msg)) {
620
+ msg = parsePluralInterval(msg, count)
621
+ }
622
+
623
+ // replace the counter
624
+ if (typeof count === 'number') {
625
+ msg = printf(msg, Number(count))
626
+ }
627
+
628
+ // if the msg string contains {{Mustache}} patterns we render it as a mini template
629
+ if (!mustacheConfig.disable && mustacheRegex.test(msg)) {
630
+ msg = Mustache.render(msg, namedValues, {}, mustacheConfig.tags)
631
+ }
632
+
633
+ // if we have extra arguments with values to get replaced,
634
+ // an additional substition injects those strings afterwards
635
+ if (/%/.test(msg) && args && args.length > 0) {
636
+ msg = printf(msg, ...args)
637
+ }
638
+
639
+ return msg
640
+ }
641
+
642
+ const argsEndWithNamedObject = (args) =>
643
+ args.length > 1 &&
644
+ args[args.length - 1] !== null &&
645
+ typeof args[args.length - 1] === 'object'
646
+
647
+ const parseArgv = (args) => {
648
+ let namedValues, returnArgs
649
+
650
+ if (argsEndWithNamedObject(args)) {
651
+ namedValues = args[args.length - 1]
652
+ returnArgs = Array.prototype.slice.call(args, 1, -1)
653
+ } else {
654
+ namedValues = {}
655
+ returnArgs = args.length >= 2 ? Array.prototype.slice.call(args, 1) : []
656
+ }
657
+
658
+ return [namedValues, returnArgs]
659
+ }
660
+
661
+ /**
662
+ * registers all public API methods to a given response object when not already declared
663
+ */
664
+ const applyAPItoObject = (object) => {
665
+ let alreadySetted = true
666
+
667
+ // attach to itself if not provided
668
+ for (const method in api) {
669
+ if (Object.prototype.hasOwnProperty.call(api, method)) {
670
+ const alias = api[method]
671
+
672
+ // be kind rewind, or better not touch anything already existing
673
+ if (!object[alias]) {
674
+ alreadySetted = false
675
+ object[alias] = i18n[method].bind(object)
676
+ }
677
+ }
678
+ }
679
+
680
+ // set initial locale if not set
681
+ if (!object.locale) {
682
+ object.locale = defaultLocale
683
+ }
684
+
685
+ // escape recursion
686
+ if (alreadySetted) {
687
+ return
688
+ }
689
+
690
+ // attach to response if present (ie. in express)
691
+ if (object.res) {
692
+ applyAPItoObject(object.res)
693
+ }
694
+
695
+ // attach to locals if present (ie. in express)
696
+ if (object.locals) {
697
+ applyAPItoObject(object.locals)
698
+ }
699
+ }
700
+
701
+ /**
702
+ * tries to guess locales by scanning the given directory
703
+ */
704
+ const guessLocales = (directory) => {
705
+ const entries = fs.readdirSync(directory)
706
+ const localesFound = []
707
+
708
+ for (let i = entries.length - 1; i >= 0; i--) {
709
+ if (entries[i].match(/^\./)) continue
710
+ const localeFromFile = guessLocaleFromFile(entries[i])
711
+ if (localeFromFile) localesFound.push(localeFromFile)
712
+ }
713
+
714
+ return localesFound.sort()
715
+ }
716
+
717
+ /**
718
+ * tries to guess locales from a given filename
719
+ */
720
+ const guessLocaleFromFile = (filename) => {
721
+ const extensionRegex = new RegExp(extension + '$', 'g')
722
+ const prefixRegex = new RegExp('^' + prefix, 'g')
723
+
724
+ if (!filename) return false
725
+ if (prefix && !filename.match(prefixRegex)) return false
726
+ if (extension && !filename.match(extensionRegex)) return false
727
+ return filename.replace(prefix, '').replace(extensionRegex, '')
728
+ }
729
+
730
+ /**
731
+ * @param queryLanguage - language query parameter, either an array or a string.
732
+ * @return the first non-empty language query parameter found, null otherwise.
733
+ */
734
+ const extractQueryLanguage = (queryLanguage) => {
735
+ if (Array.isArray(queryLanguage)) {
736
+ return queryLanguage.find((lang) => lang !== '' && lang)
737
+ }
738
+ return typeof queryLanguage === 'string' && queryLanguage
739
+ }
740
+
741
+ /**
742
+ * guess language setting based on http headers
743
+ */
744
+
745
+ const guessLanguage = (request) => {
746
+ if (typeof request === 'object') {
747
+ const languageHeader = request.headers
748
+ ? request.headers[languageHeaderName]
749
+ : undefined
750
+ const languages = []
751
+ const regions = []
752
+
753
+ request.languages = [defaultLocale]
754
+ request.regions = [defaultLocale]
755
+ request.language = defaultLocale
756
+ request.region = defaultLocale
757
+
758
+ // a query parameter overwrites all
759
+ if (queryParameter && request.url) {
760
+ const urlAsString =
761
+ typeof request.url === 'string' ? request.url : request.url.toString()
762
+
763
+ /**
764
+ * @todo WHATWG new URL() requires full URL including hostname - that might change
765
+ * @see https://github.com/nodejs/node/issues/12682
766
+ */
767
+ // eslint-disable-next-line node/no-deprecated-api
768
+ const urlObj = url.parse(urlAsString, true)
769
+ const languageQueryParameter = urlObj.query[queryParameter]
770
+ if (languageQueryParameter) {
771
+ let queryLanguage = extractQueryLanguage(languageQueryParameter)
772
+ if (queryLanguage) {
773
+ logDebug('Overriding locale from query: ' + queryLanguage)
774
+ if (preserveLegacyCase) {
775
+ queryLanguage = queryLanguage.toLowerCase()
776
+ }
777
+ return i18n.setLocale(request, queryLanguage)
778
+ }
779
+ }
780
+ }
781
+
782
+ // a cookie overwrites headers
783
+ if (cookiename && request.cookies && request.cookies[cookiename]) {
784
+ request.language = request.cookies[cookiename]
785
+ return i18n.setLocale(request, request.language)
786
+ }
787
+
788
+ // 'accept-language' is the most common source
789
+ if (languageHeader) {
790
+ const acceptedLanguages = getAcceptedLanguagesFromHeader(languageHeader)
791
+ let match
792
+ let fallbackMatch
793
+ let fallback
794
+ for (let i = 0; i < acceptedLanguages.length; i++) {
795
+ const lang = acceptedLanguages[i]
796
+ const lr = lang.split('-', 2)
797
+ const parentLang = lr[0]
798
+ const region = lr[1]
799
+
800
+ // Check if we have a configured fallback set for this language.
801
+ const fallbackLang = getFallback(lang, fallbacks)
802
+ if (fallbackLang) {
803
+ fallback = fallbackLang
804
+ // Fallbacks for languages should be inserted
805
+ // where the original, unsupported language existed.
806
+ const acceptedLanguageIndex = acceptedLanguages.indexOf(lang)
807
+ const fallbackIndex = acceptedLanguages.indexOf(fallback)
808
+ if (fallbackIndex > -1) {
809
+ acceptedLanguages.splice(fallbackIndex, 1)
810
+ }
811
+ acceptedLanguages.splice(acceptedLanguageIndex + 1, 0, fallback)
812
+ }
813
+
814
+ // Check if we have a configured fallback set for the parent language of the locale.
815
+ const fallbackParentLang = getFallback(parentLang, fallbacks)
816
+ if (fallbackParentLang) {
817
+ fallback = fallbackParentLang
818
+ // Fallbacks for a parent language should be inserted
819
+ // to the end of the list, so they're only picked
820
+ // if there is no better match.
821
+ if (acceptedLanguages.indexOf(fallback) < 0) {
822
+ acceptedLanguages.push(fallback)
823
+ }
824
+ }
825
+
826
+ if (languages.indexOf(parentLang) < 0) {
827
+ languages.push(parentLang.toLowerCase())
828
+ }
829
+ if (region) {
830
+ regions.push(region.toLowerCase())
831
+ }
832
+
833
+ if (!match && locales[lang]) {
834
+ match = lang
835
+ break
836
+ }
837
+
838
+ if (!fallbackMatch && locales[parentLang]) {
839
+ fallbackMatch = parentLang
840
+ }
841
+ }
842
+
843
+ request.language = match || fallbackMatch || request.language
844
+ request.region = regions[0] || request.region
845
+ return i18n.setLocale(request, request.language)
846
+ }
847
+ }
848
+
849
+ // last resort: defaultLocale
850
+ return i18n.setLocale(request, defaultLocale)
851
+ }
852
+
853
+ /**
854
+ * Get a sorted list of accepted languages from the HTTP Accept-Language header
855
+ */
856
+ const getAcceptedLanguagesFromHeader = (header) => {
857
+ const languages = header.split(',')
858
+ const preferences = {}
859
+ return languages
860
+ .map((item) => {
861
+ const preferenceParts = item.trim().split(';q=')
862
+ if (preferenceParts.length < 2) {
863
+ preferenceParts[1] = 1.0
864
+ } else {
865
+ const quality = parseFloat(preferenceParts[1])
866
+ preferenceParts[1] = quality || 0.0
867
+ }
868
+ preferences[preferenceParts[0]] = preferenceParts[1]
869
+
870
+ return preferenceParts[0]
871
+ })
872
+ .filter((lang) => preferences[lang] > 0)
873
+ .sort((a, b) => preferences[b] - preferences[a])
874
+ }
875
+
876
+ /**
877
+ * searches for locale in given object
878
+ */
879
+
880
+ const getLocaleFromObject = (obj) => {
881
+ let locale
882
+ if (obj && obj.scope) {
883
+ locale = obj.scope.locale
884
+ }
885
+ if (obj && obj.locale) {
886
+ locale = obj.locale
887
+ }
888
+ return locale
889
+ }
890
+
891
+ /**
892
+ * splits and parses a phrase for mathematical interval expressions
893
+ */
894
+ const parsePluralInterval = (phrase, count) => {
895
+ let returnPhrase = phrase
896
+ const phrases = phrase.split(/\|/)
897
+ let intervalRuleExists = false
898
+
899
+ // some() breaks on 1st true
900
+ phrases.some((p) => {
901
+ const matches = p.match(/^\s*([()[\]]+[\d,]+[()[\]]+)?\s*(.*)$/)
902
+
903
+ // not the same as in combined condition
904
+ if (matches != null && matches[1]) {
905
+ intervalRuleExists = true
906
+ if (matchInterval(count, matches[1]) === true) {
907
+ returnPhrase = matches[2]
908
+ return true
909
+ }
910
+ } else {
911
+ // this is a other or catch all case, this only is taken into account if there is actually another rule
912
+ if (intervalRuleExists) {
913
+ returnPhrase = p
914
+ }
915
+ }
916
+ return false
917
+ })
918
+ return returnPhrase
919
+ }
920
+
921
+ /**
922
+ * test a number to match mathematical interval expressions
923
+ * [0,2] - 0 to 2 (including, matches: 0, 1, 2)
924
+ * ]0,3[ - 0 to 3 (excluding, matches: 1, 2)
925
+ * [1] - 1 (matches: 1)
926
+ * [20,] - all numbers ≥20 (matches: 20, 21, 22, ...)
927
+ * [,20] - all numbers ≤20 (matches: 20, 21, 22, ...)
928
+ */
929
+ const matchInterval = (number, interval) => {
930
+ interval = parseInterval(interval)
931
+ if (interval && typeof number === 'number') {
932
+ if (interval.from.value === number) {
933
+ return interval.from.included
934
+ }
935
+ if (interval.to.value === number) {
936
+ return interval.to.included
937
+ }
938
+
939
+ return (
940
+ Math.min(interval.from.value, number) === interval.from.value &&
941
+ Math.max(interval.to.value, number) === interval.to.value
942
+ )
943
+ }
944
+ return false
945
+ }
946
+
947
+ /**
948
+ * read locale file, translate a msg and write to fs if new
949
+ */
950
+ const translate = (locale, singular, plural, skipSyncToAllFiles) => {
951
+ // add same key to all translations
952
+ if (!skipSyncToAllFiles && syncFiles) {
953
+ syncToAllFiles(singular, plural)
954
+ }
955
+
956
+ if (locale === undefined) {
957
+ logWarn(
958
+ 'WARN: No locale found - check the context of the call to __(). Using ' +
959
+ defaultLocale +
960
+ ' as current locale'
961
+ )
962
+ locale = defaultLocale
963
+ }
964
+
965
+ // try to get a fallback
966
+ if (!locales[locale]) {
967
+ locale = getFallback(locale, fallbacks) || locale
968
+ }
969
+
970
+ // attempt to read when defined as valid locale
971
+ if (!locales[locale]) {
972
+ read(locale)
973
+ }
974
+
975
+ // fallback to default when missed
976
+ if (!locales[locale]) {
977
+ logWarn(
978
+ 'WARN: Locale ' +
979
+ locale +
980
+ " couldn't be read - check the context of the call to $__. Using " +
981
+ defaultLocale +
982
+ ' (default) as current locale'
983
+ )
984
+
985
+ locale = defaultLocale
986
+ read(locale)
987
+ }
988
+
989
+ // dotnotaction add on, @todo: factor out
990
+ let defaultSingular = singular
991
+ let defaultPlural = plural
992
+ if (objectNotation) {
993
+ let indexOfColon = singular.indexOf(':')
994
+ // We compare against 0 instead of -1 because
995
+ // we don't really expect the string to start with ':'.
996
+ if (indexOfColon > 0) {
997
+ defaultSingular = singular.substring(indexOfColon + 1)
998
+ singular = singular.substring(0, indexOfColon)
999
+ }
1000
+ if (plural && typeof plural !== 'number') {
1001
+ indexOfColon = plural.indexOf(':')
1002
+ if (indexOfColon > 0) {
1003
+ defaultPlural = plural.substring(indexOfColon + 1)
1004
+ plural = plural.substring(0, indexOfColon)
1005
+ }
1006
+ }
1007
+ }
1008
+
1009
+ const accessor = localeAccessor(locale, singular)
1010
+ const mutator = localeMutator(locale, singular)
1011
+
1012
+ // if (plural) {
1013
+ // if (accessor() == null) {
1014
+ // mutator({
1015
+ // 'one': defaultSingular || singular,
1016
+ // 'other': defaultPlural || plural
1017
+ // });
1018
+ // write(locale);
1019
+ // }
1020
+ // }
1021
+ // if (accessor() == null) {
1022
+ // mutator(defaultSingular || singular);
1023
+ // write(locale);
1024
+ // }
1025
+ if (plural) {
1026
+ if (accessor() == null) {
1027
+ // when retryInDefaultLocale is true - try to set default value from defaultLocale
1028
+ if (retryInDefaultLocale && locale !== defaultLocale) {
1029
+ logDebug(
1030
+ 'Missing ' +
1031
+ singular +
1032
+ ' in ' +
1033
+ locale +
1034
+ ' retrying in ' +
1035
+ defaultLocale
1036
+ )
1037
+ mutator(translate(defaultLocale, singular, plural, true))
1038
+ } else {
1039
+ mutator({
1040
+ one: defaultSingular || singular,
1041
+ other: defaultPlural || plural
1042
+ })
1043
+ }
1044
+ write(locale)
1045
+ }
1046
+ }
1047
+
1048
+ if (accessor() == null) {
1049
+ // when retryInDefaultLocale is true - try to set default value from defaultLocale
1050
+ if (retryInDefaultLocale && locale !== defaultLocale) {
1051
+ logDebug(
1052
+ 'Missing ' +
1053
+ singular +
1054
+ ' in ' +
1055
+ locale +
1056
+ ' retrying in ' +
1057
+ defaultLocale
1058
+ )
1059
+ mutator(translate(defaultLocale, singular, plural, true))
1060
+ } else {
1061
+ mutator(defaultSingular || singular)
1062
+ }
1063
+ write(locale)
1064
+ }
1065
+
1066
+ return accessor()
1067
+ }
1068
+
1069
+ /**
1070
+ * initialize the same key in all locales
1071
+ * when not already existing, checked via translate
1072
+ */
1073
+ const syncToAllFiles = (singular, plural) => {
1074
+ // iterate over locales and translate again
1075
+ // this will implicitly write/sync missing keys
1076
+ // to the rest of locales
1077
+ for (const l in locales) {
1078
+ translate(l, singular, plural, true)
1079
+ }
1080
+ }
1081
+
1082
+ /**
1083
+ * Allows delayed access to translations nested inside objects.
1084
+ * @param {String} locale The locale to use.
1085
+ * @param {String} singular The singular term to look up.
1086
+ * @param {Boolean} [allowDelayedTraversal=true] Is delayed traversal of the tree allowed?
1087
+ * This parameter is used internally. It allows to signal the accessor that
1088
+ * a translation was not found in the initial lookup and that an invocation
1089
+ * of the accessor may trigger another traversal of the tree.
1090
+ * @returns {Function} A function that, when invoked, returns the current value stored
1091
+ * in the object at the requested location.
1092
+ */
1093
+ const localeAccessor = (locale, singular, allowDelayedTraversal) => {
1094
+ // Bail out on non-existent locales to defend against internal errors.
1095
+ if (!locales[locale]) return Function.prototype
1096
+
1097
+ // Handle object lookup notation
1098
+ const indexOfDot = objectNotation && singular.lastIndexOf(objectNotation)
1099
+ if (objectNotation && indexOfDot > 0 && indexOfDot < singular.length - 1) {
1100
+ // If delayed traversal wasn't specifically forbidden, it is allowed.
1101
+ if (typeof allowDelayedTraversal === 'undefined')
1102
+ allowDelayedTraversal = true
1103
+ // The accessor we're trying to find and which we want to return.
1104
+ let accessor = null
1105
+ // An accessor that returns null.
1106
+ const nullAccessor = () => null
1107
+ // Do we need to re-traverse the tree upon invocation of the accessor?
1108
+ let reTraverse = false
1109
+ // Split the provided term and run the callback for each subterm.
1110
+ singular.split(objectNotation).reduce((object, index) => {
1111
+ // Make the accessor return null.
1112
+ accessor = nullAccessor
1113
+ // If our current target object (in the locale tree) doesn't exist or
1114
+ // it doesn't have the next subterm as a member...
1115
+ if (
1116
+ object === null ||
1117
+ !Object.prototype.hasOwnProperty.call(object, index)
1118
+ ) {
1119
+ // ...remember that we need retraversal (because we didn't find our target).
1120
+ reTraverse = allowDelayedTraversal
1121
+ // Return null to avoid deeper iterations.
1122
+ return null
1123
+ }
1124
+ // We can traverse deeper, so we generate an accessor for this current level.
1125
+ accessor = () => object[index]
1126
+ // Return a reference to the next deeper level in the locale tree.
1127
+ return object[index]
1128
+ }, locales[locale])
1129
+ // Return the requested accessor.
1130
+ return () =>
1131
+ // If we need to re-traverse (because we didn't find our target term)
1132
+ // traverse again and return the new result (but don't allow further iterations)
1133
+ // or return the previously found accessor if it was already valid.
1134
+ reTraverse ? localeAccessor(locale, singular, false)() : accessor()
1135
+ } else {
1136
+ // No object notation, just return an accessor that performs array lookup.
1137
+ return () => locales[locale][singular]
1138
+ }
1139
+ }
1140
+
1141
+ /**
1142
+ * Allows delayed mutation of a translation nested inside objects.
1143
+ * @description Construction of the mutator will attempt to locate the requested term
1144
+ * inside the object, but if part of the branch does not exist yet, it will not be
1145
+ * created until the mutator is actually invoked. At that point, re-traversal of the
1146
+ * tree is performed and missing parts along the branch will be created.
1147
+ * @param {String} locale The locale to use.
1148
+ * @param {String} singular The singular term to look up.
1149
+ * @param [Boolean} [allowBranching=false] Is the mutator allowed to create previously
1150
+ * non-existent branches along the requested locale path?
1151
+ * @returns {Function} A function that takes one argument. When the function is
1152
+ * invoked, the targeted translation term will be set to the given value inside the locale table.
1153
+ */
1154
+ const localeMutator = function (locale, singular, allowBranching) {
1155
+ // Bail out on non-existent locales to defend against internal errors.
1156
+ if (!locales[locale]) return Function.prototype
1157
+
1158
+ // Handle object lookup notation
1159
+ const indexOfDot = objectNotation && singular.lastIndexOf(objectNotation)
1160
+ if (objectNotation && indexOfDot > 0 && indexOfDot < singular.length - 1) {
1161
+ // If branching wasn't specifically allowed, disable it.
1162
+ if (typeof allowBranching === 'undefined') allowBranching = false
1163
+ // This will become the function we want to return.
1164
+ let accessor = null
1165
+ // An accessor that takes one argument and returns null.
1166
+ const nullAccessor = () => null
1167
+ // Fix object path.
1168
+ let fixObject = () => ({})
1169
+ // Are we going to need to re-traverse the tree when the mutator is invoked?
1170
+ let reTraverse = false
1171
+ // Split the provided term and run the callback for each subterm.
1172
+ singular.split(objectNotation).reduce((object, index) => {
1173
+ // Make the mutator do nothing.
1174
+ accessor = nullAccessor
1175
+ // If our current target object (in the locale tree) doesn't exist or
1176
+ // it doesn't have the next subterm as a member...
1177
+ if (
1178
+ object === null ||
1179
+ !Object.prototype.hasOwnProperty.call(object, index)
1180
+ ) {
1181
+ // ...check if we're allowed to create new branches.
1182
+ if (allowBranching) {
1183
+ // Fix `object` if `object` is not Object.
1184
+ if (object === null || typeof object !== 'object') {
1185
+ object = fixObject()
1186
+ }
1187
+ // If we are allowed to, create a new object along the path.
1188
+ object[index] = {}
1189
+ } else {
1190
+ // If we aren't allowed, remember that we need to re-traverse later on and...
1191
+ reTraverse = true
1192
+ // ...return null to make the next iteration bail our early on.
1193
+ return null
1194
+ }
1195
+ }
1196
+ // Generate a mutator for the current level.
1197
+ accessor = (value) => {
1198
+ object[index] = value
1199
+ return value
1200
+ }
1201
+ // Generate a fixer for the current level.
1202
+ fixObject = () => {
1203
+ object[index] = {}
1204
+ return object[index]
1205
+ }
1206
+
1207
+ // Return a reference to the next deeper level in the locale tree.
1208
+ return object[index]
1209
+ }, locales[locale])
1210
+
1211
+ // Return the final mutator.
1212
+ return (value) => {
1213
+ // If we need to re-traverse the tree
1214
+ // invoke the search again, but allow branching
1215
+ // this time (because here the mutator is being invoked)
1216
+ // otherwise, just change the value directly.
1217
+ value = missingKeyFn(locale, value)
1218
+ return reTraverse
1219
+ ? localeMutator(locale, singular, true)(value)
1220
+ : accessor(value)
1221
+ }
1222
+ } else {
1223
+ // No object notation, just return a mutator that performs array lookup and changes the value.
1224
+ return (value) => {
1225
+ value = missingKeyFn(locale, value)
1226
+ locales[locale][singular] = value
1227
+ return value
1228
+ }
1229
+ }
1230
+ }
1231
+
1232
+ /**
1233
+ * try reading a file
1234
+ */
1235
+ const read = (locale) => {
1236
+ let localeFile = {}
1237
+ const file = getStorageFilePath(locale)
1238
+ try {
1239
+ logDebug('read ' + file + ' for locale: ' + locale)
1240
+ localeFile = fs.readFileSync(file, 'utf-8')
1241
+ try {
1242
+ // parsing filecontents to locales[locale]
1243
+ locales[locale] = parser.parse(localeFile)
1244
+ } catch (parseError) {
1245
+ logError(
1246
+ 'unable to parse locales from file (maybe ' +
1247
+ file +
1248
+ ' is empty or invalid json?): ',
1249
+ parseError
1250
+ )
1251
+ }
1252
+ } catch (readError) {
1253
+ // unable to read, so intialize that file
1254
+ // locales[locale] are already set in memory, so no extra read required
1255
+ // or locales[locale] are empty, which initializes an empty locale.json file
1256
+ // since the current invalid locale could exist, we should back it up
1257
+ if (fs.existsSync(file)) {
1258
+ logDebug(
1259
+ 'backing up invalid locale ' + locale + ' to ' + file + '.invalid'
1260
+ )
1261
+ fs.renameSync(file, file + '.invalid')
1262
+ }
1263
+
1264
+ logDebug('initializing ' + file)
1265
+ write(locale)
1266
+ }
1267
+ }
1268
+
1269
+ /**
1270
+ * try writing a file in a created directory
1271
+ */
1272
+ const write = (locale) => {
1273
+ let stats, target, tmp
1274
+
1275
+ // don't write new locale information to disk if updateFiles isn't true
1276
+ if (!updateFiles) {
1277
+ return
1278
+ }
1279
+
1280
+ // creating directory if necessary
1281
+ try {
1282
+ stats = fs.lstatSync(directory)
1283
+ } catch (e) {
1284
+ logDebug('creating locales dir in: ' + directory)
1285
+ try {
1286
+ fs.mkdirSync(directory, directoryPermissions)
1287
+ } catch (e) {
1288
+ // in case of parallel tasks utilizing in same dir
1289
+ if (e.code !== 'EEXIST') throw e
1290
+ }
1291
+ }
1292
+
1293
+ // first time init has an empty file
1294
+ if (!locales[locale]) {
1295
+ locales[locale] = {}
1296
+ }
1297
+
1298
+ // writing to tmp and rename on success
1299
+ try {
1300
+ target = getStorageFilePath(locale)
1301
+ tmp = target + '.tmp'
1302
+ fs.writeFileSync(
1303
+ tmp,
1304
+ parser.stringify(locales[locale], null, indent),
1305
+ 'utf8'
1306
+ )
1307
+ stats = fs.statSync(tmp)
1308
+ if (stats.isFile()) {
1309
+ fs.renameSync(tmp, target)
1310
+ } else {
1311
+ logError(
1312
+ 'unable to write locales to file (either ' +
1313
+ tmp +
1314
+ ' or ' +
1315
+ target +
1316
+ ' are not writeable?): '
1317
+ )
1318
+ }
1319
+ } catch (e) {
1320
+ logError(
1321
+ 'unexpected error writing files (either ' +
1322
+ tmp +
1323
+ ' or ' +
1324
+ target +
1325
+ ' are not writeable?): ',
1326
+ e
1327
+ )
1328
+ }
1329
+ }
1330
+
1331
+ /**
1332
+ * basic normalization of filepath
1333
+ */
1334
+ const getStorageFilePath = (locale) => {
1335
+ // changed API to use .json as default, #16
1336
+ const ext = extension || '.json'
1337
+ const filepath = path.normalize(directory + pathsep + prefix + locale + ext)
1338
+ const filepathJS = path.normalize(
1339
+ directory + pathsep + prefix + locale + '.js'
1340
+ )
1341
+ // use .js as fallback if already existing
1342
+ try {
1343
+ if (fs.statSync(filepathJS)) {
1344
+ logDebug('using existing file ' + filepathJS)
1345
+ extension = '.js'
1346
+ return filepathJS
1347
+ }
1348
+ } catch (e) {
1349
+ logDebug('will use ' + filepath)
1350
+ }
1351
+ return filepath
1352
+ }
1353
+
1354
+ /**
1355
+ * Get locales with wildcard support
1356
+ */
1357
+ const getFallback = (targetLocale, fallbacks) => {
1358
+ fallbacks = fallbacks || {}
1359
+ if (fallbacks[targetLocale]) return fallbacks[targetLocale]
1360
+ let fallBackLocale = null
1361
+ for (const key in fallbacks) {
1362
+ if (targetLocale.match(new RegExp('^' + key.replace('*', '.*') + '$'))) {
1363
+ fallBackLocale = fallbacks[key]
1364
+ break
1365
+ }
1366
+ }
1367
+ return fallBackLocale
1368
+ }
1369
+
1370
+ /**
1371
+ * Logging proxies
1372
+ */
1373
+ const logDebug = (msg) => {
1374
+ logDebugFn(msg)
1375
+ }
1376
+
1377
+ const logWarn = (msg) => {
1378
+ logWarnFn(msg)
1379
+ }
1380
+
1381
+ const logError = (msg) => {
1382
+ logErrorFn(msg)
1383
+ }
1384
+
1385
+ /**
1386
+ * Missing key function
1387
+ */
1388
+ const missingKey = (locale, value) => {
1389
+ return value
1390
+ }
1391
+
1392
+ /**
1393
+ * implicitly configure when created with given options
1394
+ * @example
1395
+ * const i18n = new I18n({
1396
+ * locales: ['en', 'fr']
1397
+ * });
1398
+ */
1399
+ if (_OPTS) i18n.configure(_OPTS)
1400
+
1401
+ return i18n
1402
+ }
1403
+
1404
+ module.exports = i18n