Skip to main content
Module

x/i18next/i18next.js

i18next: learn once - translate everywhere
Go to Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
import baseLogger from './logger.js';import EventEmitter from './EventEmitter.js';import ResourceStore from './ResourceStore.js';import Translator from './Translator.js';import LanguageUtils from './LanguageUtils.js';import PluralResolver from './PluralResolver.js';import Interpolator from './Interpolator.js';import Formatter from './Formatter.js';import BackendConnector from './BackendConnector.js';import { get as getDefaults, transformOptions } from './defaults.js';import postProcessor from './postProcessor.js';import { defer, isIE10 } from './utils.js';
function noop() { }
// Binds the member functions of the given class instance so that they can be// destructured or used as callbacks.function 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(); if (isIE10) { EventEmitter.call(this) // <=IE10 fix (unable to call parent constructor) }
this.options = transformOptions(options); this.services = {}; this.logger = baseLogger; this.modules = { external: [] };
bindMemberFunctions(this);
if (callback && !this.isInitialized && !options.isClone) { // https://github.com/i18next/i18next/issues/879 if (!this.options.initImmediate) { this.init(options, callback); return this; } setTimeout(() => { this.init(options, callback); }, 0); } }
init(options = {}, callback) { if (typeof options === 'function') { callback = options; options = {}; }
if (!options.defaultNS && options.ns) { if (typeof options.ns === 'string') { options.defaultNS = options.ns; } else if (options.ns.indexOf('translation') < 0) { options.defaultNS = options.ns[0]; } }
const defOpts = getDefaults(); this.options = { ...defOpts, ...this.options, ...transformOptions(options) }; if (this.options.compatibilityAPI !== 'v1') { this.options.interpolation = { ...defOpts.interpolation, ...this.options.interpolation }; // do not use reference } if (options.keySeparator !== undefined) { this.options.userDefinedKeySeparator = options.keySeparator; } if (options.nsSeparator !== undefined) { this.options.userDefinedNsSeparator = options.nsSeparator; }
function createClassOnDemand(ClassOrObject) { if (!ClassOrObject) return null; if (typeof ClassOrObject === 'function') return new ClassOrObject(); return ClassOrObject; }
// init services 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 if (typeof Intl !== 'undefined') { formatter = Formatter; }
const lu = new LanguageUtils(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, compatibilityJSON: this.options.compatibilityJSON, simplifyPluralSuffix: this.options.simplifyPluralSuffix, });
if (formatter && (!this.options.interpolation.format || this.options.interpolation.format === defOpts.interpolation.format)) { s.formatter = createClassOnDemand(formatter); 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 BackendConnector( createClassOnDemand(this.modules.backend), s.resourceStore, s, this.options, ); // pipe events from backendConnector s.backendConnector.on('*', (event, ...args) => { this.emit(event, ...args); });
if (this.modules.languageDetector) { s.languageDetector = createClassOnDemand(this.modules.languageDetector); 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); // pipe events from translator 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'); }
// append api 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) => { 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); // not rejecting on err (as err is only a loading translation failed warning) callback(err, t); }; // fix for use cases when calling changeLanguage before finished to initialized (i.e. https://github.com/i18next/i18next/issues/1552) if (this.languages && this.options.compatibilityAPI !== 'v1' && !this.isInitialized) return finish(null, this.t.bind(this)); this.changeLanguage(this.options.lng, finish); };
if (this.options.resources || !this.options.initImmediate) { load(); } else { setTimeout(load, 0); }
return deferred; }
/* eslint consistent-return: 0 */ loadResources(language, callback = noop) { let usedCallback = callback; let usedLng = typeof language === 'string' ? language : this.language; if (typeof language === 'function') usedCallback = language;
if (!this.options.resources || this.options.partialBundledLanguages) { if (usedLng && usedLng.toLowerCase() === 'cimode') return usedCallback(); // avoid loading resources for cimode
const toLoad = [];
const append = lng => { if (!lng) return; const lngs = this.services.languageUtils.toResolveHierarchy(lng); lngs.forEach(l => { if (toLoad.indexOf(l) < 0) toLoad.push(l); }); };
if (!usedLng) { // at least load fallbacks in this case const fallbacks = this.services.languageUtils.getFallbackCodes(this.options.fallbackLng); fallbacks.forEach(l => append(l)); } else { append(usedLng); }
if (this.options.preload) { 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 (!lngs) lngs = this.languages; if (!ns) ns = this.options.ns; if (!callback) callback = noop; this.services.backendConnector.reload(lngs, ns, err => { deferred.resolve(); // not rejecting on err (as err is only a loading translation failed warning) 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'].indexOf(l) > -1) return; for (let li = 0; li < this.languages.length; li++) { const lngInLngs = this.languages[li]; if (['cimode', 'dev'].indexOf(lngInLngs) > -1) continue; if (this.store.hasLanguageSomeTranslations(lngInLngs)) { this.resolvedLanguage = lngInLngs; break; } } }
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); // find the first language resolved languaged this.resolvedLanguage = undefined; this.setResolvedLanguage(l); };
const done = (err, l) => { if (l) { 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 detected lng is falsy, set it to empty array, to make sure at least the fallbackLng will be used if (!lng && !lngs && this.services.languageDetector) lngs = []; // depending on API in detector lng can be a string (old) or an array of languages ordered in priority const l = typeof lngs === 'string' ? lngs : this.services.languageUtils.getBestMatchFromCodes(lngs);
if (l) { if (!this.language) { setLngProps(l); } if (!this.translator.language) this.translator.changeLanguage(l);
if (this.services.languageDetector) 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) { this.services.languageDetector.detect(setLng); } else { setLng(lng); }
return deferred; }
getFixedT(lng, ns, keyPrefix) { const fixedT = (key, opts, ...rest) => { let options; if (typeof opts !== 'object') { options = this.options.overloadTranslationOptionHandler([key, opts].concat(rest)); } else { options = { ...opts }; }
options.lng = options.lng || fixedT.lng; options.lngs = options.lngs || fixedT.lngs; options.ns = options.ns || fixedT.ns;
const keySeparator = this.options.keySeparator || '.'; const resultKey = keyPrefix ? `${keyPrefix}${keySeparator}${key}` : key; return this.t(resultKey, options); }; if (typeof lng === 'string') { fixedT.lng = lng; } else { fixedT.lngs = lng; } fixedT.ns = ns; fixedT.keyPrefix = keyPrefix; return fixedT; }
t(...args) { return this.translator && this.translator.translate(...args); }
exists(...args) { return this.translator && 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 = this.resolvedLanguage || this.languages[0]; const fallbackLng = this.options ? this.options.fallbackLng : false; const lastLng = this.languages[this.languages.length - 1];
// we're in cimode so this shall pass if (lng.toLowerCase() === 'cimode') return true;
const loadNotPending = (l, n) => { const loadState = this.services.backendConnector.state[`${l}|${n}`]; return loadState === -1 || loadState === 2; };
// optional injected check if (options.precheck) { const preResult = options.precheck(this, loadNotPending); if (preResult !== undefined) return preResult; }
// loaded -> SUCCESS if (this.hasResourceBundle(lng, ns)) return true;
// were not loading at all -> SEMI SUCCESS if (!this.services.backendConnector.backend || (this.options.resources && !this.options.partialBundledLanguages)) return true;
// failed loading ns - but at least fallback is not pending -> SEMI SUCCESS if (loadNotPending(lng, ns) && (!fallbackLng || loadNotPending(lastLng, ns))) return true;
return false; }
loadNamespaces(ns, callback) { const deferred = defer();
if (!this.options.ns) { callback && callback(); return Promise.resolve(); } if (typeof ns === 'string') ns = [ns];
ns.forEach(n => { if (this.options.ns.indexOf(n) < 0) this.options.ns.push(n); });
this.loadResources(err => { deferred.resolve(); if (callback) callback(err); });
return deferred; }
loadLanguages(lngs, callback) { const deferred = defer();
if (typeof lngs === 'string') lngs = [lngs]; const preloaded = this.options.preload || [];
const newLngs = lngs.filter(lng => preloaded.indexOf(lng) < 0); // Exit early if all given languages are already preloaded 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 && this.languages.length > 0 ? this.languages[0] : this.language); if (!lng) return 'rtl';
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' ];
return rtlLngs.indexOf(this.services.languageUtils.getLanguagePartFromCode(lng)) > -1 || lng.toLowerCase().indexOf('-arab') > 1 ? 'rtl' : 'ltr'; }
static createInstance = (options = {}, callback) => new I18n(options, callback)
cloneInstance(options = {}, callback = noop) { const mergedOptions = { ...this.options, ...options, ...{ isClone: true } }; const clone = new I18n(mergedOptions); const membersToCopy = ['store', 'services', 'language']; membersToCopy.forEach(m => { clone[m] = this[m]; }); clone.services = { ...this.services }; clone.services.utils = { hasLoadedNamespace: clone.hasLoadedNamespace.bind(clone) }; clone.translator = new Translator(clone.services, clone.options); clone.translator.on('*', (event, ...args) => { clone.emit(event, ...args); }); clone.init(mergedOptions, callback); clone.translator.options = clone.options; // sync options 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.createInstance = I18n.createInstance;
export default instance;