diff --git a/package-lock.json b/package-lock.json index 039ef60ae8..6e997fc4fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "@mxenabled/connect-widget", "version": "0.0.0-semantic-release", + "hasInstallScript": true, "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", @@ -64,6 +65,7 @@ "jsdom": "^24.1.1", "lint-staged": "^15.2.8", "markdown-eslint-parser": "^1.2.1", + "patch-package": "^8.0.1", "prettier": "3.7.4", "rollup": "^4.59.0", "semantic-release": "^25.0.3", @@ -1631,19 +1633,6 @@ "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/@kyper/text": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@kyper/text/-/text-3.0.0.tgz", - "integrity": "sha512-Z1arAeYl+8deTQyFguImJEq5jUGvW6dkyCP52LHn76uz3PTaZTAn3dfXw7W1Zx5O55V4BYPdL+YOO2IHIajZLQ==", - "peer": true, - "peerDependencies": { - "@kyper/tokenprovider": "^4.0.0", - "@mxenabled/cssinjs": "^0.6.0", - "prop-types": "^15.7.2", - "react": "^16.14.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/@kyper/tokenprovider": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@kyper/tokenprovider/-/tokenprovider-4.0.1.tgz", @@ -5237,6 +5226,13 @@ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.26.tgz", "integrity": "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==" }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -5980,6 +5976,22 @@ "node": ">= 16" } }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/clean-stack": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-5.3.0.tgz", @@ -8141,6 +8153,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-yarn-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "micromatch": "^4.0.2" + } + }, "node_modules/findup-sync": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz", @@ -8525,6 +8547,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/gettext.js/-/gettext.js-2.0.3.tgz", "integrity": "sha512-11gyttZWBUBkenEVgPMqJTL9TIKaH4PW6ZCMZr+lNXrgiYHXBg+bOGAc8OjfLE8lvi0dgwtqrSfScd310PlKJw==", + "license": "MIT", "dependencies": { "po2json": "^1.0.0-beta-3" }, @@ -9409,6 +9432,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -9745,6 +9784,19 @@ "node": ">=0.10.0" } }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -10002,6 +10054,26 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, + "node_modules/json-stable-stringify": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", + "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -10037,6 +10109,16 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "dev": true, + "license": "Public Domain", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -10061,6 +10143,16 @@ "json-buffer": "3.0.1" } }, + "node_modules/klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.11" + } + }, "node_modules/kolorist": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", @@ -13808,6 +13900,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -14153,6 +14262,150 @@ "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", "dev": true }, + "node_modules/patch-package": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz", + "integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@yarnpkg/lockfile": "^1.1.0", + "chalk": "^4.1.2", + "ci-info": "^3.7.0", + "cross-spawn": "^7.0.3", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^10.0.0", + "json-stable-stringify": "^1.0.2", + "klaw-sync": "^6.0.0", + "minimist": "^1.2.6", + "open": "^7.4.2", + "semver": "^7.5.3", + "slash": "^2.0.0", + "tmp": "^0.2.4", + "yaml": "^2.2.2" + }, + "bin": { + "patch-package": "index.js" + }, + "engines": { + "node": ">=14", + "npm": ">5" + } + }, + "node_modules/patch-package/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/patch-package/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/patch-package/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/patch-package/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/patch-package/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/patch-package/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/patch-package/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/patch-package/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/patch-package/node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -15825,6 +16078,16 @@ "node": ">=8" } }, + "node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/slice-ansi": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", diff --git a/package.json b/package.json index b88e7d1c28..eaf6e16aef 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "lint": "eslint . --ext ts,tsx,js,jsx,md --report-unused-disable-directives --max-warnings 14", "preview": "vite preview", "prepare": "husky", + "postinstall": "patch-package", "test": "vitest run", "test:coverage": "vitest run --coverage", "test:watch": "vitest", @@ -97,6 +98,7 @@ "jsdom": "^24.1.1", "lint-staged": "^15.2.8", "markdown-eslint-parser": "^1.2.1", + "patch-package": "^8.0.1", "prettier": "3.7.4", "rollup": "^4.59.0", "semantic-release": "^25.0.3", diff --git a/patches/gettext.js+2.0.3.patch b/patches/gettext.js+2.0.3.patch new file mode 100644 index 0000000000..dddd6d100b --- /dev/null +++ b/patches/gettext.js+2.0.3.patch @@ -0,0 +1,374 @@ +diff --git a/node_modules/gettext.js/dist/gettext.cjs.js b/node_modules/gettext.js/dist/gettext.cjs.js +index 85c83de..01b22ab 100644 +--- a/node_modules/gettext.js/dist/gettext.cjs.js ++++ b/node_modules/gettext.js/dist/gettext.cjs.js +@@ -88,6 +88,93 @@ var i18n = function (options) { + return locale; + }; + ++ var parsePluralExpr = function (str) { ++ var tokens = str.match(/[0-9]+|&&|\|\||==|!=|<=|>=|[()?:!+\-*\/%<>n]/g) || []; ++ var i = 0; ++ function parseTernary() { ++ var cond = parseOr(); ++ if (tokens[i] === '?') { ++ i++; ++ var thenBranch = parseTernary(); ++ i++; // ':' ++ return { type: '?', cond: cond, thenBranch: thenBranch, elseBranch: parseTernary() }; ++ } ++ return cond; ++ } ++ function parseOr() { ++ var node = parseAnd(); ++ while (tokens[i] === '||') { i++; node = { op: '||', left: node, right: parseAnd() }; } ++ return node; ++ } ++ function parseAnd() { ++ var node = parseEquality(); ++ while (tokens[i] === '&&') { i++; node = { op: '&&', left: node, right: parseEquality() }; } ++ return node; ++ } ++ function parseEquality() { ++ var node = parseRelational(); ++ while (tokens[i] === '==' || tokens[i] === '!=') { ++ var op = tokens[i++]; ++ node = { op: op, left: node, right: parseRelational() }; ++ } ++ return node; ++ } ++ function parseRelational() { ++ var node = parseAdd(); ++ while (/^[<>]=?$/.test(tokens[i])) { ++ var op = tokens[i++]; ++ node = { op: op, left: node, right: parseAdd() }; ++ } ++ return node; ++ } ++ function parseAdd() { ++ var node = parseMul(); ++ while (tokens[i] === '+' || tokens[i] === '-') { ++ var op = tokens[i++]; ++ node = { op: op, left: node, right: parseMul() }; ++ } ++ return node; ++ } ++ function parseMul() { ++ var node = parsePrimary(); ++ while (tokens[i] === '*' || tokens[i] === '/' || tokens[i] === '%') { ++ var op = tokens[i++]; ++ node = { op: op, left: node, right: parsePrimary() }; ++ } ++ return node; ++ } ++ function parsePrimary() { ++ if (tokens[i] === '!') { i++; return { op: '!', right: parsePrimary() }; } ++ if (tokens[i] === '(') { i++; var node = parseTernary(); i++; return node; } ++ if (tokens[i] === 'n') { i++; return 'n'; } ++ return Number(tokens[i++]) || 0; ++ } ++ return parseTernary(); ++ }; ++ ++ var evalPluralExpr = function (node, n) { ++ if (node === 'n') return n; ++ if (typeof node === 'number') return node; ++ if (node.type === '?') return evalPluralExpr(node.cond, n) ? evalPluralExpr(node.thenBranch, n) : evalPluralExpr(node.elseBranch, n); ++ if (node.op === '!') return !evalPluralExpr(node.right, n); ++ var left = evalPluralExpr(node.left, n); ++ if (node.op === '&&') return left && evalPluralExpr(node.right, n); ++ if (node.op === '||') return left || evalPluralExpr(node.right, n); ++ var right = evalPluralExpr(node.right, n); ++ if (node.op === '==') return left == right; ++ if (node.op === '!=') return left != right; ++ if (node.op === '<=') return left <= right; ++ if (node.op === '>=') return left >= right; ++ if (node.op === '<') return left < right; ++ if (node.op === '>') return left > right; ++ if (node.op === '+') return left + right; ++ if (node.op === '-') return left - right; ++ if (node.op === '*') return left * right; ++ if (node.op === '/') return right ? Math.floor(left / right) : 0; ++ if (node.op === '%') return right ? left % right : 0; ++ return 0; ++ }; ++ + var getPluralFunc = function (plural_form) { + // Plural form string regexp + // taken from https://github.com/Orange-OpenSource/gettext.js/blob/master/lib.gettext.js +@@ -99,13 +186,14 @@ var i18n = function (options) { + if (!match || match[0] !== plural_form) + throw new Error(strfmt('The plural form "%1" is not valid', plural_form)); + +- console.log('>>> Plural form:', plural_form); ++ var nplurals = parseInt((plural_form.match(/nplurals\s*=\s*([0-9]+)/) || [, 2])[1], 10); ++ var expr = (plural_form.match(/plural\s*=\s*([^;]+)/) || [, 'n != 1'])[1]; ++ var ast = parsePluralExpr(expr); + +- // Careful here, this is a hidden eval() equivalent.. +- // Risk should be reasonable though since we test the plural_form through regex before +- // taken from https://github.com/Orange-OpenSource/gettext.js/blob/master/lib.gettext.js +- // TODO: should test if https://github.com/soney/jsep present and use it if so +- return new Function("n", 'var plural, nplurals; '+ plural_form +' return { nplurals: nplurals, plural: (plural === true ? 1 : (plural ? plural : 0)) };'); ++ return function (n) { ++ var plural = evalPluralExpr(ast, typeof n === 'number' ? n : Number(n) || 0); ++ return { nplurals: nplurals, plural: (plural === true ? 1 : (plural ? plural : 0)) }; ++ }; + }; + + // Proper translation function that handle plurals and directives +diff --git a/node_modules/gettext.js/dist/gettext.cjs.min.js b/node_modules/gettext.js/dist/gettext.cjs.min.js +index 9f5f2e6..ef7d73e 100644 +--- a/node_modules/gettext.js/dist/gettext.cjs.min.js ++++ b/node_modules/gettext.js/dist/gettext.cjs.min.js +@@ -1,2 +1 @@ +-"use strict"; +-/*! gettext.js - Guillaume Potier - MIT Licensed */var i18n=function(t){t=t||{},this&&(this.__version="2.0.0");var r={domain:"messages",locale:"undefined"!=typeof document&&document.documentElement.getAttribute("lang")||"en",plural_func:function(t){return{nplurals:2,plural:1!=t?1:0}},ctxt_delimiter:String.fromCharCode(4)},e=function(t){var r=typeof t;return"function"===r||"object"===r&&!!t},n={},l=t.locale||r.locale,a=t.domain||r.domain,o={},i={},u=t.ctxt_delimiter||r.ctxt_delimiter;t.messages&&(o[a]={},o[a][l]=t.messages),t.plural_forms&&(i[l]=t.plural_forms);var s=function(t){var r=arguments;return t.replace(/%%/g,"%% ").replace(/%(\d+)/g,(function(t,e){return r[e]})).replace(/%% /g,"%")},p=function(t){return-1!==t.indexOf(u)?t.split(u)[1]:t},c=function(t){for(var r=[t],e=t.lastIndexOf("-");e>0;)t=t.slice(0,e),r.push(t),e=t.lastIndexOf("-");return r},f=function(t){var r=(t=t.replace("_","-")).search(/[.@]/);return-1!=r&&(t=t.slice(0,r)),t},d=function(t){var r=new RegExp("^\\s*nplurals\\s*=\\s*[0-9]+\\s*;\\s*plural\\s*=\\s*(?:\\s|[-\\?\\|&=!<>+*/%:;n0-9_()])+"),e=t.match(r);if(!e||e[0]!==t)throw new Error(s('The plural form "%1" is not valid',t));return console.log(">>> Plural form:",t),new Function("n","var plural, nplurals; "+t+" return { nplurals: nplurals, plural: (plural === true ? 1 : (plural ? plural : 0)) };")},g=function(t,r,e){return e.plural_form?(e.plural_func?a=e.plural_func(r):(n[l]||(n[l]=d(i[l])),a=n[l](r)),(void 0===a.plural||a.plural>a.nplurals||t.length<=a.plural)&&(a.plural=0),s.apply(this,[p(t[a.plural])].concat(Array.prototype.slice.call(arguments,3)))):s.apply(this,[p(t[0])].concat(Array.prototype.slice.call(arguments,3)));var a};return{strfmt:s,expand_locale:c,__:function(){return this.gettext.apply(this,arguments)},_n:function(){return this.ngettext.apply(this,arguments)},_p:function(){return this.pgettext.apply(this,arguments)},setMessages:function(t,r,n,l){if(!t||!r||!n)throw new Error("You must provide a domain, a locale and messages");if("string"!=typeof t||"string"!=typeof r||!e(n))throw new Error("Invalid arguments");return r=f(r),l&&(i[r]=l),o[t]||(o[t]={}),o[t][r]=n,this},loadJSON:function(t,n){if(e(t)||(t=JSON.parse(t)),!t[""]||!t[""].language||!t[""]["plural-forms"])throw new Error('Wrong JSON, it must have an empty key ("") with "language" and "plural-forms" information');var l=t[""];return delete t[""],this.setMessages(n||r.domain,l.language,t,l["plural-forms"])},setLocale:function(t){return l=f(t),this},getLocale:function(){return l},textdomain:function(t){return t?(a=t,this):a},gettext:function(t){return this.dcnpgettext.apply(this,[void 0,void 0,t,void 0,void 0].concat(Array.prototype.slice.call(arguments,1)))},ngettext:function(t,r,e){return this.dcnpgettext.apply(this,[void 0,void 0,t,r,e].concat(Array.prototype.slice.call(arguments,3)))},pgettext:function(t,r){return this.dcnpgettext.apply(this,[void 0,t,r,void 0,void 0].concat(Array.prototype.slice.call(arguments,2)))},dcnpgettext:function(t,e,n,i,s){if(t=t||a,"string"!=typeof n)throw new Error(this.strfmt('Msgid "%1" is not a valid translatable string',n));var p,f,d,m={plural_form:!1},h=e?e+u+n:n,y=c(l);for(var v in y)if(d=y[v],f=o[t]&&o[t][d]&&o[t][d][h],f=i?f&&"string"!=typeof o[t][d][h]:f&&"string"==typeof o[t][d][h])break;return f?p=o[t][d][h]:(p=n,m.plural_func=r.plural_func),i?(m.plural_form=!0,g.apply(this,[f?p:[n,i],s,m].concat(Array.prototype.slice.call(arguments,5)))):g.apply(this,[[p],s,m].concat(Array.prototype.slice.call(arguments,5)))}}};module.exports=i18n; +\ No newline at end of file ++"use strict";/*! gettext.js - Guillaume Potier - MIT Licensed */var i18n=function(s){s=s||{},this&&(this.__version="2.0.0");var m={domain:"messages",locale:(typeof document<"u"?document.documentElement.getAttribute("lang"):!1)||"en",plural_func:function(r){return{nplurals:2,plural:r!=1?1:0}},ctxt_delimiter:""},b={isObject:function(r){var e=typeof r;return e==="function"||e==="object"&&!!r},isArray:function(r){return toString.call(r)==="[object Array]"}},_={},p=s.locale||m.locale,d=s.domain||m.domain,a={},w={},A=s.ctxt_delimiter||m.ctxt_delimiter;s.messages&&(a[d]={},a[d][p]=s.messages),s.plural_forms&&(w[p]=s.plural_forms);var x=function(r){var e=arguments;return r.replace(/%%/g,"%% ").replace(/%(\d+)/g,function(t,n){return e[n]}).replace(/%% /g,"%")},E=function(r){if(r.indexOf(A)!==-1){var e=r.split(A);return e[1]}return r},O=function(r){for(var e=[r],t=r.lastIndexOf("-");t>0;)r=r.slice(0,t),e.push(r),t=r.lastIndexOf("-");return e},M=function(r){r=r.replace("_","-");var e=r.search(/[.@]/);return e!=-1&&(r=r.slice(0,e)),r},S=function(r){var e=r.match(/[0-9]+|&&|\|\||==|!=|<=|>=|[()?:!+\-*\/%<>n]/g)||[],t=0;function n(){var u=v();if(e[t]==="?"){t++;var g=n();return t++,{type:"?",cond:u,thenBranch:g,elseBranch:n()}}return u}function v(){for(var u=o();e[t]==="||";)t++,u={op:"||",left:u,right:o()};return u}function o(){for(var u=i();e[t]==="&&";)t++,u={op:"&&",left:u,right:i()};return u}function i(){for(var u=l();e[t]==="=="||e[t]==="!=";){var g=e[t++];u={op:g,left:u,right:l()}}return u}function l(){for(var u=f();/^[<>]=?$/.test(e[t]);){var g=e[t++];u={op:g,left:u,right:f()}}return u}function f(){for(var u=h();e[t]==="+"||e[t]==="-";){var g=e[t++];u={op:g,left:u,right:h()}}return u}function h(){for(var u=y();e[t]==="*"||e[t]==="/"||e[t]==="%";){var g=e[t++];u={op:g,left:u,right:y()}}return u}function y(){if(e[t]==="!")return t++,{op:"!",right:y()};if(e[t]==="("){t++;var u=n();return t++,u}return e[t]==="n"?(t++,"n"):Number(e[t++])||0}return n()},c=function(r,e){if(r==="n")return e;if(typeof r=="number")return r;if(r.type==="?")return c(r.cond,e)?c(r.thenBranch,e):c(r.elseBranch,e);if(r.op==="!")return!c(r.right,e);var t=c(r.left,e);if(r.op==="&&")return t&&c(r.right,e);if(r.op==="||")return t||c(r.right,e);var n=c(r.right,e);return r.op==="=="?t==n:r.op==="!="?t!=n:r.op==="<="?t<=n:r.op===">="?t>=n:r.op==="<"?t"?t>n:r.op==="+"?t+n:r.op==="-"?t-n:r.op==="*"?t*n:r.op==="/"?n?Math.floor(t/n):0:r.op==="%"&&n?t%n:0},k=function(r){var e=new RegExp("^\\s*nplurals\\s*=\\s*[0-9]+\\s*;\\s*plural\\s*=\\s*(?:\\s|[-\\?\\|&=!<>+*/%:;n0-9_()])+"),t=r.match(e);if(!t||t[0]!==r)throw new Error(x('The plural form "%1" is not valid',r));var n=parseInt((r.match(/nplurals\s*=\s*([0-9]+)/)||[,2])[1],10),v=(r.match(/plural\s*=\s*([^;]+)/)||[,"n != 1"])[1],o=S(v);return function(i){var l=c(o,typeof i=="number"?i:Number(i)||0);return{nplurals:n,plural:l===!0?1:l||0}}},N=function(r,e,t){if(!t.plural_form)return x.apply(this,[E(r[0])].concat(Array.prototype.slice.call(arguments,3)));var n;return t.plural_func?n=t.plural_func(e):(_[p]||(_[p]=k(w[p])),n=_[p](e)),(typeof n.plural>"u"||n.plural>n.nplurals||r.length<=n.plural)&&(n.plural=0),x.apply(this,[E(r[n.plural])].concat(Array.prototype.slice.call(arguments,3)))};return{strfmt:x,expand_locale:O,__:function(){return this.gettext.apply(this,arguments)},_n:function(){return this.ngettext.apply(this,arguments)},_p:function(){return this.pgettext.apply(this,arguments)},setMessages:function(r,e,t,n){if(!r||!e||!t)throw new Error("You must provide a domain, a locale and messages");if(typeof r!="string"||typeof e!="string"||!b.isObject(t))throw new Error("Invalid arguments");return e=M(e),n&&(w[e]=n),a[r]||(a[r]={}),a[r][e]=t,this},loadJSON:function(r,e){if(b.isObject(r)||(r=JSON.parse(r)),!r[""]||!r[""].language||!r[""]["plural-forms"])throw new Error('Wrong JSON, it must have an empty key ("") with "language" and "plural-forms" information');var t=r[""];return delete r[""],this.setMessages(e||m.domain,t.language,r,t["plural-forms"])},setLocale:function(r){return p=M(r),this},getLocale:function(){return p},textdomain:function(r){return r?(d=r,this):d},gettext:function(r){return this.dcnpgettext.apply(this,[void 0,void 0,r,void 0,void 0].concat(Array.prototype.slice.call(arguments,1)))},ngettext:function(r,e,t){return this.dcnpgettext.apply(this,[void 0,void 0,r,e,t].concat(Array.prototype.slice.call(arguments,3)))},pgettext:function(r,e){return this.dcnpgettext.apply(this,[void 0,r,e,void 0,void 0].concat(Array.prototype.slice.call(arguments,2)))},dcnpgettext:function(r,e,t,n,v){if(r=r||d,typeof t!="string")throw new Error(this.strfmt('Msgid "%1" is not a valid translatable string',t));var o,i={plural_form:!1},l=e?e+A+t:t,f,h,y=O(p);for(var u in y)if(h=y[u],f=a[r]&&a[r][h]&&a[r][h][l],n?f=f&&typeof a[r][h][l]!="string":f=f&&typeof a[r][h][l]=="string",f)break;return f?o=a[r][h][l]:(o=t,i.plural_func=m.plural_func),n?(i.plural_form=!0,N.apply(this,[f?o:[t,n],v,i].concat(Array.prototype.slice.call(arguments,5)))):N.apply(this,[[o],v,i].concat(Array.prototype.slice.call(arguments,5)))}}};module.exports=i18n; +diff --git a/node_modules/gettext.js/dist/gettext.esm.js b/node_modules/gettext.js/dist/gettext.esm.js +index 416dc97..7466a38 100644 +--- a/node_modules/gettext.js/dist/gettext.esm.js ++++ b/node_modules/gettext.js/dist/gettext.esm.js +@@ -86,6 +86,93 @@ var i18n = function (options) { + return locale; + }; + ++ var parsePluralExpr = function (str) { ++ var tokens = str.match(/[0-9]+|&&|\|\||==|!=|<=|>=|[()?:!+\-*\/%<>n]/g) || []; ++ var i = 0; ++ function parseTernary() { ++ var cond = parseOr(); ++ if (tokens[i] === '?') { ++ i++; ++ var thenBranch = parseTernary(); ++ i++; // ':' ++ return { type: '?', cond: cond, thenBranch: thenBranch, elseBranch: parseTernary() }; ++ } ++ return cond; ++ } ++ function parseOr() { ++ var node = parseAnd(); ++ while (tokens[i] === '||') { i++; node = { op: '||', left: node, right: parseAnd() }; } ++ return node; ++ } ++ function parseAnd() { ++ var node = parseEquality(); ++ while (tokens[i] === '&&') { i++; node = { op: '&&', left: node, right: parseEquality() }; } ++ return node; ++ } ++ function parseEquality() { ++ var node = parseRelational(); ++ while (tokens[i] === '==' || tokens[i] === '!=') { ++ var op = tokens[i++]; ++ node = { op: op, left: node, right: parseRelational() }; ++ } ++ return node; ++ } ++ function parseRelational() { ++ var node = parseAdd(); ++ while (/^[<>]=?$/.test(tokens[i])) { ++ var op = tokens[i++]; ++ node = { op: op, left: node, right: parseAdd() }; ++ } ++ return node; ++ } ++ function parseAdd() { ++ var node = parseMul(); ++ while (tokens[i] === '+' || tokens[i] === '-') { ++ var op = tokens[i++]; ++ node = { op: op, left: node, right: parseMul() }; ++ } ++ return node; ++ } ++ function parseMul() { ++ var node = parsePrimary(); ++ while (tokens[i] === '*' || tokens[i] === '/' || tokens[i] === '%') { ++ var op = tokens[i++]; ++ node = { op: op, left: node, right: parsePrimary() }; ++ } ++ return node; ++ } ++ function parsePrimary() { ++ if (tokens[i] === '!') { i++; return { op: '!', right: parsePrimary() }; } ++ if (tokens[i] === '(') { i++; var node = parseTernary(); i++; return node; } ++ if (tokens[i] === 'n') { i++; return 'n'; } ++ return Number(tokens[i++]) || 0; ++ } ++ return parseTernary(); ++ }; ++ ++ var evalPluralExpr = function (node, n) { ++ if (node === 'n') return n; ++ if (typeof node === 'number') return node; ++ if (node.type === '?') return evalPluralExpr(node.cond, n) ? evalPluralExpr(node.thenBranch, n) : evalPluralExpr(node.elseBranch, n); ++ if (node.op === '!') return !evalPluralExpr(node.right, n); ++ var left = evalPluralExpr(node.left, n); ++ if (node.op === '&&') return left && evalPluralExpr(node.right, n); ++ if (node.op === '||') return left || evalPluralExpr(node.right, n); ++ var right = evalPluralExpr(node.right, n); ++ if (node.op === '==') return left == right; ++ if (node.op === '!=') return left != right; ++ if (node.op === '<=') return left <= right; ++ if (node.op === '>=') return left >= right; ++ if (node.op === '<') return left < right; ++ if (node.op === '>') return left > right; ++ if (node.op === '+') return left + right; ++ if (node.op === '-') return left - right; ++ if (node.op === '*') return left * right; ++ if (node.op === '/') return right ? Math.floor(left / right) : 0; ++ if (node.op === '%') return right ? left % right : 0; ++ return 0; ++ }; ++ + var getPluralFunc = function (plural_form) { + // Plural form string regexp + // taken from https://github.com/Orange-OpenSource/gettext.js/blob/master/lib.gettext.js +@@ -97,13 +184,14 @@ var i18n = function (options) { + if (!match || match[0] !== plural_form) + throw new Error(strfmt('The plural form "%1" is not valid', plural_form)); + +- console.log('>>> Plural form:', plural_form); ++ var nplurals = parseInt((plural_form.match(/nplurals\s*=\s*([0-9]+)/) || [, 2])[1], 10); ++ var expr = (plural_form.match(/plural\s*=\s*([^;]+)/) || [, 'n != 1'])[1]; ++ var ast = parsePluralExpr(expr); + +- // Careful here, this is a hidden eval() equivalent.. +- // Risk should be reasonable though since we test the plural_form through regex before +- // taken from https://github.com/Orange-OpenSource/gettext.js/blob/master/lib.gettext.js +- // TODO: should test if https://github.com/soney/jsep present and use it if so +- return new Function("n", 'var plural, nplurals; '+ plural_form +' return { nplurals: nplurals, plural: (plural === true ? 1 : (plural ? plural : 0)) };'); ++ return function (n) { ++ var plural = evalPluralExpr(ast, typeof n === 'number' ? n : Number(n) || 0); ++ return { nplurals: nplurals, plural: (plural === true ? 1 : (plural ? plural : 0)) }; ++ }; + }; + + // Proper translation function that handle plurals and directives +diff --git a/node_modules/gettext.js/dist/gettext.esm.min.js b/node_modules/gettext.js/dist/gettext.esm.min.js +index e957ef1..286e3b5 100644 +--- a/node_modules/gettext.js/dist/gettext.esm.min.js ++++ b/node_modules/gettext.js/dist/gettext.esm.min.js +@@ -1,2 +1 @@ +-/*! gettext.js - Guillaume Potier - MIT Licensed */ +-var i18n=function(t){t=t||{},this&&(this.__version="2.0.0");var r={domain:"messages",locale:"undefined"!=typeof document&&document.documentElement.getAttribute("lang")||"en",plural_func:function(t){return{nplurals:2,plural:1!=t?1:0}},ctxt_delimiter:String.fromCharCode(4)},e=function(t){var r=typeof t;return"function"===r||"object"===r&&!!t},n={},l=t.locale||r.locale,a=t.domain||r.domain,o={},i={},u=t.ctxt_delimiter||r.ctxt_delimiter;t.messages&&(o[a]={},o[a][l]=t.messages),t.plural_forms&&(i[l]=t.plural_forms);var s=function(t){var r=arguments;return t.replace(/%%/g,"%% ").replace(/%(\d+)/g,(function(t,e){return r[e]})).replace(/%% /g,"%")},p=function(t){return-1!==t.indexOf(u)?t.split(u)[1]:t},c=function(t){for(var r=[t],e=t.lastIndexOf("-");e>0;)t=t.slice(0,e),r.push(t),e=t.lastIndexOf("-");return r},f=function(t){var r=(t=t.replace("_","-")).search(/[.@]/);return-1!=r&&(t=t.slice(0,r)),t},d=function(t){var r=new RegExp("^\\s*nplurals\\s*=\\s*[0-9]+\\s*;\\s*plural\\s*=\\s*(?:\\s|[-\\?\\|&=!<>+*/%:;n0-9_()])+"),e=t.match(r);if(!e||e[0]!==t)throw new Error(s('The plural form "%1" is not valid',t));return console.log(">>> Plural form:",t),new Function("n","var plural, nplurals; "+t+" return { nplurals: nplurals, plural: (plural === true ? 1 : (plural ? plural : 0)) };")},g=function(t,r,e){return e.plural_form?(e.plural_func?a=e.plural_func(r):(n[l]||(n[l]=d(i[l])),a=n[l](r)),(void 0===a.plural||a.plural>a.nplurals||t.length<=a.plural)&&(a.plural=0),s.apply(this,[p(t[a.plural])].concat(Array.prototype.slice.call(arguments,3)))):s.apply(this,[p(t[0])].concat(Array.prototype.slice.call(arguments,3)));var a};return{strfmt:s,expand_locale:c,__:function(){return this.gettext.apply(this,arguments)},_n:function(){return this.ngettext.apply(this,arguments)},_p:function(){return this.pgettext.apply(this,arguments)},setMessages:function(t,r,n,l){if(!t||!r||!n)throw new Error("You must provide a domain, a locale and messages");if("string"!=typeof t||"string"!=typeof r||!e(n))throw new Error("Invalid arguments");return r=f(r),l&&(i[r]=l),o[t]||(o[t]={}),o[t][r]=n,this},loadJSON:function(t,n){if(e(t)||(t=JSON.parse(t)),!t[""]||!t[""].language||!t[""]["plural-forms"])throw new Error('Wrong JSON, it must have an empty key ("") with "language" and "plural-forms" information');var l=t[""];return delete t[""],this.setMessages(n||r.domain,l.language,t,l["plural-forms"])},setLocale:function(t){return l=f(t),this},getLocale:function(){return l},textdomain:function(t){return t?(a=t,this):a},gettext:function(t){return this.dcnpgettext.apply(this,[void 0,void 0,t,void 0,void 0].concat(Array.prototype.slice.call(arguments,1)))},ngettext:function(t,r,e){return this.dcnpgettext.apply(this,[void 0,void 0,t,r,e].concat(Array.prototype.slice.call(arguments,3)))},pgettext:function(t,r){return this.dcnpgettext.apply(this,[void 0,t,r,void 0,void 0].concat(Array.prototype.slice.call(arguments,2)))},dcnpgettext:function(t,e,n,i,s){if(t=t||a,"string"!=typeof n)throw new Error(this.strfmt('Msgid "%1" is not a valid translatable string',n));var p,f,d,h={plural_form:!1},m=e?e+u+n:n,y=c(l);for(var v in y)if(d=y[v],f=o[t]&&o[t][d]&&o[t][d][m],f=i?f&&"string"!=typeof o[t][d][m]:f&&"string"==typeof o[t][d][m])break;return f?p=o[t][d][m]:(p=n,h.plural_func=r.plural_func),i?(h.plural_form=!0,g.apply(this,[f?p:[n,i],s,h].concat(Array.prototype.slice.call(arguments,5)))):g.apply(this,[[p],s,h].concat(Array.prototype.slice.call(arguments,5)))}}};export default i18n; +\ No newline at end of file ++/*! gettext.js - Guillaume Potier - MIT Licensed */var B=function(s){s=s||{},this&&(this.__version="2.0.0");var m={domain:"messages",locale:(typeof document<"u"?document.documentElement.getAttribute("lang"):!1)||"en",plural_func:function(r){return{nplurals:2,plural:r!=1?1:0}},ctxt_delimiter:""},b={isObject:function(r){var e=typeof r;return e==="function"||e==="object"&&!!r},isArray:function(r){return toString.call(r)==="[object Array]"}},_={},p=s.locale||m.locale,d=s.domain||m.domain,a={},w={},A=s.ctxt_delimiter||m.ctxt_delimiter;s.messages&&(a[d]={},a[d][p]=s.messages),s.plural_forms&&(w[p]=s.plural_forms);var x=function(r){var e=arguments;return r.replace(/%%/g,"%% ").replace(/%(\d+)/g,function(t,n){return e[n]}).replace(/%% /g,"%")},E=function(r){if(r.indexOf(A)!==-1){var e=r.split(A);return e[1]}return r},O=function(r){for(var e=[r],t=r.lastIndexOf("-");t>0;)r=r.slice(0,t),e.push(r),t=r.lastIndexOf("-");return e},M=function(r){r=r.replace("_","-");var e=r.search(/[.@]/);return e!=-1&&(r=r.slice(0,e)),r},S=function(r){var e=r.match(/[0-9]+|&&|\|\||==|!=|<=|>=|[()?:!+\-*\/%<>n]/g)||[],t=0;function n(){var u=v();if(e[t]==="?"){t++;var g=n();return t++,{type:"?",cond:u,thenBranch:g,elseBranch:n()}}return u}function v(){for(var u=o();e[t]==="||";)t++,u={op:"||",left:u,right:o()};return u}function o(){for(var u=i();e[t]==="&&";)t++,u={op:"&&",left:u,right:i()};return u}function i(){for(var u=l();e[t]==="=="||e[t]==="!=";){var g=e[t++];u={op:g,left:u,right:l()}}return u}function l(){for(var u=f();/^[<>]=?$/.test(e[t]);){var g=e[t++];u={op:g,left:u,right:f()}}return u}function f(){for(var u=h();e[t]==="+"||e[t]==="-";){var g=e[t++];u={op:g,left:u,right:h()}}return u}function h(){for(var u=y();e[t]==="*"||e[t]==="/"||e[t]==="%";){var g=e[t++];u={op:g,left:u,right:y()}}return u}function y(){if(e[t]==="!")return t++,{op:"!",right:y()};if(e[t]==="("){t++;var u=n();return t++,u}return e[t]==="n"?(t++,"n"):Number(e[t++])||0}return n()},c=function(r,e){if(r==="n")return e;if(typeof r=="number")return r;if(r.type==="?")return c(r.cond,e)?c(r.thenBranch,e):c(r.elseBranch,e);if(r.op==="!")return!c(r.right,e);var t=c(r.left,e);if(r.op==="&&")return t&&c(r.right,e);if(r.op==="||")return t||c(r.right,e);var n=c(r.right,e);return r.op==="=="?t==n:r.op==="!="?t!=n:r.op==="<="?t<=n:r.op===">="?t>=n:r.op==="<"?t"?t>n:r.op==="+"?t+n:r.op==="-"?t-n:r.op==="*"?t*n:r.op==="/"?n?Math.floor(t/n):0:r.op==="%"&&n?t%n:0},k=function(r){var e=new RegExp("^\\s*nplurals\\s*=\\s*[0-9]+\\s*;\\s*plural\\s*=\\s*(?:\\s|[-\\?\\|&=!<>+*/%:;n0-9_()])+"),t=r.match(e);if(!t||t[0]!==r)throw new Error(x('The plural form "%1" is not valid',r));var n=parseInt((r.match(/nplurals\s*=\s*([0-9]+)/)||[,2])[1],10),v=(r.match(/plural\s*=\s*([^;]+)/)||[,"n != 1"])[1],o=S(v);return function(i){var l=c(o,typeof i=="number"?i:Number(i)||0);return{nplurals:n,plural:l===!0?1:l||0}}},N=function(r,e,t){if(!t.plural_form)return x.apply(this,[E(r[0])].concat(Array.prototype.slice.call(arguments,3)));var n;return t.plural_func?n=t.plural_func(e):(_[p]||(_[p]=k(w[p])),n=_[p](e)),(typeof n.plural>"u"||n.plural>n.nplurals||r.length<=n.plural)&&(n.plural=0),x.apply(this,[E(r[n.plural])].concat(Array.prototype.slice.call(arguments,3)))};return{strfmt:x,expand_locale:O,__:function(){return this.gettext.apply(this,arguments)},_n:function(){return this.ngettext.apply(this,arguments)},_p:function(){return this.pgettext.apply(this,arguments)},setMessages:function(r,e,t,n){if(!r||!e||!t)throw new Error("You must provide a domain, a locale and messages");if(typeof r!="string"||typeof e!="string"||!b.isObject(t))throw new Error("Invalid arguments");return e=M(e),n&&(w[e]=n),a[r]||(a[r]={}),a[r][e]=t,this},loadJSON:function(r,e){if(b.isObject(r)||(r=JSON.parse(r)),!r[""]||!r[""].language||!r[""]["plural-forms"])throw new Error('Wrong JSON, it must have an empty key ("") with "language" and "plural-forms" information');var t=r[""];return delete r[""],this.setMessages(e||m.domain,t.language,r,t["plural-forms"])},setLocale:function(r){return p=M(r),this},getLocale:function(){return p},textdomain:function(r){return r?(d=r,this):d},gettext:function(r){return this.dcnpgettext.apply(this,[void 0,void 0,r,void 0,void 0].concat(Array.prototype.slice.call(arguments,1)))},ngettext:function(r,e,t){return this.dcnpgettext.apply(this,[void 0,void 0,r,e,t].concat(Array.prototype.slice.call(arguments,3)))},pgettext:function(r,e){return this.dcnpgettext.apply(this,[void 0,r,e,void 0,void 0].concat(Array.prototype.slice.call(arguments,2)))},dcnpgettext:function(r,e,t,n,v){if(r=r||d,typeof t!="string")throw new Error(this.strfmt('Msgid "%1" is not a valid translatable string',t));var o,i={plural_form:!1},l=e?e+A+t:t,f,h,y=O(p);for(var u in y)if(h=y[u],f=a[r]&&a[r][h]&&a[r][h][l],n?f=f&&typeof a[r][h][l]!="string":f=f&&typeof a[r][h][l]=="string",f)break;return f?o=a[r][h][l]:(o=t,i.plural_func=m.plural_func),n?(i.plural_form=!0,N.apply(this,[f?o:[t,n],v,i].concat(Array.prototype.slice.call(arguments,5)))):N.apply(this,[[o],v,i].concat(Array.prototype.slice.call(arguments,5)))}}};export default B; +diff --git a/node_modules/gettext.js/lib/gettext.js b/node_modules/gettext.js/lib/gettext.js +index 88aaafc..7466a38 100644 +--- a/node_modules/gettext.js/lib/gettext.js ++++ b/node_modules/gettext.js/lib/gettext.js +@@ -86,6 +86,93 @@ var i18n = function (options) { + return locale; + }; + ++ var parsePluralExpr = function (str) { ++ var tokens = str.match(/[0-9]+|&&|\|\||==|!=|<=|>=|[()?:!+\-*\/%<>n]/g) || []; ++ var i = 0; ++ function parseTernary() { ++ var cond = parseOr(); ++ if (tokens[i] === '?') { ++ i++; ++ var thenBranch = parseTernary(); ++ i++; // ':' ++ return { type: '?', cond: cond, thenBranch: thenBranch, elseBranch: parseTernary() }; ++ } ++ return cond; ++ } ++ function parseOr() { ++ var node = parseAnd(); ++ while (tokens[i] === '||') { i++; node = { op: '||', left: node, right: parseAnd() }; } ++ return node; ++ } ++ function parseAnd() { ++ var node = parseEquality(); ++ while (tokens[i] === '&&') { i++; node = { op: '&&', left: node, right: parseEquality() }; } ++ return node; ++ } ++ function parseEquality() { ++ var node = parseRelational(); ++ while (tokens[i] === '==' || tokens[i] === '!=') { ++ var op = tokens[i++]; ++ node = { op: op, left: node, right: parseRelational() }; ++ } ++ return node; ++ } ++ function parseRelational() { ++ var node = parseAdd(); ++ while (/^[<>]=?$/.test(tokens[i])) { ++ var op = tokens[i++]; ++ node = { op: op, left: node, right: parseAdd() }; ++ } ++ return node; ++ } ++ function parseAdd() { ++ var node = parseMul(); ++ while (tokens[i] === '+' || tokens[i] === '-') { ++ var op = tokens[i++]; ++ node = { op: op, left: node, right: parseMul() }; ++ } ++ return node; ++ } ++ function parseMul() { ++ var node = parsePrimary(); ++ while (tokens[i] === '*' || tokens[i] === '/' || tokens[i] === '%') { ++ var op = tokens[i++]; ++ node = { op: op, left: node, right: parsePrimary() }; ++ } ++ return node; ++ } ++ function parsePrimary() { ++ if (tokens[i] === '!') { i++; return { op: '!', right: parsePrimary() }; } ++ if (tokens[i] === '(') { i++; var node = parseTernary(); i++; return node; } ++ if (tokens[i] === 'n') { i++; return 'n'; } ++ return Number(tokens[i++]) || 0; ++ } ++ return parseTernary(); ++ }; ++ ++ var evalPluralExpr = function (node, n) { ++ if (node === 'n') return n; ++ if (typeof node === 'number') return node; ++ if (node.type === '?') return evalPluralExpr(node.cond, n) ? evalPluralExpr(node.thenBranch, n) : evalPluralExpr(node.elseBranch, n); ++ if (node.op === '!') return !evalPluralExpr(node.right, n); ++ var left = evalPluralExpr(node.left, n); ++ if (node.op === '&&') return left && evalPluralExpr(node.right, n); ++ if (node.op === '||') return left || evalPluralExpr(node.right, n); ++ var right = evalPluralExpr(node.right, n); ++ if (node.op === '==') return left == right; ++ if (node.op === '!=') return left != right; ++ if (node.op === '<=') return left <= right; ++ if (node.op === '>=') return left >= right; ++ if (node.op === '<') return left < right; ++ if (node.op === '>') return left > right; ++ if (node.op === '+') return left + right; ++ if (node.op === '-') return left - right; ++ if (node.op === '*') return left * right; ++ if (node.op === '/') return right ? Math.floor(left / right) : 0; ++ if (node.op === '%') return right ? left % right : 0; ++ return 0; ++ }; ++ + var getPluralFunc = function (plural_form) { + // Plural form string regexp + // taken from https://github.com/Orange-OpenSource/gettext.js/blob/master/lib.gettext.js +@@ -97,11 +184,14 @@ var i18n = function (options) { + if (!match || match[0] !== plural_form) + throw new Error(strfmt('The plural form "%1" is not valid', plural_form)); + +- // Careful here, this is a hidden eval() equivalent.. +- // Risk should be reasonable though since we test the plural_form through regex before +- // taken from https://github.com/Orange-OpenSource/gettext.js/blob/master/lib.gettext.js +- // TODO: should test if https://github.com/soney/jsep present and use it if so +- return new Function("n", 'var plural, nplurals; '+ plural_form +' return { nplurals: nplurals, plural: (plural === true ? 1 : (plural ? plural : 0)) };'); ++ var nplurals = parseInt((plural_form.match(/nplurals\s*=\s*([0-9]+)/) || [, 2])[1], 10); ++ var expr = (plural_form.match(/plural\s*=\s*([^;]+)/) || [, 'n != 1'])[1]; ++ var ast = parsePluralExpr(expr); ++ ++ return function (n) { ++ var plural = evalPluralExpr(ast, typeof n === 'number' ? n : Number(n) || 0); ++ return { nplurals: nplurals, plural: (plural === true ? 1 : (plural ? plural : 0)) }; ++ }; + }; + + // Proper translation function that handle plurals and directives diff --git a/src/utilities/__tests__/Intl-test.tsx b/src/utilities/__tests__/Intl-test.tsx index c512730e4b..6b9c5a98e2 100644 --- a/src/utilities/__tests__/Intl-test.tsx +++ b/src/utilities/__tests__/Intl-test.tsx @@ -1,7 +1,9 @@ import React from 'react' import { render, screen } from 'src/utilities/testingLibrary' -import { __, _np, B } from 'src/utilities/Intl' +import { __, _n, _np, B, loadJSON, setLocale } from 'src/utilities/Intl' +import es from 'src/const/language/es.json' +import frCa from 'src/const/language/frCa.json' describe('Intl', () => { it('__ should produce text', () => { @@ -41,4 +43,50 @@ describe('Intl', () => { expect(boldComponents).toHaveLength(2) }) + + describe('Plural translations with Content Security Policy (no unsafe-eval)', () => { + let originalFunction: typeof globalThis.Function + + beforeEach(() => { + originalFunction = globalThis.Function + }) + + afterEach(() => { + globalThis.Function = originalFunction + }) + + it('translates Spanish plurals without violating CSP unsafe-eval', () => { + loadJSON(structuredClone(es)) + setLocale('es') + + // Disallow any dynamic code evaluation / new Function to simulate strict CSP + globalThis.Function = function () { + throw new EvalError( + "Evaluating a string as JavaScript violates the following Content Security Policy directive because 'unsafe-eval' is not an allowed source of script", + ) + } as unknown as typeof Function + + // Singular (1) + expect(_n('%1 search result', '%1 search results', 1, 1)).toBe('1 resultado de búsqueda') + // Plural (2) + expect(_n('%1 search result', '%1 search results', 2, 2)).toBe('2 Resultados de la búsqueda') + }) + + it('translates French Canadian plurals without violating CSP unsafe-eval', () => { + loadJSON(structuredClone(frCa)) + setLocale('fr-ca') + + // Disallow any dynamic code evaluation / new Function to simulate strict CSP + globalThis.Function = function () { + throw new EvalError( + "Evaluating a string as JavaScript violates the following Content Security Policy directive because 'unsafe-eval' is not an allowed source of script", + ) + } as unknown as typeof Function + + // Singular (1) + expect(_n('%1 search result', '%1 search results', 1, 1)).toBe('1 résultat de recherche') + // Plural (2) + expect(_n('%1 search result', '%1 search results', 2, 2)).toBe('2 résultats de recherche') + }) + }) }) diff --git a/src/utilities/__tests__/gettext-test.js b/src/utilities/__tests__/gettext-test.js new file mode 100644 index 0000000000..8a10926e67 --- /dev/null +++ b/src/utilities/__tests__/gettext-test.js @@ -0,0 +1,166 @@ +import i18n from 'gettext.js' + +describe('gettext safe implementation', () => { + let originalFunction + + beforeEach(() => { + originalFunction = globalThis.Function + }) + + afterEach(() => { + globalThis.Function = originalFunction + }) + + describe('CSP compliance', () => { + it('evaluates plural forms without invoking new Function or eval', () => { + globalThis.Function = function () { + throw new EvalError('CSP unsafe-eval violation') + } + + const instance = i18n() + instance.loadJSON({ + '': { + language: 'es', + 'plural-forms': 'nplurals=2; plural=(n != 1);', + }, + '%1 apple': ['%1 manzana', '%1 manzanas'], + }) + instance.setLocale('es') + + expect(instance.ngettext('%1 apple', '%1 apples', 1, 1)).toBe('1 manzana') + expect(instance.ngettext('%1 apple', '%1 apples', 5, 5)).toBe('5 manzanas') + }) + }) + + describe('Plural expression evaluator rules', () => { + const simulateCsp = () => { + globalThis.Function = function () { + throw new EvalError('CSP unsafe-eval violation') + } + } + + it('handles nplurals=1; plural=0; (e.g., Japanese, Chinese)', () => { + simulateCsp() + const instance = i18n() + instance.loadJSON({ + '': { + language: 'ja', + 'plural-forms': 'nplurals=1; plural=0;', + }, + '%1 item': ['%1 個のアイテム'], + }) + instance.setLocale('ja') + + expect(instance.ngettext('%1 item', '%1 items', 1, 1)).toBe('1 個のアイテム') + expect(instance.ngettext('%1 item', '%1 items', 0, 0)).toBe('0 個のアイテム') + expect(instance.ngettext('%1 item', '%1 items', 10, 10)).toBe('10 個のアイテム') + }) + + it('handles nplurals=2; plural=(n > 1); (French standard)', () => { + simulateCsp() + const instance = i18n() + instance.loadJSON({ + '': { + language: 'fr', + 'plural-forms': 'nplurals=2; plural=(n > 1);', + }, + '%1 file': ['%1 fichier', '%1 fichiers'], + }) + instance.setLocale('fr') + + // 0 is singular in French + expect(instance.ngettext('%1 file', '%1 files', 0, 0)).toBe('0 fichier') + // 1 is singular + expect(instance.ngettext('%1 file', '%1 files', 1, 1)).toBe('1 fichier') + // 2 is plural + expect(instance.ngettext('%1 file', '%1 files', 2, 2)).toBe('2 fichiers') + }) + + it('handles complex ternary and modulo (Slavic/Russian plural rule)', () => { + simulateCsp() + const instance = i18n() + instance.loadJSON({ + '': { + language: 'ru', + 'plural-forms': + 'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);', + }, + '%1 book': ['%1 книга', '%1 книги', '%1 книг'], + }) + instance.setLocale('ru') + + expect(instance.ngettext('%1 book', '%1 books', 1, 1)).toBe('1 книга') + expect(instance.ngettext('%1 book', '%1 books', 21, 21)).toBe('21 книга') + expect(instance.ngettext('%1 book', '%1 books', 2, 2)).toBe('2 книги') + expect(instance.ngettext('%1 book', '%1 books', 4, 4)).toBe('4 книги') + expect(instance.ngettext('%1 book', '%1 books', 5, 5)).toBe('5 книг') + expect(instance.ngettext('%1 book', '%1 books', 11, 11)).toBe('11 книг') + }) + + it('handles complex ternary with multiple branches (Arabic plural rule)', () => { + simulateCsp() + const instance = i18n() + instance.loadJSON({ + '': { + language: 'ar', + 'plural-forms': + 'nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5);', + }, + '%1 book': ['0', '1', '2', '3-10', '11+', 'other'], + }) + instance.setLocale('ar') + + expect(instance.ngettext('%1 book', '%1 books', 0)).toBe('0') + expect(instance.ngettext('%1 book', '%1 books', 1)).toBe('1') + expect(instance.ngettext('%1 book', '%1 books', 2)).toBe('2') + expect(instance.ngettext('%1 book', '%1 books', 5)).toBe('3-10') + expect(instance.ngettext('%1 book', '%1 books', 15)).toBe('11+') + expect(instance.ngettext('%1 book', '%1 books', 102)).toBe('other') + }) + }) + + describe('General gettext features', () => { + it('translates singular strings with __ and gettext', () => { + const instance = i18n() + instance.loadJSON({ + '': { + language: 'es', + 'plural-forms': 'nplurals=2; plural=(n != 1);', + }, + Hello: 'Hola', + }) + instance.setLocale('es') + + expect(instance.gettext('Hello')).toBe('Hola') + expect(instance.__('Hello')).toBe('Hola') + }) + + it('translates contextual strings with _p and pgettext', () => { + const instance = i18n() + instance.loadJSON({ + '': { + language: 'fr', + 'plural-forms': 'nplurals=2; plural=(n!=1);', + }, + 'menu\u0004File': 'Fichier', + }) + instance.setLocale('fr') + + expect(instance.pgettext('menu', 'File')).toBe('Fichier') + expect(instance._p('menu', 'File')).toBe('Fichier') + }) + + it('interpolates placeholders with %1, %2', () => { + const instance = i18n() + expect(instance.strfmt('%1 has %2 items', 'Logan', 3)).toBe('Logan has 3 items') + }) + + it('falls back to msgid when translation is missing', () => { + const instance = i18n() + instance.setLocale('fr') + expect(instance.gettext('Unknown')).toBe('Unknown') + expect(instance.ngettext('%1 result', '%1 results', 1, 1)).toBe('1 result') + expect(instance.ngettext('%1 result', '%1 results', 5, 5)).toBe('5 results') + }) + }) +})