Module

x/i18next/BackendConnector.js

i18next: learn once - translate everywhere
Go to Latest
File
import * as utils from './utils.js';import baseLogger from './logger.js';import EventEmitter from './EventEmitter.js';
function removePending(q, name) { if (q.pending[name] !== undefined) { delete q.pending[name]; q.pendingCount--; }}
class Connector extends EventEmitter { constructor(backend, store, services, options = {}) { super(); if (utils.isIE10) { EventEmitter.call(this); // <=IE10 fix (unable to call parent constructor) }
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.state = {}; this.queue = [];
if (this.backend && this.backend.init) { this.backend.init(services, options.backend, options); } }
queueLoad(languages, namespaces, options, callback) { // find what needs to be loaded 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; // loaded } else if (this.state[name] < 0) { // nothing to do for err } else if (this.state[name] === 1) { if (pending[name] === undefined) pending[name] = true; } else { this.state[name] = 1; // pending
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 (data) { this.store.addResourceBundle(lng, ns, data); }
// set loaded this.state[name] = err ? -1 : 2;
// consolidated loading done in this run - only emit once for a loaded namespace const loaded = {};
// callback if ready this.queue.forEach((q) => { utils.pushPath(q.loaded, [lng], ns); removePending(q, name);
if (err) q.errors.push(err);
if (q.pendingCount === 0 && !q.done) { // only do once per loaded -> this.emit('loaded', q.loaded); Object.keys(q.loaded).forEach((l) => { if (!loaded[l]) loaded[l] = {}; const loadedKeys = q.loaded[l]; if (loadedKeys.length) { loadedKeys.forEach((ns) => { if (loaded[l][ns] === undefined) loaded[l][ns] = true; }); } });
/* eslint no-param-reassign: 0 */ q.done = true; if (q.errors.length) { q.callback(q.errors); } else { q.callback(); } } });
// emit consolidated loaded event this.emit('loaded', loaded);
// remove done load requests this.queue = this.queue.filter((q) => !q.done); }
read(lng, ns, fcName, tried = 0, wait = 350, callback) { if (!lng.length) return callback(null, {}); // noting to load
// Limit parallelism of calls to backend // This is needed to prevent trying to open thousands of // sockets or file descriptors, which can cause failures // and actually make the entire process take longer. if (this.readingCalls >= this.maxParallelReads) { this.waitingReads.push({ lng, ns, fcName, tried, wait, callback }); return; } this.readingCalls++;
return this.backend[fcName](lng, ns, (err, data) => { if (err && data /* = retryFlag */ && tried < 5) { setTimeout(() => { this.read.call(this, lng, ns, fcName, tried + 1, wait * 2, callback); }, wait); return; } 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); } callback(err, data); }); }
/* eslint consistent-return: 0 */ 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 (typeof languages === 'string') languages = this.languageUtils.toResolveHierarchy(languages); if (typeof namespaces === 'string') namespaces = [namespaces];
const toLoad = this.queueLoad(languages, namespaces, options, callback); if (!toLoad.toLoad.length) { if (!toLoad.pending.length) callback(); // nothing to load and no pendings...callback now return null; // pendings will trigger callback }
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 = {}) { if ( this.services.utils && 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; }
// ignore non valid keys if (key === undefined || key === null || key === '') return;
if (this.backend && this.backend.create) { this.backend.create(languages, namespace, key, fallbackValue, null /* unused callback */, { ...options, isUpdate, }); }
// write to store to avoid resending if (!languages || !languages[0]) return; this.store.addResource(languages[0], namespace, key, fallbackValue); }}
export default Connector;