Skip to main content
Module

x/i18next/Translator.js

i18next: learn once - translate everywhere
Go to Latest
File
import baseLogger from './logger.js';import EventEmitter from './EventEmitter.js';import postProcessor from './postProcessor.js';import * as utils from './utils.js';
const checkedLoadedFor = {};
class Translator extends EventEmitter { constructor(services, options = {}) { super(); if (utils.isIE10) { EventEmitter.call(this); // <=IE10 fix (unable to call parent constructor) }
utils.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 && !utils.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 (!options) options = {};
// non valid keys handling if (keys === undefined || keys === null /* || keys === ''*/) return ''; if (!Array.isArray(keys)) keys = [String(keys)];
// 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; return namespace + nsSeparator + key; }
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' && resType === '[object Array]') ) { if (!options.returnObjects && !this.options.returnObjects) { if (!this.options.returnedObjectHandler) { this.logger.warn('accessing an object - but returnObjects options is not enabled!'); } return this.options.returnedObjectHandler ? this.options.returnedObjectHandler(resUsedKey, res, { ...options, ns: namespaces }) : `key '${key} (${this.language})' returned an object instead of string.`; }
// 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 = resType === '[object Array]'; const copy = resTypeIsArray ? [] : {}; // apply child translation on a copy
/* eslint no-restricted-syntax: 0 */ let 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 orginal value as fallback } } res = copy; } } else if ( handleAsObjectInI18nFormat && typeof joinArrays === 'string' && resType === '[object Array]' ) { // 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 defaultValue = options[`defaultValue${defaultValueSuffix}`] || 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) => { this.pluralResolver.getSuffixes(language).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(key, usedDefault ? res : undefined); } else { res = this.options.parseMissingKeyHandler(res); } } }
// return return res; }
extendTranslation(res, key, options, resolved, lastKey) { if (this.i18nFormat && this.i18nFormat.parse) { res = this.i18nFormat.parse( res, options, 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, 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.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, ...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 = '_zero';
// get key for plural if needed if (needsPluralHandling) { finalKeys.push(key + pluralSuffix); 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 (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); }
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; }}
export default Translator;