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

i18next / i18next / #11965

08 Jun 2023 07:01AM UTC coverage: 56.318% (-38.9%) from 95.213%
#11965

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

574 of 1535 branches covered (37.39%)

517 of 918 relevant lines covered (56.32%)

24.67 hits per line

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

41.22
/src/Interpolator.js
1
import * as utils from './utils.js';
2
import baseLogger from './logger.js';
3

1✔
4
function deepFindWithDefaults(
5
  data,
6
  defaultData,
1✔
7
  key,
1✔
8
  keySeparator = '.',
1✔
9
  ignoreJSONStructure = true,
1!
10
) {
×
11
  let path = utils.getPathWithDefaults(data, defaultData, key);
1!
12
  if (!path && ignoreJSONStructure && typeof key === 'string') {
10!
13
    path = utils.deepFind(data, key, keySeparator);
×
14
    if (path === undefined) path = utils.deepFind(defaultData, key, keySeparator);
×
15
  }
×
16
  return path;
8!
17
}
5!
18

1!
19
class Interpolator {
5!
20
  constructor(options = {}) {
5!
21
    this.logger = baseLogger.create('interpolator');
22

×
23
    this.options = options;
×
24
    this.format = (options.interpolation && options.interpolation.format) || ((value) => value);
×
25
    this.init(options);
×
26
  }
×
27

×
28
  /* eslint no-param-reassign: 0 */
29
  init(options = {}) {
×
30
    if (!options.interpolation) options.interpolation = { escapeValue: true };
31

1✔
32
    const iOpts = options.interpolation;
33

8!
34
    this.escape = iOpts.escape !== undefined ? iOpts.escape : utils.escape;
8✔
35
    this.escapeValue = iOpts.escapeValue !== undefined ? iOpts.escapeValue : true;
8✔
36
    this.useRawValueToEscape =
8✔
37
      iOpts.useRawValueToEscape !== undefined ? iOpts.useRawValueToEscape : false;
8!
38

×
39
    this.prefix = iOpts.prefix ? utils.regexEscape(iOpts.prefix) : iOpts.prefixEscaped || '{{';
40
    this.suffix = iOpts.suffix ? utils.regexEscape(iOpts.suffix) : iOpts.suffixEscaped || '}}';
8✔
41

42
    this.formatSeparator = iOpts.formatSeparator
43
      ? iOpts.formatSeparator
44
      : iOpts.formatSeparator || ',';
1✔
45

46
    this.unescapePrefix = iOpts.unescapeSuffix ? '' : iOpts.unescapePrefix || '-';
47
    this.unescapeSuffix = this.unescapePrefix ? '' : iOpts.unescapeSuffix || '';
8!
48

8!
49
    this.nestingPrefix = iOpts.nestingPrefix
50
      ? utils.regexEscape(iOpts.nestingPrefix)
51
      : iOpts.nestingPrefixEscaped || utils.regexEscape('$t(');
8✔
52
    this.nestingSuffix = iOpts.nestingSuffix
8!
53
      ? utils.regexEscape(iOpts.nestingSuffix)
8!
54
      : iOpts.nestingSuffixEscaped || utils.regexEscape(')');
8!
55

8!
56
    this.nestingOptionsSeparator = iOpts.nestingOptionsSeparator
8!
57
      ? iOpts.nestingOptionsSeparator
8!
58
      : iOpts.nestingOptionsSeparator || ',';
8!
59

8!
60
    this.maxReplaces = iOpts.maxReplaces ? iOpts.maxReplaces : 1000;
8!
61

8!
62
    this.alwaysFormat = iOpts.alwaysFormat !== undefined ? iOpts.alwaysFormat : false;
8!
63

8!
64
    // the regexp
8!
65
    this.resetRegExp();
66
  }
67

8✔
68
  reset() {
69
    if (this.options) this.init(this.options);
70
  }
71

72
  resetRegExp() {
×
73
    // the regexp
74
    const regexpStr = `${this.prefix}(.+?)${this.suffix}`;
75
    this.regexp = new RegExp(regexpStr, 'g');
76

77
    const regexpUnescapeStr = `${this.prefix}${this.unescapePrefix}(.+?)${this.unescapeSuffix}${this.suffix}`;
78
    this.regexpUnescape = new RegExp(regexpUnescapeStr, 'g');
56✔
79

56✔
80
    const nestingRegexpStr = `${this.nestingPrefix}(.+?)${this.nestingSuffix}`;
56✔
81
    this.nestingRegexp = new RegExp(nestingRegexpStr, 'g');
56✔
82
  }
56✔
83

56✔
84
  interpolate(str, data, lng, options) {
85
    let match;
86
    let value;
87
    let replaces;
88

48✔
89
    const defaultData =
90
      (this.options && this.options.interpolation && this.options.interpolation.defaultVariables) ||
91
      {};
92

48✔
93
    function regexSafe(val) {
94
      return val.replace(/\$/g, '$$$$');
×
95
    }
96

48✔
97
    const handleFormat = (key) => {
×
98
      if (key.indexOf(this.formatSeparator) < 0) {
×
99
        const path = deepFindWithDefaults(
×
100
          data,
101
          defaultData,
102
          key,
103
          this.options.keySeparator,
×
104
          this.options.ignoreJSONStructure,
×
105
        );
×
106
        return this.alwaysFormat
×
107
          ? this.format(path, undefined, lng, { ...options, ...data, interpolationkey: key })
108
          : path;
109
      }
110

48✔
111
      const p = key.split(this.formatSeparator);
48✔
112
      const k = p.shift().trim();
48!
113
      const f = p.join(this.formatSeparator).trim();
48✔
114

115
      return this.format(
116
        deepFindWithDefaults(
117
          data,
×
118
          defaultData,
119
          k,
120
          this.options.keySeparator,
121
          this.options.ignoreJSONStructure,
122
        ),
123
        f,
×
124
        lng,
125
        {
126
          ...options,
48✔
127
          ...data,
96✔
128
          interpolationkey: k,
129
        },
96✔
130
      );
×
131
    };
×
132

×
133
    this.resetRegExp();
×
134

×
135
    const missingInterpolationHandler =
×
136
      (options && options.missingInterpolationHandler) || this.options.missingInterpolationHandler;
×
137

×
138
    const skipOnVariables =
×
139
      options && options.interpolation && options.interpolation.skipOnVariables !== undefined
×
140
        ? options.interpolation.skipOnVariables
×
141
        : this.options.interpolation.skipOnVariables;
142

×
143
    const todos = [
×
144
      {
145
        // unescape if has unescapePrefix/Suffix
×
146
        regex: this.regexpUnescape,
×
147
        safeValue: (val) => regexSafe(val),
148
      },
×
149
      {
×
150
        // regular escape on demand
×
151
        regex: this.regexp,
×
152
        safeValue: (val) => (this.escapeValue ? regexSafe(this.escape(val)) : regexSafe(val)),
×
153
      },
154
    ];
×
155
    todos.forEach((todo) => {
156
      replaces = 0;
×
157
      /* eslint no-cond-assign: 0 */
×
158
      while ((match = todo.regex.exec(str))) {
×
159
        const matchedVar = match[1].trim();
160
        value = handleFormat(matchedVar);
161
        if (value === undefined) {
162
          if (typeof missingInterpolationHandler === 'function') {
48✔
163
            const temp = missingInterpolationHandler(str, match, options);
164
            value = typeof temp === 'string' ? temp : '';
165
          } else if (options && Object.prototype.hasOwnProperty.call(options, matchedVar)) {
166
            value = ''; // undefined becomes empty string
167
          } else if (skipOnVariables) {
48✔
168
            value = match[0];
48!
169
            continue; // this makes sure it continues to detect others
170
          } else {
171
            this.logger.warn(`missed to pass in variable ${matchedVar} for interpolating ${str}`);
172
            value = '';
173
          }
174
        } else if (typeof value !== 'string' && !this.useRawValueToEscape) {
175
          value = utils.makeString(value);
×
176
        }
×
177
        const safeValue = todo.safeValue(value);
×
178
        str = str.replace(match[0], safeValue);
×
179
        if (skipOnVariables) {
×
180
          todo.regex.lastIndex += value.length;
×
181
          todo.regex.lastIndex -= match[0].length;
×
182
        } else {
×
183
          todo.regex.lastIndex = 0;
×
184
        }
×
185
        replaces++;
186
        if (replaces >= this.maxReplaces) {
×
187
          break;
×
188
        }
×
189
      }
190
    });
×
191
    return str;
×
192
  }
193

194
  nest(str, fc, options = {}) {
195
    let match;
×
196
    let value;
×
197

198
    let clonedOptions;
199

200
    // if value is something like "myKey": "lorem $(anotherKey, { "count": {{aValueInOptions}} })"
48✔
201
    function handleHasOptions(key, inheritedOptions) {
×
202
      const sep = this.nestingOptionsSeparator;
×
203
      if (key.indexOf(sep) < 0) return key;
×
204

×
205
      const c = key.split(new RegExp(`${sep}[ ]*{`));
×
206

207
      let optionsString = `{${c[1]}`;
208
      key = c[0];
209
      optionsString = this.interpolate(optionsString, clonedOptions);
210
      const matchedSingleQuotes = optionsString.match(/'/g);
211
      const matchedDoubleQuotes = optionsString.match(/"/g);
212
      if (
213
        (matchedSingleQuotes && matchedSingleQuotes.length % 2 === 0 && !matchedDoubleQuotes) ||
214
        matchedDoubleQuotes.length % 2 !== 0
215
      ) {
216
        optionsString = optionsString.replace(/'/g, '"');
×
217
      }
×
218

×
219
      try {
×
220
        clonedOptions = JSON.parse(optionsString);
221

×
222
        if (inheritedOptions) clonedOptions = { ...inheritedOptions, ...clonedOptions };
×
223
      } catch (e) {
×
224
        this.logger.warn(`failed parsing options string in nesting for key ${key}`, e);
225
        return `${key}${sep}${optionsString}`;
×
226
      }
227

228
      // assert we do not get a endless loop on interpolating defaultValue again and again
×
229
      delete clonedOptions.defaultValue;
230
      return key;
231
    }
×
232

×
233
    // regular escape on demand
×
234
    while ((match = this.nestingRegexp.exec(str))) {
×
235
      let formatters = [];
236

×
237
      clonedOptions = { ...options };
×
238
      clonedOptions =
239
        clonedOptions.replace && typeof clonedOptions.replace !== 'string'
240
          ? clonedOptions.replace
×
241
          : clonedOptions;
242
      clonedOptions.applyPostProcessor = false; // avoid post processing on nested lookup
243
      delete clonedOptions.defaultValue; // assert we do not get a endless loop on interpolating defaultValue again and again
244

245
      /**
246
       * If there is more than one parameter (contains the format separator). E.g.:
247
       *   - t(a, b)
248
       *   - t(a, b, c)
×
249
       *
×
250
       * And those parameters are not dynamic values (parameters do not include curly braces). E.g.:
251
       *   - Not t(a, { "key": "{{variable}}" })
48✔
252
       *   - Not t(a, b, {"keyA": "valueA", "keyB": "valueB"})
253
       */
254
      let doReduce = false;
1✔
255
      if (match[0].indexOf(this.formatSeparator) !== -1 && !/{.*}/.test(match[1])) {
256
        const r = match[1].split(this.formatSeparator).map((elem) => elem.trim());
1✔
257
        match[1] = r.shift();
1✔
258
        formatters = r;
259
        doReduce = true;
260
      }
261

262
      value = fc(handleHasOptions.call(this, match[1].trim(), clonedOptions), clonedOptions);
263

264
      // is only the nesting key (key1 = '$(key2)') return the value without stringify
265
      if (value && match[0] === str && typeof value !== 'string') return value;
266

267
      // no string to include or empty
268
      if (typeof value !== 'string') value = utils.makeString(value);
269
      if (!value) {
270
        this.logger.warn(`missed to resolve ${match[1]} for nesting ${str}`);
271
        value = '';
272
      }
273

274
      if (doReduce) {
275
        value = formatters.reduce(
276
          // eslint-disable-next-line no-loop-func
277
          (v, f) =>
278
            this.format(v, f, options.lng, { ...options, interpolationkey: match[1].trim() }),
279
          value.trim(),
280
        );
281
      }
282

283
      // Nested keys should not be escaped by default #854
284
      // value = this.escapeValue ? regexSafe(utils.escape(value)) : regexSafe(value);
285
      str = str.replace(match[0], value);
286
      this.regexp.lastIndex = 0;
287
    }
288
    return str;
289
  }
290
}
291

292
export default Interpolator;
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