Skip to main content
Module

x/i18next/Interpolator.js

i18next: learn once - translate everywhere
Go to Latest
File
import * as utils from './utils.js';import baseLogger from './logger.js';
class Interpolator { constructor(options = {}) { this.logger = baseLogger.create('interpolator');
this.options = options; this.format = (options.interpolation && options.interpolation.format) || ((value) => value); this.init(options); }
/* eslint no-param-reassign: 0 */ init(options = {}) { if (!options.interpolation) options.interpolation = { escapeValue: true };
const iOpts = options.interpolation;
this.escape = iOpts.escape !== undefined ? iOpts.escape : utils.escape; this.escapeValue = iOpts.escapeValue !== undefined ? iOpts.escapeValue : true; this.useRawValueToEscape = iOpts.useRawValueToEscape !== undefined ? iOpts.useRawValueToEscape : false;
this.prefix = iOpts.prefix ? utils.regexEscape(iOpts.prefix) : iOpts.prefixEscaped || '{{'; this.suffix = iOpts.suffix ? utils.regexEscape(iOpts.suffix) : iOpts.suffixEscaped || '}}';
this.formatSeparator = iOpts.formatSeparator ? iOpts.formatSeparator : iOpts.formatSeparator || ',';
this.unescapePrefix = iOpts.unescapeSuffix ? '' : iOpts.unescapePrefix || '-'; this.unescapeSuffix = this.unescapePrefix ? '' : iOpts.unescapeSuffix || '';
this.nestingPrefix = iOpts.nestingPrefix ? utils.regexEscape(iOpts.nestingPrefix) : iOpts.nestingPrefixEscaped || utils.regexEscape('$t('); this.nestingSuffix = iOpts.nestingSuffix ? utils.regexEscape(iOpts.nestingSuffix) : iOpts.nestingSuffixEscaped || utils.regexEscape(')');
this.nestingOptionsSeparator = iOpts.nestingOptionsSeparator ? iOpts.nestingOptionsSeparator : iOpts.nestingOptionsSeparator || ',';
this.maxReplaces = iOpts.maxReplaces ? iOpts.maxReplaces : 1000;
this.alwaysFormat = iOpts.alwaysFormat !== undefined ? iOpts.alwaysFormat : false;
// the regexp this.resetRegExp(); }
reset() { if (this.options) this.init(this.options); }
resetRegExp() { // the regexp const regexpStr = `${this.prefix}(.+?)${this.suffix}`; this.regexp = new RegExp(regexpStr, 'g');
const regexpUnescapeStr = `${this.prefix}${this.unescapePrefix}(.+?)${this.unescapeSuffix}${this.suffix}`; this.regexpUnescape = new RegExp(regexpUnescapeStr, 'g');
const nestingRegexpStr = `${this.nestingPrefix}(.+?)${this.nestingSuffix}`; this.nestingRegexp = new RegExp(nestingRegexpStr, 'g'); }
interpolate(str, data, lng, options) { let match; let value; let replaces;
const defaultData = (this.options && this.options.interpolation && this.options.interpolation.defaultVariables) || {};
function regexSafe(val) { return val.replace(/\$/g, '$$$$'); }
const handleFormat = (key) => { if (key.indexOf(this.formatSeparator) < 0) { const path = utils.getPathWithDefaults(data, defaultData, key); return this.alwaysFormat ? this.format(path, undefined, lng, { ...options, ...data, interpolationkey: key }) : path; }
const p = key.split(this.formatSeparator); const k = p.shift().trim(); const f = p.join(this.formatSeparator).trim();
return this.format(utils.getPathWithDefaults(data, defaultData, k), f, lng, { ...options, ...data, interpolationkey: k, }); };
this.resetRegExp();
const missingInterpolationHandler = (options && options.missingInterpolationHandler) || this.options.missingInterpolationHandler;
const skipOnVariables = options && options.interpolation && options.interpolation.skipOnVariables !== undefined ? options.interpolation.skipOnVariables : this.options.interpolation.skipOnVariables;
const todos = [ { // unescape if has unescapePrefix/Suffix regex: this.regexpUnescape, safeValue: (val) => regexSafe(val), }, { // regular escape on demand regex: this.regexp, safeValue: (val) => (this.escapeValue ? regexSafe(this.escape(val)) : regexSafe(val)), }, ]; todos.forEach((todo) => { replaces = 0; /* eslint no-cond-assign: 0 */ while ((match = todo.regex.exec(str))) { const matchedVar = match[1].trim(); value = handleFormat(matchedVar); if (value === undefined) { if (typeof missingInterpolationHandler === 'function') { const temp = missingInterpolationHandler(str, match, options); value = typeof temp === 'string' ? temp : ''; } else if (options && options.hasOwnProperty(matchedVar)) { value = ''; // undefined becomes empty string } else if (skipOnVariables) { value = match[0]; continue; // this makes sure it continues to detect others } else { this.logger.warn(`missed to pass in variable ${matchedVar} for interpolating ${str}`); value = ''; } } else if (typeof value !== 'string' && !this.useRawValueToEscape) { value = utils.makeString(value); } const safeValue = todo.safeValue(value); str = str.replace(match[0], safeValue); if (skipOnVariables) { todo.regex.lastIndex += safeValue.length; todo.regex.lastIndex -= match[0].length; } else { todo.regex.lastIndex = 0; } replaces++; if (replaces >= this.maxReplaces) { break; } } }); return str; }
nest(str, fc, options = {}) { let match; let value;
let clonedOptions = { ...options }; clonedOptions.applyPostProcessor = false; // avoid post processing on nested lookup delete clonedOptions.defaultValue; // assert we do not get a endless loop on interpolating defaultValue again and again
// if value is something like "myKey": "lorem $(anotherKey, { "count": {{aValueInOptions}} })" function handleHasOptions(key, inheritedOptions) { const sep = this.nestingOptionsSeparator; if (key.indexOf(sep) < 0) return key;
const c = key.split(new RegExp(`${sep}[ ]*{`));
let optionsString = `{${c[1]}`; key = c[0]; optionsString = this.interpolate(optionsString, clonedOptions); optionsString = optionsString.replace(/'/g, '"');
try { clonedOptions = JSON.parse(optionsString);
if (inheritedOptions) clonedOptions = { ...inheritedOptions, ...clonedOptions }; } catch (e) { this.logger.warn(`failed parsing options string in nesting for key ${key}`, e); return `${key}${sep}${optionsString}`; }
// assert we do not get a endless loop on interpolating defaultValue again and again delete clonedOptions.defaultValue; return key; }
// regular escape on demand while ((match = this.nestingRegexp.exec(str))) { let formatters = [];
/** * If there is more than one parameter (contains the format separator). E.g.: * - t(a, b) * - t(a, b, c) * * And those parameters are not dynamic values (parameters do not include curly braces). E.g.: * - Not t(a, { "key": "{{variable}}" }) * - Not t(a, b, {"keyA": "valueA", "keyB": "valueB"}) */ let doReduce = false; if (match[0].indexOf(this.formatSeparator) !== -1 && !/{.*}/.test(match[1])) { const r = match[1].split(this.formatSeparator).map((elem) => elem.trim()); match[1] = r.shift(); formatters = r; doReduce = true; }
value = fc(handleHasOptions.call(this, match[1].trim(), clonedOptions), clonedOptions);
// is only the nesting key (key1 = '$(key2)') return the value without stringify if (value && match[0] === str && typeof value !== 'string') return value;
// no string to include or empty if (typeof value !== 'string') value = utils.makeString(value); if (!value) { this.logger.warn(`missed to resolve ${match[1]} for nesting ${str}`); value = ''; }
if (doReduce) { value = formatters.reduce( /* eslint-disable-line no-loop-func:0 */ (v, f) => this.format(v, f, options.lng, { ...options, interpolationkey: match[1].trim() }), value.trim(), ); }
// Nested keys should not be escaped by default #854 // value = this.escapeValue ? regexSafe(utils.escape(value)) : regexSafe(value); str = str.replace(match[0], value); this.regexp.lastIndex = 0; } return str; }}
export default Interpolator;