You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2251 lines
84 KiB
2251 lines
84 KiB
(function (global, factory) { |
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : |
|
typeof define === 'function' && define.amd ? define(factory) : |
|
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.i18next = factory()); |
|
})(this, (function () { 'use strict'; |
|
|
|
const isString = obj => typeof obj === 'string'; |
|
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 ''; |
|
return String(object); |
|
}; |
|
const copy = (a, s, t) => { |
|
a.forEach(m => { |
|
if (s[m]) t[m] = s[m]; |
|
}); |
|
}; |
|
const lastOfPathSeparatorRegExp = /###/g; |
|
const cleanKey = key => key && key.includes('###') ? key.replace(lastOfPathSeparatorRegExp, '.') : key; |
|
const canNotTraverseDeeper = object => !object || isString(object); |
|
const getLastOfPath = (object, path, Empty) => { |
|
const stack = !isString(path) ? path : path.split('.'); |
|
let stackIndex = 0; |
|
while (stackIndex < stack.length - 1) { |
|
if (canNotTraverseDeeper(object)) return {}; |
|
const key = cleanKey(stack[stackIndex]); |
|
if (!object[key] && Empty) object[key] = new Empty(); |
|
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?.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; |
|
if (!Object.prototype.hasOwnProperty.call(obj, k)) return undefined; |
|
return obj[k]; |
|
}; |
|
const getPathWithDefaults = (data, defaultData, key) => { |
|
const value = getPath(data, key); |
|
if (value !== undefined) { |
|
return value; |
|
} |
|
return getPath(defaultData, key); |
|
}; |
|
const deepExtend = (target, source, overwrite) => { |
|
for (const prop in source) { |
|
if (prop !== '__proto__' && prop !== 'constructor') { |
|
if (Object.prototype.hasOwnProperty.call(target, prop)) { |
|
if (isString(target[prop]) || target[prop] instanceof String || isString(source[prop]) || 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 => str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); |
|
const _entityMap = { |
|
'&': '&', |
|
'<': '<', |
|
'>': '>', |
|
'"': '"', |
|
"'": ''', |
|
'/': '/' |
|
}; |
|
const escape = data => { |
|
if (isString(data)) { |
|
return data.replace(/[&<>"'\/]/g, s => _entityMap[s]); |
|
} |
|
return data; |
|
}; |
|
class RegExpCache { |
|
constructor(capacity) { |
|
this.capacity = capacity; |
|
this.regExpMap = new Map(); |
|
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 = [' ', ',', '?', '!', ';']; |
|
const looksLikeObjectPathRegExpCache = new RegExpCache(20); |
|
const looksLikeObjectPath = (key, nsSeparator, keySeparator) => { |
|
nsSeparator = nsSeparator || ''; |
|
keySeparator = keySeparator || ''; |
|
const possibleChars = chars.filter(c => !nsSeparator.includes(c) && !keySeparator.includes(c)); |
|
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; |
|
}; |
|
const deepFind = (obj, path, keySeparator = '.') => { |
|
if (!obj) return undefined; |
|
if (obj[path]) { |
|
if (!Object.prototype.hasOwnProperty.call(obj, path)) return undefined; |
|
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'].includes(typeof next) && j < tokens.length - 1) { |
|
continue; |
|
} |
|
i += j - i + 1; |
|
break; |
|
} |
|
} |
|
current = next; |
|
} |
|
return current; |
|
}; |
|
const getCleanedCode = code => code?.replace(/_/g, '-'); |
|
|
|
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) { |
|
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; |
|
args = args.map(a => isString(a) ? a.replace(/[\r\n\x00-\x1F\x7F]/g, ' ') : a); |
|
if (isString(args[0])) 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.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); |
|
} |
|
once(event, listener) { |
|
const wrapper = (...args) => { |
|
listener(...args); |
|
this.off(event, wrapper); |
|
}; |
|
this.on(event, wrapper); |
|
return this; |
|
} |
|
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(event, ...args); |
|
} |
|
}); |
|
} |
|
} |
|
} |
|
|
|
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.includes(ns)) { |
|
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.includes('.')) { |
|
path = lng.split('.'); |
|
} else { |
|
path = [lng, ns]; |
|
if (key) { |
|
if (Array.isArray(key)) { |
|
path.push(...key); |
|
} else if (isString(key) && keySeparator) { |
|
path.push(...key.split(keySeparator)); |
|
} else { |
|
path.push(key); |
|
} |
|
} |
|
} |
|
const result = getPath(this.data, path); |
|
if (!result && !ns && !key && lng.includes('.')) { |
|
lng = path[0]; |
|
ns = path[1]; |
|
key = path.slice(2).join('.'); |
|
} |
|
if (result || !ignoreJSONStructure || !isString(key)) return result; |
|
return deepFind(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.includes('.')) { |
|
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 |
|
}) { |
|
for (const m in resources) { |
|
if (isString(resources[m]) || 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.includes('.')) { |
|
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)); |
|
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; |
|
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 => { |
|
value = this.processors[processor]?.process(value, key, options, translator) ?? value; |
|
}); |
|
return value; |
|
} |
|
}; |
|
|
|
const PATH_KEY = Symbol('i18next/PATH_KEY'); |
|
function createProxy() { |
|
const state = []; |
|
const handler = Object.create(null); |
|
let proxy; |
|
handler.get = (target, key) => { |
|
proxy?.revoke?.(); |
|
if (key === PATH_KEY) return state; |
|
state.push(key); |
|
proxy = Proxy.revocable(target, handler); |
|
return proxy.proxy; |
|
}; |
|
return Proxy.revocable(Object.create(null), handler).proxy; |
|
} |
|
function keysFromSelector(selector, opts) { |
|
const { |
|
[PATH_KEY]: path |
|
} = selector(createProxy()); |
|
const keySeparator = opts?.keySeparator ?? '.'; |
|
const nsSeparator = opts?.nsSeparator ?? ':'; |
|
const strict = opts?.enableSelector === 'strict'; |
|
if (path.length > 1 && nsSeparator) { |
|
const ns = opts?.ns; |
|
const nsList = strict ? Array.isArray(ns) ? ns : ns ? [ns] : null : Array.isArray(ns) ? ns : null; |
|
if (nsList) { |
|
const candidates = strict ? nsList : nsList.length > 1 ? nsList.slice(1) : []; |
|
if (candidates.includes(path[0])) { |
|
return `${path[0]}${nsSeparator}${path.slice(1).join(keySeparator)}`; |
|
} |
|
} |
|
} |
|
return path.join(keySeparator); |
|
} |
|
|
|
const shouldHandleAsObject = res => !isString(res) && typeof res !== 'boolean' && typeof res !== 'number'; |
|
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'); |
|
this.checkedLoadedFor = {}; |
|
} |
|
changeLanguage(lng) { |
|
if (lng) this.language = lng; |
|
} |
|
exists(key, o = { |
|
interpolation: {} |
|
}) { |
|
const opt = { |
|
...o |
|
}; |
|
if (key == null) return false; |
|
const resolved = this.resolve(key, opt); |
|
if (resolved?.res === undefined) return false; |
|
const isObject = shouldHandleAsObject(resolved.res); |
|
if (opt.returnObjects === false && isObject) { |
|
return false; |
|
} |
|
return true; |
|
} |
|
extractFromKey(key, opt) { |
|
let nsSeparator = opt.nsSeparator !== undefined ? opt.nsSeparator : this.options.nsSeparator; |
|
if (nsSeparator === undefined) nsSeparator = ':'; |
|
const keySeparator = opt.keySeparator !== undefined ? opt.keySeparator : this.options.keySeparator; |
|
let namespaces = opt.ns || this.options.defaultNS || []; |
|
const wouldCheckForNsInKey = nsSeparator && key.includes(nsSeparator); |
|
const seemsNaturalLanguage = !this.options.userDefinedKeySeparator && !opt.keySeparator && !this.options.userDefinedNsSeparator && !opt.nsSeparator && !looksLikeObjectPath(key, nsSeparator, keySeparator); |
|
if (wouldCheckForNsInKey && !seemsNaturalLanguage) { |
|
const m = key.match(this.interpolator.nestingRegexp); |
|
if (m && m.length > 0) { |
|
return { |
|
key, |
|
namespaces: isString(namespaces) ? [namespaces] : namespaces |
|
}; |
|
} |
|
const parts = key.split(nsSeparator); |
|
if (nsSeparator !== keySeparator || nsSeparator === keySeparator && this.options.ns.includes(parts[0])) namespaces = parts.shift(); |
|
key = parts.join(keySeparator); |
|
} |
|
return { |
|
key, |
|
namespaces: isString(namespaces) ? [namespaces] : namespaces |
|
}; |
|
} |
|
translate(keys, o, lastKey) { |
|
let opt = typeof o === 'object' ? { |
|
...o |
|
} : o; |
|
if (typeof opt !== 'object' && this.options.overloadTranslationOptionHandler) { |
|
opt = this.options.overloadTranslationOptionHandler(arguments); |
|
} |
|
if (typeof opt === 'object') opt = { |
|
...opt |
|
}; |
|
if (!opt) opt = {}; |
|
if (keys == null) return ''; |
|
if (typeof keys === 'function') keys = keysFromSelector(keys, { |
|
...this.options, |
|
...opt |
|
}); |
|
if (!Array.isArray(keys)) keys = [String(keys)]; |
|
keys = keys.map(k => typeof k === 'function' ? keysFromSelector(k, { |
|
...this.options, |
|
...opt |
|
}) : String(k)); |
|
const returnDetails = opt.returnDetails !== undefined ? opt.returnDetails : this.options.returnDetails; |
|
const keySeparator = opt.keySeparator !== undefined ? opt.keySeparator : this.options.keySeparator; |
|
const { |
|
key, |
|
namespaces |
|
} = this.extractFromKey(keys[keys.length - 1], opt); |
|
const namespace = namespaces[namespaces.length - 1]; |
|
let nsSeparator = opt.nsSeparator !== undefined ? opt.nsSeparator : this.options.nsSeparator; |
|
if (nsSeparator === undefined) nsSeparator = ':'; |
|
const lng = opt.lng || this.language; |
|
const appendNamespaceToCIMode = opt.appendNamespaceToCIMode || this.options.appendNamespaceToCIMode; |
|
if (lng?.toLowerCase() === 'cimode') { |
|
if (appendNamespaceToCIMode) { |
|
if (returnDetails) { |
|
return { |
|
res: `${namespace}${nsSeparator}${key}`, |
|
usedKey: key, |
|
exactUsedKey: key, |
|
usedLng: lng, |
|
usedNS: namespace, |
|
usedParams: this.getUsedParamsDetails(opt) |
|
}; |
|
} |
|
return `${namespace}${nsSeparator}${key}`; |
|
} |
|
if (returnDetails) { |
|
return { |
|
res: key, |
|
usedKey: key, |
|
exactUsedKey: key, |
|
usedLng: lng, |
|
usedNS: namespace, |
|
usedParams: this.getUsedParamsDetails(opt) |
|
}; |
|
} |
|
return key; |
|
} |
|
const resolved = this.resolve(keys, opt); |
|
let res = resolved?.res; |
|
const resUsedKey = resolved?.usedKey || key; |
|
const resExactUsedKey = resolved?.exactUsedKey || key; |
|
const noObject = ['[object Number]', '[object Function]', '[object RegExp]']; |
|
const joinArrays = opt.joinArrays !== undefined ? opt.joinArrays : this.options.joinArrays; |
|
const handleAsObjectInI18nFormat = !this.i18nFormat || this.i18nFormat.handleAsObject; |
|
const needsPluralHandling = opt.count !== undefined && !isString(opt.count); |
|
const hasDefaultValue = Translator.hasDefaultValue(opt); |
|
const defaultValueSuffix = needsPluralHandling ? this.pluralResolver.getSuffix(lng, opt.count, opt) : ''; |
|
const defaultValueSuffixOrdinalFallback = opt.ordinal && needsPluralHandling ? this.pluralResolver.getSuffix(lng, opt.count, { |
|
ordinal: false |
|
}) : ''; |
|
const needsZeroSuffixLookup = needsPluralHandling && !opt.ordinal && opt.count === 0; |
|
const defaultValue = needsZeroSuffixLookup && opt[`defaultValue${this.options.pluralSeparator}zero`] || opt[`defaultValue${defaultValueSuffix}`] || opt[`defaultValue${defaultValueSuffixOrdinalFallback}`] || opt.defaultValue; |
|
let resForObjHndl = res; |
|
if (handleAsObjectInI18nFormat && !res && hasDefaultValue) { |
|
resForObjHndl = defaultValue; |
|
} |
|
const handleAsObject = shouldHandleAsObject(resForObjHndl); |
|
const resType = Object.prototype.toString.apply(resForObjHndl); |
|
if (handleAsObjectInI18nFormat && resForObjHndl && handleAsObject && !noObject.includes(resType) && !(isString(joinArrays) && Array.isArray(resForObjHndl))) { |
|
if (!opt.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, resForObjHndl, { |
|
...opt, |
|
ns: namespaces |
|
}) : `key '${key} (${this.language})' returned an object instead of string.`; |
|
if (returnDetails) { |
|
resolved.res = r; |
|
resolved.usedParams = this.getUsedParamsDetails(opt); |
|
return resolved; |
|
} |
|
return r; |
|
} |
|
if (keySeparator) { |
|
const resTypeIsArray = Array.isArray(resForObjHndl); |
|
const copy = resTypeIsArray ? [] : {}; |
|
const newKeyToUse = resTypeIsArray ? resExactUsedKey : resUsedKey; |
|
for (const m in resForObjHndl) { |
|
if (Object.prototype.hasOwnProperty.call(resForObjHndl, m)) { |
|
const deepKey = `${newKeyToUse}${keySeparator}${m}`; |
|
if (hasDefaultValue && !res) { |
|
copy[m] = this.translate(deepKey, { |
|
...opt, |
|
defaultValue: shouldHandleAsObject(defaultValue) ? defaultValue[m] : undefined, |
|
...{ |
|
joinArrays: false, |
|
ns: namespaces |
|
} |
|
}); |
|
} else { |
|
copy[m] = this.translate(deepKey, { |
|
...opt, |
|
...{ |
|
joinArrays: false, |
|
ns: namespaces |
|
} |
|
}); |
|
} |
|
if (copy[m] === deepKey) copy[m] = resForObjHndl[m]; |
|
} |
|
} |
|
res = copy; |
|
} |
|
} else if (handleAsObjectInI18nFormat && isString(joinArrays) && Array.isArray(res)) { |
|
res = res.join(joinArrays); |
|
if (res) res = this.extendTranslation(res, keys, opt, lastKey); |
|
} else { |
|
let usedDefault = false; |
|
let usedKey = false; |
|
if (!this.isValidLookup(res) && hasDefaultValue) { |
|
usedDefault = true; |
|
res = defaultValue; |
|
} |
|
if (!this.isValidLookup(res)) { |
|
usedKey = true; |
|
res = key; |
|
} |
|
const missingKeyNoValueFallbackToKey = opt.missingKeyNoValueFallbackToKey || this.options.missingKeyNoValueFallbackToKey; |
|
const resForMissing = missingKeyNoValueFallbackToKey && usedKey ? undefined : res; |
|
const updateMissing = hasDefaultValue && defaultValue !== res && this.options.updateMissing; |
|
if (usedKey || usedDefault || updateMissing) { |
|
this.logger.log(updateMissing ? 'updateKey' : 'missingKey', lng, namespace, needsPluralHandling && !updateMissing ? `${key}${this.pluralResolver.getSuffix(lng, opt.count, opt)}` : key, updateMissing ? defaultValue : res); |
|
if (keySeparator) { |
|
const fk = this.resolve(key, { |
|
...opt, |
|
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, opt.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(opt.lng || this.language); |
|
} else { |
|
lngs.push(opt.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, opt); |
|
} else if (this.backendConnector?.saveMissing) { |
|
this.backendConnector.saveMissing(l, namespace, k, defaultForMissing, updateMissing, opt); |
|
} |
|
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, opt); |
|
if (needsZeroSuffixLookup && opt[`defaultValue${this.options.pluralSeparator}zero`] && !suffixes.includes(`${this.options.pluralSeparator}zero`)) { |
|
suffixes.push(`${this.options.pluralSeparator}zero`); |
|
} |
|
suffixes.forEach(suffix => { |
|
send([language], key + suffix, opt[`defaultValue${suffix}`] || defaultValue); |
|
}); |
|
}); |
|
} else { |
|
send(lngs, key, defaultValue); |
|
} |
|
} |
|
} |
|
res = this.extendTranslation(res, keys, opt, resolved, lastKey); |
|
if (usedKey && res === key && this.options.appendNamespaceToMissingKey) { |
|
res = `${namespace}${nsSeparator}${key}`; |
|
} |
|
if ((usedKey || usedDefault) && this.options.parseMissingKeyHandler) { |
|
res = this.options.parseMissingKeyHandler(this.options.appendNamespaceToMissingKey ? `${namespace}${nsSeparator}${key}` : key, usedDefault ? res : undefined, opt); |
|
} |
|
} |
|
if (returnDetails) { |
|
resolved.res = res; |
|
resolved.usedParams = this.getUsedParamsDetails(opt); |
|
return resolved; |
|
} |
|
return res; |
|
} |
|
extendTranslation(res, key, opt, resolved, lastKey) { |
|
if (this.i18nFormat?.parse) { |
|
res = this.i18nFormat.parse(res, { |
|
...this.options.interpolation.defaultVariables, |
|
...opt |
|
}, opt.lng || this.language || resolved.usedLng, resolved.usedNS, resolved.usedKey, { |
|
resolved |
|
}); |
|
} else if (!opt.skipInterpolation) { |
|
if (opt.interpolation) this.interpolator.init({ |
|
...opt, |
|
...{ |
|
interpolation: { |
|
...this.options.interpolation, |
|
...opt.interpolation |
|
} |
|
} |
|
}); |
|
const skipOnVariables = isString(res) && (opt?.interpolation?.skipOnVariables !== undefined ? opt.interpolation.skipOnVariables : this.options.interpolation.skipOnVariables); |
|
let nestBef; |
|
if (skipOnVariables) { |
|
const nb = res.match(this.interpolator.nestingRegexp); |
|
nestBef = nb && nb.length; |
|
} |
|
let data = opt.replace && !isString(opt.replace) ? opt.replace : opt; |
|
if (this.options.interpolation.defaultVariables) data = { |
|
...this.options.interpolation.defaultVariables, |
|
...data |
|
}; |
|
res = this.interpolator.interpolate(res, data, opt.lng || this.language || resolved.usedLng, opt); |
|
if (skipOnVariables) { |
|
const na = res.match(this.interpolator.nestingRegexp); |
|
const nestAft = na && na.length; |
|
if (nestBef < nestAft) opt.nest = false; |
|
} |
|
if (!opt.lng && resolved && resolved.res) opt.lng = this.language || resolved.usedLng; |
|
if (opt.nest !== false) res = this.interpolator.nest(res, (...args) => { |
|
if (lastKey?.[0] === args[0] && !opt.context) { |
|
this.logger.warn(`It seems you are nesting recursively key: ${args[0]} in key: ${key[0]}`); |
|
return null; |
|
} |
|
return this.translate(...args, key); |
|
}, opt); |
|
if (opt.interpolation) this.interpolator.reset(); |
|
} |
|
const postProcess = opt.postProcess || this.options.postProcess; |
|
const postProcessorNames = isString(postProcess) ? [postProcess] : postProcess; |
|
if (res != null && postProcessorNames?.length && opt.applyPostProcessor !== false) { |
|
res = postProcessor.handle(postProcessorNames, res, key, this.options && this.options.postProcessPassResolved ? { |
|
i18nResolved: { |
|
...resolved, |
|
usedParams: this.getUsedParamsDetails(opt) |
|
}, |
|
...opt |
|
} : opt, this); |
|
} |
|
return res; |
|
} |
|
resolve(keys, opt = {}) { |
|
let found; |
|
let usedKey; |
|
let exactUsedKey; |
|
let usedLng; |
|
let usedNS; |
|
if (isString(keys)) keys = [keys]; |
|
if (Array.isArray(keys)) keys = keys.map(k => typeof k === 'function' ? keysFromSelector(k, { |
|
...this.options, |
|
...opt |
|
}) : k); |
|
keys.forEach(k => { |
|
if (this.isValidLookup(found)) return; |
|
const extracted = this.extractFromKey(k, opt); |
|
const key = extracted.key; |
|
usedKey = key; |
|
let namespaces = extracted.namespaces; |
|
if (this.options.fallbackNS) namespaces = namespaces.concat(this.options.fallbackNS); |
|
const needsPluralHandling = opt.count !== undefined && !isString(opt.count); |
|
const needsZeroSuffixLookup = needsPluralHandling && !opt.ordinal && opt.count === 0; |
|
const needsContextHandling = opt.context !== undefined && (isString(opt.context) || typeof opt.context === 'number') && opt.context !== ''; |
|
const codes = opt.lngs ? opt.lngs : this.languageUtils.toResolveHierarchy(opt.lng || this.language, opt.fallbackLng); |
|
namespaces.forEach(ns => { |
|
if (this.isValidLookup(found)) return; |
|
usedNS = ns; |
|
if (!this.checkedLoadedFor[`${codes[0]}-${ns}`] && this.utils?.hasLoadedNamespace && !this.utils?.hasLoadedNamespace(usedNS)) { |
|
this.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?.addLookupKeys) { |
|
this.i18nFormat.addLookupKeys(finalKeys, key, code, ns, opt); |
|
} else { |
|
let pluralSuffix; |
|
if (needsPluralHandling) pluralSuffix = this.pluralResolver.getSuffix(code, opt.count, opt); |
|
const zeroSuffix = `${this.options.pluralSeparator}zero`; |
|
const ordinalPrefix = `${this.options.pluralSeparator}ordinal${this.options.pluralSeparator}`; |
|
if (needsPluralHandling) { |
|
if (opt.ordinal && pluralSuffix.startsWith(ordinalPrefix)) { |
|
finalKeys.push(key + pluralSuffix.replace(ordinalPrefix, this.options.pluralSeparator)); |
|
} |
|
finalKeys.push(key + pluralSuffix); |
|
if (needsZeroSuffixLookup) { |
|
finalKeys.push(key + zeroSuffix); |
|
} |
|
} |
|
if (needsContextHandling) { |
|
const contextKey = `${key}${this.options.contextSeparator || '_'}${opt.context}`; |
|
finalKeys.push(contextKey); |
|
if (needsPluralHandling) { |
|
if (opt.ordinal && pluralSuffix.startsWith(ordinalPrefix)) { |
|
finalKeys.push(contextKey + pluralSuffix.replace(ordinalPrefix, this.options.pluralSeparator)); |
|
} |
|
finalKeys.push(contextKey + pluralSuffix); |
|
if (needsZeroSuffixLookup) { |
|
finalKeys.push(contextKey + zeroSuffix); |
|
} |
|
} |
|
} |
|
} |
|
let possibleKey; |
|
while (possibleKey = finalKeys.pop()) { |
|
if (!this.isValidLookup(found)) { |
|
exactUsedKey = possibleKey; |
|
found = this.getResource(code, ns, possibleKey, opt); |
|
} |
|
} |
|
}); |
|
}); |
|
}); |
|
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?.getResource) return this.i18nFormat.getResource(code, ns, key, options); |
|
return this.resourceStore.getResource(code, ns, key, options); |
|
} |
|
getUsedParamsDetails(options = {}) { |
|
const optionsKeys = ['defaultValue', 'ordinal', 'context', 'replace', 'lng', 'lngs', 'fallbackLng', 'ns', 'keySeparator', 'nsSeparator', 'returnObjects', 'returnDetails', 'joinArrays', 'postProcess', 'interpolation']; |
|
const useOptionsReplaceForData = options.replace && !isString(options.replace); |
|
let data = useOptionsReplaceForData ? options.replace : options; |
|
if (useOptionsReplaceForData && typeof options.count !== 'undefined') { |
|
data = { |
|
...data, |
|
count: options.count |
|
}; |
|
} |
|
if (this.options.interpolation.defaultVariables) { |
|
data = { |
|
...this.options.interpolation.defaultVariables, |
|
...data |
|
}; |
|
} |
|
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) && option.startsWith(prefix) && undefined !== options[option]) { |
|
return true; |
|
} |
|
} |
|
return false; |
|
} |
|
} |
|
|
|
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.includes('-')) 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.includes('-')) return code; |
|
const p = code.split('-'); |
|
return this.formatLanguageCode(p[0]); |
|
} |
|
formatLanguageCode(code) { |
|
if (isString(code) && code.includes('-')) { |
|
let formattedCode; |
|
try { |
|
formattedCode = Intl.getCanonicalLocales(code)[0]; |
|
} catch (e) {} |
|
if (formattedCode && this.options.lowerCaseLng) { |
|
formattedCode = formattedCode.toLowerCase(); |
|
} |
|
if (formattedCode) return formattedCode; |
|
if (this.options.lowerCaseLng) { |
|
return code.toLowerCase(); |
|
} |
|
return code; |
|
} |
|
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.includes(code); |
|
} |
|
getBestMatchFromCodes(codes) { |
|
if (!codes) return null; |
|
let found; |
|
codes.forEach(code => { |
|
if (found) return; |
|
const cleanedLng = this.formatLanguageCode(code); |
|
if (!this.options.supportedLngs || this.isSupportedCode(cleanedLng)) found = cleanedLng; |
|
}); |
|
if (!found && this.options.supportedLngs) { |
|
codes.forEach(code => { |
|
if (found) return; |
|
const lngScOnly = this.getScriptPartFromCode(code); |
|
if (this.isSupportedCode(lngScOnly)) return found = lngScOnly; |
|
const lngOnly = this.getLanguagePartFromCode(code); |
|
if (this.isSupportedCode(lngOnly)) return found = lngOnly; |
|
found = this.options.supportedLngs.find(supportedLng => { |
|
if (supportedLng === lngOnly) return true; |
|
if (!supportedLng.includes('-') && !lngOnly.includes('-')) return false; |
|
if (supportedLng.includes('-') && !lngOnly.includes('-') && supportedLng.slice(0, supportedLng.indexOf('-')) === lngOnly) return true; |
|
if (supportedLng.startsWith(lngOnly) && lngOnly.length > 1) return true; |
|
return false; |
|
}); |
|
}); |
|
} |
|
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 (isString(fallbacks)) fallbacks = [fallbacks]; |
|
if (Array.isArray(fallbacks)) return fallbacks; |
|
if (!code) return fallbacks.default || []; |
|
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 === false ? [] : 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 (isString(code) && (code.includes('-') || code.includes('_'))) { |
|
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 (isString(code)) { |
|
addCode(this.formatLanguageCode(code)); |
|
} |
|
fallbackCodes.forEach(fc => { |
|
if (!codes.includes(fc)) addCode(this.formatLanguageCode(fc)); |
|
}); |
|
return codes; |
|
} |
|
} |
|
|
|
const suffixesOrder = { |
|
zero: 0, |
|
one: 1, |
|
two: 2, |
|
few: 3, |
|
many: 4, |
|
other: 5 |
|
}; |
|
const dummyRule = { |
|
select: count => count === 1 ? 'one' : 'other', |
|
resolvedOptions: () => ({ |
|
pluralCategories: ['one', 'other'] |
|
}) |
|
}; |
|
class PluralResolver { |
|
constructor(languageUtils, options = {}) { |
|
this.languageUtils = languageUtils; |
|
this.options = options; |
|
this.logger = baseLogger.create('pluralResolver'); |
|
this.pluralRulesCache = {}; |
|
} |
|
clearCache() { |
|
this.pluralRulesCache = {}; |
|
} |
|
getRule(code, options = {}) { |
|
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]; |
|
} |
|
let rule; |
|
try { |
|
rule = new Intl.PluralRules(cleanedCode, { |
|
type |
|
}); |
|
} catch (err) { |
|
if (typeof Intl === 'undefined') { |
|
this.logger.error('No Intl support, please use an Intl polyfill!'); |
|
return dummyRule; |
|
} |
|
if (!code.match(/-|_/)) return dummyRule; |
|
const lngPart = this.languageUtils.getLanguagePartFromCode(code); |
|
rule = this.getRule(lngPart, options); |
|
} |
|
this.pluralRulesCache[cacheKey] = rule; |
|
return rule; |
|
} |
|
needsPlural(code, options = {}) { |
|
let rule = this.getRule(code, options); |
|
if (!rule) rule = this.getRule('dev', options); |
|
return rule?.resolvedOptions().pluralCategories.length > 1; |
|
} |
|
getPluralFormsOfKey(code, key, options = {}) { |
|
return this.getSuffixes(code, options).map(suffix => `${key}${suffix}`); |
|
} |
|
getSuffixes(code, options = {}) { |
|
let rule = this.getRule(code, options); |
|
if (!rule) rule = this.getRule('dev', options); |
|
if (!rule) return []; |
|
return rule.resolvedOptions().pluralCategories.sort((pluralCategory1, pluralCategory2) => suffixesOrder[pluralCategory1] - suffixesOrder[pluralCategory2]).map(pluralCategory => `${this.options.prepend}${options.ordinal ? `ordinal${this.options.prepend}` : ''}${pluralCategory}`); |
|
} |
|
getSuffix(code, count, options = {}) { |
|
const rule = this.getRule(code, options); |
|
if (rule) { |
|
return `${this.options.prepend}${options.ordinal ? `ordinal${this.options.prepend}` : ''}${rule.select(count)}`; |
|
} |
|
this.logger.warn(`no plural rule found for: ${code}`); |
|
return this.getSuffix('dev', count, options); |
|
} |
|
} |
|
|
|
const deepFindWithDefaults = (data, defaultData, key, keySeparator = '.', ignoreJSONStructure = true) => { |
|
let path = getPathWithDefaults(data, defaultData, key); |
|
if (!path && ignoreJSONStructure && isString(key)) { |
|
path = deepFind(data, key, keySeparator); |
|
if (path === undefined) path = deepFind(defaultData, key, keySeparator); |
|
} |
|
return path; |
|
}; |
|
const regexSafe = val => val.replace(/\$/g, '$$$$'); |
|
class Interpolator { |
|
constructor(options = {}) { |
|
this.logger = baseLogger.create('interpolator'); |
|
this.options = options; |
|
this.format = options?.interpolation?.format || (value => value); |
|
this.init(options); |
|
} |
|
init(options = {}) { |
|
if (!options.interpolation) options.interpolation = { |
|
escapeValue: true |
|
}; |
|
const { |
|
escape: escape$1, |
|
escapeValue, |
|
useRawValueToEscape, |
|
prefix, |
|
prefixEscaped, |
|
suffix, |
|
suffixEscaped, |
|
formatSeparator, |
|
unescapeSuffix, |
|
unescapePrefix, |
|
nestingPrefix, |
|
nestingPrefixEscaped, |
|
nestingSuffix, |
|
nestingSuffixEscaped, |
|
nestingOptionsSeparator, |
|
maxReplaces, |
|
alwaysFormat |
|
} = options.interpolation; |
|
this.escape = escape$1 !== undefined ? escape$1 : escape; |
|
this.escapeValue = escapeValue !== undefined ? escapeValue : true; |
|
this.useRawValueToEscape = useRawValueToEscape !== undefined ? useRawValueToEscape : false; |
|
this.prefix = prefix ? regexEscape(prefix) : prefixEscaped || '{{'; |
|
this.suffix = suffix ? regexEscape(suffix) : suffixEscaped || '}}'; |
|
this.formatSeparator = formatSeparator || ','; |
|
this.unescapePrefix = unescapeSuffix ? '' : unescapePrefix ? regexEscape(unescapePrefix) : '-'; |
|
this.unescapeSuffix = this.unescapePrefix ? '' : unescapeSuffix ? regexEscape(unescapeSuffix) : ''; |
|
this.nestingPrefix = nestingPrefix ? regexEscape(nestingPrefix) : nestingPrefixEscaped || regexEscape('$t('); |
|
this.nestingSuffix = nestingSuffix ? regexEscape(nestingSuffix) : nestingSuffixEscaped || regexEscape(')'); |
|
this.nestingOptionsSeparator = nestingOptionsSeparator || ','; |
|
this.maxReplaces = maxReplaces || 1000; |
|
this.alwaysFormat = alwaysFormat !== undefined ? alwaysFormat : false; |
|
this.resetRegExp(); |
|
} |
|
reset() { |
|
if (this.options) this.init(this.options); |
|
} |
|
resetRegExp() { |
|
const getOrResetRegExp = (existingRegExp, pattern) => { |
|
if (existingRegExp?.source === pattern) { |
|
existingRegExp.lastIndex = 0; |
|
return existingRegExp; |
|
} |
|
return new RegExp(pattern, 'g'); |
|
}; |
|
this.regexp = getOrResetRegExp(this.regexp, `${this.prefix}(.+?)${this.suffix}`); |
|
this.regexpUnescape = getOrResetRegExp(this.regexpUnescape, `${this.prefix}${this.unescapePrefix}(.+?)${this.unescapeSuffix}${this.suffix}`); |
|
this.nestingRegexp = getOrResetRegExp(this.nestingRegexp, `${this.nestingPrefix}((?:[^()"']+|"[^"]*"|'[^']*'|\\((?:[^()]|"[^"]*"|'[^']*')*\\))*?)${this.nestingSuffix}`); |
|
} |
|
interpolate(str, data, lng, options) { |
|
let match; |
|
let value; |
|
let replaces; |
|
const defaultData = this.options && this.options.interpolation && this.options.interpolation.defaultVariables || {}; |
|
const handleFormat = key => { |
|
if (!key.includes(this.formatSeparator)) { |
|
const path = deepFindWithDefaults(data, defaultData, key, this.options.keySeparator, this.options.ignoreJSONStructure); |
|
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(deepFindWithDefaults(data, defaultData, k, this.options.keySeparator, this.options.ignoreJSONStructure), f, lng, { |
|
...options, |
|
...data, |
|
interpolationkey: k |
|
}); |
|
}; |
|
this.resetRegExp(); |
|
if (!this.escapeValue && typeof str === 'string' && /\$t\([^)]*\{[^}]*\{\{/.test(str)) { |
|
this.logger.warn('nesting options string contains interpolated variables with escapeValue: false — ' + 'if any of those values are attacker-controlled they can inject additional ' + 'nesting options (e.g. redirect lng/ns). Sanitise untrusted input before passing ' + 'it to t(), or keep escapeValue: true.'); |
|
} |
|
const missingInterpolationHandler = options?.missingInterpolationHandler || this.options.missingInterpolationHandler; |
|
const skipOnVariables = options?.interpolation?.skipOnVariables !== undefined ? options.interpolation.skipOnVariables : this.options.interpolation.skipOnVariables; |
|
const todos = [{ |
|
regex: this.regexpUnescape, |
|
safeValue: val => val |
|
}, { |
|
regex: this.regexp, |
|
safeValue: val => this.escapeValue ? this.escape(val) : val |
|
}]; |
|
todos.forEach(todo => { |
|
replaces = 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 = isString(temp) ? temp : ''; |
|
} else if (options && Object.prototype.hasOwnProperty.call(options, matchedVar)) { |
|
value = ''; |
|
} else if (skipOnVariables) { |
|
value = match[0]; |
|
continue; |
|
} else { |
|
this.logger.warn(`missed to pass in variable ${matchedVar} for interpolating ${str}`); |
|
value = ''; |
|
} |
|
} else if (!isString(value) && !this.useRawValueToEscape) { |
|
value = makeString(value); |
|
} |
|
const safeValue = todo.safeValue(value); |
|
str = str.replace(match[0], regexSafe(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; |
|
const handleHasOptions = (key, inheritedOptions) => { |
|
const sep = this.nestingOptionsSeparator; |
|
if (!key.includes(sep)) return key; |
|
const c = key.split(new RegExp(`${regexEscape(sep)}[ ]*{`)); |
|
let optionsString = `{${c[1]}`; |
|
key = c[0]; |
|
optionsString = this.interpolate(optionsString, clonedOptions); |
|
const matchedSingleQuotes = optionsString.match(/'/g); |
|
const matchedDoubleQuotes = optionsString.match(/"/g); |
|
if ((matchedSingleQuotes?.length ?? 0) % 2 === 0 && !matchedDoubleQuotes || (matchedDoubleQuotes?.length ?? 0) % 2 !== 0) { |
|
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}`; |
|
} |
|
if (clonedOptions.defaultValue && clonedOptions.defaultValue.includes(this.prefix)) delete clonedOptions.defaultValue; |
|
return key; |
|
}; |
|
while (match = this.nestingRegexp.exec(str)) { |
|
let formatters = []; |
|
clonedOptions = { |
|
...options |
|
}; |
|
clonedOptions = clonedOptions.replace && !isString(clonedOptions.replace) ? clonedOptions.replace : clonedOptions; |
|
clonedOptions.applyPostProcessor = false; |
|
delete clonedOptions.defaultValue; |
|
const keyEndIndex = /{.*}/s.test(match[1]) ? match[1].lastIndexOf('}') + 1 : match[1].indexOf(this.formatSeparator); |
|
if (keyEndIndex !== -1) { |
|
formatters = match[1].slice(keyEndIndex).split(this.formatSeparator).map(elem => elem.trim()).filter(Boolean); |
|
match[1] = match[1].slice(0, keyEndIndex); |
|
} |
|
value = fc(handleHasOptions.call(this, match[1].trim(), clonedOptions), clonedOptions); |
|
if (value && match[0] === str && !isString(value)) return value; |
|
if (!isString(value)) value = makeString(value); |
|
if (!value) { |
|
this.logger.warn(`missed to resolve ${match[1]} for nesting ${str}`); |
|
value = ''; |
|
} |
|
if (formatters.length) { |
|
value = formatters.reduce((v, f) => this.format(v, f, options.lng, { |
|
...options, |
|
interpolationkey: match[1].trim() |
|
}), value.trim()); |
|
} |
|
str = str.replace(match[0], value); |
|
this.regexp.lastIndex = 0; |
|
} |
|
return str; |
|
} |
|
} |
|
|
|
const parseFormatStr = formatStr => { |
|
let formatName = formatStr.toLowerCase().trim(); |
|
const formatOptions = {}; |
|
if (formatStr.includes('(')) { |
|
const p = formatStr.split('('); |
|
formatName = p[0].toLowerCase().trim(); |
|
const optStr = p[1].slice(0, -1); |
|
if (formatName === 'currency' && !optStr.includes(':')) { |
|
if (!formatOptions.currency) formatOptions.currency = optStr.trim(); |
|
} else if (formatName === 'relativetime' && !optStr.includes(':')) { |
|
if (!formatOptions.range) formatOptions.range = optStr.trim(); |
|
} else { |
|
const opts = optStr.split(';'); |
|
opts.forEach(opt => { |
|
if (opt) { |
|
const [key, ...rest] = opt.split(':'); |
|
const val = rest.join(':').trim().replace(/^'+|'+$/g, ''); |
|
const trimmedKey = key.trim(); |
|
if (!formatOptions[trimmedKey]) formatOptions[trimmedKey] = val; |
|
if (val === 'false') formatOptions[trimmedKey] = false; |
|
if (val === 'true') formatOptions[trimmedKey] = true; |
|
if (!isNaN(val)) formatOptions[trimmedKey] = parseInt(val, 10); |
|
} |
|
}); |
|
} |
|
} |
|
return { |
|
formatName, |
|
formatOptions |
|
}; |
|
}; |
|
const createCachedFormatter = fn => { |
|
const cache = {}; |
|
return (v, l, o) => { |
|
let optForCache = o; |
|
if (o && o.interpolationkey && o.formatParams && o.formatParams[o.interpolationkey] && o[o.interpolationkey]) { |
|
optForCache = { |
|
...optForCache, |
|
[o.interpolationkey]: undefined |
|
}; |
|
} |
|
const key = l + JSON.stringify(optForCache); |
|
let frm = cache[key]; |
|
if (!frm) { |
|
frm = fn(getCleanedCode(l), o); |
|
cache[key] = frm; |
|
} |
|
return frm(v); |
|
}; |
|
}; |
|
const createNonCachedFormatter = fn => (v, l, o) => fn(getCleanedCode(l), o)(v); |
|
class Formatter { |
|
constructor(options = {}) { |
|
this.logger = baseLogger.create('formatter'); |
|
this.options = options; |
|
this.init(options); |
|
} |
|
init(services, options = { |
|
interpolation: {} |
|
}) { |
|
this.formatSeparator = options.interpolation.formatSeparator || ','; |
|
const cf = options.cacheInBuiltFormats ? createCachedFormatter : createNonCachedFormatter; |
|
this.formats = { |
|
number: cf((lng, opt) => { |
|
const formatter = new Intl.NumberFormat(lng, { |
|
...opt |
|
}); |
|
return val => formatter.format(val); |
|
}), |
|
currency: cf((lng, opt) => { |
|
const formatter = new Intl.NumberFormat(lng, { |
|
...opt, |
|
style: 'currency' |
|
}); |
|
return val => formatter.format(val); |
|
}), |
|
datetime: cf((lng, opt) => { |
|
const formatter = new Intl.DateTimeFormat(lng, { |
|
...opt |
|
}); |
|
return val => formatter.format(val); |
|
}), |
|
relativetime: cf((lng, opt) => { |
|
const formatter = new Intl.RelativeTimeFormat(lng, { |
|
...opt |
|
}); |
|
return val => formatter.format(val, opt.range || 'day'); |
|
}), |
|
list: cf((lng, opt) => { |
|
const formatter = new Intl.ListFormat(lng, { |
|
...opt |
|
}); |
|
return val => formatter.format(val); |
|
}) |
|
}; |
|
} |
|
add(name, fc) { |
|
this.formats[name.toLowerCase().trim()] = fc; |
|
} |
|
addCached(name, fc) { |
|
this.formats[name.toLowerCase().trim()] = createCachedFormatter(fc); |
|
} |
|
format(value, format, lng, options = {}) { |
|
if (!format) return value; |
|
if (value == null) return value; |
|
const rawFormats = format.split(this.formatSeparator); |
|
const formats = []; |
|
for (let i = 0; i < rawFormats.length; i++) { |
|
let f = rawFormats[i]; |
|
while (f.indexOf('(') > -1 && !f.includes(')') && i + 1 < rawFormats.length) { |
|
f = `${f}${this.formatSeparator}${rawFormats[++i]}`; |
|
} |
|
formats.push(f); |
|
} |
|
const result = formats.reduce((mem, f) => { |
|
const { |
|
formatName, |
|
formatOptions |
|
} = parseFormatStr(f); |
|
if (this.formats[formatName]) { |
|
let formatted = mem; |
|
try { |
|
const valOptions = options?.formatParams?.[options.interpolationkey] || {}; |
|
const l = valOptions.locale || valOptions.lng || options.locale || options.lng || lng; |
|
formatted = this.formats[formatName](mem, l, { |
|
...formatOptions, |
|
...options, |
|
...valOptions |
|
}); |
|
} catch (error) { |
|
this.logger.warn(error); |
|
} |
|
return formatted; |
|
} else { |
|
this.logger.warn(`there was no format function for ${formatName}`); |
|
} |
|
return mem; |
|
}, value); |
|
return result; |
|
} |
|
} |
|
|
|
const removePending = (q, name) => { |
|
if (q.pending[name] !== undefined) { |
|
delete q.pending[name]; |
|
q.pendingCount--; |
|
} |
|
}; |
|
class Connector extends EventEmitter { |
|
constructor(backend, store, services, options = {}) { |
|
super(); |
|
this.backend = backend; |
|
this.store = store; |
|
this.services = services; |
|
this.languageUtils = services.languageUtils; |
|
this.options = options; |
|
this.logger = baseLogger.create('backendConnector'); |
|
this.waitingReads = []; |
|
this.maxParallelReads = options.maxParallelReads || 10; |
|
this.readingCalls = 0; |
|
this.maxRetries = options.maxRetries >= 0 ? options.maxRetries : 5; |
|
this.retryTimeout = options.retryTimeout >= 1 ? options.retryTimeout : 350; |
|
this.state = {}; |
|
this.queue = []; |
|
this.backend?.init?.(services, options.backend, options); |
|
} |
|
queueLoad(languages, namespaces, options, callback) { |
|
const toLoad = {}; |
|
const pending = {}; |
|
const toLoadLanguages = {}; |
|
const toLoadNamespaces = {}; |
|
languages.forEach(lng => { |
|
let hasAllNamespaces = true; |
|
namespaces.forEach(ns => { |
|
const name = `${lng}|${ns}`; |
|
if (!options.reload && this.store.hasResourceBundle(lng, ns)) { |
|
this.state[name] = 2; |
|
} else if (this.state[name] < 0) ; else if (this.state[name] === 1) { |
|
if (pending[name] === undefined) pending[name] = true; |
|
} else { |
|
this.state[name] = 1; |
|
hasAllNamespaces = false; |
|
if (pending[name] === undefined) pending[name] = true; |
|
if (toLoad[name] === undefined) toLoad[name] = true; |
|
if (toLoadNamespaces[ns] === undefined) toLoadNamespaces[ns] = true; |
|
} |
|
}); |
|
if (!hasAllNamespaces) toLoadLanguages[lng] = true; |
|
}); |
|
if (Object.keys(toLoad).length || Object.keys(pending).length) { |
|
this.queue.push({ |
|
pending, |
|
pendingCount: Object.keys(pending).length, |
|
loaded: {}, |
|
errors: [], |
|
callback |
|
}); |
|
} |
|
return { |
|
toLoad: Object.keys(toLoad), |
|
pending: Object.keys(pending), |
|
toLoadLanguages: Object.keys(toLoadLanguages), |
|
toLoadNamespaces: Object.keys(toLoadNamespaces) |
|
}; |
|
} |
|
loaded(name, err, data) { |
|
const s = name.split('|'); |
|
const lng = s[0]; |
|
const ns = s[1]; |
|
if (err) this.emit('failedLoading', lng, ns, err); |
|
if (!err && data) { |
|
this.store.addResourceBundle(lng, ns, data, undefined, undefined, { |
|
skipCopy: true |
|
}); |
|
} |
|
this.state[name] = err ? -1 : 2; |
|
if (err && data) this.state[name] = 0; |
|
const loaded = {}; |
|
this.queue.forEach(q => { |
|
pushPath(q.loaded, [lng], ns); |
|
removePending(q, name); |
|
if (err) q.errors.push(err); |
|
if (q.pendingCount === 0 && !q.done) { |
|
Object.keys(q.loaded).forEach(l => { |
|
if (!loaded[l]) loaded[l] = {}; |
|
const loadedKeys = q.loaded[l]; |
|
if (loadedKeys.length) { |
|
loadedKeys.forEach(n => { |
|
if (loaded[l][n] === undefined) loaded[l][n] = true; |
|
}); |
|
} |
|
}); |
|
q.done = true; |
|
if (q.errors.length) { |
|
q.callback(q.errors); |
|
} else { |
|
q.callback(); |
|
} |
|
} |
|
}); |
|
this.emit('loaded', loaded); |
|
this.queue = this.queue.filter(q => !q.done); |
|
} |
|
read(lng, ns, fcName, tried = 0, wait = this.retryTimeout, callback) { |
|
if (!lng.length) return callback(null, {}); |
|
if (this.readingCalls >= this.maxParallelReads) { |
|
this.waitingReads.push({ |
|
lng, |
|
ns, |
|
fcName, |
|
tried, |
|
wait, |
|
callback |
|
}); |
|
return; |
|
} |
|
this.readingCalls++; |
|
const resolver = (err, data) => { |
|
this.readingCalls--; |
|
if (this.waitingReads.length > 0) { |
|
const next = this.waitingReads.shift(); |
|
this.read(next.lng, next.ns, next.fcName, next.tried, next.wait, next.callback); |
|
} |
|
if (err && data && tried < this.maxRetries) { |
|
setTimeout(() => { |
|
this.read(lng, ns, fcName, tried + 1, wait * 2, callback); |
|
}, wait); |
|
return; |
|
} |
|
callback(err, data); |
|
}; |
|
const fc = this.backend[fcName].bind(this.backend); |
|
if (fc.length === 2) { |
|
try { |
|
const r = fc(lng, ns); |
|
if (r && typeof r.then === 'function') { |
|
r.then(data => resolver(null, data)).catch(resolver); |
|
} else { |
|
resolver(null, r); |
|
} |
|
} catch (err) { |
|
resolver(err); |
|
} |
|
return; |
|
} |
|
return fc(lng, ns, resolver); |
|
} |
|
prepareLoading(languages, namespaces, options = {}, callback) { |
|
if (!this.backend) { |
|
this.logger.warn('No backend was added via i18next.use. Will not load resources.'); |
|
return callback && callback(); |
|
} |
|
if (isString(languages)) languages = this.languageUtils.toResolveHierarchy(languages); |
|
if (isString(namespaces)) namespaces = [namespaces]; |
|
const toLoad = this.queueLoad(languages, namespaces, options, callback); |
|
if (!toLoad.toLoad.length) { |
|
if (!toLoad.pending.length) callback(); |
|
return null; |
|
} |
|
toLoad.toLoad.forEach(name => { |
|
this.loadOne(name); |
|
}); |
|
} |
|
load(languages, namespaces, callback) { |
|
this.prepareLoading(languages, namespaces, {}, callback); |
|
} |
|
reload(languages, namespaces, callback) { |
|
this.prepareLoading(languages, namespaces, { |
|
reload: true |
|
}, callback); |
|
} |
|
loadOne(name, prefix = '') { |
|
const s = name.split('|'); |
|
const lng = s[0]; |
|
const ns = s[1]; |
|
this.read(lng, ns, 'read', undefined, undefined, (err, data) => { |
|
if (err) this.logger.warn(`${prefix}loading namespace ${ns} for language ${lng} failed`, err); |
|
if (!err && data) this.logger.log(`${prefix}loaded namespace ${ns} for language ${lng}`, data); |
|
this.loaded(name, err, data); |
|
}); |
|
} |
|
saveMissing(languages, namespace, key, fallbackValue, isUpdate, options = {}, clb = () => {}) { |
|
if (this.services?.utils?.hasLoadedNamespace && !this.services?.utils?.hasLoadedNamespace(namespace)) { |
|
this.logger.warn(`did not save key "${key}" as the namespace "${namespace}" 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!!!'); |
|
return; |
|
} |
|
if (key === undefined || key === null || key === '') return; |
|
if (this.backend?.create) { |
|
const opts = { |
|
...options, |
|
isUpdate |
|
}; |
|
const fc = this.backend.create.bind(this.backend); |
|
if (fc.length < 6) { |
|
try { |
|
let r; |
|
if (fc.length === 5) { |
|
r = fc(languages, namespace, key, fallbackValue, opts); |
|
} else { |
|
r = fc(languages, namespace, key, fallbackValue); |
|
} |
|
if (r && typeof r.then === 'function') { |
|
r.then(data => clb(null, data)).catch(clb); |
|
} else { |
|
clb(null, r); |
|
} |
|
} catch (err) { |
|
clb(err); |
|
} |
|
} else { |
|
fc(languages, namespace, key, fallbackValue, clb, opts); |
|
} |
|
} |
|
if (!languages || !languages[0]) return; |
|
this.store.addResource(languages[0], namespace, key, fallbackValue); |
|
} |
|
} |
|
|
|
const get = () => ({ |
|
debug: false, |
|
initAsync: true, |
|
ns: ['translation'], |
|
defaultNS: ['translation'], |
|
fallbackLng: ['dev'], |
|
fallbackNS: false, |
|
supportedLngs: false, |
|
nonExplicitSupportedLngs: false, |
|
load: 'all', |
|
preload: false, |
|
keySeparator: '.', |
|
nsSeparator: ':', |
|
pluralSeparator: '_', |
|
contextSeparator: '_', |
|
enableSelector: false, |
|
partialBundledLanguages: false, |
|
saveMissing: false, |
|
updateMissing: false, |
|
saveMissingTo: 'fallback', |
|
saveMissingPlurals: true, |
|
missingKeyHandler: false, |
|
missingInterpolationHandler: false, |
|
postProcess: false, |
|
postProcessPassResolved: false, |
|
returnNull: false, |
|
returnEmptyString: true, |
|
returnObjects: false, |
|
joinArrays: false, |
|
returnedObjectHandler: false, |
|
parseMissingKeyHandler: false, |
|
appendNamespaceToMissingKey: false, |
|
appendNamespaceToCIMode: false, |
|
overloadTranslationOptionHandler: args => { |
|
let ret = {}; |
|
if (typeof args[1] === 'object') ret = args[1]; |
|
if (isString(args[1])) ret.defaultValue = args[1]; |
|
if (isString(args[2])) ret.tDescription = args[2]; |
|
if (typeof args[2] === 'object' || typeof args[3] === 'object') { |
|
const options = args[3] || args[2]; |
|
Object.keys(options).forEach(key => { |
|
ret[key] = options[key]; |
|
}); |
|
} |
|
return ret; |
|
}, |
|
interpolation: { |
|
escapeValue: true, |
|
prefix: '{{', |
|
suffix: '}}', |
|
formatSeparator: ',', |
|
unescapePrefix: '-', |
|
nestingPrefix: '$t(', |
|
nestingSuffix: ')', |
|
nestingOptionsSeparator: ',', |
|
maxReplaces: 1000, |
|
skipOnVariables: true |
|
}, |
|
cacheInBuiltFormats: true |
|
}); |
|
const transformOptions = options => { |
|
if (isString(options.ns)) options.ns = [options.ns]; |
|
if (isString(options.fallbackLng)) options.fallbackLng = [options.fallbackLng]; |
|
if (isString(options.fallbackNS)) options.fallbackNS = [options.fallbackNS]; |
|
if (options.supportedLngs && !options.supportedLngs.includes('cimode')) { |
|
options.supportedLngs = options.supportedLngs.concat(['cimode']); |
|
} |
|
return options; |
|
}; |
|
|
|
const noop = () => {}; |
|
const bindMemberFunctions = inst => { |
|
const mems = Object.getOwnPropertyNames(Object.getPrototypeOf(inst)); |
|
mems.forEach(mem => { |
|
if (typeof inst[mem] === 'function') { |
|
inst[mem] = inst[mem].bind(inst); |
|
} |
|
}); |
|
}; |
|
class I18n extends EventEmitter { |
|
constructor(options = {}, callback) { |
|
super(); |
|
this.options = transformOptions(options); |
|
this.services = {}; |
|
this.logger = baseLogger; |
|
this.modules = { |
|
external: [] |
|
}; |
|
bindMemberFunctions(this); |
|
if (callback && !this.isInitialized && !options.isClone) { |
|
if (!this.options.initAsync) { |
|
this.init(options, callback); |
|
return this; |
|
} |
|
setTimeout(() => { |
|
this.init(options, callback); |
|
}, 0); |
|
} |
|
} |
|
init(options = {}, callback) { |
|
this.isInitializing = true; |
|
if (typeof options === 'function') { |
|
callback = options; |
|
options = {}; |
|
} |
|
if (options.defaultNS == null && options.ns) { |
|
if (isString(options.ns)) { |
|
options.defaultNS = options.ns; |
|
} else if (!options.ns.includes('translation')) { |
|
options.defaultNS = options.ns[0]; |
|
} |
|
} |
|
const defOpts = get(); |
|
this.options = { |
|
...defOpts, |
|
...this.options, |
|
...transformOptions(options) |
|
}; |
|
this.options.interpolation = { |
|
...defOpts.interpolation, |
|
...this.options.interpolation |
|
}; |
|
if (options.keySeparator !== undefined) { |
|
this.options.userDefinedKeySeparator = options.keySeparator; |
|
} |
|
if (options.nsSeparator !== undefined) { |
|
this.options.userDefinedNsSeparator = options.nsSeparator; |
|
} |
|
if (typeof this.options.overloadTranslationOptionHandler !== 'function') { |
|
this.options.overloadTranslationOptionHandler = defOpts.overloadTranslationOptionHandler; |
|
} |
|
const createClassOnDemand = ClassOrObject => { |
|
if (!ClassOrObject) return null; |
|
if (typeof ClassOrObject === 'function') return new ClassOrObject(); |
|
return ClassOrObject; |
|
}; |
|
if (!this.options.isClone) { |
|
if (this.modules.logger) { |
|
baseLogger.init(createClassOnDemand(this.modules.logger), this.options); |
|
} else { |
|
baseLogger.init(null, this.options); |
|
} |
|
let formatter; |
|
if (this.modules.formatter) { |
|
formatter = this.modules.formatter; |
|
} else { |
|
formatter = Formatter; |
|
} |
|
const lu = new LanguageUtil(this.options); |
|
this.store = new ResourceStore(this.options.resources, this.options); |
|
const s = this.services; |
|
s.logger = baseLogger; |
|
s.resourceStore = this.store; |
|
s.languageUtils = lu; |
|
s.pluralResolver = new PluralResolver(lu, { |
|
prepend: this.options.pluralSeparator |
|
}); |
|
if (formatter) { |
|
s.formatter = createClassOnDemand(formatter); |
|
if (s.formatter.init) s.formatter.init(s, this.options); |
|
this.options.interpolation.format = s.formatter.format.bind(s.formatter); |
|
} |
|
s.interpolator = new Interpolator(this.options); |
|
s.utils = { |
|
hasLoadedNamespace: this.hasLoadedNamespace.bind(this) |
|
}; |
|
s.backendConnector = new Connector(createClassOnDemand(this.modules.backend), s.resourceStore, s, this.options); |
|
s.backendConnector.on('*', (event, ...args) => { |
|
this.emit(event, ...args); |
|
}); |
|
if (this.modules.languageDetector) { |
|
s.languageDetector = createClassOnDemand(this.modules.languageDetector); |
|
if (s.languageDetector.init) s.languageDetector.init(s, this.options.detection, this.options); |
|
} |
|
if (this.modules.i18nFormat) { |
|
s.i18nFormat = createClassOnDemand(this.modules.i18nFormat); |
|
if (s.i18nFormat.init) s.i18nFormat.init(this); |
|
} |
|
this.translator = new Translator(this.services, this.options); |
|
this.translator.on('*', (event, ...args) => { |
|
this.emit(event, ...args); |
|
}); |
|
this.modules.external.forEach(m => { |
|
if (m.init) m.init(this); |
|
}); |
|
} |
|
this.format = this.options.interpolation.format; |
|
if (!callback) callback = noop; |
|
if (this.options.fallbackLng && !this.services.languageDetector && !this.options.lng) { |
|
const codes = this.services.languageUtils.getFallbackCodes(this.options.fallbackLng); |
|
if (codes.length > 0 && codes[0] !== 'dev') this.options.lng = codes[0]; |
|
} |
|
if (!this.services.languageDetector && !this.options.lng) { |
|
this.logger.warn('init: no languageDetector is used and no lng is defined'); |
|
} |
|
const storeApi = ['getResource', 'hasResourceBundle', 'getResourceBundle', 'getDataByLanguage']; |
|
storeApi.forEach(fcName => { |
|
this[fcName] = (...args) => this.store[fcName](...args); |
|
}); |
|
const storeApiChained = ['addResource', 'addResources', 'addResourceBundle', 'removeResourceBundle']; |
|
storeApiChained.forEach(fcName => { |
|
this[fcName] = (...args) => { |
|
this.store[fcName](...args); |
|
return this; |
|
}; |
|
}); |
|
const deferred = defer(); |
|
const load = () => { |
|
const finish = (err, t) => { |
|
this.isInitializing = false; |
|
if (this.isInitialized && !this.initializedStoreOnce) this.logger.warn('init: i18next is already initialized. You should call init just once!'); |
|
this.isInitialized = true; |
|
if (!this.options.isClone) this.logger.log('initialized', this.options); |
|
this.emit('initialized', this.options); |
|
deferred.resolve(t); |
|
callback(err, t); |
|
}; |
|
if ((this.languages || this.isLanguageChangingTo) && !this.isInitialized) return finish(null, this.t.bind(this)); |
|
this.changeLanguage(this.options.lng, finish); |
|
}; |
|
if (this.options.resources || !this.options.initAsync) { |
|
load(); |
|
} else { |
|
setTimeout(load, 0); |
|
} |
|
return deferred; |
|
} |
|
loadResources(language, callback = noop) { |
|
let usedCallback = callback; |
|
const usedLng = isString(language) ? language : this.language; |
|
if (typeof language === 'function') usedCallback = language; |
|
if (!this.options.resources || this.options.partialBundledLanguages) { |
|
if (usedLng?.toLowerCase() === 'cimode' && (!this.options.preload || this.options.preload.length === 0)) return usedCallback(); |
|
const toLoad = []; |
|
const append = lng => { |
|
if (!lng) return; |
|
if (lng === 'cimode') return; |
|
const lngs = this.services.languageUtils.toResolveHierarchy(lng); |
|
lngs.forEach(l => { |
|
if (l === 'cimode') return; |
|
if (!toLoad.includes(l)) toLoad.push(l); |
|
}); |
|
}; |
|
if (!usedLng) { |
|
const fallbacks = this.services.languageUtils.getFallbackCodes(this.options.fallbackLng); |
|
fallbacks.forEach(l => append(l)); |
|
} else { |
|
append(usedLng); |
|
} |
|
this.options.preload?.forEach?.(l => append(l)); |
|
this.services.backendConnector.load(toLoad, this.options.ns, e => { |
|
if (!e && !this.resolvedLanguage && this.language) this.setResolvedLanguage(this.language); |
|
usedCallback(e); |
|
}); |
|
} else { |
|
usedCallback(null); |
|
} |
|
} |
|
reloadResources(lngs, ns, callback) { |
|
const deferred = defer(); |
|
if (typeof lngs === 'function') { |
|
callback = lngs; |
|
lngs = undefined; |
|
} |
|
if (typeof ns === 'function') { |
|
callback = ns; |
|
ns = undefined; |
|
} |
|
if (!lngs) lngs = this.languages; |
|
if (!ns) ns = this.options.ns; |
|
if (!callback) callback = noop; |
|
this.services.backendConnector.reload(lngs, ns, err => { |
|
deferred.resolve(); |
|
callback(err); |
|
}); |
|
return deferred; |
|
} |
|
use(module) { |
|
if (!module) throw new Error('You are passing an undefined module! Please check the object you are passing to i18next.use()'); |
|
if (!module.type) throw new Error('You are passing a wrong module! Please check the object you are passing to i18next.use()'); |
|
if (module.type === 'backend') { |
|
this.modules.backend = module; |
|
} |
|
if (module.type === 'logger' || module.log && module.warn && module.error) { |
|
this.modules.logger = module; |
|
} |
|
if (module.type === 'languageDetector') { |
|
this.modules.languageDetector = module; |
|
} |
|
if (module.type === 'i18nFormat') { |
|
this.modules.i18nFormat = module; |
|
} |
|
if (module.type === 'postProcessor') { |
|
postProcessor.addPostProcessor(module); |
|
} |
|
if (module.type === 'formatter') { |
|
this.modules.formatter = module; |
|
} |
|
if (module.type === '3rdParty') { |
|
this.modules.external.push(module); |
|
} |
|
return this; |
|
} |
|
setResolvedLanguage(l) { |
|
if (!l || !this.languages) return; |
|
if (['cimode', 'dev'].includes(l)) return; |
|
for (let li = 0; li < this.languages.length; li++) { |
|
const lngInLngs = this.languages[li]; |
|
if (['cimode', 'dev'].includes(lngInLngs)) continue; |
|
if (this.store.hasLanguageSomeTranslations(lngInLngs)) { |
|
this.resolvedLanguage = lngInLngs; |
|
break; |
|
} |
|
} |
|
if (!this.resolvedLanguage && !this.languages.includes(l) && this.store.hasLanguageSomeTranslations(l)) { |
|
this.resolvedLanguage = l; |
|
this.languages.unshift(l); |
|
} |
|
} |
|
changeLanguage(lng, callback) { |
|
this.isLanguageChangingTo = lng; |
|
const deferred = defer(); |
|
this.emit('languageChanging', lng); |
|
const setLngProps = l => { |
|
this.language = l; |
|
this.languages = this.services.languageUtils.toResolveHierarchy(l); |
|
this.resolvedLanguage = undefined; |
|
this.setResolvedLanguage(l); |
|
}; |
|
const done = (err, l) => { |
|
if (l) { |
|
if (this.isLanguageChangingTo === lng) { |
|
setLngProps(l); |
|
this.translator.changeLanguage(l); |
|
this.isLanguageChangingTo = undefined; |
|
this.emit('languageChanged', l); |
|
this.logger.log('languageChanged', l); |
|
} |
|
} else { |
|
this.isLanguageChangingTo = undefined; |
|
} |
|
deferred.resolve((...args) => this.t(...args)); |
|
if (callback) callback(err, (...args) => this.t(...args)); |
|
}; |
|
const setLng = lngs => { |
|
if (!lng && !lngs && this.services.languageDetector) lngs = []; |
|
const fl = isString(lngs) ? lngs : lngs && lngs[0]; |
|
const l = this.store.hasLanguageSomeTranslations(fl) ? fl : this.services.languageUtils.getBestMatchFromCodes(isString(lngs) ? [lngs] : lngs); |
|
if (l) { |
|
if (!this.language) { |
|
setLngProps(l); |
|
} |
|
if (!this.translator.language) this.translator.changeLanguage(l); |
|
this.services.languageDetector?.cacheUserLanguage?.(l); |
|
} |
|
this.loadResources(l, err => { |
|
done(err, l); |
|
}); |
|
}; |
|
if (!lng && this.services.languageDetector && !this.services.languageDetector.async) { |
|
setLng(this.services.languageDetector.detect()); |
|
} else if (!lng && this.services.languageDetector && this.services.languageDetector.async) { |
|
if (this.services.languageDetector.detect.length === 0) { |
|
this.services.languageDetector.detect().then(setLng); |
|
} else { |
|
this.services.languageDetector.detect(setLng); |
|
} |
|
} else { |
|
setLng(lng); |
|
} |
|
return deferred; |
|
} |
|
getFixedT(lng, ns, keyPrefix, fixedOpts) { |
|
const scopeNs = fixedOpts?.scopeNs; |
|
const fixedT = (key, opts, ...rest) => { |
|
let o; |
|
if (typeof opts !== 'object') { |
|
o = this.options.overloadTranslationOptionHandler([key, opts].concat(rest)); |
|
} else { |
|
o = { |
|
...opts |
|
}; |
|
} |
|
o.lng = o.lng || fixedT.lng; |
|
o.lngs = o.lngs || fixedT.lngs; |
|
const explicitCallNs = o.ns !== undefined && o.ns !== null; |
|
o.ns = o.ns || fixedT.ns; |
|
if (o.keyPrefix !== '') o.keyPrefix = o.keyPrefix || keyPrefix || fixedT.keyPrefix; |
|
const selectorOpts = { |
|
...this.options, |
|
...o |
|
}; |
|
if (Array.isArray(scopeNs) && !explicitCallNs) selectorOpts.ns = scopeNs; |
|
if (typeof o.keyPrefix === 'function') o.keyPrefix = keysFromSelector(o.keyPrefix, selectorOpts); |
|
const keySeparator = this.options.keySeparator || '.'; |
|
let resultKey; |
|
if (o.keyPrefix && Array.isArray(key)) { |
|
resultKey = key.map(k => { |
|
if (typeof k === 'function') k = keysFromSelector(k, selectorOpts); |
|
return `${o.keyPrefix}${keySeparator}${k}`; |
|
}); |
|
} else { |
|
if (typeof key === 'function') key = keysFromSelector(key, selectorOpts); |
|
resultKey = o.keyPrefix ? `${o.keyPrefix}${keySeparator}${key}` : key; |
|
} |
|
return this.t(resultKey, o); |
|
}; |
|
if (isString(lng)) { |
|
fixedT.lng = lng; |
|
} else { |
|
fixedT.lngs = lng; |
|
} |
|
fixedT.ns = ns; |
|
fixedT.keyPrefix = keyPrefix; |
|
return fixedT; |
|
} |
|
t(...args) { |
|
return this.translator?.translate(...args); |
|
} |
|
exists(...args) { |
|
return this.translator?.exists(...args); |
|
} |
|
setDefaultNamespace(ns) { |
|
this.options.defaultNS = ns; |
|
} |
|
hasLoadedNamespace(ns, options = {}) { |
|
if (!this.isInitialized) { |
|
this.logger.warn('hasLoadedNamespace: i18next was not initialized', this.languages); |
|
return false; |
|
} |
|
if (!this.languages || !this.languages.length) { |
|
this.logger.warn('hasLoadedNamespace: i18n.languages were undefined or empty', this.languages); |
|
return false; |
|
} |
|
const lng = options.lng || this.resolvedLanguage || this.languages[0]; |
|
const fallbackLng = this.options ? this.options.fallbackLng : false; |
|
const lastLng = this.languages[this.languages.length - 1]; |
|
if (lng.toLowerCase() === 'cimode') return true; |
|
const loadNotPending = (l, n) => { |
|
const loadState = this.services.backendConnector.state[`${l}|${n}`]; |
|
return loadState === -1 || loadState === 0 || loadState === 2; |
|
}; |
|
if (options.precheck) { |
|
const preResult = options.precheck(this, loadNotPending); |
|
if (preResult !== undefined) return preResult; |
|
} |
|
if (this.hasResourceBundle(lng, ns)) return true; |
|
if (!this.services.backendConnector.backend || this.options.resources && !this.options.partialBundledLanguages) return true; |
|
if (loadNotPending(lng, ns) && (!fallbackLng || loadNotPending(lastLng, ns))) return true; |
|
return false; |
|
} |
|
loadNamespaces(ns, callback) { |
|
const deferred = defer(); |
|
if (!this.options.ns) { |
|
if (callback) callback(); |
|
return Promise.resolve(); |
|
} |
|
if (isString(ns)) ns = [ns]; |
|
ns.forEach(n => { |
|
if (!this.options.ns.includes(n)) this.options.ns.push(n); |
|
}); |
|
this.loadResources(err => { |
|
deferred.resolve(); |
|
if (callback) callback(err); |
|
}); |
|
return deferred; |
|
} |
|
loadLanguages(lngs, callback) { |
|
const deferred = defer(); |
|
if (isString(lngs)) lngs = [lngs]; |
|
const preloaded = this.options.preload || []; |
|
const newLngs = lngs.filter(lng => !preloaded.includes(lng) && this.services.languageUtils.isSupportedCode(lng)); |
|
if (!newLngs.length) { |
|
if (callback) callback(); |
|
return Promise.resolve(); |
|
} |
|
this.options.preload = preloaded.concat(newLngs); |
|
this.loadResources(err => { |
|
deferred.resolve(); |
|
if (callback) callback(err); |
|
}); |
|
return deferred; |
|
} |
|
dir(lng) { |
|
if (!lng) lng = this.resolvedLanguage || (this.languages?.length > 0 ? this.languages[0] : this.language); |
|
if (!lng) return 'rtl'; |
|
try { |
|
const l = new Intl.Locale(lng); |
|
if (l && l.getTextInfo) { |
|
const ti = l.getTextInfo(); |
|
if (ti && ti.direction) return ti.direction; |
|
} |
|
} catch (e) {} |
|
const rtlLngs = ['ar', 'shu', 'sqr', 'ssh', 'xaa', 'yhd', 'yud', 'aao', 'abh', 'abv', 'acm', 'acq', 'acw', 'acx', 'acy', 'adf', 'ads', 'aeb', 'aec', 'afb', 'ajp', 'apc', 'apd', 'arb', 'arq', 'ars', 'ary', 'arz', 'auz', 'avl', 'ayh', 'ayl', 'ayn', 'ayp', 'bbz', 'pga', 'he', 'iw', 'ps', 'pbt', 'pbu', 'pst', 'prp', 'prd', 'ug', 'ur', 'ydd', 'yds', 'yih', 'ji', 'yi', 'hbo', 'men', 'xmn', 'fa', 'jpr', 'peo', 'pes', 'prs', 'dv', 'sam', 'ckb']; |
|
const languageUtils = this.services?.languageUtils || new LanguageUtil(get()); |
|
if (lng.toLowerCase().indexOf('-latn') > 1) return 'ltr'; |
|
return rtlLngs.includes(languageUtils.getLanguagePartFromCode(lng)) || lng.toLowerCase().indexOf('-arab') > 1 ? 'rtl' : 'ltr'; |
|
} |
|
static createInstance(options = {}, callback) { |
|
const instance = new I18n(options, callback); |
|
instance.createInstance = I18n.createInstance; |
|
return instance; |
|
} |
|
cloneInstance(options = {}, callback = noop) { |
|
const forkResourceStore = options.forkResourceStore; |
|
if (forkResourceStore) delete options.forkResourceStore; |
|
const mergedOptions = { |
|
...this.options, |
|
...options, |
|
...{ |
|
isClone: true |
|
} |
|
}; |
|
const clone = new I18n(mergedOptions); |
|
if (options.debug !== undefined || options.prefix !== undefined) { |
|
clone.logger = clone.logger.clone(options); |
|
} |
|
const membersToCopy = ['store', 'services', 'language']; |
|
membersToCopy.forEach(m => { |
|
clone[m] = this[m]; |
|
}); |
|
clone.services = { |
|
...this.services |
|
}; |
|
clone.services.utils = { |
|
hasLoadedNamespace: clone.hasLoadedNamespace.bind(clone) |
|
}; |
|
if (forkResourceStore) { |
|
const clonedData = Object.keys(this.store.data).reduce((prev, l) => { |
|
prev[l] = { |
|
...this.store.data[l] |
|
}; |
|
prev[l] = Object.keys(prev[l]).reduce((acc, n) => { |
|
acc[n] = { |
|
...prev[l][n] |
|
}; |
|
return acc; |
|
}, prev[l]); |
|
return prev; |
|
}, {}); |
|
clone.store = new ResourceStore(clonedData, mergedOptions); |
|
clone.services.resourceStore = clone.store; |
|
} |
|
if (options.interpolation) { |
|
const defOpts = get(); |
|
const mergedInterpolation = { |
|
...defOpts.interpolation, |
|
...this.options.interpolation, |
|
...options.interpolation |
|
}; |
|
const mergedForInterpolator = { |
|
...mergedOptions, |
|
interpolation: mergedInterpolation |
|
}; |
|
clone.services.interpolator = new Interpolator(mergedForInterpolator); |
|
} |
|
clone.translator = new Translator(clone.services, mergedOptions); |
|
clone.translator.on('*', (event, ...args) => { |
|
clone.emit(event, ...args); |
|
}); |
|
clone.init(mergedOptions, callback); |
|
clone.translator.options = mergedOptions; |
|
clone.translator.backendConnector.services.utils = { |
|
hasLoadedNamespace: clone.hasLoadedNamespace.bind(clone) |
|
}; |
|
return clone; |
|
} |
|
toJSON() { |
|
return { |
|
options: this.options, |
|
store: this.store, |
|
language: this.language, |
|
languages: this.languages, |
|
resolvedLanguage: this.resolvedLanguage |
|
}; |
|
} |
|
} |
|
const instance = I18n.createInstance(); |
|
|
|
instance.keyFromSelector = keysFromSelector; |
|
|
|
return instance; |
|
|
|
}));
|
|
|