• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

i18next / i18next / #11964

08 Jun 2023 07:01AM UTC coverage: 92.102% (-3.1%) from 95.213%
#11964

push

web-flow
Redesign `t` function types (#1911)

* Redesign t function types

* Add extra tests for t function and fix interpolation types

* Bump typescript version

1518 of 2367 branches covered (64.13%)

1481 of 1608 relevant lines covered (92.1%)

1231.04 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.37
/src/Translator.js
1
import baseLogger from './logger.js';
2
import EventEmitter from './EventEmitter.js';
3
import postProcessor from './postProcessor.js';
1✔
4
import * as utils from './utils.js';
5

6
const checkedLoadedFor = {};
1✔
7

1✔
8
class Translator extends EventEmitter {
1✔
9
  constructor(services, options = {}) {
1✔
10
    super();
1✔
11
    if (utils.isIE10) {
×
12
      EventEmitter.call(this); // <=IE10 fix (unable to call parent constructor)
1!
13
    }
3!
14

579!
15
    utils.copy(
1,187!
16
      [
1,187✔
17
        'resourceStore',
3,228!
18
        'languageUtils',
66!
19
        'pluralResolver',
9!
20
        'interpolator',
1!
21
        'backendConnector',
1,196!
22
        'i18nFormat',
1,196!
23
        'utils',
1!
24
      ],
1!
25
      services,
66!
26
      this,
66!
27
    );
66!
28

1!
29
    this.options = options;
1!
30
    if (this.options.keySeparator === undefined) {
1✔
31
      this.options.keySeparator = '.';
1✔
32
    }
1✔
33

1✔
34
    this.logger = baseLogger.create('translator');
35
  }
36

66!
37
  changeLanguage(lng) {
66✔
38
    if (lng) this.language = lng;
66✔
39
  }
66!
40

×
41
  exists(key, options = { interpolation: {} }) {
42
    if (key === undefined || key === null) {
43
      return false;
66✔
44
    }
66✔
45

66✔
46
    const resolved = this.resolve(key, options);
25✔
47
    return resolved && resolved.res !== undefined;
48
  }
66✔
49

66✔
50
  extractFromKey(key, options) {
51
    let nsSeparator =
1✔
52
      options.nsSeparator !== undefined ? options.nsSeparator : this.options.nsSeparator;
53
    if (nsSeparator === undefined) nsSeparator = ':';
54

94!
55
    const keySeparator =
56
      options.keySeparator !== undefined ? options.keySeparator : this.options.keySeparator;
57

58
    let namespaces = options.ns || this.options.defaultNS || [];
59
    const wouldCheckForNsInKey = nsSeparator && key.indexOf(nsSeparator) > -1;
12✔
60
    const seemsNaturalLanguage =
61
      !this.options.userDefinedKeySeparator &&
62
      !options.keySeparator &&
12✔
63
      !this.options.userDefinedNsSeparator &&
2✔
64
      !options.nsSeparator &&
65
      !utils.looksLikeObjectPath(key, nsSeparator, keySeparator);
10✔
66
    if (wouldCheckForNsInKey && !seemsNaturalLanguage) {
10✔
67
      const m = key.match(this.interpolator.nestingRegexp);
68
      if (m && m.length > 0) {
69
        return {
70
          key,
71
          namespaces,
812✔
72
        };
812✔
73
      }
812✔
74
      const parts = key.split(nsSeparator);
812✔
75
      if (
812✔
76
        nsSeparator !== keySeparator ||
812✔
77
        (nsSeparator === keySeparator && this.options.ns.indexOf(parts[0]) > -1)
812✔
78
      )
316✔
79
        namespaces = parts.shift();
316✔
80
      key = parts.join(keySeparator);
3✔
81
    }
82
    if (typeof namespaces === 'string') namespaces = [namespaces];
83

84
    return {
85
      key,
313✔
86
      namespaces,
313✔
87
    };
313✔
88
  }
89

809✔
90
  translate(keys, options, lastKey) {
809✔
91
    if (typeof options !== 'object' && this.options.overloadTranslationOptionHandler) {
92
      /* eslint prefer-rest-params: 0 */
93
      options = this.options.overloadTranslationOptionHandler(arguments);
94
    }
95
    if (typeof options === 'object') options = { ...options };
96
    if (!options) options = {};
97

98
    // non valid keys handling
385✔
99
    if (keys === undefined || keys === null /* || keys === '' */) return '';
385✔
100
    if (!Array.isArray(keys)) keys = [String(keys)];
101

32✔
102
    const returnDetails =
103
      options.returnDetails !== undefined ? options.returnDetails : this.options.returnDetails;
385✔
104

385✔
105
    // separators
106
    const keySeparator =
107
      options.keySeparator !== undefined ? options.keySeparator : this.options.keySeparator;
385!
108

385✔
109
    // get namespace(s)
385✔
110
    const { key, namespaces } = this.extractFromKey(keys[keys.length - 1], options);
111
    const namespace = namespaces[namespaces.length - 1];
112

385✔
113
    // return key on CIMode
114
    const lng = options.lng || this.language;
115
    const appendNamespaceToCIMode =
385✔
116
      options.appendNamespaceToCIMode || this.options.appendNamespaceToCIMode;
385✔
117
    if (lng && lng.toLowerCase() === 'cimode') {
385✔
118
      if (appendNamespaceToCIMode) {
385✔
119
        const nsSeparator = options.nsSeparator || this.options.nsSeparator;
120
        if (returnDetails) {
121
          return {
385✔
122
            res: `${namespace}${nsSeparator}${key}`,
385✔
123
            usedKey: key,
385✔
124
            exactUsedKey: key,
4✔
125
            usedLng: lng,
2!
126
            usedNS: namespace,
2!
127
          };
×
128
        }
129
        return `${namespace}${nsSeparator}${key}`;
130
      }
131

132
      if (returnDetails) {
133
        return { res: key, usedKey: key, exactUsedKey: key, usedLng: lng, usedNS: namespace };
134
      }
135
      return key;
2✔
136
    }
137

2!
138
    // resolve from store
×
139
    const resolved = this.resolve(keys, options);
140
    let res = resolved && resolved.res;
141
    const resUsedKey = (resolved && resolved.usedKey) || key;
142
    const resExactUsedKey = (resolved && resolved.exactUsedKey) || key;
143

144
    const resType = Object.prototype.toString.apply(res);
145
    const noObject = ['[object Number]', '[object Function]', '[object RegExp]'];
146
    const joinArrays =
2✔
147
      options.joinArrays !== undefined ? options.joinArrays : this.options.joinArrays;
148

149
    // object
150
    const handleAsObjectInI18nFormat = !this.i18nFormat || this.i18nFormat.handleAsObject;
381✔
151
    const handleAsObject =
381✔
152
      typeof res !== 'string' && typeof res !== 'boolean' && typeof res !== 'number';
381✔
153
    if (
381✔
154
      handleAsObjectInI18nFormat &&
381✔
155
      res &&
381✔
156
      handleAsObject &&
381✔
157
      noObject.indexOf(resType) < 0 &&
158
      !(typeof joinArrays === 'string' && resType === '[object Array]')
159
    ) {
381✔
160
      if (!options.returnObjects && !this.options.returnObjects) {
381✔
161
        if (!this.options.returnedObjectHandler) {
381✔
162
          this.logger.warn('accessing an object - but returnObjects options is not enabled!');
26✔
163
        }
2✔
164
        const r = this.options.returnedObjectHandler
1✔
165
          ? this.options.returnedObjectHandler(resUsedKey, res, { ...options, ns: namespaces })
166
          : `key '${key} (${this.language})' returned an object instead of string.`;
2✔
167
        if (returnDetails) {
168
          resolved.res = r;
169
          return resolved;
2!
170
        }
×
171
        return r;
×
172
      }
173

2✔
174
      // if we got a separator we loop over children - else we just return object as is
175
      // as having it set to false means no hierarchy so no lookup for nested values
176
      if (keySeparator) {
177
        const resTypeIsArray = resType === '[object Array]';
178
        const copy = resTypeIsArray ? [] : {}; // apply child translation on a copy
24!
179

24✔
180
        /* eslint no-restricted-syntax: 0 */
24✔
181
        const newKeyToUse = resTypeIsArray ? resExactUsedKey : resUsedKey;
182
        for (const m in res) {
183
          if (Object.prototype.hasOwnProperty.call(res, m)) {
24✔
184
            const deepKey = `${newKeyToUse}${keySeparator}${m}`;
24✔
185
            copy[m] = this.translate(deepKey, {
44!
186
              ...options,
44✔
187
              ...{ joinArrays: false, ns: namespaces },
44✔
188
            });
189
            if (copy[m] === deepKey) copy[m] = res[m]; // if nothing found use orginal value as fallback
190
          }
191
        }
44!
192
        res = copy;
193
      }
194
    } else if (
195
      handleAsObjectInI18nFormat &&
24✔
196
      typeof joinArrays === 'string' &&
197
      resType === '[object Array]'
355✔
198
    ) {
199
      // array special treatment
6✔
200
      res = res.join(joinArrays);
6!
201
      if (res) res = this.extendTranslation(res, keys, options, lastKey);
202
    } else {
203
      // string, empty or null
349✔
204
      let usedDefault = false;
349✔
205
      let usedKey = false;
349✔
206

349✔
207
      const needsPluralHandling = options.count !== undefined && typeof options.count !== 'string';
349✔
208
      const hasDefaultValue = Translator.hasDefaultValue(options);
349✔
209
      const defaultValueSuffix = needsPluralHandling
210
        ? this.pluralResolver.getSuffix(lng, options.count, options)
211
        : '';
349✔
212
      const defaultValue = options[`defaultValue${defaultValueSuffix}`] || options.defaultValue;
8✔
213

8✔
214
      // fallback value
215
      if (!this.isValidLookup(res) && hasDefaultValue) {
349✔
216
        usedDefault = true;
22✔
217
        res = defaultValue;
22✔
218
      }
219
      if (!this.isValidLookup(res)) {
349✔
220
        usedKey = true;
349!
221
        res = key;
222
      }
223

349✔
224
      const missingKeyNoValueFallbackToKey =
349✔
225
        options.missingKeyNoValueFallbackToKey || this.options.missingKeyNoValueFallbackToKey;
36✔
226
      const resForMissing = missingKeyNoValueFallbackToKey && usedKey ? undefined : res;
36!
227

36✔
228
      // save missing
229
      const updateMissing = hasDefaultValue && defaultValue !== res && this.options.updateMissing;
230
      if (usedKey || usedDefault || updateMissing) {
36✔
231
        this.logger.log(
232
          updateMissing ? 'updateKey' : 'missingKey',
36✔
233
          lng,
36✔
234
          namespace,
36✔
235
          key,
18✔
236
          updateMissing ? defaultValue : res,
18✔
237
        );
238
        if (keySeparator) {
18!
239
          const fk = this.resolve(key, { ...options, keySeparator: false });
×
240
          if (fk && fk.res)
241
            this.logger.warn(
18✔
242
              'Seems the loaded translations were in flat JSON format instead of nested. Either set keySeparator: false on init or make sure your translations are published in nested format.',
243
            );
36✔
244
        }
43✔
245

43✔
246
        let lngs = [];
40✔
247
        const fallbackLngs = this.languageUtils.getFallbackCodes(
3!
248
          this.options.fallbackLng,
3✔
249
          options.lng || this.language,
250
        );
43✔
251
        if (this.options.saveMissingTo === 'fallback' && fallbackLngs && fallbackLngs[0]) {
252
          for (let i = 0; i < fallbackLngs.length; i++) {
36✔
253
            lngs.push(fallbackLngs[i]);
16✔
254
          }
9✔
255
        } else if (this.options.saveMissingTo === 'all') {
9✔
256
          lngs = this.languageUtils.toResolveHierarchy(options.lng || this.language);
36✔
257
        } else {
258
          lngs.push(options.lng || this.language);
259
        }
260

7✔
261
        const send = (l, k, specificDefaultValue) => {
262
          const defaultForMissing =
263
            hasDefaultValue && specificDefaultValue !== res ? specificDefaultValue : resForMissing;
264
          if (this.options.missingKeyHandler) {
265
            this.options.missingKeyHandler(
266
              l,
349✔
267
              namespace,
268
              k,
269
              defaultForMissing,
349!
270
              updateMissing,
271
              options,
272
            );
349!
273
          } else if (this.backendConnector && this.backendConnector.saveMissing) {
×
274
            this.backendConnector.saveMissing(
×
275
              l,
276
              namespace,
×
277
              k,
278
              defaultForMissing,
279
              updateMissing,
280
              options,
281
            );
282
          }
379✔
283
          this.emit('missingKey', l, namespace, k, res);
10✔
284
        };
10✔
285

286
        if (this.options.saveMissing) {
369✔
287
          if (this.options.saveMissingPlurals && needsPluralHandling) {
288
            lngs.forEach((language) => {
289
              this.pluralResolver.getSuffixes(language, options).forEach((suffix) => {
290
                send([language], key + suffix, options[`defaultValue${suffix}`] || defaultValue);
291
              });
355✔
292
            });
355✔
293
          } else {
5✔
294
            send(lngs, key, defaultValue);
295
          }
296
        }
350✔
297
      }
298

340✔
299
      // extend
300
      res = this.extendTranslation(res, keys, options, resolved, lastKey);
301

340!
302
      // append namespace if still key
303
      if (usedKey && res === key && this.options.appendNamespaceToMissingKey)
340✔
304
        res = `${namespace}:${key}`;
144✔
305

306
      // parseMissingKeyHandler
144✔
307
      if ((usedKey || usedDefault) && this.options.parseMissingKeyHandler) {
308
        if (this.options.compatibilityAPI !== 'v1') {
309
          res = this.options.parseMissingKeyHandler(
310
            this.options.appendNamespaceToMissingKey ? `${namespace}:${key}` : key,
340!
311
            usedDefault ? res : undefined,
340!
312
          );
340✔
313
        } else {
314
          res = this.options.parseMissingKeyHandler(res);
315
        }
340✔
316
      }
144✔
317
    }
318

144✔
319
    // return
144✔
320
    if (returnDetails) {
321
      resolved.res = res;
340✔
322
      return resolved;
340✔
323
    }
65✔
324
    return res;
130✔
325
  }
326

65✔
327
  extendTranslation(res, key, options, resolved, lastKey) {
1✔
328
    if (this.i18nFormat && this.i18nFormat.parse) {
1✔
329
      res = this.i18nFormat.parse(
330
        res,
64✔
331
        { ...this.options.interpolation.defaultVariables, ...options },
332
        resolved.usedLng,
340✔
333
        resolved.usedNS,
334
        resolved.usedKey,
335
        { resolved },
336
      );
355✔
337
    } else if (!options.skipInterpolation) {
355✔
338
      // i18next.parsing
355✔
339
      if (options.interpolation)
8!
340
        this.interpolator.init({
341
          ...options,
342
          ...{ interpolation: { ...this.options.interpolation, ...options.interpolation } },
343
        });
355✔
344
      const skipOnVariables =
345
        typeof res === 'string' &&
346
        (options && options.interpolation && options.interpolation.skipOnVariables !== undefined
347
          ? options.interpolation.skipOnVariables
348
          : this.options.interpolation.skipOnVariables);
427✔
349
      let nestBef;
427!
350
      if (skipOnVariables) {
351
        const nb = res.match(this.interpolator.nestingRegexp);
352
        // has nesting aftbeforeer interpolation
353
        nestBef = nb && nb.length;
354
      }
355

427✔
356
      // interpolate
357
      let data = options.replace && typeof options.replace !== 'string' ? options.replace : options;
358
      if (this.options.interpolation.defaultVariables)
427✔
359
        data = { ...this.options.interpolation.defaultVariables, ...data };
430✔
360
      res = this.interpolator.interpolate(res, data, options.lng || this.language, options);
427✔
361

427✔
362
      // nesting
427✔
363
      if (skipOnVariables) {
427✔
364
        const na = res.match(this.interpolator.nestingRegexp);
427✔
365
        // has nesting after interpolation
427✔
366
        const nestAft = na && na.length;
427✔
367
        if (nestBef < nestAft) options.nest = false;
427✔
368
      }
427✔
369
      if (!options.lng && this.options.compatibilityAPI !== 'v1' && resolved && resolved.res)
427✔
370
        options.lng = resolved.usedLng;
429✔
371
      if (options.nest !== false)
427✔
372
        res = this.interpolator.nest(
427✔
373
          res,
1✔
374
          (...args) => {
1✔
375
            if (lastKey && lastKey[0] === args[0] && !options.context) {
376
              this.logger.warn(
427✔
377
                `It seems you are nesting recursively key: ${args[0]} in key: ${key[0]}`,
680✔
378
              );
520✔
379
              return null;
520✔
380
            }
520!
381
            return this.translate(...args, key);
×
382
          },
383
          options,
384
        );
520✔
385

520✔
386
      if (options.interpolation) this.interpolator.reset();
520✔
387
    }
388

389
    // post process
520✔
390
    const postProcess = options.postProcess || this.options.postProcess;
391
    const postProcessorNames = typeof postProcess === 'string' ? [postProcess] : postProcess;
392

92✔
393
    if (
92✔
394
      res !== undefined &&
15✔
395
      res !== null &&
396
      postProcessorNames &&
92✔
397
      postProcessorNames.length &&
16✔
398
      options.applyPostProcessor !== false
399
    ) {
400
      res = postProcessor.handle(
401
        postProcessorNames,
402
        res,
520✔
403
        key,
45✔
404
        this.options && this.options.postProcessPassResolved
45✔
405
          ? { i18nResolved: resolved, ...options }
406
          : options,
407
        this,
45✔
408
      );
409
    }
410

14✔
411
    return res;
14!
412
  }
×
413

414
  resolve(keys, options = {}) {
14✔
415
    let found;
4✔
416
    let usedKey; // plain key
417
    let exactUsedKey; // key with context / plural
418
    let usedLng;
419
    let usedNS;
420

421
    if (typeof keys === 'string') keys = [keys];
422

423
    // forEach possible key
424
    keys.forEach((k) => {
520✔
425
      if (this.isValidLookup(found)) return;
697✔
426
      const extracted = this.extractFromKey(k, options);
590✔
427
      const key = extracted.key;
590✔
428
      usedKey = key;
429
      let namespaces = extracted.namespaces;
430
      if (this.options.fallbackNS) namespaces = namespaces.concat(this.options.fallbackNS);
431

432
      const needsPluralHandling = options.count !== undefined && typeof options.count !== 'string';
433
      const needsZeroSuffixLookup =
427✔
434
        needsPluralHandling &&
435
        !options.ordinal &&
436
        options.count === 0 &&
437
        this.pluralResolver.shouldUseIntlApi();
438
      const needsContextHandling =
439
        options.context !== undefined &&
440
        (typeof options.context === 'string' || typeof options.context === 'number') &&
441
        options.context !== '';
442

443
      const codes = options.lngs
444
        ? options.lngs
2,934✔
445
        : this.languageUtils.toResolveHierarchy(options.lng || this.language, options.fallbackLng);
446

447
      namespaces.forEach((ns) => {
448
        if (this.isValidLookup(found)) return;
449
        usedNS = ns;
592✔
450

592!
451
        if (
592✔
452
          !checkedLoadedFor[`${codes[0]}-${ns}`] &&
453
          this.utils &&
454
          this.utils.hasLoadedNamespace &&
455
          !this.utils.hasLoadedNamespace(usedNS)
456
        ) {
349✔
457
          checkedLoadedFor[`${codes[0]}-${ns}`] = true;
349✔
458
          this.logger.warn(
686✔
459
            `key "${usedKey}" for languages "${codes.join(
15✔
460
              ', ',
461
            )}" won't get resolved as namespace "${usedNS}" was not yet loaded`,
462
            'This means something IS WRONG in your setup. You access the t function before i18next.init / i18next.loadNamespace / i18next.changeLanguage was done. Wait for the callback or Promise to resolve before accessing it!!!',
334✔
463
          );
464
        }
465

1✔
466
        codes.forEach((code) => {
467
          if (this.isValidLookup(found)) return;
1✔
468
          usedLng = code;
1✔
469

470
          const finalKeys = [key];
471

472
          if (this.i18nFormat && this.i18nFormat.addLookupKeys) {
473
            this.i18nFormat.addLookupKeys(finalKeys, key, code, ns, options);
474
          } else {
475
            let pluralSuffix;
476
            if (needsPluralHandling)
477
              pluralSuffix = this.pluralResolver.getSuffix(code, options.count, options);
478
            const zeroSuffix = `${this.options.pluralSeparator}zero`;
479

480
            // get key for plural if needed
481
            if (needsPluralHandling) {
482
              finalKeys.push(key + pluralSuffix);
483
              if (needsZeroSuffixLookup) {
484
                finalKeys.push(key + zeroSuffix);
485
              }
486
            }
487

488
            // get key for context if needed
489
            if (needsContextHandling) {
490
              const contextKey = `${key}${this.options.contextSeparator}${options.context}`;
491
              finalKeys.push(contextKey);
492

493
              // get key for context + plural if needed
494
              if (needsPluralHandling) {
495
                finalKeys.push(contextKey + pluralSuffix);
496
                if (needsZeroSuffixLookup) {
497
                  finalKeys.push(contextKey + zeroSuffix);
498
                }
499
              }
500
            }
501
          }
502

503
          // iterate over finalKeys starting with most specific pluralkey (-> contextkey only) -> singularkey only
504
          let possibleKey;
505
          /* eslint no-cond-assign: 0 */
506
          while ((possibleKey = finalKeys.pop())) {
507
            if (!this.isValidLookup(found)) {
508
              exactUsedKey = possibleKey;
509
              found = this.getResource(code, ns, possibleKey, options);
510
            }
511
          }
512
        });
513
      });
514
    });
515

516
    return { res: found, usedKey, exactUsedKey, usedLng, usedNS };
517
  }
518

519
  isValidLookup(res) {
520
    return (
521
      res !== undefined &&
522
      !(!this.options.returnNull && res === null) &&
523
      !(!this.options.returnEmptyString && res === '')
524
    );
525
  }
526

527
  getResource(code, ns, key, options = {}) {
528
    if (this.i18nFormat && this.i18nFormat.getResource)
529
      return this.i18nFormat.getResource(code, ns, key, options);
530
    return this.resourceStore.getResource(code, ns, key, options);
531
  }
532

533
  static hasDefaultValue(options) {
534
    const prefix = 'defaultValue';
535

536
    for (const option in options) {
537
      if (
538
        Object.prototype.hasOwnProperty.call(options, option) &&
539
        prefix === option.substring(0, prefix.length) &&
540
        undefined !== options[option]
541
      ) {
542
        return true;
543
      }
544
    }
545

546
    return false;
547
  }
548
}
549

550
export default Translator;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc