2022-07-04 14:47:12 +00:00
|
|
|
import { defaults, makePromise } from "./utils.js";
|
|
|
|
import request from "./request.js";
|
|
|
|
|
|
|
|
const getDefaults = () => {
|
|
|
|
return {
|
|
|
|
loadPath: "/locales/{{lng}}/{{ns}}.json",
|
|
|
|
addPath: "/locales/add/{{lng}}/{{ns}}",
|
|
|
|
allowMultiLoading: false,
|
|
|
|
parse: (data) => JSON.parse(data),
|
|
|
|
stringify: JSON.stringify,
|
|
|
|
parsePayload: (namespace, key, fallbackValue) => ({
|
|
|
|
[key]: fallbackValue || "",
|
|
|
|
}),
|
|
|
|
request,
|
|
|
|
reloadInterval: typeof window !== "undefined" ? false : 60 * 60 * 1000,
|
|
|
|
customHeaders: {},
|
|
|
|
queryStringParams: {},
|
|
|
|
crossDomain: false, // used for XmlHttpRequest
|
|
|
|
withCredentials: false, // used for XmlHttpRequest
|
|
|
|
overrideMimeType: false, // used for XmlHttpRequest
|
|
|
|
requestOptions: {
|
|
|
|
// used for fetch
|
|
|
|
mode: "cors",
|
|
|
|
credentials: "same-origin",
|
|
|
|
cache: "default",
|
|
|
|
},
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
class Backend {
|
|
|
|
constructor(services, options = {}, allOptions = {}) {
|
|
|
|
this.services = services;
|
|
|
|
this.options = options;
|
|
|
|
this.allOptions = allOptions;
|
|
|
|
this.type = "backend";
|
|
|
|
this.init(services, options, allOptions);
|
|
|
|
}
|
|
|
|
|
|
|
|
init(services, options = {}, allOptions = {}) {
|
|
|
|
this.services = services;
|
|
|
|
this.options = defaults(options, this.options || {}, getDefaults());
|
|
|
|
this.allOptions = allOptions;
|
|
|
|
if (this.services && this.options.reloadInterval) {
|
|
|
|
setInterval(() => this.reload(), this.options.reloadInterval);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
readMulti(languages, namespaces, callback) {
|
|
|
|
this._readAny(languages, languages, namespaces, namespaces, callback);
|
|
|
|
}
|
|
|
|
|
|
|
|
read(language, namespace, callback) {
|
|
|
|
this._readAny([language], language, [namespace], namespace, callback);
|
|
|
|
}
|
|
|
|
|
|
|
|
_readAny(
|
|
|
|
languages,
|
|
|
|
loadUrlLanguages,
|
|
|
|
namespaces,
|
|
|
|
loadUrlNamespaces,
|
|
|
|
callback
|
|
|
|
) {
|
|
|
|
let loadPath = this.options.loadPath;
|
|
|
|
if (typeof this.options.loadPath === "function") {
|
|
|
|
loadPath = this.options.loadPath(languages, namespaces);
|
|
|
|
}
|
|
|
|
|
|
|
|
loadPath = makePromise(loadPath);
|
|
|
|
|
|
|
|
loadPath.then((resolvedLoadPath) => {
|
|
|
|
if (!resolvedLoadPath) return callback(null, {});
|
|
|
|
const url = this.services.interpolator.interpolate(resolvedLoadPath, {
|
|
|
|
lng: languages.join("+"),
|
|
|
|
ns: namespaces.join("+"),
|
|
|
|
});
|
|
|
|
this.loadUrl(url, callback, loadUrlLanguages, loadUrlNamespaces);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
loadUrl(url, callback, languages, namespaces) {
|
|
|
|
//console.log("loadUrl", url, languages, namespaces);
|
|
|
|
|
|
|
|
if (!window.i18n) {
|
|
|
|
window.i18n = {
|
|
|
|
inLoad: [],
|
|
|
|
loaded: {},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
var index = window.i18n.inLoad.findIndex((item) => item.url == url);
|
|
|
|
|
|
|
|
if (index > -1) {
|
|
|
|
//console.log("skip already in load url", url);
|
|
|
|
window.i18n.inLoad[index].callbacks.push(callback);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-07-04 15:49:46 +00:00
|
|
|
if (window.i18n.loaded[url]) {
|
|
|
|
return callback(null, window.i18n.loaded[url].data);
|
|
|
|
}
|
|
|
|
|
2022-07-04 14:47:12 +00:00
|
|
|
if (namespaces == "translation") {
|
|
|
|
//console.log("skip defaultNS");
|
|
|
|
return callback(
|
|
|
|
`failed loading ${url}; status code: 404`,
|
|
|
|
false /* retry */
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
window.i18n.inLoad.push({ url, callbacks: [callback] });
|
|
|
|
|
|
|
|
this.options.request(this.options, url, undefined, (err, res) => {
|
|
|
|
if (res && ((res.status >= 500 && res.status < 600) || !res.status))
|
|
|
|
return this.sendCallbacks(
|
|
|
|
url,
|
|
|
|
namespaces,
|
|
|
|
`failed loading ${url}; status code: ${res.status}`,
|
|
|
|
true /* retry */
|
|
|
|
);
|
|
|
|
if (res && res.status >= 400 && res.status < 500)
|
|
|
|
return this.sendCallbacks(
|
|
|
|
url,
|
|
|
|
namespaces,
|
|
|
|
`failed loading ${url}; status code: ${res.status}`,
|
|
|
|
false /* no retry */
|
|
|
|
);
|
|
|
|
if (
|
|
|
|
!res &&
|
|
|
|
err &&
|
|
|
|
err.message &&
|
|
|
|
err.message.indexOf("Failed to fetch") > -1
|
|
|
|
)
|
|
|
|
return this.sendCallbacks(
|
|
|
|
url,
|
|
|
|
namespaces,
|
|
|
|
`failed loading ${url}: ${err.message}`,
|
|
|
|
true /* retry */
|
|
|
|
);
|
|
|
|
if (err) return this.sendCallbacks(url, namespaces, err, false);
|
|
|
|
|
|
|
|
let ret, parseErr;
|
|
|
|
try {
|
|
|
|
if (typeof res.data === "string") {
|
|
|
|
ret = this.options.parse(res.data, languages, namespaces);
|
|
|
|
} else {
|
|
|
|
// fallback, which omits calling the parse function
|
|
|
|
ret = res.data;
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
parseErr = `failed parsing ${url} to json`;
|
|
|
|
}
|
|
|
|
if (parseErr) return this.sendCallbacks(url, namespaces, parseErr, false);
|
|
|
|
this.sendCallbacks(url, namespaces, null, ret);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
sendCallbacks(url, namespaces, error, data) {
|
|
|
|
var index = window.i18n.inLoad.findIndex((item) => item.url == url);
|
|
|
|
if (index == -1) return;
|
|
|
|
|
|
|
|
window.i18n.inLoad[index].callbacks.forEach((cb) => cb(error, data));
|
|
|
|
|
|
|
|
window.i18n.inLoad.splice(index, 1);
|
|
|
|
|
|
|
|
if (!error) {
|
|
|
|
window.i18n.loaded[url] = {
|
|
|
|
namespaces,
|
|
|
|
data,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
create(languages, namespace, key, fallbackValue, callback) {
|
|
|
|
// If there is a falsey addPath, then abort -- this has been disabled.
|
|
|
|
if (!this.options.addPath) return;
|
|
|
|
if (typeof languages === "string") languages = [languages];
|
|
|
|
const payload = this.options.parsePayload(namespace, key, fallbackValue);
|
|
|
|
let finished = 0;
|
|
|
|
const dataArray = [];
|
|
|
|
const resArray = [];
|
|
|
|
languages.forEach((lng) => {
|
|
|
|
let addPath = this.options.addPath;
|
|
|
|
if (typeof this.options.addPath === "function") {
|
|
|
|
addPath = this.options.addPath(lng, namespace);
|
|
|
|
}
|
|
|
|
const url = this.services.interpolator.interpolate(addPath, {
|
|
|
|
lng: lng,
|
|
|
|
ns: namespace,
|
|
|
|
});
|
|
|
|
|
|
|
|
this.options.request(this.options, url, payload, (data, res) => {
|
|
|
|
// TODO: if res.status === 4xx do log
|
|
|
|
finished += 1;
|
|
|
|
dataArray.push(data);
|
|
|
|
resArray.push(res);
|
|
|
|
if (finished === languages.length) {
|
|
|
|
if (callback) callback(dataArray, resArray);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
reload() {
|
|
|
|
const { backendConnector, languageUtils, logger } = this.services;
|
|
|
|
const currentLanguage = backendConnector.language;
|
|
|
|
if (currentLanguage && currentLanguage.toLowerCase() === "cimode") return; // avoid loading resources for cimode
|
|
|
|
|
|
|
|
const toLoad = [];
|
|
|
|
const append = (lng) => {
|
|
|
|
const lngs = languageUtils.toResolveHierarchy(lng);
|
|
|
|
lngs.forEach((l) => {
|
|
|
|
if (toLoad.indexOf(l) < 0) toLoad.push(l);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
append(currentLanguage);
|
|
|
|
|
|
|
|
if (this.allOptions.preload)
|
|
|
|
this.allOptions.preload.forEach((l) => append(l));
|
|
|
|
|
|
|
|
toLoad.forEach((lng) => {
|
|
|
|
this.allOptions.ns.forEach((ns) => {
|
|
|
|
backendConnector.read(lng, ns, "read", null, null, (err, data) => {
|
|
|
|
if (err)
|
|
|
|
logger.warn(
|
|
|
|
`loading namespace ${ns} for language ${lng} failed`,
|
|
|
|
err
|
|
|
|
);
|
|
|
|
if (!err && data)
|
|
|
|
logger.log(`loaded namespace ${ns} for language ${lng}`, data);
|
|
|
|
|
|
|
|
backendConnector.loaded(`${lng}|${ns}`, err, data);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Backend.type = "backend";
|
|
|
|
|
|
|
|
export default Backend;
|