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

i18next / i18next / #12469

19 Apr 2018 07:07AM UTC coverage: 87.43% (+2.0%) from 85.448%
#12469

push

jamuhl
rebuild

932 of 1066 relevant lines covered (87.43%)

38.46 hits per line

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

92.86
/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
class Translator extends EventEmitter {
7
  constructor(services, options = {}) {
102✔
8
    super();
9

103✔
10
    utils.copy(['resourceStore', 'languageUtils', 'pluralResolver', 'interpolator', 'backendConnector', 'i18nFormat'], services, this);
11

1✔
12
    this.options = options;
13
    this.logger = baseLogger.create('translator');
1✔
14
  }
15

1✔
16
  changeLanguage(lng) {
17
    if (lng) this.language = lng;
1✔
18
  }
19

1✔
20
  exists(key, options = { interpolation: {} }) {
21
    const resolved = this.resolve(key, options);
1✔
22
    return  resolved && resolved.res !== undefined;
23
  }
1✔
24

25
  extractFromKey(key, options) {
1✔
26
    let nsSeparator = options.nsSeparator || this.options.nsSeparator;
27
    if (nsSeparator === undefined) nsSeparator = ':';
1✔
28
    const keySeparator = options.keySeparator || this.options.keySeparator || '.';
29

3✔
30
    let namespaces = options.ns || this.options.defaultNS;
31
    if (nsSeparator && key.indexOf(nsSeparator) > -1) {
5✔
32
      const parts = key.split(nsSeparator);
33
      if (nsSeparator !== keySeparator || (nsSeparator === keySeparator && this.options.ns.indexOf(parts[0]) > -1)) namespaces = parts.shift();
20✔
34
      key = parts.join(keySeparator);
35
    }
20✔
36
    if (typeof namespaces === 'string') namespaces = [namespaces];
37

1✔
38
    return {
39
      key,
1✔
40
      namespaces
1✔
41
    };
42
  }
1✔
43

20✔
44
  translate(keys, options) {
45
    if (typeof options !== 'object' && this.options.overloadTranslationOptionHandler) {
20✔
46
      /* eslint prefer-rest-params: 0 */
47
      options = this.options.overloadTranslationOptionHandler(arguments);
20✔
48
    }
49
    if (!options) options = {};
20✔
50

51
    // non valid keys handling
20✔
52
    if (keys === undefined || keys === null || keys === '') return '';
20✔
53
    if (typeof keys === 'number') keys = String(keys);
20✔
54
    if (typeof keys === 'string') keys = [keys];
55

56
    // separators
1✔
57
    const keySeparator = options.keySeparator || this.options.keySeparator || '.';
26✔
58

59
    // get namespace(s)
60
    const { key, namespaces } = this.extractFromKey(keys[keys.length - 1], options);
1✔
61
    const namespace = namespaces[namespaces.length - 1];
9✔
62

63
    // return key on CIMode
9✔
64
    const lng = options.lng || this.language;
9✔
65
    const appendNamespaceToCIMode = options.appendNamespaceToCIMode || this.options.appendNamespaceToCIMode;
66
    if (lng && lng.toLowerCase() === 'cimode') {
67
      if (appendNamespaceToCIMode) {
1✔
68
        const nsSeparator = options.nsSeparator || this.options.nsSeparator;
243✔
69
        return namespace + nsSeparator + key;
243✔
70
      }
243✔
71

72
      return key;
243✔
73
    }
243✔
74

139✔
75
    // resolve from store
139✔
76
    const resolved = this.resolve(keys, options);
139✔
77
    let res = resolved && resolved.res;
78
    const resUsedKey = (resolved && resolved.usedKey) || key;
243✔
79

80
    const resType = Object.prototype.toString.apply(res);
243✔
81
    const noObject = ['[object Number]', '[object Function]', '[object RegExp]'];
82
    const joinArrays = options.joinArrays !== undefined ? options.joinArrays : this.options.joinArrays;
83

84
    // object
85
    const handleAsObject = typeof res !== 'string' && typeof res !== 'boolean' && typeof res !== 'number';
86
    if (res && handleAsObject && noObject.indexOf(resType) < 0 && !(joinArrays && resType === '[object Array]')) {
1✔
87
      if (!options.returnObjects && !this.options.returnObjects) {
119✔
88
        this.logger.warn('accessing an object - but returnObjects options is not enabled!');
89
        return this.options.returnedObjectHandler ? this.options.returnedObjectHandler(resUsedKey, res, options) : `key '${key} (${this.language})' returned an object instead of string.`;
119✔
90
      }
91

×
92
      // if we got a separator we loop over children - else we just return object as is
93
      // as having it set to false means no hierarchy so no lookup for nested values
119✔
94
      if (options.keySeparator || this.options.keySeparator) {
95
        const copy = (resType === '[object Array]') ? [] : {}; // apply child translation on a copy
96

119✔
97
        /* eslint no-restricted-syntax: 0 */
119✔
98
        for (const m in res) {
119✔
99
          if (Object.prototype.hasOwnProperty.call(res, m)) {
100
            const deepKey = `${resUsedKey}${keySeparator}${m}`;
101
            copy[m] = this.translate(deepKey, { ...options, ...{ joinArrays: false, ns: namespaces } });
119✔
102
            if (copy[m] === deepKey) copy[m] = res[m]; // if nothing found use orginal value as fallback
103
          }
104
        }
105
        res = copy;
119✔
106
      }
107
    } else if (joinArrays && resType === '[object Array]') {
108
      // array special treatment
109
      res = res.join(joinArrays);
119✔
110
      if (res) res = this.extendTranslation(res, keys, options);
111
    } else {
112
      // string, empty or null
119✔
113
      let usedDefault = false;
119✔
114
      let usedKey = false;
119✔
115

4✔
116
      // fallback value
2✔
117
      if (!this.isValidLookup(res) && options.defaultValue !== undefined) {
2✔
118
        usedDefault = true;
119
        res = options.defaultValue;
120
      }
2✔
121
      if (!this.isValidLookup(res)) {
122
        usedKey = true;
123
        res = key;
124
      }
115✔
125

115✔
126
      // save missing
115✔
127
      const updateMissing = options.defaultValue && options.defaultValue !== res && this.options.updateMissing;
128
      if (usedKey || usedDefault || updateMissing) {
115✔
129
        this.logger.log(updateMissing ? 'updateKey' : 'missingKey', lng, namespace, key, updateMissing ? options.defaultValue : res);
115✔
130

115✔
131
        let lngs = [];
132
        const fallbackLngs = this.languageUtils.getFallbackCodes(this.options.fallbackLng, options.lng || this.language);
133
        if (this.options.saveMissingTo === 'fallback' && fallbackLngs && fallbackLngs[0]) {
115✔
134
          for (let i = 0; i < fallbackLngs.length; i++) {
115✔
135
            lngs.push(fallbackLngs[i]);
16✔
136
          }
×
137
        } else if (this.options.saveMissingTo === 'all') {
×
138
          lngs = this.languageUtils.toResolveHierarchy(options.lng || this.language);
139
        } else {
140
          lngs.push(options.lng || this.language);
141
        }
142

16✔
143
        const send = (l, k) => {
16✔
144
          if (this.options.missingKeyHandler) {
145
            this.options.missingKeyHandler(l, namespace, k, updateMissing ? options.defaultValue : res, updateMissing, options);
146
          } else if (this.backendConnector && this.backendConnector.saveMissing) {
16✔
147
            this.backendConnector.saveMissing(l, namespace, k, updateMissing ? options.defaultValue : res, updateMissing, options);
29✔
148
          }
29✔
149
          this.emit('missingKey', l, namespace, k, res);
29✔
150
        };
29✔
151

152
        if (this.options.saveMissing) {
153
          if (this.options.saveMissingPlurals && options.count) {
16✔
154
            lngs.forEach((l) => {
155
              const plurals = this.pluralResolver.getPluralFormsOfKey(l, key);
99✔
156

157
              plurals.forEach(p => send([l], p));
2✔
158
            });
2✔
159
          } else {
160
            send(lngs, key);
161
          }
97✔
162
        }
97✔
163
      }
164

165
      // extend
97✔
166
      res = this.extendTranslation(res, keys, options, resolved);
×
167

×
168
      // append namespace if still key
169
      if (usedKey && res === key && this.options.appendNamespaceToMissingKey) res = `${namespace}:${key}`;
97✔
170

4✔
171
      // parseMissingKeyHandler
4✔
172
      if (usedKey && this.options.parseMissingKeyHandler) res = this.options.parseMissingKeyHandler(res);
173
    }
174

175
    // return
97✔
176
    return res;
97✔
177
  }
5✔
178

179
  extendTranslation(res, key, options, resolved) {
5✔
180
    if (this.i18nFormat && this.i18nFormat.parse) {
5✔
181
      res = this.i18nFormat.parse(res, options, resolved.usedLng, resolved.usedNS, resolved.usedKey);
5✔
182
    } else { // i18next.parsing
×
183
      if (options.interpolation) this.interpolator.init({ ...options, ...{ interpolation: { ...this.options.interpolation, ...options.interpolation } } });
×
184

185
      // interpolate
5✔
186
      let data = options.replace && typeof options.replace !== 'string' ? options.replace : options;
×
187
      if (this.options.interpolation.defaultVariables) data = { ...this.options.interpolation.defaultVariables, ...data };
188
      res = this.interpolator.interpolate(res, data, options.lng || this.language);
5✔
189

190
      // nesting
191
      if (options.nest !== false) res = this.interpolator.nest(res, (...args) => this.translate(...args), options);
5✔
192

8✔
193
      if (options.interpolation) this.interpolator.reset();
8✔
194
    }
×
195

×
196
    // post process
197
    const postProcess = options.postProcess || this.options.postProcess;
8✔
198
    const postProcessorNames = typeof postProcess === 'string' ? [postProcess] : postProcess;
199

200
    if (res !== undefined &&
5✔
201
      res !== null &&
3✔
202
      postProcessorNames &&
1✔
203
      postProcessorNames.length &&
1✔
204
      options.applyPostProcessor !== false) {
205
      res = postProcessor.handle(postProcessorNames, res, key, options, this);
1✔
206
    }
6✔
207

208
    return res;
209
  }
210

2✔
211
  resolve(keys, options = {}) {
212
    let found;
213
    let usedKey;
214
    let usedLng;
215
    let usedNS;
216

97✔
217
    if (typeof keys === 'string') keys = [keys];
218

219
    // forEach possible key
97✔
220
    keys.forEach((k) => {
221
      if (this.isValidLookup(found)) return;
222
      const extracted = this.extractFromKey(k, options);
97✔
223
      const key = extracted.key;
224
      usedKey = key;
225
      let namespaces = extracted.namespaces;
226
      if (this.options.fallbackNS) namespaces = namespaces.concat(this.options.fallbackNS);
115✔
227

228
      const needsPluralHandling = options.count !== undefined && typeof options.count !== 'string';
229
      const needsContextHandling = options.context !== undefined && typeof options.context === 'string' && options.context !== '';
1✔
230

99✔
231
      const codes = options.lngs ? options.lngs : this.languageUtils.toResolveHierarchy(options.lng || this.language);
232

99✔
233
      namespaces.forEach((ns) => {
×
234
        if (this.isValidLookup(found)) return;
235
        usedNS = ns;
236

99✔
237
        codes.forEach((code) => {
238
          if (this.isValidLookup(found)) return;
97✔
239
          usedLng = code;
240

241
          let finalKey = key;
97✔
242
          const finalKeys = [finalKey];
97✔
243

97✔
244
          if (this.i18nFormat && this.i18nFormat.addLookupKeys) {
245
            this.i18nFormat.addLookupKeys(finalKeys, key, code, ns, options);
246
          } else {
97✔
247
            let pluralSuffix;
3✔
248
            if (needsPluralHandling) pluralSuffix = this.pluralResolver.getSuffix(code, options.count);
249

250
            // fallback for plural if context not found
97✔
251
            if (needsPluralHandling && needsContextHandling) finalKeys.push(finalKey + pluralSuffix);
252

253
            // get key for context if needed
254
            if (needsContextHandling) finalKeys.push(finalKey += `${this.options.contextSeparator}${options.context}`);
255

99✔
256
            // get key for plural if needed
99✔
257
            if (needsPluralHandling) finalKeys.push(finalKey += pluralSuffix);
258
          }
99✔
259

×
260
          // iterate over finalKeys starting with most specific pluralkey (-> contextkey only) -> singularkey only
261
          let possibleKey;
262
          /* eslint no-cond-assign: 0 */
99✔
263
          while (possibleKey = finalKeys.pop()) {
264
            if (!this.isValidLookup(found)) {
265
              found = this.getResource(code, ns, possibleKey, options);
1✔
266
            }
124✔
267
          }
268
        });
124✔
269
      });
270
    });
124✔
271

124✔
272
    return { res: found, usedKey, usedLng, usedNS };
124✔
273
  }
124✔
274

275
  isValidLookup(res) {
124✔
276
    return res !== undefined &&
277
      !(!this.options.returnNull && res === null) &&
278
      !(!this.options.returnEmptyString && res === '');
124✔
279
  }
126✔
280

124✔
281
  getResource(code, ns, key, options = {}) {
124✔
282
    return this.resourceStore.getResource(code, ns, key, options);
124✔
283
  }
124✔
284
}
124✔
285

286
export default Translator;
124✔
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