i18next-locize-backend
Version:
i18next-locize-backend is a backend layer for i18next to use locize service which can be used in node.js, in the browser and for deno.
658 lines (657 loc) • 27.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("./utils.js");
var _request = _interopRequireDefault(require("./request.js"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); }
function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } }
function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
var getDefaults = function getDefaults() {
return {
loadPath: 'https://api.locize.app/{{projectId}}/{{version}}/{{lng}}/{{ns}}',
privatePath: 'https://api.locize.app/private/{{projectId}}/{{version}}/{{lng}}/{{ns}}',
getLanguagesPath: 'https://api.locize.app/languages/{{projectId}}',
addPath: 'https://api.locize.app/missing/{{projectId}}/{{version}}/{{lng}}/{{ns}}',
updatePath: 'https://api.locize.app/update/{{projectId}}/{{version}}/{{lng}}/{{ns}}',
referenceLng: 'en',
crossDomain: true,
setContentTypeJSON: false,
version: 'latest',
private: false,
translatedPercentageThreshold: 0.9,
failLoadingOnEmptyJSON: false,
allowedAddOrUpdateHosts: ['localhost'],
onSaved: false,
reloadInterval: typeof window !== 'undefined' ? false : 60 * 60 * 1000,
checkForProjectTimeout: 3 * 1000,
storageExpiration: 60 * 60 * 1000,
writeDebounce: 5 * 1000
};
};
var hasLocalStorageSupport;
try {
hasLocalStorageSupport = typeof window !== 'undefined' && window.localStorage !== null;
var testKey = 'notExistingLocizeProject';
window.localStorage.setItem(testKey, 'foo');
window.localStorage.removeItem(testKey);
} catch (e) {
hasLocalStorageSupport = false;
}
function getStorage(storageExpiration) {
var setProjectNotExisting = function setProjectNotExisting() {};
var isProjectNotExisting = function isProjectNotExisting() {};
if (hasLocalStorageSupport) {
setProjectNotExisting = function setProjectNotExisting(projectId) {
window.localStorage.setItem("notExistingLocizeProject_".concat(projectId), Date.now());
};
isProjectNotExisting = function isProjectNotExisting(projectId) {
var ret = window.localStorage.getItem("notExistingLocizeProject_".concat(projectId));
if (!ret) return false;
if (Date.now() - ret > storageExpiration) {
window.localStorage.removeItem("notExistingLocizeProject_".concat(projectId));
return false;
}
return true;
};
} else if (typeof document !== 'undefined') {
setProjectNotExisting = function setProjectNotExisting(projectId) {
var date = new Date();
date.setTime(date.getTime() + storageExpiration);
var expires = "; expires=".concat(date.toGMTString());
var name = "notExistingLocizeProject_".concat(projectId);
try {
document.cookie = "".concat(name, "=").concat(Date.now()).concat(expires, ";path=/");
} catch (err) {}
};
isProjectNotExisting = function isProjectNotExisting(projectId) {
var name = "notExistingLocizeProject_".concat(projectId);
var nameEQ = "".concat(name, "=");
try {
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return true;
}
} catch (err) {}
return false;
};
}
return {
setProjectNotExisting: setProjectNotExisting,
isProjectNotExisting: isProjectNotExisting
};
}
var getCustomRequestInfo = function getCustomRequestInfo(url, options, payload) {
var headers = {};
if (options.authorize && options.apiKey) {
headers.Authorization = options.apiKey;
}
if (payload || options.setContentTypeJSON) {
headers['Content-Type'] = 'application/json';
}
return {
method: payload ? 'POST' : 'GET',
url: url,
headers: headers,
body: payload
};
};
var handleCustomRequest = function handleCustomRequest(opt, info, cb) {
if (opt.request.length === 1) {
try {
var r = opt.request(info);
if (r && typeof r.then === 'function') {
r.then(function (data) {
return cb(null, data);
}).catch(cb);
} else {
cb(null, r);
}
} catch (err) {
cb(err);
}
return;
}
opt.request(info, cb);
};
var I18NextLocizeBackend = function () {
function I18NextLocizeBackend(services) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var allOptions = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
var callback = arguments.length > 3 ? arguments[3] : undefined;
_classCallCheck(this, I18NextLocizeBackend);
this.services = services;
this.options = options;
this.allOptions = allOptions;
this.type = 'backend';
if (services && services.projectId) {
this.init(null, services, allOptions, options);
} else {
this.init(services, options, allOptions, callback);
}
}
return _createClass(I18NextLocizeBackend, [{
key: "init",
value: function init(services) {
var _this = this;
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var allOptions = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
var callback = arguments.length > 3 ? arguments[3] : undefined;
if (!options.referenceLng && allOptions.fallbackLng && Array.isArray(allOptions.fallbackLng) && allOptions.fallbackLng[0] !== 'dev') {
options.referenceLng = allOptions.fallbackLng[0];
}
this.services = services;
var defOpt = getDefaults();
var passedOpt = (0, _utils.defaults)(options, this.options || {});
if (passedOpt.reloadInterval && passedOpt.reloadInterval < 5 * 60 * 1000) {
console.warn('Your configured reloadInterval option is to low.');
passedOpt.reloadInterval = defOpt.reloadInterval;
}
this.options = (0, _utils.defaults)(options, this.options || {}, defOpt);
this.allOptions = allOptions;
this.somethingLoaded = false;
this.isProjectNotExisting = false;
this.storage = getStorage(this.options.storageExpiration);
if (this.options.pull) {
console.warn('The pull API was removed use "private: true" option instead: https://www.locize.com/docs/api#fetch-private-namespace-resources');
}
var hostname = typeof window !== 'undefined' && window.location && window.location.hostname;
if (hostname) {
this.isAddOrUpdateAllowed = typeof this.options.allowedAddOrUpdateHosts === 'function' ? this.options.allowedAddOrUpdateHosts(hostname) : this.options.allowedAddOrUpdateHosts.indexOf(hostname) > -1;
if (services && services.logger && (allOptions.saveMissing || allOptions.updateMissing)) {
if (!this.isAddOrUpdateAllowed) {
services.logger.warn(typeof this.options.allowedAddOrUpdateHosts === 'function' ? "locize-backend: will not save or update missings because allowedAddOrUpdateHosts returned false for the host \"".concat(hostname, "\".") : "locize-backend: will not save or update missings because the host \"".concat(hostname, "\" was not in the list of allowedAddOrUpdateHosts: ").concat(this.options.allowedAddOrUpdateHosts.join(', '), " (matches need to be exact)."));
} else if (hostname !== 'localhost') {
services.logger.warn("locize-backend: you are using the save or update missings feature from this host \"".concat(hostname, "\".\nMake sure you will not use it in production!\nhttps://www.locize.com/docs/going-to-production"));
}
}
} else {
this.isAddOrUpdateAllowed = true;
}
if (typeof callback === 'function') {
this.getOptions(function (err, opts, languages) {
if (err) return callback(err);
_this.options.referenceLng = options.referenceLng || opts.referenceLng || _this.options.referenceLng;
callback(null, opts, languages);
});
}
this.queuedWrites = {
pending: {}
};
this.debouncedProcess = (0, _utils.debounce)(this.process, this.options.writeDebounce);
if (this.interval) clearInterval(this.interval);
if (this.options.reloadInterval && this.options.projectId) {
this.interval = setInterval(function () {
return _this.reload();
}, this.options.reloadInterval);
if (_typeof(this.interval) === 'object' && typeof this.interval.unref === 'function') this.interval.unref();
}
}
}, {
key: "reload",
value: function reload() {
var _this2 = this;
var _ref = this.services || {
logger: console
},
backendConnector = _ref.backendConnector,
languageUtils = _ref.languageUtils,
logger = _ref.logger;
if (!backendConnector) return;
var currentLanguage = backendConnector.language;
if (currentLanguage && currentLanguage.toLowerCase() === 'cimode') return;
var toLoad = [];
var append = function append(lng) {
var lngs = languageUtils.toResolveHierarchy(lng);
lngs.forEach(function (l) {
if (toLoad.indexOf(l) < 0) toLoad.push(l);
});
};
append(currentLanguage);
if (this.allOptions.preload) this.allOptions.preload.forEach(function (l) {
return append(l);
});
toLoad.forEach(function (lng) {
_this2.allOptions.ns.forEach(function (ns) {
backendConnector.read(lng, ns, 'read', null, null, function (err, data) {
if (err) logger.warn("loading namespace ".concat(ns, " for language ").concat(lng, " failed"), err);
if (!err && data) logger.log("loaded namespace ".concat(ns, " for language ").concat(lng), data);
backendConnector.loaded("".concat(lng, "|").concat(ns), err, data);
});
});
});
}
}, {
key: "getLanguages",
value: function getLanguages(callback) {
var _this3 = this;
var deferred;
if (!callback) {
deferred = (0, _utils.defer)();
callback = function callback(err, ret) {
if (err) return deferred.reject(err);
deferred.resolve(ret);
};
}
var isMissing = (0, _utils.isMissingOption)(this.options, ['projectId']);
if (isMissing) {
callback(new Error(isMissing));
return deferred;
}
var url = (0, _utils.interpolate)(this.options.getLanguagesPath, {
projectId: this.options.projectId
});
if (!this.isProjectNotExisting && this.storage.isProjectNotExisting(this.options.projectId)) {
this.isProjectNotExisting = true;
}
if (this.isProjectNotExisting) {
callback(new Error("locize project ".concat(this.options.projectId, " does not exist!")));
return deferred;
}
this.getLanguagesCalls = this.getLanguagesCalls || [];
this.getLanguagesCalls.push(callback);
if (this.getLanguagesCalls.length > 1) return deferred;
this.loadUrl({}, url, function (err, ret, info) {
if (!_this3.somethingLoaded && info && info.resourceNotExisting) {
_this3.isProjectNotExisting = true;
_this3.storage.setProjectNotExisting(_this3.options.projectId);
var e = new Error("locize project ".concat(_this3.options.projectId, " does not exist!"));
var _clbs = _this3.getLanguagesCalls;
_this3.getLanguagesCalls = [];
return _clbs.forEach(function (clb) {
return clb(e);
});
}
if (ret) {
var referenceLng = Object.keys(ret).reduce(function (mem, k) {
var item = ret[k];
if (item.isReferenceLanguage) mem = k;
return mem;
}, '');
if (referenceLng && _this3.options.referenceLng !== referenceLng) {
_this3.options.referenceLng = referenceLng;
}
}
_this3.somethingLoaded = true;
var clbs = _this3.getLanguagesCalls;
_this3.getLanguagesCalls = [];
clbs.forEach(function (clb) {
return clb(err, ret);
});
});
return deferred;
}
}, {
key: "getOptions",
value: function getOptions(callback) {
var _this4 = this;
var deferred;
if (!callback) {
deferred = (0, _utils.defer)();
callback = function callback(err, ret) {
if (err) return deferred.reject(err);
deferred.resolve(ret);
};
}
this.getLanguages(function (err, data) {
if (err) return callback(err);
var keys = Object.keys(data);
if (!keys.length) {
return callback(new Error('was unable to load languages via API'));
}
var lngs = keys.reduce(function (mem, k) {
var item = data[k];
if (item.translated[_this4.options.version] && item.translated[_this4.options.version] >= _this4.options.translatedPercentageThreshold) {
mem.push(k);
}
return mem;
}, []);
var hasRegion = keys.reduce(function (mem, k) {
if (k.indexOf('-') > -1) return true;
return mem;
}, false);
callback(null, {
fallbackLng: _this4.options.referenceLng,
referenceLng: _this4.options.referenceLng,
supportedLngs: lngs.length === 0 && _this4.options.referenceLng ? [_this4.options.referenceLng] : lngs,
load: hasRegion ? 'all' : 'languageOnly'
}, data);
});
return deferred;
}
}, {
key: "checkIfProjectExists",
value: function checkIfProjectExists(callback) {
var _this5 = this;
var _ref2 = this.services || {
logger: console
},
logger = _ref2.logger;
if (this.somethingLoaded) {
if (callback) callback(null);
return;
}
if (this.alreadyRequestedCheckIfProjectExists) {
setTimeout(function () {
return _this5.checkIfProjectExists(callback);
}, this.options.checkForProjectTimeout);
return;
}
this.alreadyRequestedCheckIfProjectExists = true;
this.getLanguages(function (err) {
if (err && err.message && err.message.indexOf('does not exist') > 0) {
if (logger) logger.error(err.message);
}
if (callback) callback(err);
});
}
}, {
key: "read",
value: function read(language, namespace, callback) {
var _this6 = this;
var _ref3 = this.services || {
logger: console
},
logger = _ref3.logger;
var url;
var options = {};
if (this.options.private) {
var isMissing = (0, _utils.isMissingOption)(this.options, ['projectId', 'version', 'apiKey']);
if (isMissing) return callback(new Error(isMissing), false);
url = (0, _utils.interpolate)(this.options.privatePath, {
lng: language,
ns: namespace,
projectId: this.options.projectId,
version: this.options.version
});
options = {
authorize: true
};
} else {
var _isMissing = (0, _utils.isMissingOption)(this.options, ['projectId', 'version']);
if (_isMissing) return callback(new Error(_isMissing), false);
url = (0, _utils.interpolate)(this.options.loadPath, {
lng: language,
ns: namespace,
projectId: this.options.projectId,
version: this.options.version
});
}
if (!this.isProjectNotExisting && this.storage.isProjectNotExisting(this.options.projectId)) {
this.isProjectNotExisting = true;
}
if (this.isProjectNotExisting) {
var err = new Error("locize project ".concat(this.options.projectId, " does not exist!"));
if (logger) logger.error(err.message);
if (callback) callback(err);
return;
}
this.loadUrl(options, url, function (err, ret, info) {
if (!_this6.somethingLoaded) {
if (info && info.resourceNotExisting) {
setTimeout(function () {
return _this6.checkIfProjectExists();
}, _this6.options.checkForProjectTimeout);
} else {
_this6.somethingLoaded = true;
}
}
callback(err, ret);
});
}
}, {
key: "loadUrl",
value: function loadUrl(options, url, payload, callback) {
var _this7 = this;
options = (0, _utils.defaults)(options, this.options);
if (typeof payload === 'function') {
callback = payload;
payload = undefined;
}
callback = callback || function () {};
var clb = function clb(err, res) {
var resourceNotExisting = res && res.resourceNotExisting;
if (res && (res.status === 408 || res.status === 400)) {
return callback('failed loading ' + url, true, {
resourceNotExisting: resourceNotExisting
});
}
if (res && (res.status >= 500 && res.status < 600 || !res.status)) {
return callback('failed loading ' + url, true, {
resourceNotExisting: resourceNotExisting
});
}
if (res && res.status >= 400 && res.status < 500) {
return callback('failed loading ' + url, false, {
resourceNotExisting: resourceNotExisting
});
}
if (!res && err && err.message) {
var errorMessage = err.message.toLowerCase();
var isNetworkError = ['failed', 'fetch', 'network', 'load'].find(function (term) {
return errorMessage.indexOf(term) > -1;
});
if (isNetworkError) {
return callback('failed loading ' + url + ': ' + err.message, true, {
resourceNotExisting: resourceNotExisting
});
}
}
if (err) return callback(err, false);
var ret, parseErr;
try {
if (typeof res.data === 'string') {
ret = JSON.parse(res.data);
} else {
ret = res.data;
}
} catch (e) {
parseErr = 'failed parsing ' + url + ' to json';
}
if (parseErr) return callback(parseErr, false);
if (_this7.options.failLoadingOnEmptyJSON && !Object.keys(ret).length) {
return callback('loaded result empty for ' + url, false, {
resourceNotExisting: resourceNotExisting
});
}
callback(null, ret, {
resourceNotExisting: resourceNotExisting
});
};
if (!this.options.request || url.indexOf("/languages/".concat(options.projectId)) > 0) return (0, _request.default)(options, url, payload, clb);
var info = getCustomRequestInfo(url, options, payload);
handleCustomRequest(this.options, info, clb);
}
}, {
key: "create",
value: function create(languages, namespace, key, fallbackValue, callback, options) {
var _this8 = this;
if (typeof callback !== 'function') callback = function callback() {};
this.checkIfProjectExists(function (err) {
if (err) return callback(err);
var isMissing = (0, _utils.isMissingOption)(_this8.options, ['projectId', 'version', 'apiKey', 'referenceLng']);
if (isMissing) return callback(new Error(isMissing));
if (!_this8.isAddOrUpdateAllowed) {
return callback('host is not allowed to create key.');
}
if (typeof languages === 'string') languages = [languages];
if (languages.filter(function (l) {
return l === _this8.options.referenceLng;
}).length < 1) {
_this8.services && _this8.services.logger && _this8.services.logger.warn("locize-backend: will not save missings because the reference language \"".concat(_this8.options.referenceLng, "\" was not in the list of to save languages: ").concat(languages.join(', '), " (open your site in the reference language to save missings)."));
}
languages.forEach(function (lng) {
if (lng === _this8.options.referenceLng) {
_this8.queue.call(_this8, _this8.options.referenceLng, namespace, key, fallbackValue, callback, options);
}
});
});
}
}, {
key: "update",
value: function update(languages, namespace, key, fallbackValue, callback, options) {
var _this9 = this;
if (typeof callback !== 'function') callback = function callback() {};
this.checkIfProjectExists(function (err) {
if (err) return callback(err);
var isMissing = (0, _utils.isMissingOption)(_this9.options, ['projectId', 'version', 'apiKey', 'referenceLng']);
if (isMissing) return callback(new Error(isMissing));
if (!_this9.isAddOrUpdateAllowed) {
return callback('host is not allowed to update key.');
}
if (!options) options = {};
if (typeof languages === 'string') languages = [languages];
options.isUpdate = true;
languages.forEach(function (lng) {
if (lng === _this9.options.referenceLng) {
_this9.queue.call(_this9, _this9.options.referenceLng, namespace, key, fallbackValue, callback, options);
}
});
});
}
}, {
key: "writePage",
value: function writePage(lng, namespace, missings, callback) {
var missingUrl = (0, _utils.interpolate)(this.options.addPath, {
lng: lng,
ns: namespace,
projectId: this.options.projectId,
version: this.options.version
});
var updatesUrl = (0, _utils.interpolate)(this.options.updatePath, {
lng: lng,
ns: namespace,
projectId: this.options.projectId,
version: this.options.version
});
var hasMissing = false;
var hasUpdates = false;
var payloadMissing = {};
var payloadUpdate = {};
missings.forEach(function (item) {
var value = item.options && item.options.tDescription ? {
value: item.fallbackValue || '',
context: {
text: item.options.tDescription
}
} : item.fallbackValue || '';
if (item.options && item.options.isUpdate) {
if (!hasUpdates) hasUpdates = true;
payloadUpdate[item.key] = value;
} else {
if (!hasMissing) hasMissing = true;
payloadMissing[item.key] = value;
}
});
var todo = 0;
if (hasMissing) todo++;
if (hasUpdates) todo++;
var doneOne = function doneOne(err) {
todo--;
if (!todo) callback(err);
};
if (!todo) doneOne();
if (hasMissing) {
if (!this.options.request) {
(0, _request.default)((0, _utils.defaults)({
authorize: true
}, this.options), missingUrl, payloadMissing, doneOne);
} else {
var info = getCustomRequestInfo(missingUrl, (0, _utils.defaults)({
authorize: true
}, this.options), payloadMissing);
handleCustomRequest(this.options, info, doneOne);
}
}
if (hasUpdates) {
if (!this.options.request) {
(0, _request.default)((0, _utils.defaults)({
authorize: true
}, this.options), updatesUrl, payloadUpdate, doneOne);
} else {
var _info = getCustomRequestInfo(updatesUrl, (0, _utils.defaults)({
authorize: true
}, this.options), payloadUpdate);
handleCustomRequest(this.options, _info, doneOne);
}
}
}
}, {
key: "write",
value: function write(lng, namespace) {
var _this10 = this;
var lock = (0, _utils.getPath)(this.queuedWrites, ['locks', lng, namespace]);
if (lock) return;
var missings = (0, _utils.getPath)(this.queuedWrites, [lng, namespace]);
(0, _utils.setPath)(this.queuedWrites, [lng, namespace], []);
var pageSize = 1000;
var clbs = missings.filter(function (m) {
return m.callback;
}).map(function (missing) {
return missing.callback;
});
if (missings.length) {
(0, _utils.setPath)(this.queuedWrites, ['locks', lng, namespace], true);
var namespaceSaved = function namespaceSaved() {
(0, _utils.setPath)(_this10.queuedWrites, ['locks', lng, namespace], false);
clbs.forEach(function (clb) {
return clb();
});
if (_this10.options.onSaved) _this10.options.onSaved(lng, namespace);
_this10.debouncedProcess(lng, namespace);
};
var amountOfPages = missings.length / pageSize;
var pagesDone = 0;
var page = missings.splice(0, pageSize);
this.writePage(lng, namespace, page, function () {
pagesDone++;
if (pagesDone >= amountOfPages) namespaceSaved();
});
while (page.length === pageSize) {
page = missings.splice(0, pageSize);
if (page.length) {
this.writePage(lng, namespace, page, function () {
pagesDone++;
if (pagesDone >= amountOfPages) namespaceSaved();
});
}
}
}
}
}, {
key: "process",
value: function process() {
var _this11 = this;
Object.keys(this.queuedWrites).forEach(function (lng) {
if (lng === 'locks') return;
Object.keys(_this11.queuedWrites[lng]).forEach(function (ns) {
var todo = _this11.queuedWrites[lng][ns];
if (todo.length) {
_this11.write(lng, ns);
}
});
});
}
}, {
key: "queue",
value: function queue(lng, namespace, key, fallbackValue, callback, options) {
(0, _utils.pushPath)(this.queuedWrites, [lng, namespace], {
key: key,
fallbackValue: fallbackValue || '',
callback: callback,
options: options
});
this.debouncedProcess();
}
}]);
}();
I18NextLocizeBackend.type = 'backend';
var _default = exports.default = I18NextLocizeBackend;
module.exports = exports.default;