UNPKG

@browsery/i18next

Version:

Browser compatible i18next module

1,567 lines (1,346 loc) 97.4 kB
'use strict'; function assertOk(a){ throw new TypeError('AssertionError [ERR_ASSERTION]: ' + JSON.stringify(a) + ' == true'); }; function assertEqual(a, b){ throw new TypeError('AssertionError [ERR_ASSERTION]: ' + JSON.stringify(a) + ' == ' + JSON.stringify(b)); }; Object.defineProperty(exports, '__esModule', { value: true }); const consoleLogger = { type: 'logger', log(args) { this.output('log', args); }, warn(args) { this.output('warn', args); }, error(args) { this.output('error', args); }, output(type, args) { /* eslint no-console: 0 */ if (console && console[type]) console[type].apply(console, args); }, }; class Logger { constructor(concreteLogger, options = {}) { this.init(concreteLogger, options); } init(concreteLogger, options = {}) { this.prefix = options.prefix || 'i18next:'; this.logger = concreteLogger || consoleLogger; this.options = options; this.debug = options.debug; } log(...args) { return this.forward(args, 'log', '', true); } warn(...args) { return this.forward(args, 'warn', '', true); } error(...args) { return this.forward(args, 'error', ''); } deprecate(...args) { return this.forward(args, 'warn', 'WARNING DEPRECATED: ', true); } forward(args, lvl, prefix, debugOnly) { if (debugOnly && !this.debug) return null; if (typeof args[0] === 'string') args[0] = `${prefix}${this.prefix} ${args[0]}`; return this.logger[lvl](args); } create(moduleName) { return new Logger(this.logger, { ...{ prefix: `${this.prefix}:${moduleName}:` }, ...this.options, }); } clone(options) { options = options || this.options; options.prefix = options.prefix || this.prefix; return new Logger(this.logger, options); } } var baseLogger = new Logger(); class EventEmitter { constructor() { // This is an Object containing Maps: // // { [event: string]: Map<listener: function, numTimesAdded: number> } // // We use a Map for O(1) insertion/deletion and because it can have functions as keys. // // We keep track of numTimesAdded (the number of times it was added) because if you attach the same listener twice, // we should actually call it twice for each emitted event. this.observers = {}; } on(events, listener) { events.split(' ').forEach((event) => { if (!this.observers[event]) this.observers[event] = new Map(); const numListeners = this.observers[event].get(listener) || 0; this.observers[event].set(listener, numListeners + 1); }); return this; } off(event, listener) { if (!this.observers[event]) return; if (!listener) { delete this.observers[event]; return; } this.observers[event].delete(listener); } emit(event, ...args) { if (this.observers[event]) { const cloned = Array.from(this.observers[event].entries()); cloned.forEach(([observer, numTimesAdded]) => { for (let i = 0; i < numTimesAdded; i++) { observer(...args); } }); } if (this.observers['*']) { const cloned = Array.from(this.observers['*'].entries()); cloned.forEach(([observer, numTimesAdded]) => { for (let i = 0; i < numTimesAdded; i++) { observer.apply(observer, [event, ...args]); } }); } } } // http://lea.verou.me/2016/12/resolve-promises-externally-with-this-one-weird-trick/ const defer = () => { let res; let rej; const promise = new Promise((resolve, reject) => { res = resolve; rej = reject; }); promise.resolve = res; promise.reject = rej; return promise; }; const makeString = (object) => { if (object == null) return ''; /* eslint prefer-template: 0 */ return '' + object; }; const copy = (a, s, t) => { a.forEach((m) => { if (s[m]) t[m] = s[m]; }); }; // We extract out the RegExp definition to improve performance with React Native Android, which has poor RegExp // initialization performance const lastOfPathSeparatorRegExp = /###/g; const cleanKey = (key) => key && key.indexOf('###') > -1 ? key.replace(lastOfPathSeparatorRegExp, '.') : key; const canNotTraverseDeeper = (object) => !object || typeof object === 'string'; const getLastOfPath = (object, path, Empty) => { const stack = typeof path !== 'string' ? path : path.split('.'); let stackIndex = 0; // iterate through the stack, but leave the last item while (stackIndex < stack.length - 1) { if (canNotTraverseDeeper(object)) return {}; const key = cleanKey(stack[stackIndex]); if (!object[key] && Empty) object[key] = new Empty(); // prevent prototype pollution if (Object.prototype.hasOwnProperty.call(object, key)) { object = object[key]; } else { object = {}; } ++stackIndex; } if (canNotTraverseDeeper(object)) return {}; return { obj: object, k: cleanKey(stack[stackIndex]), }; }; const setPath = (object, path, newValue) => { const { obj, k } = getLastOfPath(object, path, Object); if (obj !== undefined || path.length === 1) { obj[k] = newValue; return; } let e = path[path.length - 1]; let p = path.slice(0, path.length - 1); let last = getLastOfPath(object, p, Object); while (last.obj === undefined && p.length) { e = `${p[p.length - 1]}.${e}`; p = p.slice(0, p.length - 1); last = getLastOfPath(object, p, Object); if (last && last.obj && typeof last.obj[`${last.k}.${e}`] !== 'undefined') { last.obj = undefined; } } last.obj[`${last.k}.${e}`] = newValue; }; const pushPath = (object, path, newValue, concat) => { const { obj, k } = getLastOfPath(object, path, Object); obj[k] = obj[k] || []; obj[k].push(newValue); }; const getPath = (object, path) => { const { obj, k } = getLastOfPath(object, path); if (!obj) return undefined; return obj[k]; }; const getPathWithDefaults = (data, defaultData, key) => { const value = getPath(data, key); if (value !== undefined) { return value; } // Fallback to default values return getPath(defaultData, key); }; const deepExtend = (target, source, overwrite) => { /* eslint no-restricted-syntax: 0 */ for (const prop in source) { if (prop !== '__proto__' && prop !== 'constructor') { if (prop in target) { // If we reached a leaf string in target or source then replace with source or skip depending on the 'overwrite' switch if ( typeof target[prop] === 'string' || target[prop] instanceof String || typeof source[prop] === 'string' || source[prop] instanceof String ) { if (overwrite) target[prop] = source[prop]; } else { deepExtend(target[prop], source[prop], overwrite); } } else { target[prop] = source[prop]; } } } return target; }; const regexEscape = (str) => /* eslint no-useless-escape: 0 */ str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); /* eslint-disable */ var _entityMap = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;', '/': '&#x2F;', }; /* eslint-enable */ const escape = (data) => { if (typeof data === 'string') { return data.replace(/[&<>"'\/]/g, (s) => _entityMap[s]); } return data; }; /** * This is a reusable regular expression cache class. Given a certain maximum number of regular expressions we're * allowed to store in the cache, it provides a way to avoid recreating regular expression objects over and over. * When it needs to evict something, it evicts the oldest one. */ class RegExpCache { constructor(capacity) { this.capacity = capacity; this.regExpMap = new Map(); // Since our capacity tends to be fairly small, `.shift()` will be fairly quick despite being O(n). We just use a // normal array to keep it simple. this.regExpQueue = []; } getRegExp(pattern) { const regExpFromCache = this.regExpMap.get(pattern); if (regExpFromCache !== undefined) { return regExpFromCache; } const regExpNew = new RegExp(pattern); if (this.regExpQueue.length === this.capacity) { this.regExpMap.delete(this.regExpQueue.shift()); } this.regExpMap.set(pattern, regExpNew); this.regExpQueue.push(pattern); return regExpNew; } } const chars = [' ', ',', '?', '!', ';']; // We cache RegExps to improve performance with React Native Android, which has poor RegExp initialization performance. // Capacity of 20 should be plenty, as nsSeparator/keySeparator don't tend to vary much across calls. const looksLikeObjectPathRegExpCache = new RegExpCache(20); const looksLikeObjectPath = (key, nsSeparator, keySeparator) => { nsSeparator = nsSeparator || ''; keySeparator = keySeparator || ''; const possibleChars = chars.filter( (c) => nsSeparator.indexOf(c) < 0 && keySeparator.indexOf(c) < 0, ); if (possibleChars.length === 0) return true; const r = looksLikeObjectPathRegExpCache.getRegExp( `(${possibleChars.map((c) => (c === '?' ? '\\?' : c)).join('|')})`, ); let matched = !r.test(key); if (!matched) { const ki = key.indexOf(keySeparator); if (ki > 0 && !r.test(key.substring(0, ki))) { matched = true; } } return matched; }; /** * Given * * 1. a top level object obj, and * 2. a path to a deeply nested string or object within it * * Find and return that deeply nested string or object. The caveat is that the keys of objects within the nesting chain * may contain period characters. Therefore, we need to DFS and explore all possible keys at each step until we find the * deeply nested string or object. */ const deepFind = (obj, path, keySeparator = '.') => { if (!obj) return undefined; if (obj[path]) return obj[path]; const tokens = path.split(keySeparator); let current = obj; for (let i = 0; i < tokens.length; ) { if (!current || typeof current !== 'object') { return undefined; } let next; let nextPath = ''; for (let j = i; j < tokens.length; ++j) { if (j !== i) { nextPath += keySeparator; } nextPath += tokens[j]; next = current[nextPath]; if (next !== undefined) { if (['string', 'number', 'boolean'].indexOf(typeof next) > -1 && j < tokens.length - 1) { continue; } i += j - i + 1; break; } } current = next; } return current; }; const getCleanedCode = (code) => { if (code && code.indexOf('_') > 0) return code.replace('_', '-'); return code; }; class ResourceStore extends EventEmitter { constructor(data, options = { ns: ['translation'], defaultNS: 'translation' }) { super(); this.data = data || {}; this.options = options; if (this.options.keySeparator === undefined) { this.options.keySeparator = '.'; } if (this.options.ignoreJSONStructure === undefined) { this.options.ignoreJSONStructure = true; } } addNamespaces(ns) { if (this.options.ns.indexOf(ns) < 0) { this.options.ns.push(ns); } } removeNamespaces(ns) { const index = this.options.ns.indexOf(ns); if (index > -1) { this.options.ns.splice(index, 1); } } getResource(lng, ns, key, options = {}) { const keySeparator = options.keySeparator !== undefined ? options.keySeparator : this.options.keySeparator; const ignoreJSONStructure = options.ignoreJSONStructure !== undefined ? options.ignoreJSONStructure : this.options.ignoreJSONStructure; let path; if (lng.indexOf('.') > -1) { path = lng.split('.'); } else { path = [lng, ns]; if (key) { if (Array.isArray(key)) { path.push(...key); } else if (typeof key === 'string' && keySeparator) { path.push(...key.split(keySeparator)); } else { path.push(key); } } } const result = getPath(this.data, path); if (!result && !ns && !key && lng.indexOf('.') > -1) { lng = path[0]; ns = path[1]; key = path.slice(2).join('.'); } if (result || !ignoreJSONStructure || typeof key !== 'string') return result; return deepFind(this.data && this.data[lng] && this.data[lng][ns], key, keySeparator); } addResource(lng, ns, key, value, options = { silent: false }) { const keySeparator = options.keySeparator !== undefined ? options.keySeparator : this.options.keySeparator; let path = [lng, ns]; if (key) path = path.concat(keySeparator ? key.split(keySeparator) : key); if (lng.indexOf('.') > -1) { path = lng.split('.'); value = ns; ns = path[1]; } this.addNamespaces(ns); setPath(this.data, path, value); if (!options.silent) this.emit('added', lng, ns, key, value); } addResources(lng, ns, resources, options = { silent: false }) { /* eslint no-restricted-syntax: 0 */ for (const m in resources) { if (typeof resources[m] === 'string' || Array.isArray(resources[m])) this.addResource(lng, ns, m, resources[m], { silent: true }); } if (!options.silent) this.emit('added', lng, ns, resources); } addResourceBundle( lng, ns, resources, deep, overwrite, options = { silent: false, skipCopy: false }, ) { let path = [lng, ns]; if (lng.indexOf('.') > -1) { path = lng.split('.'); deep = resources; resources = ns; ns = path[1]; } this.addNamespaces(ns); let pack = getPath(this.data, path) || {}; if (!options.skipCopy) resources = JSON.parse(JSON.stringify(resources)); // make a copy to fix #2081 if (deep) { deepExtend(pack, resources, overwrite); } else { pack = { ...pack, ...resources }; } setPath(this.data, path, pack); if (!options.silent) this.emit('added', lng, ns, resources); } removeResourceBundle(lng, ns) { if (this.hasResourceBundle(lng, ns)) { delete this.data[lng][ns]; } this.removeNamespaces(ns); this.emit('removed', lng, ns); } hasResourceBundle(lng, ns) { return this.getResource(lng, ns) !== undefined; } getResourceBundle(lng, ns) { if (!ns) ns = this.options.defaultNS; // COMPATIBILITY: remove extend in v2.1.0 if (this.options.compatibilityAPI === 'v1') return { ...{}, ...this.getResource(lng, ns) }; return this.getResource(lng, ns); } getDataByLanguage(lng) { return this.data[lng]; } hasLanguageSomeTranslations(lng) { const data = this.getDataByLanguage(lng); const n = (data && Object.keys(data)) || []; return !!n.find((v) => data[v] && Object.keys(data[v]).length > 0); } toJSON() { return this.data; } } var postProcessor = { processors: {}, addPostProcessor(module) { this.processors[module.name] = module; }, handle(processors, value, key, options, translator) { processors.forEach((processor) => { if (this.processors[processor]) value = this.processors[processor].process(value, key, options, translator); }); return value; }, }; const checkedLoadedFor = {}; class Translator extends EventEmitter { constructor(services, options = {}) { super(); copy( [ 'resourceStore', 'languageUtils', 'pluralResolver', 'interpolator', 'backendConnector', 'i18nFormat', 'utils', ], services, this, ); this.options = options; if (this.options.keySeparator === undefined) { this.options.keySeparator = '.'; } this.logger = baseLogger.create('translator'); } changeLanguage(lng) { if (lng) this.language = lng; } exists(key, options = { interpolation: {} }) { if (key === undefined || key === null) { return false; } const resolved = this.resolve(key, options); return resolved && resolved.res !== undefined; } extractFromKey(key, options) { let nsSeparator = options.nsSeparator !== undefined ? options.nsSeparator : this.options.nsSeparator; if (nsSeparator === undefined) nsSeparator = ':'; const keySeparator = options.keySeparator !== undefined ? options.keySeparator : this.options.keySeparator; let namespaces = options.ns || this.options.defaultNS || []; const wouldCheckForNsInKey = nsSeparator && key.indexOf(nsSeparator) > -1; const seemsNaturalLanguage = !this.options.userDefinedKeySeparator && !options.keySeparator && !this.options.userDefinedNsSeparator && !options.nsSeparator && !looksLikeObjectPath(key, nsSeparator, keySeparator); if (wouldCheckForNsInKey && !seemsNaturalLanguage) { const m = key.match(this.interpolator.nestingRegexp); if (m && m.length > 0) { return { key, namespaces, }; } const parts = key.split(nsSeparator); if ( nsSeparator !== keySeparator || (nsSeparator === keySeparator && this.options.ns.indexOf(parts[0]) > -1) ) namespaces = parts.shift(); key = parts.join(keySeparator); } if (typeof namespaces === 'string') namespaces = [namespaces]; return { key, namespaces, }; } translate(keys, options, lastKey) { if (typeof options !== 'object' && this.options.overloadTranslationOptionHandler) { /* eslint prefer-rest-params: 0 */ options = this.options.overloadTranslationOptionHandler(arguments); } if (typeof options === 'object') options = { ...options }; if (!options) options = {}; // non valid keys handling if (keys === undefined || keys === null /* || keys === '' */) return ''; if (!Array.isArray(keys)) keys = [String(keys)]; const returnDetails = options.returnDetails !== undefined ? options.returnDetails : this.options.returnDetails; // separators const keySeparator = options.keySeparator !== undefined ? options.keySeparator : this.options.keySeparator; // get namespace(s) const { key, namespaces } = this.extractFromKey(keys[keys.length - 1], options); const namespace = namespaces[namespaces.length - 1]; // return key on CIMode const lng = options.lng || this.language; const appendNamespaceToCIMode = options.appendNamespaceToCIMode || this.options.appendNamespaceToCIMode; if (lng && lng.toLowerCase() === 'cimode') { if (appendNamespaceToCIMode) { const nsSeparator = options.nsSeparator || this.options.nsSeparator; if (returnDetails) { return { res: `${namespace}${nsSeparator}${key}`, usedKey: key, exactUsedKey: key, usedLng: lng, usedNS: namespace, usedParams: this.getUsedParamsDetails(options), }; } return `${namespace}${nsSeparator}${key}`; } if (returnDetails) { return { res: key, usedKey: key, exactUsedKey: key, usedLng: lng, usedNS: namespace, usedParams: this.getUsedParamsDetails(options), }; } return key; } // resolve from store const resolved = this.resolve(keys, options); let res = resolved && resolved.res; const resUsedKey = (resolved && resolved.usedKey) || key; const resExactUsedKey = (resolved && resolved.exactUsedKey) || key; const resType = Object.prototype.toString.apply(res); const noObject = ['[object Number]', '[object Function]', '[object RegExp]']; const joinArrays = options.joinArrays !== undefined ? options.joinArrays : this.options.joinArrays; // object const handleAsObjectInI18nFormat = !this.i18nFormat || this.i18nFormat.handleAsObject; const handleAsObject = typeof res !== 'string' && typeof res !== 'boolean' && typeof res !== 'number'; if ( handleAsObjectInI18nFormat && res && handleAsObject && noObject.indexOf(resType) < 0 && !(typeof joinArrays === 'string' && Array.isArray(res)) ) { if (!options.returnObjects && !this.options.returnObjects) { if (!this.options.returnedObjectHandler) { this.logger.warn('accessing an object - but returnObjects options is not enabled!'); } const r = this.options.returnedObjectHandler ? this.options.returnedObjectHandler(resUsedKey, res, { ...options, ns: namespaces }) : `key '${key} (${this.language})' returned an object instead of string.`; if (returnDetails) { resolved.res = r; resolved.usedParams = this.getUsedParamsDetails(options); return resolved; } return r; } // if we got a separator we loop over children - else we just return object as is // as having it set to false means no hierarchy so no lookup for nested values if (keySeparator) { const resTypeIsArray = Array.isArray(res); const copy = resTypeIsArray ? [] : {}; // apply child translation on a copy /* eslint no-restricted-syntax: 0 */ const newKeyToUse = resTypeIsArray ? resExactUsedKey : resUsedKey; for (const m in res) { if (Object.prototype.hasOwnProperty.call(res, m)) { const deepKey = `${newKeyToUse}${keySeparator}${m}`; copy[m] = this.translate(deepKey, { ...options, ...{ joinArrays: false, ns: namespaces }, }); if (copy[m] === deepKey) copy[m] = res[m]; // if nothing found use original value as fallback } } res = copy; } } else if (handleAsObjectInI18nFormat && typeof joinArrays === 'string' && Array.isArray(res)) { // array special treatment res = res.join(joinArrays); if (res) res = this.extendTranslation(res, keys, options, lastKey); } else { // string, empty or null let usedDefault = false; let usedKey = false; const needsPluralHandling = options.count !== undefined && typeof options.count !== 'string'; const hasDefaultValue = Translator.hasDefaultValue(options); const defaultValueSuffix = needsPluralHandling ? this.pluralResolver.getSuffix(lng, options.count, options) : ''; const defaultValueSuffixOrdinalFallback = options.ordinal && needsPluralHandling ? this.pluralResolver.getSuffix(lng, options.count, { ordinal: false }) : ''; const needsZeroSuffixLookup = needsPluralHandling && !options.ordinal && options.count === 0 && this.pluralResolver.shouldUseIntlApi(); const defaultValue = (needsZeroSuffixLookup && options[`defaultValue${this.options.pluralSeparator}zero`]) || options[`defaultValue${defaultValueSuffix}`] || options[`defaultValue${defaultValueSuffixOrdinalFallback}`] || options.defaultValue; // fallback value if (!this.isValidLookup(res) && hasDefaultValue) { usedDefault = true; res = defaultValue; } if (!this.isValidLookup(res)) { usedKey = true; res = key; } const missingKeyNoValueFallbackToKey = options.missingKeyNoValueFallbackToKey || this.options.missingKeyNoValueFallbackToKey; const resForMissing = missingKeyNoValueFallbackToKey && usedKey ? undefined : res; // save missing const updateMissing = hasDefaultValue && defaultValue !== res && this.options.updateMissing; if (usedKey || usedDefault || updateMissing) { this.logger.log( updateMissing ? 'updateKey' : 'missingKey', lng, namespace, key, updateMissing ? defaultValue : res, ); if (keySeparator) { const fk = this.resolve(key, { ...options, keySeparator: false }); if (fk && fk.res) this.logger.warn( '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.', ); } let lngs = []; const fallbackLngs = this.languageUtils.getFallbackCodes( this.options.fallbackLng, options.lng || this.language, ); if (this.options.saveMissingTo === 'fallback' && fallbackLngs && fallbackLngs[0]) { for (let i = 0; i < fallbackLngs.length; i++) { lngs.push(fallbackLngs[i]); } } else if (this.options.saveMissingTo === 'all') { lngs = this.languageUtils.toResolveHierarchy(options.lng || this.language); } else { lngs.push(options.lng || this.language); } const send = (l, k, specificDefaultValue) => { const defaultForMissing = hasDefaultValue && specificDefaultValue !== res ? specificDefaultValue : resForMissing; if (this.options.missingKeyHandler) { this.options.missingKeyHandler( l, namespace, k, defaultForMissing, updateMissing, options, ); } else if (this.backendConnector && this.backendConnector.saveMissing) { this.backendConnector.saveMissing( l, namespace, k, defaultForMissing, updateMissing, options, ); } this.emit('missingKey', l, namespace, k, res); }; if (this.options.saveMissing) { if (this.options.saveMissingPlurals && needsPluralHandling) { lngs.forEach((language) => { const suffixes = this.pluralResolver.getSuffixes(language, options); if ( needsZeroSuffixLookup && options[`defaultValue${this.options.pluralSeparator}zero`] && suffixes.indexOf(`${this.options.pluralSeparator}zero`) < 0 ) { suffixes.push(`${this.options.pluralSeparator}zero`); } suffixes.forEach((suffix) => { send([language], key + suffix, options[`defaultValue${suffix}`] || defaultValue); }); }); } else { send(lngs, key, defaultValue); } } } // extend res = this.extendTranslation(res, keys, options, resolved, lastKey); // append namespace if still key if (usedKey && res === key && this.options.appendNamespaceToMissingKey) res = `${namespace}:${key}`; // parseMissingKeyHandler if ((usedKey || usedDefault) && this.options.parseMissingKeyHandler) { if (this.options.compatibilityAPI !== 'v1') { res = this.options.parseMissingKeyHandler( this.options.appendNamespaceToMissingKey ? `${namespace}:${key}` : key, usedDefault ? res : undefined, ); } else { res = this.options.parseMissingKeyHandler(res); } } } // return if (returnDetails) { resolved.res = res; resolved.usedParams = this.getUsedParamsDetails(options); return resolved; } return res; } extendTranslation(res, key, options, resolved, lastKey) { if (this.i18nFormat && this.i18nFormat.parse) { res = this.i18nFormat.parse( res, { ...this.options.interpolation.defaultVariables, ...options }, options.lng || this.language || resolved.usedLng, resolved.usedNS, resolved.usedKey, { resolved }, ); } else if (!options.skipInterpolation) { // i18next.parsing if (options.interpolation) this.interpolator.init({ ...options, ...{ interpolation: { ...this.options.interpolation, ...options.interpolation } }, }); const skipOnVariables = typeof res === 'string' && (options && options.interpolation && options.interpolation.skipOnVariables !== undefined ? options.interpolation.skipOnVariables : this.options.interpolation.skipOnVariables); let nestBef; if (skipOnVariables) { const nb = res.match(this.interpolator.nestingRegexp); // has nesting aftbeforeer interpolation nestBef = nb && nb.length; } // interpolate let data = options.replace && typeof options.replace !== 'string' ? options.replace : options; if (this.options.interpolation.defaultVariables) data = { ...this.options.interpolation.defaultVariables, ...data }; res = this.interpolator.interpolate( res, data, options.lng || this.language || resolved.usedLng, options, ); // nesting if (skipOnVariables) { const na = res.match(this.interpolator.nestingRegexp); // has nesting after interpolation const nestAft = na && na.length; if (nestBef < nestAft) options.nest = false; } if (!options.lng && this.options.compatibilityAPI !== 'v1' && resolved && resolved.res) options.lng = this.language || resolved.usedLng; if (options.nest !== false) res = this.interpolator.nest( res, (...args) => { if (lastKey && lastKey[0] === args[0] && !options.context) { this.logger.warn( `It seems you are nesting recursively key: ${args[0]} in key: ${key[0]}`, ); return null; } return this.translate(...args, key); }, options, ); if (options.interpolation) this.interpolator.reset(); } // post process const postProcess = options.postProcess || this.options.postProcess; const postProcessorNames = typeof postProcess === 'string' ? [postProcess] : postProcess; if ( res !== undefined && res !== null && postProcessorNames && postProcessorNames.length && options.applyPostProcessor !== false ) { res = postProcessor.handle( postProcessorNames, res, key, this.options && this.options.postProcessPassResolved ? { i18nResolved: { ...resolved, usedParams: this.getUsedParamsDetails(options) }, ...options, } : options, this, ); } return res; } resolve(keys, options = {}) { let found; let usedKey; // plain key let exactUsedKey; // key with context / plural let usedLng; let usedNS; if (typeof keys === 'string') keys = [keys]; // forEach possible key keys.forEach((k) => { if (this.isValidLookup(found)) return; const extracted = this.extractFromKey(k, options); const key = extracted.key; usedKey = key; let namespaces = extracted.namespaces; if (this.options.fallbackNS) namespaces = namespaces.concat(this.options.fallbackNS); const needsPluralHandling = options.count !== undefined && typeof options.count !== 'string'; const needsZeroSuffixLookup = needsPluralHandling && !options.ordinal && options.count === 0 && this.pluralResolver.shouldUseIntlApi(); const needsContextHandling = options.context !== undefined && (typeof options.context === 'string' || typeof options.context === 'number') && options.context !== ''; const codes = options.lngs ? options.lngs : this.languageUtils.toResolveHierarchy(options.lng || this.language, options.fallbackLng); namespaces.forEach((ns) => { if (this.isValidLookup(found)) return; usedNS = ns; if ( !checkedLoadedFor[`${codes[0]}-${ns}`] && this.utils && this.utils.hasLoadedNamespace && !this.utils.hasLoadedNamespace(usedNS) ) { checkedLoadedFor[`${codes[0]}-${ns}`] = true; this.logger.warn( `key "${usedKey}" for languages "${codes.join( ', ', )}" won't get resolved as namespace "${usedNS}" was not yet loaded`, '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!!!', ); } codes.forEach((code) => { if (this.isValidLookup(found)) return; usedLng = code; const finalKeys = [key]; if (this.i18nFormat && this.i18nFormat.addLookupKeys) { this.i18nFormat.addLookupKeys(finalKeys, key, code, ns, options); } else { let pluralSuffix; if (needsPluralHandling) pluralSuffix = this.pluralResolver.getSuffix(code, options.count, options); const zeroSuffix = `${this.options.pluralSeparator}zero`; const ordinalPrefix = `${this.options.pluralSeparator}ordinal${this.options.pluralSeparator}`; // get key for plural if needed if (needsPluralHandling) { finalKeys.push(key + pluralSuffix); if (options.ordinal && pluralSuffix.indexOf(ordinalPrefix) === 0) { finalKeys.push( key + pluralSuffix.replace(ordinalPrefix, this.options.pluralSeparator), ); } if (needsZeroSuffixLookup) { finalKeys.push(key + zeroSuffix); } } // get key for context if needed if (needsContextHandling) { const contextKey = `${key}${this.options.contextSeparator}${options.context}`; finalKeys.push(contextKey); // get key for context + plural if needed if (needsPluralHandling) { finalKeys.push(contextKey + pluralSuffix); if (options.ordinal && pluralSuffix.indexOf(ordinalPrefix) === 0) { finalKeys.push( contextKey + pluralSuffix.replace(ordinalPrefix, this.options.pluralSeparator), ); } if (needsZeroSuffixLookup) { finalKeys.push(contextKey + zeroSuffix); } } } } // iterate over finalKeys starting with most specific pluralkey (-> contextkey only) -> singularkey only let possibleKey; /* eslint no-cond-assign: 0 */ while ((possibleKey = finalKeys.pop())) { if (!this.isValidLookup(found)) { exactUsedKey = possibleKey; found = this.getResource(code, ns, possibleKey, options); } } }); }); }); return { res: found, usedKey, exactUsedKey, usedLng, usedNS }; } isValidLookup(res) { return ( res !== undefined && !(!this.options.returnNull && res === null) && !(!this.options.returnEmptyString && res === '') ); } getResource(code, ns, key, options = {}) { if (this.i18nFormat && this.i18nFormat.getResource) return this.i18nFormat.getResource(code, ns, key, options); return this.resourceStore.getResource(code, ns, key, options); } getUsedParamsDetails(options = {}) { // we need to remember to extend this array whenever new option properties are added const optionsKeys = [ 'defaultValue', 'ordinal', 'context', 'replace', 'lng', 'lngs', 'fallbackLng', 'ns', 'keySeparator', 'nsSeparator', 'returnObjects', 'returnDetails', 'joinArrays', 'postProcess', 'interpolation', ]; const useOptionsReplaceForData = options.replace && typeof options.replace !== 'string'; let data = useOptionsReplaceForData ? options.replace : options; if (useOptionsReplaceForData && typeof options.count !== 'undefined') { data.count = options.count; } if (this.options.interpolation.defaultVariables) { data = { ...this.options.interpolation.defaultVariables, ...data }; } // avoid reporting options (execpt count) as usedParams if (!useOptionsReplaceForData) { data = { ...data }; for (const key of optionsKeys) { delete data[key]; } } return data; } static hasDefaultValue(options) { const prefix = 'defaultValue'; for (const option in options) { if ( Object.prototype.hasOwnProperty.call(options, option) && prefix === option.substring(0, prefix.length) && undefined !== options[option] ) { return true; } } return false; } } const capitalize = (string) => string.charAt(0).toUpperCase() + string.slice(1); class LanguageUtil { constructor(options) { this.options = options; this.supportedLngs = this.options.supportedLngs || false; this.logger = baseLogger.create('languageUtils'); } getScriptPartFromCode(code) { code = getCleanedCode(code); if (!code || code.indexOf('-') < 0) return null; const p = code.split('-'); if (p.length === 2) return null; p.pop(); if (p[p.length - 1].toLowerCase() === 'x') return null; return this.formatLanguageCode(p.join('-')); } getLanguagePartFromCode(code) { code = getCleanedCode(code); if (!code || code.indexOf('-') < 0) return code; const p = code.split('-'); return this.formatLanguageCode(p[0]); } formatLanguageCode(code) { // http://www.iana.org/assignments/language-tags/language-tags.xhtml if (typeof code === 'string' && code.indexOf('-') > -1) { const specialCases = ['hans', 'hant', 'latn', 'cyrl', 'cans', 'mong', 'arab']; let p = code.split('-'); if (this.options.lowerCaseLng) { p = p.map((part) => part.toLowerCase()); } else if (p.length === 2) { p[0] = p[0].toLowerCase(); p[1] = p[1].toUpperCase(); if (specialCases.indexOf(p[1].toLowerCase()) > -1) p[1] = capitalize(p[1].toLowerCase()); } else if (p.length === 3) { p[0] = p[0].toLowerCase(); // if length 2 guess it's a country if (p[1].length === 2) p[1] = p[1].toUpperCase(); if (p[0] !== 'sgn' && p[2].length === 2) p[2] = p[2].toUpperCase(); if (specialCases.indexOf(p[1].toLowerCase()) > -1) p[1] = capitalize(p[1].toLowerCase()); if (specialCases.indexOf(p[2].toLowerCase()) > -1) p[2] = capitalize(p[2].toLowerCase()); } return p.join('-'); } return this.options.cleanCode || this.options.lowerCaseLng ? code.toLowerCase() : code; } isSupportedCode(code) { if (this.options.load === 'languageOnly' || this.options.nonExplicitSupportedLngs) { code = this.getLanguagePartFromCode(code); } return ( !this.supportedLngs || !this.supportedLngs.length || this.supportedLngs.indexOf(code) > -1 ); } getBestMatchFromCodes(codes) { if (!codes) return null; let found; // pick first supported code or if no restriction pick the first one (highest prio) codes.forEach((code) => { if (found) return; const cleanedLng = this.formatLanguageCode(code); if (!this.options.supportedLngs || this.isSupportedCode(cleanedLng)) found = cleanedLng; }); // if we got no match in supportedLngs yet - check for similar locales // first de-CH --> de // second de-CH --> de-DE if (!found && this.options.supportedLngs) { codes.forEach((code) => { if (found) return; const lngOnly = this.getLanguagePartFromCode(code); // eslint-disable-next-line no-return-assign if (this.isSupportedCode(lngOnly)) return (found = lngOnly); // eslint-disable-next-line array-callback-return found = this.options.supportedLngs.find((supportedLng) => { if (supportedLng === lngOnly) return supportedLng; if (supportedLng.indexOf('-') < 0 && lngOnly.indexOf('-') < 0) return; if ( supportedLng.indexOf('-') > 0 && lngOnly.indexOf('-') < 0 && supportedLng.substring(0, supportedLng.indexOf('-')) === lngOnly ) return supportedLng; if (supportedLng.indexOf(lngOnly) === 0 && lngOnly.length > 1) return supportedLng; }); }); } // if nothing found, use fallbackLng if (!found) found = this.getFallbackCodes(this.options.fallbackLng)[0]; return found; } getFallbackCodes(fallbacks, code) { if (!fallbacks) return []; if (typeof fallbacks === 'function') fallbacks = fallbacks(code); if (typeof fallbacks === 'string') fallbacks = [fallbacks]; if (Array.isArray(fallbacks)) return fallbacks; if (!code) return fallbacks.default || []; // assume we have an object defining fallbacks let found = fallbacks[code]; if (!found) found = fallbacks[this.getScriptPartFromCode(code)]; if (!found) found = fallbacks[this.formatLanguageCode(code)]; if (!found) found = fallbacks[this.getLanguagePartFromCode(code)]; if (!found) found = fallbacks.default; return found || []; } toResolveHierarchy(code, fallbackCode) { const fallbackCodes = this.getFallbackCodes( fallbackCode || this.options.fallbackLng || [], code, ); const codes = []; const addCode = (c) => { if (!c) return; if (this.isSupportedCode(c)) { codes.push(c); } else { this.logger.warn(`rejecting language code not found in supportedLngs: ${c}`); } }; if (typeof code === 'string' && (code.indexOf('-') > -1 || code.indexOf('_') > -1)) { if (this.options.load !== 'languageOnly') addCode(this.formatLanguageCode(code)); if (this.options.load !== 'languageOnly' && this.options.load !== 'currentOnly') addCode(this.getScriptPartFromCode(code)); if (this.options.load !== 'currentOnly') addCode(this.getLanguagePartFromCode(code)); } else if (typeof code === 'string') { addCode(this.formatLanguageCode(code)); } fallbackCodes.forEach((fc) => { if (codes.indexOf(fc) < 0) addCode(this.formatLanguageCode(fc)); }); return codes; } } // definition http://translate.sourceforge.net/wiki/l10n/pluralforms /* eslint-disable */ let sets = [ { lngs: ['ach','ak','am','arn','br','fil','gun','ln','mfe','mg','mi','oc', 'pt', 'pt-BR', 'tg', 'tl', 'ti','tr','uz','wa'], nr: [1,2], fc: 1 }, { lngs: ['af','an','ast','az','bg','bn','ca','da','de','dev','el','en', 'eo','es','et','eu','fi','fo','fur','fy','gl','gu','ha','hi', 'hu','hy','ia','it','kk','kn','ku','lb','mai','ml','mn','mr','nah','nap','nb', 'ne','nl','nn','no','nso','pa','pap','pms','ps','pt-PT','rm','sco', 'se','si','so','son','sq','sv','sw','ta','te','tk','ur','yo'], nr: [1,2], fc: 2 }, { lngs: ['ay','bo','cgg','fa','ht','id','ja','jbo','ka','km','ko','ky','lo', 'ms','sah','su','th','tt','ug','vi','wo','zh'], nr: [1], fc: 3 }, { lngs: ['be','bs', 'cnr', 'dz','hr','ru','sr','uk'], nr: [1,2,5], fc: 4 }, { lngs: ['ar'], nr: [0,1,2,3,11,100], fc: 5 }, { lngs: ['cs','sk'], nr: [1,2,5], fc: 6 }, { lngs: ['csb','pl'], nr: [1,2,5], fc: 7 }, { lngs: ['cy'], nr: [1,2,3,8], fc: 8 }, { lngs: ['fr'], nr: [1,2], fc: 9 }, { lngs: ['ga'], nr: [1,2,3,7,11], fc: 10 }, { lngs: ['gd'], nr: [1,2,3,20], fc: 11 }, { lngs: ['is'], nr: [1,2], fc: 12 }, { lngs: ['jv'], nr: [0,1], fc: 13 }, { lngs: ['kw'], nr: [1,2,3,4], fc: 14 }, { lngs: ['lt'], nr: [1,2,10], fc: 15 }, { lngs: ['lv'], nr: [1,2,0], fc: 16 }, { lngs: ['mk'], nr: [1,2], fc: 17 }, { lngs: ['mnk'], nr: [0,1,2], fc: 18 }, { lngs: ['mt'], nr: [1,2,11,20], fc: 19 }, { lngs: ['or'], nr: [2,1], fc: 2 }, { lngs: ['ro'], nr: [1,2,20], fc: 20 }, { lngs: ['sl'], nr: [5,1,2,3], fc: 21 }, { lngs: ['he','iw'], nr: [1,2,20,21], fc: 22 } ]; let _rulesPluralsTypes = { 1: (n) => Number(n > 1), 2: (n) => Number(n != 1), 3: (n) => 0, 4: (n) => Number(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2), 5: (n) => Number(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5), 6: (n) => Number((n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2), 7: (n) => Number(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2), 8: (n) => Number((n==1) ? 0 : (n==2) ? 1 : (n != 8 && n != 11) ? 2 : 3), 9: (n) => Number(n >= 2), 10: (n) => Number(n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : 4), 11: (n) => Number((n==1 || n==11) ? 0 : (n==2 || n==12) ? 1 : (n > 2 && n < 20) ? 2 : 3), 12: (n) => Number(n%10!=1 || n%100==11), 13: (n) => Number(n !== 0), 14: (n) => Number((n==1) ? 0 : (n==2) ? 1 : (n == 3) ? 2 : 3), 15: (n) => Number(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2), 16: (n) => Number(n%10==1 && n%100!=11 ? 0 : n !== 0 ? 1 : 2), 17: (n) => Number(n==1 || n%10==1 && n%100!=11 ? 0 : 1), 18: (n) => Number(n==0 ? 0 : n==1 ? 1 : 2), 19: (n) => Number(n==1 ? 0 : n==0 || ( n%100>1 && n%100<11) ? 1 : (n%100>10 && n%100<20 ) ? 2 : 3), 20: (n) => Number(n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2), 21: (n) => Number(n%100==1 ? 1 : n%100==2 ? 2 : n%100==3 || n%100==4 ? 3 : 0), 22: (n) => Number(n==1 ? 0 : n==2 ? 1 : (n<0 || n>10) && n%10==0 ? 2 : 3) }; /* eslint-enable */ const nonIntlVersions = ['v1', 'v2', 'v3']; const intlVersions = ['v4']; const suffixesOrder = { zero: 0, one: 1, two: 2, few: 3, many: 4, other: 5, }; const createRules = () => { const rules = {}; sets.forEach((set) => { set.lngs.forEach((l) => { rules[l] = { numbers: set.nr, plurals: _rulesPluralsTypes[set.fc] }; }); }); return rules; }; class PluralResolver { constructor(languageUtils, options = {}) { this.languageUtils = languageUtils; this.options = options; this.logger = baseLogger.create('pluralResolver'); if ((!this.options.compatibilityJSON || intlVersions.includes(this.options.compatibilityJSON)) && (typeof Intl === 'undefined' || !Intl.PluralRules)) { this.options.compatibilityJSON = 'v3'; this.logger.error('Your environment seems not to be Intl API compatible, use an Intl.PluralRules polyfill. Will fallback to the compatibilityJSON v3 format handling.'); } this.rules = createRules(); // Cache calls to Intl.PluralRules, since repeated calls can be slow in runtimes like React Native // and the memory usage difference is negligible this.pluralRulesCache = {}; } addRule(lng, obj) { this.rules[lng] = obj; } clearCache() { this.pluralRulesCache = {}; } getRule(code, options = {}) { if (this.shouldUseIntlApi()) { try { const cleanedCode = getCleanedCode(code === 'dev' ? 'en' : code); const type = options.ordinal ? 'ordinal' : 'cardinal'; const cacheKey = JSON.stringify({ cleanedCode, type }); if (cacheKey in this.pluralRulesCache) { return this.pluralRulesCache[cacheKey]; } const rule = new Intl.PluralRules(cleanedCode, { type }); this.pluralRulesCache[cacheKey] = rule; return rule; } catch (err) { return; } } return this.rules[code] || this.rules[this.languageUtils.getLanguagePartFromCode(code)]; } needsPlural(code, options = {}) { const rule = this.getRule(code, options); if (this.shouldUseIntlApi()) { return rule && rule.resolvedOptions().pluralCategories.length > 1; } return rule && rule.numbers.length > 1; } getPluralFormsOfKey(code, key, options = {}) { return this.getSuffixes(code, options).map((suffix) => `${key}${suffix}`); } getSuffixes(code, options = {}) { const rule = this.getRule(code, options); if (!rule) { return []; } if (this.shouldUseIntlApi()) { return rule.resolvedOptions().pluralCategories .sort((pluralCategory1, pluralCategory2) => suffixesOrder[pluralCategory1] - suffixesOrder[pluralCategory2]) .map(pluralCategory => `${this.options.prepend}${options.ordinal ? `ordinal${this.options.prepend}` : ''}${pluralCategory}`); } return rule.numbers.map((number) => this.getSuffix(code, number, options)); } getSuffix(code, count, options = {}) { const rule = this.getRule(code, options); if (rule) { if (this.shouldUseIntlApi()) { return `${this.options.prepend}${options.ordinal ? `ordinal${this.options.prepend}` : ''}${rule.select(count)}`; } return this.getSuffixRetroCompatible(rule, count); } this.logger.warn(`no plural rule found for: ${code}`); return ''; } getSuffixRetroCompatible(rule, count) { const idx = rule.noAbs ? rule.plurals(count) : rule.plurals(Math.abs(count)); let suffix = rule.numbers[idx]; // special treatment for lngs only having singular and plural if (this.options.simplifyPluralSuffix && rule.numbers.length === 2 && rule.numbers[0] === 1) { if (suffix === 2) { suffix = 'plural'; } else if (suffix === 1) { suffix = ''; } } const returnSuffix = () => ( this.options.prepend && suffix.toString() ? this.options.prepend + suffix.toString() : suffix.toString() ); // COMPATIBILITY JSON // v1 if (this.options.compatibilityJSON === 'v1') { if (suffix === 1) return ''; if (typeof suffix === 'number') return `_plural_${suffix.toString()}`; return returnSuffix(); // eslint-disable-next-line no-else-return } else if (/* v2 */ this.options.compatibilityJSON === 'v2') { return returnSuffix(); } else if (/* v3 - gettext index */ this.options.simplifyPluralSuffix && rule.numbers.length === 2 && rule.numbers[0] === 1) { return returnSuffix(); } return this.options.prepend && idx.toString() ? this.options.prepend + idx.t