CVE-2026-41885
Description
i18next-locize-backend is a simple i18next backend for locize.com which can be used in Node.js, in the browser and for Deno. Prior to version 9.0.2, i18next-locize-backend interpolates lng, ns, projectId, and version directly into the configured loadPath / privatePath / addPath / updatePath / getLanguagesPath URL templates with no path-component validation and no encoding. When an application exposes any of these values to user-controlled input (?lng= / ?ns= query parameters via i18next-browser-languagedetector, cookies, request headers, or a URL-derived projectId), a crafted value can change the structure of the outgoing request URL. Affected call sites in lib/index.js (pre-patch): the interpolate() helper is used at the five URL-build sites — _readAny/read (line 415 for private, 426 for public), getLanguages (lines 271 and 296), and writePage (lines 616 and 622) for the missing-key and update POST paths. The helper interpolate in lib/utils.js substitutes raw values with no encoding. This issue has been patched in version 9.0.2.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
i18next-locize-backendnpm | < 9.0.2 | 9.0.2 |
Affected products
1- Range: < 9.0.2
Patches
18f81ad4707aasecurity: hardening for 9.0.2
11 files changed · +369 −35
CHANGELOG.md+10 −0 modified@@ -1,3 +1,13 @@ +### 9.0.2 + +Security release — all issues found via an internal audit. GHSA advisory filed after release. + +- security: refuse to build request URLs when `lng`, `ns`, `projectId`, or `version` values contain path-traversal (`..`), path separators (`/`, `\`), URL-structure characters (`?`, `#`, `%`, `@`, whitespace), control characters, prototype keys (`__proto__`, `constructor`, `prototype`), or exceed 128 chars. Prevents path traversal / URL injection via attacker-controlled option values (same class of fix as `i18next-http-backend@3.0.5`) (GHSA-TBD) +- security: guard `interpolate` against prototype-chain lookups — a polluted `Object.prototype.__proto__` no longer leaks into URL substitution +- security: replace `for...in` iteration over the `defaults` source with `Object.keys()` + explicit prototype-key guard +- chore: remove unused `.coveralls.yml` +- chore: ignore `.env*` and `*.pem`/`*.key` files in `.gitignore` + ### 9.0.1 - improve handling for inexisting projects
.coveralls.yml+0 −1 removed@@ -1 +0,0 @@ -repo_token: dkgQM5oEcrciHIlCo1DJtSMyW7bRdv9Vb
.github/workflows/node.yml+1 −1 modified@@ -11,7 +11,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - node: [ '24.x', '22.x', '20.x', '18.x' ] + node: [ '24.x', '22.x', '20.x' ] # os: [ubuntu-latest, windows-latest, macOS-latest] os: [ubuntu-latest] steps:
.gitignore+8 −1 modified@@ -21,4 +21,11 @@ bin/**/* package-lock.json cjs -esm \ No newline at end of file +esm + +# Secrets & credentials +.env +.env.* +!.env.example +*.pem +*.key
i18nextLocizeBackend.js+110 −14 modified@@ -270,14 +270,18 @@ var I18NextLocizeBackend = function () { callback(new Error(isMissing)); return deferred; } - var url = (0, _utils.interpolate)(this.options.getLanguagesPath, { + var url = (0, _utils.interpolateUrl)(this.options.getLanguagesPath, { projectId: this.options.projectId }); + if (url == null) { + callback(new Error('i18next-locize-backend: unsafe projectId — refusing to build request URL for projectId=' + (0, _utils.sanitizeLogValue)(String(this.options.projectId)))); + return deferred; + } if (!this.isProjectNotExisting && this.storage.isProjectNotExisting(this.options.projectId)) { this.isProjectNotExisting = true; } if (this.isProjectNotExisting) { - callback(new Error(this.isProjectNotExistingErrorMessage || "locize project ".concat(this.options.projectId, " does not exist!"))); + callback(new Error(this.isProjectNotExistingErrorMessage || "Locize project ".concat(this.options.projectId, " does not exist!"))); return deferred; } this.getLanguagesCalls = this.getLanguagesCalls || []; @@ -286,16 +290,17 @@ var I18NextLocizeBackend = function () { this.loadUrl({}, url, function (err, ret, info) { if (!_this3.somethingLoaded && info && info.resourceNotExisting) { _this3.isProjectNotExisting = true; - var errMsg = "locize project ".concat(_this3.options.projectId, " does not exist!"); + var errMsg = "Locize project ".concat(_this3.options.projectId, " does not exist!"); _this3.isProjectNotExistingErrorMessage = errMsg; var cdnTypeAlt = _this3.options.cdnType === 'standard' ? 'pro' : 'standard'; var otherEndpointApiPaths = getApiPaths(cdnTypeAlt); - var urlAlt = (0, _utils.interpolate)(otherEndpointApiPaths.getLanguagesPath, { + var urlAlt = (0, _utils.interpolateUrl)(otherEndpointApiPaths.getLanguagesPath, { projectId: _this3.options.projectId }); + if (urlAlt == null) return; _this3.loadUrl({}, urlAlt, function (errAlt, retAlt, infoAlt) { if (!errAlt && retAlt && (!infoAlt || !infoAlt.resourceNotExisting)) { - errMsg += " It seems you're using the wrong cdnType. Your locize project is configured to use \"".concat(cdnTypeAlt, "\" but here you've configured \"").concat(_this3.options.cdnType, "\"."); + errMsg += " It seems you're using the wrong cdnType. Your Locize project is configured to use \"".concat(cdnTypeAlt, "\" but here you've configured \"").concat(_this3.options.cdnType, "\"."); _this3.isProjectNotExistingErrorMessage = errMsg; } else if (!_this3.somethingLoaded && infoAlt && infoAlt.resourceNotExisting) { _this3.isProjectNotExisting = true; @@ -425,7 +430,7 @@ var I18NextLocizeBackend = function () { 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, { + url = (0, _utils.interpolateUrl)(this.options.privatePath, { lng: language, ns: namespace, projectId: this.options.projectId, @@ -437,24 +442,27 @@ var I18NextLocizeBackend = function () { } 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, { + url = (0, _utils.interpolateUrl)(this.options.loadPath, { lng: language, ns: namespace, projectId: this.options.projectId, version: this.options.version }); } + if (url == null) { + return callback(new Error('i18next-locize-backend: unsafe lng/ns/projectId/version — refusing to build request URL for lng=' + (0, _utils.sanitizeLogValue)(String(language)) + ' ns=' + (0, _utils.sanitizeLogValue)(String(namespace))), false); + } if (!this.isProjectNotExisting && this.storage.isProjectNotExisting(this.options.projectId)) { this.isProjectNotExisting = true; } if (this.isProjectNotExisting) { - var err = new Error(this.isProjectNotExistingErrorMessage || "locize project ".concat(this.options.projectId, " does not exist!")); + var err = new Error(this.isProjectNotExistingErrorMessage || "Locize project ".concat(this.options.projectId, " does not exist!")); if (logger) logger.error(err.message); if (callback) callback(err); return; } if (this.warnedLanguages && this.warnedLanguages.indexOf(language) > -1) { - var _err = new Error("Will not continue to load language \"".concat(language, "\" since it is not available in locize project ").concat(this.options.projectId, "!")); + var _err = new Error("Will not continue to load language \"".concat(language, "\" since it is not available in Locize project ").concat(this.options.projectId, "!")); if (logger) logger.error(_err.message); if (callback) callback(_err); return; @@ -473,7 +481,7 @@ var I18NextLocizeBackend = function () { if (_this6.warnedLanguages && _this6.warnedLanguages.indexOf(language) > -1) return; _this6.warnedLanguages || (_this6.warnedLanguages = []); _this6.warnedLanguages.push(language); - if (logger) logger.error("Language \"".concat(language, "\" is not available in locize project ").concat(_this6.options.projectId, "!")); + if (logger) logger.error("Language \"".concat(language, "\" is not available in Locize project ").concat(_this6.options.projectId, "!")); }); }, randomizeTimeout(_this6.options.checkForProjectTimeout)); } @@ -602,18 +610,24 @@ var I18NextLocizeBackend = function () { }, { key: "writePage", value: function writePage(lng, namespace, missings, callback) { - var missingUrl = (0, _utils.interpolate)(this.options.addPath, { + var missingUrl = (0, _utils.interpolateUrl)(this.options.addPath, { lng: lng, ns: namespace, projectId: this.options.projectId, version: this.options.version }); - var updatesUrl = (0, _utils.interpolate)(this.options.updatePath, { + var updatesUrl = (0, _utils.interpolateUrl)(this.options.updatePath, { lng: lng, ns: namespace, projectId: this.options.projectId, version: this.options.version }); + if (missingUrl == null || updatesUrl == null) { + if (typeof callback === 'function') { + callback(new Error('i18next-locize-backend: unsafe lng/ns/projectId/version — refusing to persist missing keys for lng=' + (0, _utils.sanitizeLogValue)(String(lng)) + ' ns=' + (0, _utils.sanitizeLogValue)(String(namespace)))); + } + return; + } var hasMissing = false; var hasUpdates = false; var payloadMissing = {}; @@ -910,28 +924,67 @@ module.exports = exports.default; Object.defineProperty(exports, "__esModule", { value: true }); +exports.UNSAFE_KEYS = void 0; exports.debounce = debounce; exports.defaults = defaults; exports.defer = defer; exports.getPath = getPath; exports.interpolate = interpolate; +exports.interpolateUrl = interpolateUrl; exports.isMissingOption = isMissingOption; +exports.isSafeUrlSegment = isSafeUrlSegment; exports.optionExist = optionExist; exports.pushPath = pushPath; +exports.redactUrlCredentials = redactUrlCredentials; +exports.sanitizeLogValue = sanitizeLogValue; exports.setPath = setPath; +function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; } +function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } +function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } var arr = []; var each = arr.forEach; var slice = arr.slice; +var UNSAFE_KEYS = exports.UNSAFE_KEYS = ['__proto__', 'constructor', 'prototype']; function defaults(obj) { each.call(slice.call(arguments, 1), function (source) { if (source) { - for (var prop in source) { + for (var _i = 0, _Object$keys = Object.keys(source); _i < _Object$keys.length; _i++) { + var prop = _Object$keys[_i]; + if (UNSAFE_KEYS.indexOf(prop) > -1) continue; if (obj[prop] === undefined) obj[prop] = source[prop]; } } }); return obj; } +function isSafeUrlSegment(v) { + if (typeof v !== 'string') return false; + if (v.length === 0 || v.length > 128) return false; + if (UNSAFE_KEYS.indexOf(v) > -1) return false; + if (v.indexOf('..') > -1) return false; + if (v.indexOf('/') > -1 || v.indexOf('\\') > -1) return false; + if (/[?#%\s@]/.test(v)) return false; + if (/[\x00-\x1F\x7F]/.test(v)) return false; + return true; +} +function sanitizeLogValue(v) { + if (typeof v !== 'string') return v; + return v.replace(/[\r\n\x00-\x1F\x7F]/g, ' '); +} +function redactUrlCredentials(u) { + if (typeof u !== 'string' || u.length === 0) return u; + try { + var parsed = new URL(u); + if (parsed.username || parsed.password) { + parsed.username = ''; + parsed.password = ''; + return parsed.toString(); + } + return u; + } catch (e) { + return u.replace(/(\/\/)[^/@\s]+@/g, '$1'); + } +} function debounce(func, wait, immediate) { var timeout; return function () { @@ -1001,11 +1054,54 @@ function interpolate(str, data, lng) { if (typeof value !== 'string') value = makeString(value); if (!value) value = ''; value = regexSafe(value); - str = str.replace(match[0], data[value] || value); + var subst = UNSAFE_KEYS.indexOf(value) > -1 ? value : data[value] || value; + str = str.replace(match[0], subst); regexp.lastIndex = 0; } return str; } +function interpolateUrl(str, data) { + var match; + var unsafe = false; + while (match = regexp.exec(str)) { + var key = match[1].trim(); + if (UNSAFE_KEYS.indexOf(key) > -1) { + regexp.lastIndex = 0; + continue; + } + var raw = data[key]; + if (raw == null) { + regexp.lastIndex = 0; + continue; + } + var value = makeString(raw); + var segments = value.split('+'); + var segmentsOk = true; + var _iterator = _createForOfIteratorHelper(segments), + _step; + try { + for (_iterator.s(); !(_step = _iterator.n()).done;) { + var seg = _step.value; + if (!isSafeUrlSegment(seg)) { + segmentsOk = false; + break; + } + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } + if (!segmentsOk) { + unsafe = true; + break; + } + str = str.replace(match[0], segments.join('+')); + regexp.lastIndex = 0; + } + regexp.lastIndex = 0; + return unsafe ? null : str; +} function isMissingOption(obj, props) { return props.reduce(function (mem, p) { if (mem) return mem;
i18nextLocizeBackend.min.js+1 −1 modified@@ -1 +1 @@ -!function(e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).i18nextLocizeBackend=e()}(function(){return function n(i,s,r){function a(t,e){if(!s[t]){if(!i[t]){var o="function"==typeof require&&require;if(!e&&o)return o(t,!0);if(c)return c(t,!0);throw(e=new Error("Cannot find module '"+t+"'")).code="MODULE_NOT_FOUND",e}o=s[t]={exports:{}},i[t][0].call(o.exports,function(e){return a(i[t][1][e]||e)},o,o.exports,n,i,s,r)}return s[t].exports}for(var c="function"==typeof require&&require,e=0;e<r.length;e++)a(r[e]);return a}({1:[function(e,t,o){Object.defineProperty(o,"__esModule",{value:!0}),o.default=void 0;var l=e("./utils.js"),f=(e=e("./request.js"))&&e.__esModule?e:{default:e};function d(e){return(d="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function n(e,t){for(var o=0;o<t.length;o++){var n=t[o];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,(e=>(e=((e,t)=>{if("object"!=d(e)||!e)return e;var o=e[Symbol.toPrimitive];if(void 0===o)return("string"===t?String:Number)(e);if("object"!=d(o=o.call(e,t||"default")))return o;throw new TypeError("@@toPrimitive must return a primitive value.")})(e,"string"),"symbol"==d(e)?e:e+""))(n.key),n)}}function p(e){return{loadPath:"https://api".concat("standard"===(e=e||"standard")?".lite":"",".locize.app/{{projectId}}/{{version}}/{{lng}}/{{ns}}"),privatePath:"https://api".concat("standard"===e?".lite":"",".locize.app/private/{{projectId}}/{{version}}/{{lng}}/{{ns}}"),getLanguagesPath:"https://api".concat("standard"===e?".lite":"",".locize.app/languages/{{projectId}}"),addPath:"https://api".concat("standard"===e?".lite":"",".locize.app/missing/{{projectId}}/{{version}}/{{lng}}/{{ns}}"),updatePath:"https://api".concat("standard"===e?".lite":"",".locize.app/update/{{projectId}}/{{version}}/{{lng}}/{{ns}}")}}try{var g="undefined"!=typeof window&&null!==window.localStorage,i="notExistingLocizeProject";window.localStorage.setItem(i,"foo"),window.localStorage.removeItem(i)}catch(e){g=!1}function h(e,t,o){var n={};return t.authorize&&t.apiKey&&(n.Authorization=t.apiKey),(o||t.setContentTypeJSON)&&(n["Content-Type"]="application/json"),{method:o?"POST":"GET",url:e,headers:n,body:o}}function v(e,t,o){if(1===e.request.length)try{var n=e.request(t);n&&"function"==typeof n.then?n.then(function(e){return o(null,e)}).catch(o):o(null,n)}catch(e){o(e)}else e.request(t,o)}function c(e){var t=.25*e,o=Math.max(0,e-t),e=e+t;return Math.floor(o+Math.random()*(e-o))}e=function e(t){var o=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:{},i=3<arguments.length?arguments[3]:void 0,s=this,r=e;if(!(s instanceof r))throw new TypeError("Cannot call a class as a function");this.services=t,this.options=o,this.allOptions=n,this.type="backend",t&&t.projectId?this.init(null,t,n,o):this.init(t,o,n,i)},(i=[{key:"init",value:function(e){var o,n=this,i=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},t=2<arguments.length&&void 0!==arguments[2]?arguments[2]:{},s=3<arguments.length?arguments[3]:void 0,r=(!i.referenceLng&&t.fallbackLng&&Array.isArray(t.fallbackLng)&&"dev"!==t.fallbackLng[0]&&(i.referenceLng=t.fallbackLng[0]),this.services=e,(0,l.defaults)({},i)),a=(0,l.defaults)(i,this.options||{}),c=(c=a.cdnType,(0,l.defaults)({cdnType:c=c||"standard",noCache:!1,referenceLng:"en",crossDomain:!0,setContentTypeJSON:!1,version:"latest",private:!1,translatedPercentageThreshold:.9,failLoadingOnEmptyJSON:!1,allowedAddOrUpdateHosts:["localhost"],onSaved:!1,reloadInterval:"undefined"==typeof window&&36e5,checkForProjectTimeout:3e3,storageExpiration:36e5,writeDebounce:5e3,useCacheLayer:"undefined"==typeof window},p(c))),u=(a.reloadInterval&&a.reloadInterval<3e5&&(console.warn("Your configured reloadInterval option is to low."),a.reloadInterval=c.reloadInterval),this.options=(0,l.defaults)(i,this.options||{},c),this.allOptions=t,this.somethingLoaded=!1,this.isProjectNotExisting=!1,this.storage=(o=this.options.storageExpiration,a=function(){},c=function(){},g?(a=function(e){window.localStorage.setItem("notExistingLocizeProject_".concat(e),Date.now())},c=function(e){var t=window.localStorage.getItem("notExistingLocizeProject_".concat(e));return!(!t||Date.now()-t>o&&(window.localStorage.removeItem("notExistingLocizeProject_".concat(e)),1))}):"undefined"!=typeof document&&(a=function(e){var t=new Date,t=(t.setTime(t.getTime()+o),"; expires=".concat(t.toGMTString())),e="notExistingLocizeProject_".concat(e);try{document.cookie="".concat(e,"=").concat(Date.now()).concat(t,";path=/")}catch(e){}},c=function(e){var e="notExistingLocizeProject_".concat(e),t="".concat(e,"=");try{for(var o=document.cookie.split(";"),n=0;n<o.length;n++){for(var i=o[n];" "===i.charAt(0);)i=i.substring(1,i.length);if(0===i.indexOf(t))return!0}}catch(e){}return!1}),{setProjectNotExisting:a,isProjectNotExisting:c}),p(this.options.cdnType)),a=(Object.keys(u).forEach(function(e){r[e]||(n.options[e]=u[e])}),t.debug&&void 0===r.noCache&&"standard"===this.options.cdnType&&(this.options.noCache=!0),this.options.noCache&&"standard"!==this.options.cdnType&&console.warn("The 'noCache' option is not available for 'cdnType' '".concat(this.options.cdnType,"'!")),"undefined"!=typeof window&&window.location&&window.location.hostname);a?(this.isAddOrUpdateAllowed="function"==typeof this.options.allowedAddOrUpdateHosts?this.options.allowedAddOrUpdateHosts(a):-1<this.options.allowedAddOrUpdateHosts.indexOf(a),e&&e.logger&&(t.saveMissing||t.updateMissing)&&(this.isAddOrUpdateAllowed?"localhost"!==a&&e.logger.warn('locize-backend: you are using the save or update missings feature from this host "'.concat(a,'".\nMake sure you will not use it in production!\nhttps://www.locize.com/docs/going-to-production')):e.logger.warn("function"==typeof this.options.allowedAddOrUpdateHosts?'locize-backend: will not save or update missings because allowedAddOrUpdateHosts returned false for the host "'.concat(a,'".'):'locize-backend: will not save or update missings because the host "'.concat(a,'" was not in the list of allowedAddOrUpdateHosts: ').concat(this.options.allowedAddOrUpdateHosts.join(", ")," (matches need to be exact).")))):this.isAddOrUpdateAllowed=!0,"function"==typeof s&&this.getOptions(function(e,t,o){if(e)return s(e);n.options.referenceLng=i.referenceLng||t.referenceLng||n.options.referenceLng,s(null,t,o)}),this.queuedWrites={pending:{}},this.debouncedProcess=(0,l.debounce)(this.process,this.options.writeDebounce),this.interval&&clearInterval(this.interval),this.options.reloadInterval&&this.options.projectId&&(this.interval=setInterval(function(){return n.reload()},this.options.reloadInterval),"object"===d(this.interval))&&"function"==typeof this.interval.unref&&this.interval.unref()}},{key:"reload",value:function(){var t,e,o=this,n=this.services||{logger:console},i=n.backendConnector,s=n.languageUtils,r=n.logger;!i||(n=i.language)&&"cimode"===n.toLowerCase()||(t=[],(e=function(e){s.toResolveHierarchy(e).forEach(function(e){t.indexOf(e)<0&&t.push(e)})})(n),this.allOptions.preload&&this.allOptions.preload.forEach(e),t.forEach(function(n){o.allOptions.ns.forEach(function(o){i.read(n,o,"read",null,null,function(e,t){e&&r.warn("loading namespace ".concat(o," for language ").concat(n," failed"),e),!e&&t&&r.log("loaded namespace ".concat(o," for language ").concat(n),t),i.loaded("".concat(n,"|").concat(o),e,t)})})}))}},{key:"getLanguages",value:function(e){var o,r=this,t=(e||(o=(0,l.defer)(),e=function(e,t){if(e)return o.reject(e);o.resolve(t)}),(0,l.isMissingOption)(this.options,["projectId"]));return t?e(new Error(t)):(t=(0,l.interpolate)(this.options.getLanguagesPath,{projectId:this.options.projectId}),!this.isProjectNotExisting&&this.storage.isProjectNotExisting(this.options.projectId)&&(this.isProjectNotExisting=!0),this.isProjectNotExisting?e(new Error(this.isProjectNotExistingErrorMessage||"locize project ".concat(this.options.projectId," does not exist!"))):(this.getLanguagesCalls=this.getLanguagesCalls||[],this.getLanguagesCalls.push(e),1<this.getLanguagesCalls.length||this.loadUrl({},t,function(t,o,e){var i,s;!r.somethingLoaded&&e&&e.resourceNotExisting?(r.isProjectNotExisting=!0,i="locize project ".concat(r.options.projectId," does not exist!"),r.isProjectNotExistingErrorMessage=i,s="standard"===r.options.cdnType?"pro":"standard",e=p(s),e=(0,l.interpolate)(e.getLanguagesPath,{projectId:r.options.projectId}),r.loadUrl({},e,function(e,t,o){e||!t||o&&o.resourceNotExisting?!r.somethingLoaded&&o&&o.resourceNotExisting&&(r.isProjectNotExisting=!0,r.storage.setProjectNotExisting(r.options.projectId)):(i+=" It seems you're using the wrong cdnType. Your locize project is configured to use \"".concat(s,'" but here you\'ve configured "').concat(r.options.cdnType,'".'),r.isProjectNotExistingErrorMessage=i);var n=new Error(i),e=r.getLanguagesCalls;r.getLanguagesCalls=[],e.forEach(function(e){return e(n)})})):(o&&(r.loadedLanguages=Object.keys(o),e=r.loadedLanguages.reduce(function(e,t){return e=o[t].isReferenceLanguage?t:e},""))&&r.options.referenceLng!==e&&(r.options.referenceLng=e),r.somethingLoaded=!0,e=r.getLanguagesCalls,r.getLanguagesCalls=[],e.forEach(function(e){return e(t,o)}))}))),o}},{key:"getOptions",value:function(o){var n,i=this;return o||(n=(0,l.defer)(),o=function(e,t){if(e)return n.reject(e);n.resolve(t)}),this.getLanguages(function(e,n){var t;return e?o(e):(e=Object.keys(n)).length?(t=e.reduce(function(e,t){var o=n[t];return o.translated[i.options.version]&&o.translated[i.options.version]>=i.options.translatedPercentageThreshold&&e.push(t),e},[]),e=e.reduce(function(e,t){return-1<t.indexOf("-")||e},!1),void o(null,{fallbackLng:i.options.referenceLng,referenceLng:i.options.referenceLng,supportedLngs:0===t.length&&i.options.referenceLng?[i.options.referenceLng]:t,load:e?"all":"languageOnly"},n)):o(new Error("was unable to load languages via API"))}),n}},{key:"checkIfProjectExists",value:function(t){var e=this,o=(this.services||{logger:console}).logger;this.somethingLoaded?t&&t(null):this.alreadyRequestedCheckIfProjectExists?setTimeout(function(){return e.checkIfProjectExists(t)},c(this.options.checkForProjectTimeout)):(this.alreadyRequestedCheckIfProjectExists=!0,this.getLanguages(function(e){e&&e.message&&0<e.message.indexOf("does not exist")&&o&&o.error(e.message),t&&t(e)}))}},{key:"checkIfLanguagesLoaded",value:function(t){var o=(this.services||{logger:console}).logger;this.loadedLanguages?t&&t(null):this.getLanguages(function(e){e&&e.message&&0<e.message.indexOf("does not exist")&&o&&o.error(e.message),t&&t(e)})}},{key:"read",value:function(n,e,i){var s=this,r=(this.services||{logger:console}).logger,t={};if(this.options.private){var o=(0,l.isMissingOption)(this.options,["projectId","version","apiKey"]);if(o)return i(new Error(o),!1);o=(0,l.interpolate)(this.options.privatePath,{lng:n,ns:e,projectId:this.options.projectId,version:this.options.version}),t={authorize:!0}}else{var a=(0,l.isMissingOption)(this.options,["projectId","version"]);if(a)return i(new Error(a),!1);o=(0,l.interpolate)(this.options.loadPath,{lng:n,ns:e,projectId:this.options.projectId,version:this.options.version})}!this.isProjectNotExisting&&this.storage.isProjectNotExisting(this.options.projectId)&&(this.isProjectNotExisting=!0),this.isProjectNotExisting?(a=new Error(this.isProjectNotExistingErrorMessage||"locize project ".concat(this.options.projectId," does not exist!")),r&&r.error(a.message),i&&i(a)):this.warnedLanguages&&-1<this.warnedLanguages.indexOf(n)?(e=new Error('Will not continue to load language "'.concat(n,'" since it is not available in locize project ').concat(this.options.projectId,"!")),r&&r.error(e.message),i&&i(e)):this.loadUrl(t,o,function(e,t,o){o=o&&o.resourceNotExisting;o||(s.hasResourcesForLng||(s.hasResourcesForLng={}),s.hasResourcesForLng[n]=!0),!o||s.hasResourcesForLng&&s.hasResourcesForLng[n]||setTimeout(function(){s.checkIfLanguagesLoaded(function(){!s.loadedLanguages||-1<s.loadedLanguages.indexOf(n)||s.warnedLanguages&&-1<s.warnedLanguages.indexOf(n)||(s.warnedLanguages||(s.warnedLanguages=[]),s.warnedLanguages.push(n),r&&r.error('Language "'.concat(n,'" is not available in locize project ').concat(s.options.projectId,"!")))})},c(s.options.checkForProjectTimeout)),s.somethingLoaded||(o?setTimeout(function(){return s.checkIfProjectExists()},c(s.options.checkForProjectTimeout)):s.somethingLoaded=!0),i(e,t)})}},{key:"loadUrl",value:function(e,r,t,a){function o(e,t){var o,n,i=t&&t.resourceNotExisting;if(t&&(408===t.status||400===t.status))return a("failed loading "+r,!0,{resourceNotExisting:i});if(t&&(500<=t.status&&t.status<600||!t.status))return a("failed loading "+r,!0,{resourceNotExisting:i});if(t&&400<=t.status&&t.status<500)return a("failed loading "+r,!1,{resourceNotExisting:i});if(!t&&e&&e.message){var s=e.message.toLowerCase();if(["failed","fetch","network","load"].find(function(e){return-1<s.indexOf(e)}))return a("failed loading "+r+": "+e.message,!0,{resourceNotExisting:i})}if(e)return a(e,!1);try{o="string"==typeof t.data?JSON.parse(t.data):t.data}catch(e){n="failed parsing "+r+" to json"}return n?a(n,!1):c.options.failLoadingOnEmptyJSON&&!Object.keys(o).length?a("loaded result empty for "+r,!1,{resourceNotExisting:i}):void a(null,o,{resourceNotExisting:i})}var c=this;e=(0,l.defaults)(e,this.options),"function"==typeof t&&(a=t,t=void 0),a=a||function(){};if(!this.options.request||0<r.indexOf("/languages/".concat(e.projectId)))return(0,f.default)(e,r,t,o);e=h(r,e,t);v(this.options,e,o)}},{key:"create",value:function(t,o,n,i,s,r){var a=this;"function"!=typeof s&&(s=function(){}),this.checkIfProjectExists(function(e){return e?s(e):(e=(0,l.isMissingOption)(a.options,["projectId","version","apiKey","referenceLng"]))?s(new Error(e)):a.isAddOrUpdateAllowed?((t="string"==typeof t?[t]:t).filter(function(e){return e===a.options.referenceLng}).length<1&&a.services&&a.services.logger&&a.services.logger.warn('locize-backend: will not save missings because the reference language "'.concat(a.options.referenceLng,'" was not in the list of to save languages: ').concat(t.join(", ")," (open your site in the reference language to save missings).")),void t.forEach(function(e){e===a.options.referenceLng&&a.queue.call(a,a.options.referenceLng,o,n,i,s,r)})):s("host is not allowed to create key.")})}},{key:"update",value:function(t,o,n,i,s,r){var a=this;"function"!=typeof s&&(s=function(){}),this.checkIfProjectExists(function(e){return e?s(e):(e=(0,l.isMissingOption)(a.options,["projectId","version","apiKey","referenceLng"]))?s(new Error(e)):a.isAddOrUpdateAllowed?("string"==typeof t&&(t=[t]),(r=r||{}).isUpdate=!0,void t.forEach(function(e){e===a.options.referenceLng&&a.queue.call(a,a.options.referenceLng,o,n,i,s,r)})):s("host is not allowed to update key.")})}},{key:"writePage",value:function(e,t,o,n){function i(e){--d||n(e)}var s=(0,l.interpolate)(this.options.addPath,{lng:e,ns:t,projectId:this.options.projectId,version:this.options.version}),e=(0,l.interpolate)(this.options.updatePath,{lng:e,ns:t,projectId:this.options.projectId,version:this.options.version}),r=!1,a=!1,c={},u={},d=(o.forEach(function(e){var t=e.options&&e.options.tDescription?{value:e.fallbackValue||"",context:{text:e.options.tDescription}}:e.fallbackValue||"";e.options&&e.options.isUpdate?(a=a||!0,u[e.key]=t):(r=r||!0,c[e.key]=t)}),0);r&&d++,a&&d++;d||i(),r&&(this.options.request?(t=h(s,(0,l.defaults)({authorize:!0},this.options),c),v(this.options,t,i)):(0,f.default)((0,l.defaults)({authorize:!0},this.options),s,c,i)),a&&(this.options.request?(o=h(e,(0,l.defaults)({authorize:!0},this.options),u),v(this.options,o,i)):(0,f.default)((0,l.defaults)({authorize:!0},this.options),e,u,i))}},{key:"write",value:function(e,t){var o=this,n=(0,l.getPath)(this.queuedWrites,["locks",e,t]);if(!n){var i=(0,l.getPath)(this.queuedWrites,[e,t]),s=((0,l.setPath)(this.queuedWrites,[e,t],[]),i.filter(function(e){return e.callback}).map(function(e){return e.callback}));if(i.length){(0,l.setPath)(this.queuedWrites,["locks",e,t],!0);var r=function(){(0,l.setPath)(o.queuedWrites,["locks",e,t],!1),s.forEach(function(e){return e()}),o.options.onSaved&&o.options.onSaved(e,t),o.debouncedProcess(e,t)},a=i.length/1e3,c=0,u=i.splice(0,1e3);for(this.writePage(e,t,u,function(){a<=++c&&r()});1e3===u.length;)(u=i.splice(0,1e3)).length&&this.writePage(e,t,u,function(){a<=++c&&r()})}}}},{key:"process",value:function(){var o=this;Object.keys(this.queuedWrites).forEach(function(t){"locks"!==t&&Object.keys(o.queuedWrites[t]).forEach(function(e){o.queuedWrites[t][e].length&&o.write(t,e)})})}},{key:"queue",value:function(e,t,o,n,i,s){(0,l.pushPath)(this.queuedWrites,[e,t],{key:o,fallbackValue:n||"",callback:i,options:s}),this.debouncedProcess()}}])&&n(e.prototype,i),s&&n(e,s),Object.defineProperty(e,"prototype",{writable:!1});var s=e;s.type="backend",o.default=s;t.exports=o.default},{"./request.js":2,"./utils.js":3}],2:[function(e,t,o){!function(r){!function(){function d(e){return(d="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}Object.defineProperty(o,"__esModule",{value:!0}),o.default=void 0;var l,f,p="function"==typeof fetch?fetch:void 0;if(void 0!==r&&r.fetch?p=r.fetch:"undefined"!=typeof window&&window.fetch&&(p=window.fetch),"function"!=typeof XMLHttpRequest&&"object"!==("undefined"==typeof XMLHttpRequest?"undefined":d(XMLHttpRequest))||(void 0!==r&&r.XMLHttpRequest?l=r.XMLHttpRequest:"undefined"!=typeof window&&window.XMLHttpRequest&&(l=window.XMLHttpRequest)),"function"==typeof ActiveXObject&&(void 0!==r&&r.ActiveXObject?f=r.ActiveXObject:"undefined"!=typeof window&&window.ActiveXObject&&(f=window.ActiveXObject)),!(p="function"!=typeof p?void 0:p)&&!l&&!f)try{p=e("cross-fetch")}catch(e){}function g(e,t,o,i){function n(t){var o,n=t.headers&&"Error from cloudfront"===t.headers.get("x-cache");return"standard"!==e.cdnType||404!==t.status||t.headers&&t.headers.get("x-cache")?t.ok?(o=t.headers&&t.headers.get("cache-control"),void t.text().then(function(e){i(null,{status:t.status,data:e,resourceNotExisting:n,cacheControl:o})}).catch(i)):i(t.statusText||"Error",{status:t.status,resourceNotExisting:n}):i(null,{status:200,data:"{}",resourceNotExisting:n=!0})}var s={};"undefined"==typeof window&&void 0!==r&&void 0!==r.process&&r.process.versions&&r.process.versions.node&&(s["User-Agent"]="i18next-locize-backend (node/".concat(r.process.version,"; ").concat(r.process.platform," ").concat(r.process.arch,")")),e.authorize&&e.apiKey&&(s.Authorization=e.apiKey),(o||e.setContentTypeJSON)&&(s["Content-Type"]="application/json"),("function"==typeof fetch?fetch:p)(t,{method:o?"POST":"GET",body:o?JSON.stringify(o):void 0,headers:s}).then(n).catch(i)}var h={};o.default=function(e,n,i,t){"function"==typeof i&&(t=i,i=void 0),t=t||function(){};var s="undefined"==typeof window&&e.useCacheLayer;if(s&&!i&&!e.noCache&&h[n]&&h[n].expires>Date.now())return t(null,h[n].data);var r=t;if(t=function(e,t){var o;s&&!e&&t&&!i&&t.cacheControl&&0<(o=(o=(o=t.cacheControl)&&o.match(/max-age=([0-9]+)/))?parseInt(o[1],10):0)&&(h[n]={data:t,expires:Date.now()+1e3*o}),r(e,t)},!i&&e.noCache&&"standard"===e.cdnType&&(n+=(0<=n.indexOf("?")?"&":"?")+"cache=no"),p)return g(e,n,i,t);if("function"==typeof XMLHttpRequest||"object"===("undefined"==typeof XMLHttpRequest?"undefined":d(XMLHttpRequest))||"function"==typeof ActiveXObject){var o=e,e=n,a=i,c=t;try{var u=l?new l:new f("MSXML2.XMLHTTP.3.0");u.open(a?"POST":"GET",e,1),o.crossDomain||u.setRequestHeader("X-Requested-With","XMLHttpRequest"),o.authorize&&o.apiKey&&u.setRequestHeader("Authorization",o.apiKey),(a||o.setContentTypeJSON)&&u.setRequestHeader("Content-Type","application/json"),u.onreadystatechange=function(){var e="Error from cloudfront"===u.getResponseHeader("x-cache");if("standard"===o.cdnType&&404===u.status&&!u.getResponseHeader("x-cache"))return e=!0,3<u.readyState&&c(null,{status:200,data:"{}",resourceNotExisting:e});var t=u.getResponseHeader("Cache-Control");3<u.readyState&&c(400<=u.status?u.statusText:null,{status:u.status,data:u.responseText,resourceNotExisting:e,cacheControl:t})},u.send(JSON.stringify(a))}catch(e){console&&console.log(e)}}else t(new Error("No fetch and no xhr implementation found!"))};t.exports=o.default}.call(this)}.call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"cross-fetch":4}],3:[function(e,t,o){Object.defineProperty(o,"__esModule",{value:!0}),o.debounce=function(n,i,s){var r;return function(){var e=this,t=arguments,o=s&&!r;clearTimeout(r),r=setTimeout(function(){r=null,s||n.apply(e,t)},i),o&&n.apply(e,t)}},o.defaults=function(o){return n.call(i.call(arguments,1),function(e){if(e)for(var t in e)void 0===o[t]&&(o[t]=e[t])}),o},o.defer=function(){var o,n,e=new Promise(function(e,t){o=e,n=t});return e.resolve=o,e.reject=n,e},o.getPath=function(e,t){e=s(e,t),t=e.obj,e=e.k;if(t)return t[e]},o.interpolate=function(e,t,o){var n,i;for(;n=r.exec(e);)i=(i=(i="string"!=typeof(i=n[1].trim())?(e=>null==e?"":""+e)(i):i)||"").replace(/\$/g,"$$$$"),e=e.replace(n[0],t[i]||i),r.lastIndex=0;return e},o.isMissingOption=a,o.optionExist=function(e,t){return!a(e,t)},o.pushPath=function(e,t,o,n){e=s(e,t,Object),t=e.obj,e=e.k;t[e]=t[e]||[],n&&(t[e]=t[e].concat(o));n||t[e].push(o)},o.setPath=function(e,t,o){e=s(e,t,Object);e.obj[e.k]=o};var o=[],n=o.forEach,i=o.slice;function s(e,t,o){function n(e){return e&&-1<e.indexOf("###")?e.replace(/###/g,"."):e}for(var i="string"!=typeof t?[].concat(t):t.split(".");1<i.length;){if(!e)return{};var s=n(i.shift());!e[s]&&o&&(e[s]=new o),e=e[s]}return e?{obj:e,k:n(i.shift())}:{}}var r=new RegExp("{{(.+?)}}","g");function a(o,e){return e.reduce(function(e,t){return e||(!o||!o[t]||"string"!=typeof o[t]||!o[t].toLowerCase()===t.toLowerCase())&&(e='i18next-locize-backend :: got "'.concat(o[t],'" in options for ').concat(t," which is invalid."),console.warn(e),e)},!1)}},{}],4:[function(e,t,o){},{}]},{},[1])(1)}); \ No newline at end of file +!function(e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).i18nextLocizeBackend=e()}(function(){return function o(i,r,s){function a(t,e){if(!r[t]){if(!i[t]){var n="function"==typeof require&&require;if(!e&&n)return n(t,!0);if(c)return c(t,!0);throw(e=new Error("Cannot find module '"+t+"'")).code="MODULE_NOT_FOUND",e}n=r[t]={exports:{}},i[t][0].call(n.exports,function(e){return a(i[t][1][e]||e)},n,n.exports,o,i,r,s)}return r[t].exports}for(var c="function"==typeof require&&require,e=0;e<s.length;e++)a(s[e]);return a}({1:[function(e,t,n){Object.defineProperty(n,"__esModule",{value:!0}),n.default=void 0;var d=e("./utils.js"),f=(e=e("./request.js"))&&e.__esModule?e:{default:e};function l(e){return(l="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function o(e,t){for(var n=0;n<t.length;n++){var o=t[n];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(e,(e=>(e=((e,t)=>{if("object"!=l(e)||!e)return e;var n=e[Symbol.toPrimitive];if(void 0===n)return("string"===t?String:Number)(e);if("object"!=l(n=n.call(e,t||"default")))return n;throw new TypeError("@@toPrimitive must return a primitive value.")})(e,"string"),"symbol"==l(e)?e:e+""))(o.key),o)}}function p(e){return{loadPath:"https://api".concat("standard"===(e=e||"standard")?".lite":"",".locize.app/{{projectId}}/{{version}}/{{lng}}/{{ns}}"),privatePath:"https://api".concat("standard"===e?".lite":"",".locize.app/private/{{projectId}}/{{version}}/{{lng}}/{{ns}}"),getLanguagesPath:"https://api".concat("standard"===e?".lite":"",".locize.app/languages/{{projectId}}"),addPath:"https://api".concat("standard"===e?".lite":"",".locize.app/missing/{{projectId}}/{{version}}/{{lng}}/{{ns}}"),updatePath:"https://api".concat("standard"===e?".lite":"",".locize.app/update/{{projectId}}/{{version}}/{{lng}}/{{ns}}")}}try{var g="undefined"!=typeof window&&null!==window.localStorage,i="notExistingLocizeProject";window.localStorage.setItem(i,"foo"),window.localStorage.removeItem(i)}catch(e){g=!1}function h(e,t,n){var o={};return t.authorize&&t.apiKey&&(o.Authorization=t.apiKey),(n||t.setContentTypeJSON)&&(o["Content-Type"]="application/json"),{method:n?"POST":"GET",url:e,headers:o,body:n}}function v(e,t,n){if(1===e.request.length)try{var o=e.request(t);o&&"function"==typeof o.then?o.then(function(e){return n(null,e)}).catch(n):n(null,o)}catch(e){n(e)}else e.request(t,n)}function c(e){var t=.25*e,n=Math.max(0,e-t),e=e+t;return Math.floor(n+Math.random()*(e-n))}e=function e(t){var n=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:{},i=3<arguments.length?arguments[3]:void 0,r=this,s=e;if(!(r instanceof s))throw new TypeError("Cannot call a class as a function");this.services=t,this.options=n,this.allOptions=o,this.type="backend",t&&t.projectId?this.init(null,t,o,n):this.init(t,n,o,i)},(i=[{key:"init",value:function(e){var n,o=this,i=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},t=2<arguments.length&&void 0!==arguments[2]?arguments[2]:{},r=3<arguments.length?arguments[3]:void 0,s=(!i.referenceLng&&t.fallbackLng&&Array.isArray(t.fallbackLng)&&"dev"!==t.fallbackLng[0]&&(i.referenceLng=t.fallbackLng[0]),this.services=e,(0,d.defaults)({},i)),a=(0,d.defaults)(i,this.options||{}),c=(c=a.cdnType,(0,d.defaults)({cdnType:c=c||"standard",noCache:!1,referenceLng:"en",crossDomain:!0,setContentTypeJSON:!1,version:"latest",private:!1,translatedPercentageThreshold:.9,failLoadingOnEmptyJSON:!1,allowedAddOrUpdateHosts:["localhost"],onSaved:!1,reloadInterval:"undefined"==typeof window&&36e5,checkForProjectTimeout:3e3,storageExpiration:36e5,writeDebounce:5e3,useCacheLayer:"undefined"==typeof window},p(c))),u=(a.reloadInterval&&a.reloadInterval<3e5&&(console.warn("Your configured reloadInterval option is to low."),a.reloadInterval=c.reloadInterval),this.options=(0,d.defaults)(i,this.options||{},c),this.allOptions=t,this.somethingLoaded=!1,this.isProjectNotExisting=!1,this.storage=(n=this.options.storageExpiration,a=function(){},c=function(){},g?(a=function(e){window.localStorage.setItem("notExistingLocizeProject_".concat(e),Date.now())},c=function(e){var t=window.localStorage.getItem("notExistingLocizeProject_".concat(e));return!(!t||Date.now()-t>n&&(window.localStorage.removeItem("notExistingLocizeProject_".concat(e)),1))}):"undefined"!=typeof document&&(a=function(e){var t=new Date,t=(t.setTime(t.getTime()+n),"; expires=".concat(t.toGMTString())),e="notExistingLocizeProject_".concat(e);try{document.cookie="".concat(e,"=").concat(Date.now()).concat(t,";path=/")}catch(e){}},c=function(e){var e="notExistingLocizeProject_".concat(e),t="".concat(e,"=");try{for(var n=document.cookie.split(";"),o=0;o<n.length;o++){for(var i=n[o];" "===i.charAt(0);)i=i.substring(1,i.length);if(0===i.indexOf(t))return!0}}catch(e){}return!1}),{setProjectNotExisting:a,isProjectNotExisting:c}),p(this.options.cdnType)),a=(Object.keys(u).forEach(function(e){s[e]||(o.options[e]=u[e])}),t.debug&&void 0===s.noCache&&"standard"===this.options.cdnType&&(this.options.noCache=!0),this.options.noCache&&"standard"!==this.options.cdnType&&console.warn("The 'noCache' option is not available for 'cdnType' '".concat(this.options.cdnType,"'!")),"undefined"!=typeof window&&window.location&&window.location.hostname);a?(this.isAddOrUpdateAllowed="function"==typeof this.options.allowedAddOrUpdateHosts?this.options.allowedAddOrUpdateHosts(a):-1<this.options.allowedAddOrUpdateHosts.indexOf(a),e&&e.logger&&(t.saveMissing||t.updateMissing)&&(this.isAddOrUpdateAllowed?"localhost"!==a&&e.logger.warn('locize-backend: you are using the save or update missings feature from this host "'.concat(a,'".\nMake sure you will not use it in production!\nhttps://www.locize.com/docs/going-to-production')):e.logger.warn("function"==typeof this.options.allowedAddOrUpdateHosts?'locize-backend: will not save or update missings because allowedAddOrUpdateHosts returned false for the host "'.concat(a,'".'):'locize-backend: will not save or update missings because the host "'.concat(a,'" was not in the list of allowedAddOrUpdateHosts: ').concat(this.options.allowedAddOrUpdateHosts.join(", ")," (matches need to be exact).")))):this.isAddOrUpdateAllowed=!0,"function"==typeof r&&this.getOptions(function(e,t,n){if(e)return r(e);o.options.referenceLng=i.referenceLng||t.referenceLng||o.options.referenceLng,r(null,t,n)}),this.queuedWrites={pending:{}},this.debouncedProcess=(0,d.debounce)(this.process,this.options.writeDebounce),this.interval&&clearInterval(this.interval),this.options.reloadInterval&&this.options.projectId&&(this.interval=setInterval(function(){return o.reload()},this.options.reloadInterval),"object"===l(this.interval))&&"function"==typeof this.interval.unref&&this.interval.unref()}},{key:"reload",value:function(){var t,e,n=this,o=this.services||{logger:console},i=o.backendConnector,r=o.languageUtils,s=o.logger;!i||(o=i.language)&&"cimode"===o.toLowerCase()||(t=[],(e=function(e){r.toResolveHierarchy(e).forEach(function(e){t.indexOf(e)<0&&t.push(e)})})(o),this.allOptions.preload&&this.allOptions.preload.forEach(e),t.forEach(function(o){n.allOptions.ns.forEach(function(n){i.read(o,n,"read",null,null,function(e,t){e&&s.warn("loading namespace ".concat(n," for language ").concat(o," failed"),e),!e&&t&&s.log("loaded namespace ".concat(n," for language ").concat(o),t),i.loaded("".concat(o,"|").concat(n),e,t)})})}))}},{key:"getLanguages",value:function(e){var n,s=this,t=(e||(n=(0,d.defer)(),e=function(e,t){if(e)return n.reject(e);n.resolve(t)}),(0,d.isMissingOption)(this.options,["projectId"]));return t?e(new Error(t)):null==(t=(0,d.interpolateUrl)(this.options.getLanguagesPath,{projectId:this.options.projectId}))?e(new Error("i18next-locize-backend: unsafe projectId — refusing to build request URL for projectId="+(0,d.sanitizeLogValue)(String(this.options.projectId)))):(!this.isProjectNotExisting&&this.storage.isProjectNotExisting(this.options.projectId)&&(this.isProjectNotExisting=!0),this.isProjectNotExisting?e(new Error(this.isProjectNotExistingErrorMessage||"Locize project ".concat(this.options.projectId," does not exist!"))):(this.getLanguagesCalls=this.getLanguagesCalls||[],this.getLanguagesCalls.push(e),1<this.getLanguagesCalls.length||this.loadUrl({},t,function(t,n,e){var i,r;if(!s.somethingLoaded&&e&&e.resourceNotExisting)return s.isProjectNotExisting=!0,i="Locize project ".concat(s.options.projectId," does not exist!"),s.isProjectNotExistingErrorMessage=i,r="standard"===s.options.cdnType?"pro":"standard",e=p(r),null==(e=(0,d.interpolateUrl)(e.getLanguagesPath,{projectId:s.options.projectId}))?void 0:void s.loadUrl({},e,function(e,t,n){e||!t||n&&n.resourceNotExisting?!s.somethingLoaded&&n&&n.resourceNotExisting&&(s.isProjectNotExisting=!0,s.storage.setProjectNotExisting(s.options.projectId)):(i+=" It seems you're using the wrong cdnType. Your Locize project is configured to use \"".concat(r,'" but here you\'ve configured "').concat(s.options.cdnType,'".'),s.isProjectNotExistingErrorMessage=i);var o=new Error(i),e=s.getLanguagesCalls;s.getLanguagesCalls=[],e.forEach(function(e){return e(o)})});n&&(s.loadedLanguages=Object.keys(n),e=s.loadedLanguages.reduce(function(e,t){return e=n[t].isReferenceLanguage?t:e},""))&&s.options.referenceLng!==e&&(s.options.referenceLng=e),s.somethingLoaded=!0;e=s.getLanguagesCalls;s.getLanguagesCalls=[],e.forEach(function(e){return e(t,n)})}))),n}},{key:"getOptions",value:function(n){var o,i=this;return n||(o=(0,d.defer)(),n=function(e,t){if(e)return o.reject(e);o.resolve(t)}),this.getLanguages(function(e,o){var t;return e?n(e):(e=Object.keys(o)).length?(t=e.reduce(function(e,t){var n=o[t];return n.translated[i.options.version]&&n.translated[i.options.version]>=i.options.translatedPercentageThreshold&&e.push(t),e},[]),e=e.reduce(function(e,t){return-1<t.indexOf("-")||e},!1),void n(null,{fallbackLng:i.options.referenceLng,referenceLng:i.options.referenceLng,supportedLngs:0===t.length&&i.options.referenceLng?[i.options.referenceLng]:t,load:e?"all":"languageOnly"},o)):n(new Error("was unable to load languages via API"))}),o}},{key:"checkIfProjectExists",value:function(t){var e=this,n=(this.services||{logger:console}).logger;this.somethingLoaded?t&&t(null):this.alreadyRequestedCheckIfProjectExists?setTimeout(function(){return e.checkIfProjectExists(t)},c(this.options.checkForProjectTimeout)):(this.alreadyRequestedCheckIfProjectExists=!0,this.getLanguages(function(e){e&&e.message&&0<e.message.indexOf("does not exist")&&n&&n.error(e.message),t&&t(e)}))}},{key:"checkIfLanguagesLoaded",value:function(t){var n=(this.services||{logger:console}).logger;this.loadedLanguages?t&&t(null):this.getLanguages(function(e){e&&e.message&&0<e.message.indexOf("does not exist")&&n&&n.error(e.message),t&&t(e)})}},{key:"read",value:function(o,e,i){var r=this,s=(this.services||{logger:console}).logger,t={};if(this.options.private){var n=(0,d.isMissingOption)(this.options,["projectId","version","apiKey"]);if(n)return i(new Error(n),!1);n=(0,d.interpolateUrl)(this.options.privatePath,{lng:o,ns:e,projectId:this.options.projectId,version:this.options.version}),t={authorize:!0}}else{var a=(0,d.isMissingOption)(this.options,["projectId","version"]);if(a)return i(new Error(a),!1);n=(0,d.interpolateUrl)(this.options.loadPath,{lng:o,ns:e,projectId:this.options.projectId,version:this.options.version})}if(null==n)return i(new Error("i18next-locize-backend: unsafe lng/ns/projectId/version — refusing to build request URL for lng="+(0,d.sanitizeLogValue)(String(o))+" ns="+(0,d.sanitizeLogValue)(String(e))),!1);!this.isProjectNotExisting&&this.storage.isProjectNotExisting(this.options.projectId)&&(this.isProjectNotExisting=!0),this.isProjectNotExisting?(a=new Error(this.isProjectNotExistingErrorMessage||"Locize project ".concat(this.options.projectId," does not exist!")),s&&s.error(a.message),i&&i(a)):this.warnedLanguages&&-1<this.warnedLanguages.indexOf(o)?(e=new Error('Will not continue to load language "'.concat(o,'" since it is not available in Locize project ').concat(this.options.projectId,"!")),s&&s.error(e.message),i&&i(e)):this.loadUrl(t,n,function(e,t,n){n=n&&n.resourceNotExisting;n||(r.hasResourcesForLng||(r.hasResourcesForLng={}),r.hasResourcesForLng[o]=!0),!n||r.hasResourcesForLng&&r.hasResourcesForLng[o]||setTimeout(function(){r.checkIfLanguagesLoaded(function(){!r.loadedLanguages||-1<r.loadedLanguages.indexOf(o)||r.warnedLanguages&&-1<r.warnedLanguages.indexOf(o)||(r.warnedLanguages||(r.warnedLanguages=[]),r.warnedLanguages.push(o),s&&s.error('Language "'.concat(o,'" is not available in Locize project ').concat(r.options.projectId,"!")))})},c(r.options.checkForProjectTimeout)),r.somethingLoaded||(n?setTimeout(function(){return r.checkIfProjectExists()},c(r.options.checkForProjectTimeout)):r.somethingLoaded=!0),i(e,t)})}},{key:"loadUrl",value:function(e,s,t,a){function n(e,t){var n,o,i=t&&t.resourceNotExisting;if(t&&(408===t.status||400===t.status))return a("failed loading "+s,!0,{resourceNotExisting:i});if(t&&(500<=t.status&&t.status<600||!t.status))return a("failed loading "+s,!0,{resourceNotExisting:i});if(t&&400<=t.status&&t.status<500)return a("failed loading "+s,!1,{resourceNotExisting:i});if(!t&&e&&e.message){var r=e.message.toLowerCase();if(["failed","fetch","network","load"].find(function(e){return-1<r.indexOf(e)}))return a("failed loading "+s+": "+e.message,!0,{resourceNotExisting:i})}if(e)return a(e,!1);try{n="string"==typeof t.data?JSON.parse(t.data):t.data}catch(e){o="failed parsing "+s+" to json"}return o?a(o,!1):c.options.failLoadingOnEmptyJSON&&!Object.keys(n).length?a("loaded result empty for "+s,!1,{resourceNotExisting:i}):void a(null,n,{resourceNotExisting:i})}var c=this;e=(0,d.defaults)(e,this.options),"function"==typeof t&&(a=t,t=void 0),a=a||function(){};if(!this.options.request||0<s.indexOf("/languages/".concat(e.projectId)))return(0,f.default)(e,s,t,n);e=h(s,e,t);v(this.options,e,n)}},{key:"create",value:function(t,n,o,i,r,s){var a=this;"function"!=typeof r&&(r=function(){}),this.checkIfProjectExists(function(e){return e?r(e):(e=(0,d.isMissingOption)(a.options,["projectId","version","apiKey","referenceLng"]))?r(new Error(e)):a.isAddOrUpdateAllowed?((t="string"==typeof t?[t]:t).filter(function(e){return e===a.options.referenceLng}).length<1&&a.services&&a.services.logger&&a.services.logger.warn('locize-backend: will not save missings because the reference language "'.concat(a.options.referenceLng,'" was not in the list of to save languages: ').concat(t.join(", ")," (open your site in the reference language to save missings).")),void t.forEach(function(e){e===a.options.referenceLng&&a.queue.call(a,a.options.referenceLng,n,o,i,r,s)})):r("host is not allowed to create key.")})}},{key:"update",value:function(t,n,o,i,r,s){var a=this;"function"!=typeof r&&(r=function(){}),this.checkIfProjectExists(function(e){return e?r(e):(e=(0,d.isMissingOption)(a.options,["projectId","version","apiKey","referenceLng"]))?r(new Error(e)):a.isAddOrUpdateAllowed?("string"==typeof t&&(t=[t]),(s=s||{}).isUpdate=!0,void t.forEach(function(e){e===a.options.referenceLng&&a.queue.call(a,a.options.referenceLng,n,o,i,r,s)})):r("host is not allowed to update key.")})}},{key:"writePage",value:function(e,t,n,o){var i,r,s,a,c,u=(0,d.interpolateUrl)(this.options.addPath,{lng:e,ns:t,projectId:this.options.projectId,version:this.options.version}),l=(0,d.interpolateUrl)(this.options.updatePath,{lng:e,ns:t,projectId:this.options.projectId,version:this.options.version});null==u||null==l?"function"==typeof o&&o(new Error("i18next-locize-backend: unsafe lng/ns/projectId/version — refusing to persist missing keys for lng="+(0,d.sanitizeLogValue)(String(e))+" ns="+(0,d.sanitizeLogValue)(String(t)))):(r=i=!1,s={},a={},n.forEach(function(e){var t=e.options&&e.options.tDescription?{value:e.fallbackValue||"",context:{text:e.options.tDescription}}:e.fallbackValue||"";e.options&&e.options.isUpdate?(r=r||!0,a[e.key]=t):(i=i||!0,s[e.key]=t)}),c=0,i&&c++,r&&c++,e=function(e){--c||o(e)},c||e(),i&&(this.options.request?(t=h(u,(0,d.defaults)({authorize:!0},this.options),s),v(this.options,t,e)):(0,f.default)((0,d.defaults)({authorize:!0},this.options),u,s,e)),r&&(this.options.request?(n=h(l,(0,d.defaults)({authorize:!0},this.options),a),v(this.options,n,e)):(0,f.default)((0,d.defaults)({authorize:!0},this.options),l,a,e)))}},{key:"write",value:function(e,t){var n=this,o=(0,d.getPath)(this.queuedWrites,["locks",e,t]);if(!o){var i=(0,d.getPath)(this.queuedWrites,[e,t]),r=((0,d.setPath)(this.queuedWrites,[e,t],[]),i.filter(function(e){return e.callback}).map(function(e){return e.callback}));if(i.length){(0,d.setPath)(this.queuedWrites,["locks",e,t],!0);var s=function(){(0,d.setPath)(n.queuedWrites,["locks",e,t],!1),r.forEach(function(e){return e()}),n.options.onSaved&&n.options.onSaved(e,t),n.debouncedProcess(e,t)},a=i.length/1e3,c=0,u=i.splice(0,1e3);for(this.writePage(e,t,u,function(){a<=++c&&s()});1e3===u.length;)(u=i.splice(0,1e3)).length&&this.writePage(e,t,u,function(){a<=++c&&s()})}}}},{key:"process",value:function(){var n=this;Object.keys(this.queuedWrites).forEach(function(t){"locks"!==t&&Object.keys(n.queuedWrites[t]).forEach(function(e){n.queuedWrites[t][e].length&&n.write(t,e)})})}},{key:"queue",value:function(e,t,n,o,i,r){(0,d.pushPath)(this.queuedWrites,[e,t],{key:n,fallbackValue:o||"",callback:i,options:r}),this.debouncedProcess()}}])&&o(e.prototype,i),r&&o(e,r),Object.defineProperty(e,"prototype",{writable:!1});var r=e;r.type="backend",n.default=r;t.exports=n.default},{"./request.js":2,"./utils.js":3}],2:[function(e,t,n){!function(s){!function(){function l(e){return(l="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}Object.defineProperty(n,"__esModule",{value:!0}),n.default=void 0;var d,f,p="function"==typeof fetch?fetch:void 0;if(void 0!==s&&s.fetch?p=s.fetch:"undefined"!=typeof window&&window.fetch&&(p=window.fetch),"function"!=typeof XMLHttpRequest&&"object"!==("undefined"==typeof XMLHttpRequest?"undefined":l(XMLHttpRequest))||(void 0!==s&&s.XMLHttpRequest?d=s.XMLHttpRequest:"undefined"!=typeof window&&window.XMLHttpRequest&&(d=window.XMLHttpRequest)),"function"==typeof ActiveXObject&&(void 0!==s&&s.ActiveXObject?f=s.ActiveXObject:"undefined"!=typeof window&&window.ActiveXObject&&(f=window.ActiveXObject)),!(p="function"!=typeof p?void 0:p)&&!d&&!f)try{p=e("cross-fetch")}catch(e){}function g(e,t,n,i){function o(t){var n,o=t.headers&&"Error from cloudfront"===t.headers.get("x-cache");return"standard"!==e.cdnType||404!==t.status||t.headers&&t.headers.get("x-cache")?t.ok?(n=t.headers&&t.headers.get("cache-control"),void t.text().then(function(e){i(null,{status:t.status,data:e,resourceNotExisting:o,cacheControl:n})}).catch(i)):i(t.statusText||"Error",{status:t.status,resourceNotExisting:o}):i(null,{status:200,data:"{}",resourceNotExisting:o=!0})}var r={};"undefined"==typeof window&&void 0!==s&&void 0!==s.process&&s.process.versions&&s.process.versions.node&&(r["User-Agent"]="i18next-locize-backend (node/".concat(s.process.version,"; ").concat(s.process.platform," ").concat(s.process.arch,")")),e.authorize&&e.apiKey&&(r.Authorization=e.apiKey),(n||e.setContentTypeJSON)&&(r["Content-Type"]="application/json"),("function"==typeof fetch?fetch:p)(t,{method:n?"POST":"GET",body:n?JSON.stringify(n):void 0,headers:r}).then(o).catch(i)}var h={};n.default=function(e,o,i,t){"function"==typeof i&&(t=i,i=void 0),t=t||function(){};var r="undefined"==typeof window&&e.useCacheLayer;if(r&&!i&&!e.noCache&&h[o]&&h[o].expires>Date.now())return t(null,h[o].data);var s=t;if(t=function(e,t){var n;r&&!e&&t&&!i&&t.cacheControl&&0<(n=(n=(n=t.cacheControl)&&n.match(/max-age=([0-9]+)/))?parseInt(n[1],10):0)&&(h[o]={data:t,expires:Date.now()+1e3*n}),s(e,t)},!i&&e.noCache&&"standard"===e.cdnType&&(o+=(0<=o.indexOf("?")?"&":"?")+"cache=no"),p)return g(e,o,i,t);if("function"==typeof XMLHttpRequest||"object"===("undefined"==typeof XMLHttpRequest?"undefined":l(XMLHttpRequest))||"function"==typeof ActiveXObject){var n=e,e=o,a=i,c=t;try{var u=d?new d:new f("MSXML2.XMLHTTP.3.0");u.open(a?"POST":"GET",e,1),n.crossDomain||u.setRequestHeader("X-Requested-With","XMLHttpRequest"),n.authorize&&n.apiKey&&u.setRequestHeader("Authorization",n.apiKey),(a||n.setContentTypeJSON)&&u.setRequestHeader("Content-Type","application/json"),u.onreadystatechange=function(){var e="Error from cloudfront"===u.getResponseHeader("x-cache");if("standard"===n.cdnType&&404===u.status&&!u.getResponseHeader("x-cache"))return e=!0,3<u.readyState&&c(null,{status:200,data:"{}",resourceNotExisting:e});var t=u.getResponseHeader("Cache-Control");3<u.readyState&&c(400<=u.status?u.statusText:null,{status:u.status,data:u.responseText,resourceNotExisting:e,cacheControl:t})},u.send(JSON.stringify(a))}catch(e){console&&console.log(e)}}else t(new Error("No fetch and no xhr implementation found!"))};t.exports=n.default}.call(this)}.call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"cross-fetch":4}],3:[function(e,t,n){function c(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,o=Array(t);n<t;n++)o[n]=e[n];return o}Object.defineProperty(n,"__esModule",{value:!0}),n.UNSAFE_KEYS=void 0,n.debounce=function(o,i,r){var s;return function(){var e=this,t=arguments,n=r&&!s;clearTimeout(s),s=setTimeout(function(){s=null,r||o.apply(e,t)},i),n&&o.apply(e,t)}},n.defaults=function(i){return r.call(s.call(arguments,1),function(e){if(e)for(var t=0,n=Object.keys(e);t<n.length;t++){var o=n[t];-1<u.indexOf(o)||void 0===i[o]&&(i[o]=e[o])}}),i},n.defer=function(){var n,o,e=new Promise(function(e,t){n=e,o=t});return e.resolve=n,e.reject=o,e},n.getPath=function(e,t){e=i(e,t),t=e.obj,e=e.k;if(t)return t[e]},n.interpolate=function(e,t,n){var o;for(;o=d.exec(e);){i=(i=(i="string"!=typeof(i=o[1].trim())?f(i):i)||"").replace(/\$/g,"$$$$");var i=!(-1<u.indexOf(i))&&t[i]||i;e=e.replace(o[0],i),d.lastIndex=0}return e},n.interpolateUrl=function(e,t){var n,o=!1;for(;n=d.exec(e);){var i=n[1].trim();if(-1<u.indexOf(i));else{i=t[i];if(null!=i){var r,i=f(i).split("+"),s=!0,a=((e,t)=>{var n,o,i,r,s="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(s)return i=!(o=!0),{s:function(){s=s.call(e)},n:function(){var e=s.next();return o=e.done,e},e:function(e){i=!0,n=e},f:function(){try{o||null==s.return||s.return()}finally{if(i)throw n}}};if(Array.isArray(e)||(s=((e,t)=>{var n;if(e)return"string"==typeof e?c(e,t):"Map"===(n="Object"===(n={}.toString.call(e).slice(8,-1))&&e.constructor?e.constructor.name:n)||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?c(e,t):void 0})(e))||t&&e&&"number"==typeof e.length)return s&&(e=s),r=0,{s:t=function(){},n:function(){return r>=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:t};throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")})(i);try{for(a.s();!(r=a.n()).done;)if(!l(r.value)){s=!1;break}}catch(e){a.e(e)}finally{a.f()}if(!s){o=!0;break}e=e.replace(n[0],i.join("+"))}}d.lastIndex=0}return d.lastIndex=0,o?null:e},n.isMissingOption=a,n.isSafeUrlSegment=l,n.optionExist=function(e,t){return!a(e,t)},n.pushPath=function(e,t,n,o){e=i(e,t,Object),t=e.obj,e=e.k;t[e]=t[e]||[],o&&(t[e]=t[e].concat(n));o||t[e].push(n)},n.redactUrlCredentials=function(t){if("string"!=typeof t||0===t.length)return t;try{var e=new URL(t);return e.username||e.password?(e.username="",e.password="",e.toString()):t}catch(e){return t.replace(/(\/\/)[^/@\s]+@/g,"$1")}},n.sanitizeLogValue=function(e){return"string"!=typeof e?e:e.replace(/[\r\n\x00-\x1F\x7F]/g," ")},n.setPath=function(e,t,n){e=i(e,t,Object);e.obj[e.k]=n};var o=[],r=o.forEach,s=o.slice,u=n.UNSAFE_KEYS=["__proto__","constructor","prototype"];function l(e){return"string"==typeof e&&!(0===e.length||128<e.length||-1<u.indexOf(e)||-1<e.indexOf("..")||-1<e.indexOf("/")||-1<e.indexOf("\\")||/[?#%\s@]/.test(e)||/[\x00-\x1F\x7F]/.test(e))}function i(e,t,n){function o(e){return e&&-1<e.indexOf("###")?e.replace(/###/g,"."):e}for(var i="string"!=typeof t?[].concat(t):t.split(".");1<i.length;){if(!e)return{};var r=o(i.shift());!e[r]&&n&&(e[r]=new n),e=e[r]}return e?{obj:e,k:o(i.shift())}:{}}var d=new RegExp("{{(.+?)}}","g");function f(e){return null==e?"":""+e}function a(n,e){return e.reduce(function(e,t){return e||(!n||!n[t]||"string"!=typeof n[t]||!n[t].toLowerCase()===t.toLowerCase())&&(e='i18next-locize-backend :: got "'.concat(n[t],'" in options for ').concat(t," which is invalid."),console.warn(e),e)},!1)}},{}],4:[function(e,t,n){},{}]},{},[1])(1)}); \ No newline at end of file
lib/index.js+24 −7 modified@@ -1,4 +1,4 @@ -import { defaults, debounce, isMissingOption, interpolate, getPath, setPath, pushPath, defer } from './utils.js' +import { defaults, debounce, isMissingOption, interpolateUrl, getPath, setPath, pushPath, defer, sanitizeLogValue } from './utils.js' import request from './request.js' const getApiPaths = (cdnType) => { @@ -268,9 +268,13 @@ class I18NextLocizeBackend { return deferred } - const url = interpolate(this.options.getLanguagesPath, { + const url = interpolateUrl(this.options.getLanguagesPath, { projectId: this.options.projectId }) + if (url == null) { + callback(new Error('i18next-locize-backend: unsafe projectId — refusing to build request URL for projectId=' + sanitizeLogValue(String(this.options.projectId)))) + return deferred + } if (!this.isProjectNotExisting && this.storage.isProjectNotExisting(this.options.projectId)) { this.isProjectNotExisting = true @@ -293,9 +297,10 @@ class I18NextLocizeBackend { this.isProjectNotExistingErrorMessage = errMsg const cdnTypeAlt = this.options.cdnType === 'standard' ? 'pro' : 'standard' const otherEndpointApiPaths = getApiPaths(cdnTypeAlt) - const urlAlt = interpolate(otherEndpointApiPaths.getLanguagesPath, { + const urlAlt = interpolateUrl(otherEndpointApiPaths.getLanguagesPath, { projectId: this.options.projectId }) + if (urlAlt == null) return // already rejected above; defensive no-op this.loadUrl({}, urlAlt, (errAlt, retAlt, infoAlt) => { if (!errAlt && retAlt && (!infoAlt || !infoAlt.resourceNotExisting)) { errMsg += ` It seems you're using the wrong cdnType. Your Locize project is configured to use "${cdnTypeAlt}" but here you've configured "${this.options.cdnType}".` @@ -412,7 +417,7 @@ class I18NextLocizeBackend { const isMissing = isMissingOption(this.options, ['projectId', 'version', 'apiKey']) if (isMissing) return callback(new Error(isMissing), false) - url = interpolate(this.options.privatePath, { + url = interpolateUrl(this.options.privatePath, { lng: language, ns: namespace, projectId: this.options.projectId, @@ -423,14 +428,18 @@ class I18NextLocizeBackend { const isMissing = isMissingOption(this.options, ['projectId', 'version']) if (isMissing) return callback(new Error(isMissing), false) - url = interpolate(this.options.loadPath, { + url = interpolateUrl(this.options.loadPath, { lng: language, ns: namespace, projectId: this.options.projectId, version: this.options.version }) } + if (url == null) { + return callback(new Error('i18next-locize-backend: unsafe lng/ns/projectId/version — refusing to build request URL for lng=' + sanitizeLogValue(String(language)) + ' ns=' + sanitizeLogValue(String(namespace))), false) + } + if (!this.isProjectNotExisting && this.storage.isProjectNotExisting(this.options.projectId)) { this.isProjectNotExisting = true } @@ -613,18 +622,26 @@ class I18NextLocizeBackend { } writePage (lng, namespace, missings, callback) { - const missingUrl = interpolate(this.options.addPath, { + const missingUrl = interpolateUrl(this.options.addPath, { lng, ns: namespace, projectId: this.options.projectId, version: this.options.version }) - const updatesUrl = interpolate(this.options.updatePath, { + const updatesUrl = interpolateUrl(this.options.updatePath, { lng, ns: namespace, projectId: this.options.projectId, version: this.options.version }) + if (missingUrl == null || updatesUrl == null) { + // Drop the queued writes silently rather than POST to an attacker-crafted + // URL. Log-safe values only. + if (typeof callback === 'function') { + callback(new Error('i18next-locize-backend: unsafe lng/ns/projectId/version — refusing to persist missing keys for lng=' + sanitizeLogValue(String(lng)) + ' ns=' + sanitizeLogValue(String(namespace)))) + } + return + } let hasMissing = false let hasUpdates = false
lib/utils.js+84 −2 modified@@ -2,17 +2,60 @@ const arr = [] const each = arr.forEach const slice = arr.slice +export const UNSAFE_KEYS = ['__proto__', 'constructor', 'prototype'] + export function defaults (obj) { each.call(slice.call(arguments, 1), (source) => { if (source) { - for (const prop in source) { + for (const prop of Object.keys(source)) { + if (UNSAFE_KEYS.indexOf(prop) > -1) continue if (obj[prop] === undefined) obj[prop] = source[prop] } } }) return obj } +// Returns true if `v` can be safely interpolated into a URL path segment. +// Denylist approach — blocks path traversal, path separators, URL-structure +// characters, control characters, prototype keys, and oversized inputs. +// `+` is allowed (used by callers to join multiple languages/namespaces). +export function isSafeUrlSegment (v) { + if (typeof v !== 'string') return false + if (v.length === 0 || v.length > 128) return false + if (UNSAFE_KEYS.indexOf(v) > -1) return false + if (v.indexOf('..') > -1) return false + if (v.indexOf('/') > -1 || v.indexOf('\\') > -1) return false + if (/[?#%\s@]/.test(v)) return false + // eslint-disable-next-line no-control-regex + if (/[\x00-\x1F\x7F]/.test(v)) return false + return true +} + +// Strip control characters from a string before it goes into an error +// message / log line (CWE-117). +export function sanitizeLogValue (v) { + if (typeof v !== 'string') return v + // eslint-disable-next-line no-control-regex + return v.replace(/[\r\n\x00-\x1F\x7F]/g, ' ') +} + +// Redact user:password from a URL-like string before logging it. +export function redactUrlCredentials (u) { + if (typeof u !== 'string' || u.length === 0) return u + try { + const parsed = new URL(u) + if (parsed.username || parsed.password) { + parsed.username = '' + parsed.password = '' + return parsed.toString() + } + return u + } catch (e) { + return u.replace(/(\/\/)[^/@\s]+@/g, '$1') + } +} + export function debounce (func, wait, immediate) { let timeout return function () { @@ -92,12 +135,51 @@ export function interpolate (str, data, lng) { if (typeof value !== 'string') value = makeString(value) if (!value) value = '' value = regexSafe(value) - str = str.replace(match[0], data[value] || value) + // Skip prototype-chain key lookups on `data` — a polluted + // Object.prototype.__proto__ would otherwise leak into the substitution. + const subst = UNSAFE_KEYS.indexOf(value) > -1 ? value : (data[value] || value) + str = str.replace(match[0], subst) regexp.lastIndex = 0 } return str } +// URL-specific variant: reject values that fail isSafeUrlSegment. Returns +// `null` if any substitution is unsafe — callers bail out rather than issue +// the HTTP request. Multi-value `+` joins are validated per segment. +export function interpolateUrl (str, data) { + let match + let unsafe = false + // eslint-disable-next-line no-cond-assign + while (match = regexp.exec(str)) { + const key = match[1].trim() + if (UNSAFE_KEYS.indexOf(key) > -1) { + regexp.lastIndex = 0 + continue + } + const raw = data[key] + if (raw == null) { + regexp.lastIndex = 0 + continue + } + const value = makeString(raw) + // validate each + -separated segment independently + const segments = value.split('+') + let segmentsOk = true + for (const seg of segments) { + if (!isSafeUrlSegment(seg)) { segmentsOk = false; break } + } + if (!segmentsOk) { + unsafe = true + break + } + str = str.replace(match[0], segments.join('+')) + regexp.lastIndex = 0 + } + regexp.lastIndex = 0 + return unsafe ? null : str +} + export function isMissingOption (obj, props) { return props.reduce((mem, p) => { if (mem) return mem
package.json+6 −6 modified@@ -42,22 +42,22 @@ }, "types": "./index.d.mts", "devDependencies": { - "@babel/cli": "7.28.3", - "@babel/core": "7.28.5", - "@babel/preset-env": "7.28.5", + "@babel/cli": "7.28.6", + "@babel/core": "7.29.0", + "@babel/preset-env": "7.29.2", "babel-plugin-add-module-exports": "1.0.4", "browserify": "17.0.1", "dtslint": "4.2.1", "eslint": "9.39.1", "eslint-plugin-import": "2.32.0", "expect.js": "0.3.1", - "i18next": "25.7.2", + "i18next": "26.0.6", "json-server": "0.17.4", "mocha": "11.7.5", - "neostandard": "0.12.2", + "neostandard": "0.13.0", "tslint": "5.20.1", "tsd": "0.33.0", - "typescript": "5.9.3", + "typescript": "6.0.3", "uglify-js": "3.19.3", "xmlhttprequest": "1.8.0" },
test/security.spec.js+122 −0 added@@ -0,0 +1,122 @@ +import expect from 'expect.js' +import { + interpolate, + interpolateUrl, + isSafeUrlSegment, + sanitizeLogValue, + redactUrlCredentials +} from '../lib/utils.js' + +// Security tests for the 9.0.2 hardening. + +describe('security', () => { + describe('isSafeUrlSegment', () => { + it('accepts arbitrary language / project / version codes', () => { + expect(isSafeUrlSegment('en')).to.be(true) + expect(isSafeUrlSegment('de-DE')).to.be(true) + expect(isSafeUrlSegment('en_US')).to.be(true) + expect(isSafeUrlSegment('zh-Hant-HK')).to.be(true) + expect(isSafeUrlSegment('pirate-speak')).to.be(true) + expect(isSafeUrlSegment('my-custom.ns')).to.be(true) + // projectId UUID shape + expect(isSafeUrlSegment('abc1d4f3-d0a7-4794-b74f-b72134d82d35')).to.be(true) + // version strings + expect(isSafeUrlSegment('latest')).to.be(true) + expect(isSafeUrlSegment('production')).to.be(true) + }) + it('rejects traversal / separator / URL-structure / control chars / prototype keys', () => { + expect(isSafeUrlSegment('../etc/passwd')).to.be(false) + expect(isSafeUrlSegment('..')).to.be(false) + expect(isSafeUrlSegment('foo/bar')).to.be(false) + expect(isSafeUrlSegment('foo\\bar')).to.be(false) + expect(isSafeUrlSegment('en?admin=true')).to.be(false) + expect(isSafeUrlSegment('en#frag')).to.be(false) + expect(isSafeUrlSegment('en%2F..')).to.be(false) + expect(isSafeUrlSegment('en evil')).to.be(false) + expect(isSafeUrlSegment('en@host')).to.be(false) + expect(isSafeUrlSegment('__proto__')).to.be(false) + expect(isSafeUrlSegment('en\r\nX-Injected: bad')).to.be(false) + expect(isSafeUrlSegment('')).to.be(false) + expect(isSafeUrlSegment('a'.repeat(200))).to.be(false) + }) + }) + + describe('interpolate (guarded)', () => { + it('does not leak prototype-chain properties on __proto__ lookups', () => { + const out = interpolate('x/{{__proto__}}/y', { __proto__: { polluted: true } }) + // Should return the key itself, not dereference __proto__ + expect(out).to.equal('x/__proto__/y') + }) + it('substitutes normal keys', () => { + expect(interpolate('/{{projectId}}/{{lng}}', { projectId: 'abc', lng: 'en' })) + .to.equal('/abc/en') + }) + }) + + describe('interpolateUrl', () => { + const template = 'https://api.locize.app/{{projectId}}/{{version}}/{{lng}}/{{ns}}' + + it('accepts a plain URL', () => { + expect(interpolateUrl(template, { + projectId: 'abc1d4f3-d0a7-4794-b74f-b72134d82d35', + version: 'latest', + lng: 'en', + ns: 'common' + })).to.equal('https://api.locize.app/abc1d4f3-d0a7-4794-b74f-b72134d82d35/latest/en/common') + }) + + it('accepts + joins', () => { + expect(interpolateUrl(template, { + projectId: 'p', version: 'v', lng: 'en+de', ns: 'a+b' + })).to.equal('https://api.locize.app/p/v/en+de/a+b') + }) + + it('returns null for path traversal in lng', () => { + expect(interpolateUrl(template, { + projectId: 'p', version: 'v', lng: '../../etc', ns: 'x' + })).to.equal(null) + }) + + it('returns null for path traversal in projectId', () => { + expect(interpolateUrl(template, { + projectId: '../admin', version: 'v', lng: 'en', ns: 'x' + })).to.equal(null) + }) + + it('returns null for query-string / fragment injection', () => { + expect(interpolateUrl(template, { + projectId: 'p', version: 'v', lng: 'en?admin=true', ns: 'x' + })).to.equal(null) + expect(interpolateUrl(template, { + projectId: 'p', version: 'v', lng: 'en#frag', ns: 'x' + })).to.equal(null) + }) + + it('returns null when any + segment is unsafe', () => { + expect(interpolateUrl(template, { + projectId: 'p', version: 'v', lng: 'en+../etc/passwd', ns: 'x' + })).to.equal(null) + }) + }) + + describe('sanitizeLogValue', () => { + it('strips CR, LF, NUL and other control chars', () => { + expect(sanitizeLogValue('en\r\n2026-04-18 admin login')) + .to.equal('en 2026-04-18 admin login') + expect(sanitizeLogValue('en\u0000')).to.equal('en ') + }) + it('passes non-strings through unchanged', () => { + expect(sanitizeLogValue(undefined)).to.equal(undefined) + expect(sanitizeLogValue(42)).to.equal(42) + }) + }) + + describe('redactUrlCredentials', () => { + it('strips user:password from URLs', () => { + expect(redactUrlCredentials('https://user:pass@api.locize.app/p/v/en/x')) + .to.equal('https://api.locize.app/p/v/en/x') + expect(redactUrlCredentials('https://api.locize.app/p/v/en/x')) + .to.equal('https://api.locize.app/p/v/en/x') + }) + }) +})
tsconfig.json+3 −2 modified@@ -1,13 +1,14 @@ { "compilerOptions": { "module": "commonjs", - "target": "es5", - "lib": ["es6", "dom"], + "target": "es2017", + "lib": ["es2017", "dom"], "jsx": "react", "moduleResolution": "node", "forceConsistentCasingInFileNames": true, "strict": true, "noEmit": true, + "ignoreDeprecations": "6.0", "baseUrl": ".", "paths": { "i18next-locize-backend": ["./index.d.mts"] },
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-mgcp-mfp8-3q45ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-41885ghsaADVISORY
- github.com/locize/i18next-locize-backend/commit/8f81ad4707aa0e90647fde4da5fbe5b23153c6e1ghsaWEB
- github.com/locize/i18next-locize-backend/releases/tag/v9.0.2ghsaWEB
- github.com/locize/i18next-locize-backend/security/advisories/GHSA-mgcp-mfp8-3q45nvdWEB
News mentions
0No linked articles in our index yet.