diff --git a/backend/package-lock.json b/backend/package-lock.json index 85b168c1ef..3f555b8dea 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -35,6 +35,8 @@ "query-string": "^7.1.3", "rimraf": "^3.0.2", "stripe": "^10.7.0", + "swagger-jsdoc": "^6.2.5", + "swagger-ui-express": "^4.6.0", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1", "typescript": "^4.9.3", @@ -81,6 +83,46 @@ "node": ">=6.0.0" } }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.0.tgz", + "integrity": "sha512-teB30tFooE3iQs2HQIKJ02D8UZA1Xy1zaczzhUjJs0CymYxeC0g+y5rCY2p8NHBM6DBUVoR8rSM4kHLj1WE9mQ==", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.2.tgz", + "integrity": "sha512-JFxcEyp8RlNHgBCE98nwuTkZT6eNFPc1aosWV6wPcQph72TSEEu1k3baJD4/x1qznU+JiDdz8F5pTwabZh+Dhg==", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "z-schema": "^4.2.3" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, "node_modules/@aws-crypto/ie11-detection": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-2.0.2.tgz", @@ -2571,6 +2613,11 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" + }, "node_modules/@juanelas/base64": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@juanelas/base64/-/base64-1.0.5.tgz", @@ -3147,8 +3194,7 @@ "node_modules/@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", - "dev": true + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==" }, "node_modules/@types/jsonwebtoken": { "version": "8.5.9", @@ -3628,8 +3674,7 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/array-flatten": { "version": "1.1.1", @@ -4013,6 +4058,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==" + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -4535,7 +4585,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, "dependencies": { "esutils": "^2.0.2" }, @@ -4848,7 +4897,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -6447,7 +6495,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -9789,6 +9836,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openapi-types": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.0.2.tgz", + "integrity": "sha512-GuTo7FyZjOIWVhIhQSWJVaws6A82sWIGyQogxxYBYKZ0NBdyP2CYSIgOwFfSB+UVoPExk/YzFpyYitHS8KVZtA==", + "peer": true + }, "node_modules/optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -10935,6 +10988,74 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swagger-jsdoc": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.5.tgz", + "integrity": "sha512-l+cdsKS2y+QDhrH1TJSUiE0y9XKuf5xaGSatjf0hR/wjTlMpO8WfubBK9d/nASdbHPMtj9iJZLBH2ogBEhL7Sw==", + "dependencies": { + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "7.1.6", + "lodash.mergewith": "^4.6.2", + "swagger-parser": "10.0.2", + "yaml": "2.0.0-1" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/swagger-jsdoc/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-parser": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.2.tgz", + "integrity": "sha512-9jHkHM+QXyLGFLk1DkXBwV+4HyNm0Za3b8/zk/+mjr8jgOSiqm3FOTHBSDsBjtn9scdL+8eWcHdupp2NLM8tDw==", + "dependencies": { + "@apidevtools/swagger-parser": "10.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/swagger-ui-dist": { + "version": "4.15.5", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.15.5.tgz", + "integrity": "sha512-V3eIa28lwB6gg7/wfNvAbjwJYmDXy1Jo1POjyTzlB6wPcHiGlRxq39TSjYGVjQrUSAzpv+a7nzp7mDxgNy57xA==" + }, + "node_modules/swagger-ui-express": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.6.0.tgz", + "integrity": "sha512-ZxpQFp1JR2RF8Ar++CyJzEDdvufa08ujNUJgMVTMWPi86CuQeVdBtvaeO/ysrz6dJAYXf9kbVNhWD7JWocwqsA==", + "dependencies": { + "swagger-ui-dist": ">=4.11.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -11520,6 +11641,14 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "engines": { + "node": ">= 6" + } + }, "node_modules/yargs": { "version": "17.6.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", @@ -11567,6 +11696,31 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/z-schema": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-4.2.4.tgz", + "integrity": "sha512-YvBeW5RGNeNzKOUJs3rTL4+9rpcvHXt5I051FJbOcitV8bl40pEfcG0Q+dWSwS0/BIYrMZ/9HHoqLllMkFhD0w==", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.6.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=6.0.0" + }, + "optionalDependencies": { + "commander": "^2.7.1" + } + }, + "node_modules/z-schema/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "optional": true } }, "dependencies": { @@ -11580,6 +11734,40 @@ "@jridgewell/trace-mapping": "^0.3.9" } }, + "@apidevtools/json-schema-ref-parser": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.0.tgz", + "integrity": "sha512-teB30tFooE3iQs2HQIKJ02D8UZA1Xy1zaczzhUjJs0CymYxeC0g+y5rCY2p8NHBM6DBUVoR8rSM4kHLj1WE9mQ==", + "requires": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==" + }, + "@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==" + }, + "@apidevtools/swagger-parser": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.2.tgz", + "integrity": "sha512-JFxcEyp8RlNHgBCE98nwuTkZT6eNFPc1aosWV6wPcQph72TSEEu1k3baJD4/x1qznU+JiDdz8F5pTwabZh+Dhg==", + "requires": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "z-schema": "^4.2.3" + } + }, "@aws-crypto/ie11-detection": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-2.0.2.tgz", @@ -13763,6 +13951,11 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" + }, "@juanelas/base64": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@juanelas/base64/-/base64-1.0.5.tgz", @@ -14266,8 +14459,7 @@ "@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", - "dev": true + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==" }, "@types/jsonwebtoken": { "version": "8.5.9", @@ -14615,8 +14807,7 @@ "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "array-flatten": { "version": "1.1.1", @@ -14900,6 +15091,11 @@ "get-intrinsic": "^1.0.2" } }, + "call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==" + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -15298,7 +15494,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, "requires": { "esutils": "^2.0.2" } @@ -15533,8 +15728,7 @@ "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" }, "etag": { "version": "1.8.1", @@ -16735,7 +16929,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "requires": { "argparse": "^2.0.1" } @@ -19125,6 +19318,12 @@ "mimic-fn": "^2.1.0" } }, + "openapi-types": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.0.2.tgz", + "integrity": "sha512-GuTo7FyZjOIWVhIhQSWJVaws6A82sWIGyQogxxYBYKZ0NBdyP2CYSIgOwFfSB+UVoPExk/YzFpyYitHS8KVZtA==", + "peer": true + }, "optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -19960,6 +20159,55 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, + "swagger-jsdoc": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.5.tgz", + "integrity": "sha512-l+cdsKS2y+QDhrH1TJSUiE0y9XKuf5xaGSatjf0hR/wjTlMpO8WfubBK9d/nASdbHPMtj9iJZLBH2ogBEhL7Sw==", + "requires": { + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "7.1.6", + "lodash.mergewith": "^4.6.2", + "swagger-parser": "10.0.2", + "yaml": "2.0.0-1" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "swagger-parser": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.2.tgz", + "integrity": "sha512-9jHkHM+QXyLGFLk1DkXBwV+4HyNm0Za3b8/zk/+mjr8jgOSiqm3FOTHBSDsBjtn9scdL+8eWcHdupp2NLM8tDw==", + "requires": { + "@apidevtools/swagger-parser": "10.0.2" + } + }, + "swagger-ui-dist": { + "version": "4.15.5", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.15.5.tgz", + "integrity": "sha512-V3eIa28lwB6gg7/wfNvAbjwJYmDXy1Jo1POjyTzlB6wPcHiGlRxq39TSjYGVjQrUSAzpv+a7nzp7mDxgNy57xA==" + }, + "swagger-ui-express": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.6.0.tgz", + "integrity": "sha512-ZxpQFp1JR2RF8Ar++CyJzEDdvufa08ujNUJgMVTMWPi86CuQeVdBtvaeO/ysrz6dJAYXf9kbVNhWD7JWocwqsA==", + "requires": { + "swagger-ui-dist": ">=4.11.0" + } + }, "test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -20371,6 +20619,11 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==" + }, "yargs": { "version": "17.6.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", @@ -20403,6 +20656,25 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true + }, + "z-schema": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-4.2.4.tgz", + "integrity": "sha512-YvBeW5RGNeNzKOUJs3rTL4+9rpcvHXt5I051FJbOcitV8bl40pEfcG0Q+dWSwS0/BIYrMZ/9HHoqLllMkFhD0w==", + "requires": { + "commander": "^2.7.1", + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.6.0" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "optional": true + } + } } } } diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 15a200783e..6cf75ad47d 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -48,6 +48,8 @@ services: - ./frontend/public:/app/public - ./frontend/styles:/app/styles - ./frontend/components:/app/components + - ./frontend/locales:/app/locales + - ./frontend/next-i18next.config.js:/app/next-i18next.config.js env_file: .env environment: - NEXT_PUBLIC_ENV=development diff --git a/frontend/.gitignore b/frontend/.gitignore index 83e774c8cf..5edd5a7fa3 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -32,3 +32,5 @@ yarn-error.log* .env.production.local .vercel .env.infisical + +.vscode \ No newline at end of file diff --git a/frontend/components/basic/Layout.tsx b/frontend/components/basic/Layout.tsx index 56a16cb3de..5a8d4098ce 100644 --- a/frontend/components/basic/Layout.tsx +++ b/frontend/components/basic/Layout.tsx @@ -1,38 +1,39 @@ /* eslint-disable no-unexpected-multiline */ /* eslint-disable react-hooks/exhaustive-deps */ -import { useEffect, useState } from 'react'; -import Link from 'next/link'; -import { useRouter } from 'next/router'; +import { useEffect, useMemo, useState } from "react"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { useTranslation } from "next-i18next"; import { faBookOpen, faGear, faKey, faMobile, faPlug, - faUser -} from '@fortawesome/free-solid-svg-icons'; -import { faPlus } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + faUser, +} from "@fortawesome/free-solid-svg-icons"; +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import getOrganizations from '~/pages/api/organization/getOrgs'; -import getOrganizationUserProjects from '~/pages/api/organization/GetOrgUserProjects'; -import getOrganizationUsers from '~/pages/api/organization/GetOrgUsers'; -import checkUserAction from '~/pages/api/userActions/checkUserAction'; -import addUserToWorkspace from '~/pages/api/workspace/addUserToWorkspace'; -import createWorkspace from '~/pages/api/workspace/createWorkspace'; -import getWorkspaces from '~/pages/api/workspace/getWorkspaces'; -import uploadKeys from '~/pages/api/workspace/uploadKeys'; +import getOrganizations from "~/pages/api/organization/getOrgs"; +import getOrganizationUserProjects from "~/pages/api/organization/GetOrgUserProjects"; +import getOrganizationUsers from "~/pages/api/organization/GetOrgUsers"; +import checkUserAction from "~/pages/api/userActions/checkUserAction"; +import addUserToWorkspace from "~/pages/api/workspace/addUserToWorkspace"; +import createWorkspace from "~/pages/api/workspace/createWorkspace"; +import getWorkspaces from "~/pages/api/workspace/getWorkspaces"; +import uploadKeys from "~/pages/api/workspace/uploadKeys"; -import NavBarDashboard from '../navigation/NavBarDashboard'; -import onboardingCheck from '../utilities/checks/OnboardingCheck'; -import { tempLocalStorage } from '../utilities/checks/tempLocalStorage'; +import NavBarDashboard from "../navigation/NavBarDashboard"; +import onboardingCheck from "../utilities/checks/OnboardingCheck"; +import { tempLocalStorage } from "../utilities/checks/tempLocalStorage"; import { decryptAssymmetric, - encryptAssymmetric -} from '../utilities/cryptography/crypto'; -import Button from './buttons/Button'; -import AddWorkspaceDialog from './dialog/AddWorkspaceDialog'; -import Listbox from './Listbox'; + encryptAssymmetric, +} from "../utilities/cryptography/crypto"; +import Button from "./buttons/Button"; +import AddWorkspaceDialog from "./dialog/AddWorkspaceDialog"; +import Listbox from "./Listbox"; interface LayoutProps { children: React.ReactNode; @@ -41,15 +42,17 @@ interface LayoutProps { export default function Layout({ children }: LayoutProps) { const router = useRouter(); const [workspaceList, setWorkspaceList] = useState([]); - const [workspaceMapping, setWorkspaceMapping] = useState([{ '1': '2' }]); - const [workspaceSelected, setWorkspaceSelected] = useState('∞'); - const [newWorkspaceName, setNewWorkspaceName] = useState(''); + const [workspaceMapping, setWorkspaceMapping] = useState([{ "1": "2" }]); + const [workspaceSelected, setWorkspaceSelected] = useState("∞"); + const [newWorkspaceName, setNewWorkspaceName] = useState(""); const [isOpen, setIsOpen] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(false); const [totalOnboardingActionsDone, setTotalOnboardingActionsDone] = useState(0); + const { t } = useTranslation(); + function closeModal() { setIsOpen(false); } @@ -75,35 +78,35 @@ export default function Layout({ children }: LayoutProps) { if (!currentWorkspaces.includes(workspaceName)) { const newWorkspace = await createWorkspace({ workspaceName, - organizationId: tempLocalStorage('orgData.id') + organizationId: tempLocalStorage("orgData.id"), }); const newWorkspaceId = newWorkspace._id; if (addAllUsers) { const orgUsers = await getOrganizationUsers({ - orgId: tempLocalStorage('orgData.id') + orgId: tempLocalStorage("orgData.id"), }); orgUsers.map(async (user: any) => { - if (user.status == 'accepted') { + if (user.status == "accepted") { const result = await addUserToWorkspace( user.user.email, newWorkspaceId ); if (result?.invitee && result?.latestKey) { - const PRIVATE_KEY = tempLocalStorage('PRIVATE_KEY'); + const PRIVATE_KEY = tempLocalStorage("PRIVATE_KEY"); // assymmetrically decrypt symmetric key with local private key const key = decryptAssymmetric({ ciphertext: result.latestKey.encryptedKey, nonce: result.latestKey.nonce, publicKey: result.latestKey.sender.publicKey, - privateKey: PRIVATE_KEY + privateKey: PRIVATE_KEY, }); const { ciphertext, nonce } = encryptAssymmetric({ plaintext: key, publicKey: result.invitee.publicKey, - privateKey: PRIVATE_KEY + privateKey: PRIVATE_KEY, }) as { ciphertext: string; nonce: string }; uploadKeys( @@ -116,11 +119,11 @@ export default function Layout({ children }: LayoutProps) { } }); } - router.push('/dashboard/' + newWorkspaceId + '?Development'); + router.push("/dashboard/" + newWorkspaceId + "?Development"); setIsOpen(false); - setNewWorkspaceName(''); + setNewWorkspaceName(""); } else { - console.error('A project with this name already exists.'); + console.error("A project with this name already exists."); setError(true); setLoading(false); } @@ -131,62 +134,65 @@ export default function Layout({ children }: LayoutProps) { } } - const menuItems = [ - { - href: - '/dashboard/' + - workspaceMapping[workspaceSelected as any] + - '?Development', - title: 'Secrets', - emoji: - }, - { - href: '/users/' + workspaceMapping[workspaceSelected as any], - title: 'Members', - emoji: - }, - { - href: '/integrations/' + workspaceMapping[workspaceSelected as any], - title: 'Integrations', - emoji: - }, - { - href: '/settings/project/' + workspaceMapping[workspaceSelected as any], - title: 'Project Settings', - emoji: - } - ]; + const menuItems = useMemo( + () => [ + { + href: + "/dashboard/" + + workspaceMapping[workspaceSelected as any] + + "?Development", + title: t("nav:menu.secrets"), + emoji: , + }, + { + href: "/users/" + workspaceMapping[workspaceSelected as any], + title: t("nav:menu.members"), + emoji: , + }, + { + href: "/integrations/" + workspaceMapping[workspaceSelected as any], + title: t("nav:menu.integrations"), + emoji: , + }, + { + href: "/settings/project/" + workspaceMapping[workspaceSelected as any], + title: t("nav:menu.project-settings"), + emoji: , + }, + ], + [t, workspaceMapping, workspaceSelected] + ); useEffect(() => { // Put a user in a workspace if they're not in one yet const putUserInWorkSpace = async () => { - if (tempLocalStorage('orgData.id') === '') { + if (tempLocalStorage("orgData.id") === "") { const userOrgs = await getOrganizations(); - localStorage.setItem('orgData.id', userOrgs[0]._id); + localStorage.setItem("orgData.id", userOrgs[0]._id); } const orgUserProjects = await getOrganizationUserProjects({ - orgId: tempLocalStorage('orgData.id') + orgId: tempLocalStorage("orgData.id"), }); const userWorkspaces = orgUserProjects; if ( userWorkspaces.length == 0 && - router.asPath != '/noprojects' && - !router.asPath.includes('settings') + router.asPath != "/noprojects" && + !router.asPath.includes("settings") ) { - router.push('/noprojects'); - } else if (router.asPath != '/noprojects') { + router.push("/noprojects"); + } else if (router.asPath != "/noprojects") { const intendedWorkspaceId = router.asPath - .split('/') - [router.asPath.split('/').length - 1].split('?')[0]; + .split("/") + [router.asPath.split("/").length - 1].split("?")[0]; // If a user is not a member of a workspace they are trying to access, just push them to one of theirs if ( - intendedWorkspaceId != 'heroku' && + intendedWorkspaceId != "heroku" && !userWorkspaces .map((workspace: { _id: string }) => workspace._id) .includes(intendedWorkspaceId) ) { - router.push('/dashboard/' + userWorkspaces[0]._id + '?Development'); + router.push("/dashboard/" + userWorkspaces[0]._id + "?Development"); } else { setWorkspaceList( userWorkspaces.map((workspace: any) => workspace.name) @@ -195,7 +201,7 @@ export default function Layout({ children }: LayoutProps) { Object.fromEntries( userWorkspaces.map((workspace: any) => [ workspace.name, - workspace._id + workspace._id, ]) ) as any ); @@ -203,12 +209,12 @@ export default function Layout({ children }: LayoutProps) { Object.fromEntries( userWorkspaces.map((workspace: any) => [ workspace._id, - workspace.name + workspace.name, ]) )[ router.asPath - .split('/') - [router.asPath.split('/').length - 1].split('?')[0] + .split("/") + [router.asPath.split("/").length - 1].split("?")[0] ] ); } @@ -224,16 +230,16 @@ export default function Layout({ children }: LayoutProps) { workspaceMapping[workspaceSelected as any] && `${workspaceMapping[workspaceSelected as any]}` !== router.asPath - .split('/') - [router.asPath.split('/').length - 1].split('?')[0] + .split("/") + [router.asPath.split("/").length - 1].split("?")[0] ) { router.push( - '/dashboard/' + + "/dashboard/" + workspaceMapping[workspaceSelected as any] + - '?Development' + "?Development" ); localStorage.setItem( - 'projectData.id', + "projectData.id", `${workspaceMapping[workspaceSelected as any]}` ); } @@ -257,7 +263,7 @@ export default function Layout({ children }: LayoutProps) {
- PROJECT + {t("nav:menu.project")}
{workspaceList.length > 0 ? ( 0 && menuItems.map(({ href, title, emoji }) => (
  • - {router.asPath.split('/')[1] === href.split('/')[1] && - (['project', 'billing', 'org', 'personal'].includes( - router.asPath.split('/')[2] + {router.asPath.split("/")[1] === href.split("/")[1] && + (["project", "billing", "org", "personal"].includes( + router.asPath.split("/")[2] ) - ? router.asPath.split('/')[2] === href.split('/')[2] + ? router.asPath.split("/")[2] === href.split("/")[2] : true) ? (
    {title}
    - ) : router.asPath == '/noprojects' ? ( + ) : router.asPath == "/noprojects" ? (
    @@ -323,7 +329,7 @@ export default function Layout({ children }: LayoutProps) {
    - {router.asPath.split('/')[1] === 'home' ? ( + {router.asPath.split("/")[1] === "home" ? (
    @@ -334,12 +340,12 @@ export default function Layout({ children }: LayoutProps) { Infisical Guide progress bar

    - {' '} - To use Infisical, please log in through a device with larger - dimensions.{' '} + {` ${t("common:no-mobile")} `}

    diff --git a/frontend/components/basic/dialog/AddIncidentContactDialog.js b/frontend/components/basic/dialog/AddIncidentContactDialog.js index fad13cdee2..535004045d 100644 --- a/frontend/components/basic/dialog/AddIncidentContactDialog.js +++ b/frontend/components/basic/dialog/AddIncidentContactDialog.js @@ -1,4 +1,5 @@ import { Fragment, useState } from "react"; +import { useTranslation } from "next-i18next"; import { Dialog, Transition } from "@headlessui/react"; import addIncidentContact from "~/pages/api/organization/addIncidentContact"; @@ -14,6 +15,7 @@ const AddIncidentContactDialog = ({ setIncidentContacts, }) => { let [incidentContactEmail, setIncidentContactEmail] = useState(""); + const { t } = useTranslation(); const submit = () => { setIncidentContacts( @@ -59,17 +61,16 @@ const AddIncidentContactDialog = ({ as="h3" className="text-lg font-medium leading-6 text-gray-400" > - Add an Incident Contact + {t("section-incident:add-dialog.title")}

    - This contact will be notified in the unlikely event of a - severe incident. + {t("section-incident:add-dialog.description")}

    diff --git a/frontend/components/basic/dialog/AddProjectMemberDialog.js b/frontend/components/basic/dialog/AddProjectMemberDialog.js index 0d18bc3ca9..e8dd610de6 100644 --- a/frontend/components/basic/dialog/AddProjectMemberDialog.js +++ b/frontend/components/basic/dialog/AddProjectMemberDialog.js @@ -1,5 +1,6 @@ import { Fragment, useState } from "react"; import { useRouter } from "next/router"; +import { Trans, useTranslation } from "next-i18next"; import { Dialog, Transition } from "@headlessui/react"; import Button from "../buttons/Button"; @@ -15,6 +16,7 @@ const AddProjectMemberDialog = ({ setEmail, }) => { const router = useRouter(); + const { t } = useTranslation(); return (
    @@ -49,48 +51,55 @@ const AddProjectMemberDialog = ({ as="h3" className="text-lg font-medium leading-6 text-gray-400 z-50" > - Add a member to your project + {t("section-members:add-dialog.add-member-to-project")} ) : ( - All the users in your organization are already invited. + {t("section-members:add-dialog.already-all-invited")} )}
    {data?.length > 0 ? (

    - The user will receive an email with the instructions. + {t("section-members:add-dialog.user-will-email")}

    - - + + router.push( + "/settings/org/" + router.query.id + ) + } + />, + // eslint-disable-next-line react/jsx-key +
    ) : (

    - Add more users to the organization first. + {t("section-members:add-dialog.add-user-org-first")}

    )}
    @@ -110,7 +119,7 @@ const AddProjectMemberDialog = ({
    @@ -120,7 +129,7 @@ const AddProjectMemberDialog = ({ router.push("/settings/org/" + router.query.id) } color="mineshaft" - text="Add Users to Organization" + text={t("section-members:add-dialog.add-user-to-org")} size="md" /> )} diff --git a/frontend/components/basic/dialog/AddServiceTokenDialog.js b/frontend/components/basic/dialog/AddServiceTokenDialog.js index ddb20cce35..b02a804b70 100644 --- a/frontend/components/basic/dialog/AddServiceTokenDialog.js +++ b/frontend/components/basic/dialog/AddServiceTokenDialog.js @@ -1,40 +1,42 @@ -import { Fragment, useState } from 'react'; -import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Dialog, Transition } from '@headlessui/react'; -import nacl from 'tweetnacl'; +import { Fragment, useState } from "react"; +import { useTranslation } from "next-i18next"; +import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Dialog, Transition } from "@headlessui/react"; +import nacl from "tweetnacl"; -import addServiceToken from '~/pages/api/serviceToken/addServiceToken'; -import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey'; +import addServiceToken from "~/pages/api/serviceToken/addServiceToken"; +import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey"; -import { envMapping } from '../../../public/data/frequentConstants'; +import { envMapping } from "../../../public/data/frequentConstants"; import { decryptAssymmetric, - encryptAssymmetric -} from '../../utilities/cryptography/crypto'; -import Button from '../buttons/Button'; -import InputField from '../InputField'; -import ListBox from '../Listbox'; + encryptAssymmetric, +} from "../../utilities/cryptography/crypto"; +import Button from "../buttons/Button"; +import InputField from "../InputField"; +import ListBox from "../Listbox"; const expiryMapping = { - '1 day': 86400, - '7 days': 604800, - '1 month': 2592000, - '6 months': 15552000, - '12 months': 31104000 + "1 day": 86400, + "7 days": 604800, + "1 month": 2592000, + "6 months": 15552000, + "12 months": 31104000, }; const AddServiceTokenDialog = ({ isOpen, closeModal, workspaceId, - workspaceName + workspaceName, }) => { - const [serviceToken, setServiceToken] = useState(''); - const [serviceTokenName, setServiceTokenName] = useState(''); - const [serviceTokenEnv, setServiceTokenEnv] = useState('Development'); - const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState('1 day'); + const [serviceToken, setServiceToken] = useState(""); + const [serviceTokenName, setServiceTokenName] = useState(""); + const [serviceTokenEnv, setServiceTokenEnv] = useState("Development"); + const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState("1 day"); const [serviceTokenCopied, setServiceTokenCopied] = useState(false); + const { t } = useTranslation(); const generateServiceToken = async () => { const latestFileKey = await getLatestFileKey({ workspaceId }); @@ -43,7 +45,7 @@ const AddServiceTokenDialog = ({ ciphertext: latestFileKey.latestKey.encryptedKey, nonce: latestFileKey.latestKey.nonce, publicKey: latestFileKey.latestKey.sender.publicKey, - privateKey: localStorage.getItem('PRIVATE_KEY') + privateKey: localStorage.getItem("PRIVATE_KEY"), }); // generate new public/private key pair @@ -55,7 +57,7 @@ const AddServiceTokenDialog = ({ const { ciphertext: encryptedKey, nonce } = encryptAssymmetric({ plaintext: key, publicKey, - privateKey + privateKey, }); let newServiceToken = await addServiceToken({ @@ -65,16 +67,16 @@ const AddServiceTokenDialog = ({ expiresIn: expiryMapping[serviceTokenExpiresIn], publicKey, encryptedKey, - nonce + nonce, }); - const serviceToken = newServiceToken + ',' + privateKey; + const serviceToken = newServiceToken + "," + privateKey; setServiceToken(serviceToken); }; function copyToClipboard() { // Get the text field - var copyText = document.getElementById('serviceToken'); + var copyText = document.getElementById("serviceToken"); // Select the text field copyText.select(); @@ -91,8 +93,8 @@ const AddServiceTokenDialog = ({ const closeAddServiceTokenModal = () => { closeModal(); - setServiceTokenName(''); - setServiceToken(''); + setServiceTokenName(""); + setServiceToken(""); }; return ( @@ -122,27 +124,26 @@ const AddServiceTokenDialog = ({ leaveFrom="opacity-100 scale-100" leaveTo="opacity-0 scale-95" > - {serviceToken == '' ? ( + {serviceToken == "" ? ( - Add a service token for {workspaceName} + {t("section-token:add-dialog.title", { + target: workspaceName, + })}

    - Specify the name, environment, and expiry period. When - a token is generated, you will only be able to see it - once before it disappears. Make sure to save it - somewhere. + {t("section-token:add-dialog.description")}

    @@ -169,14 +170,14 @@ const AddServiceTokenDialog = ({ selected={serviceTokenExpiresIn} onChange={setServiceTokenExpiresIn} data={[ - '1 day', - '7 days', - '1 month', - '6 months', - '12 months' + "1 day", + "7 days", + "1 month", + "6 months", + "12 months", ]} - isFull={true} - text="Expires in: " + width="full" + text={`${t("common:expired-in")}: `} />
    @@ -184,10 +185,10 @@ const AddServiceTokenDialog = ({
    @@ -198,13 +199,14 @@ const AddServiceTokenDialog = ({ as="h3" className="text-lg font-medium leading-6 text-gray-400 z-50" > - Copy your service token + {t("section-token:add-dialog.copy-service-token")}

    - Once you close this popup, you will never see your - service token again + {t( + "section-token:add-dialog.copy-service-token-description" + )}

    @@ -234,7 +236,7 @@ const AddServiceTokenDialog = ({ )} - Click to Copy + {t("common.click-to-copy")}
  • diff --git a/frontend/components/dashboard/DropZone.tsx b/frontend/components/dashboard/DropZone.tsx index e9c533b058..66be964cc0 100644 --- a/frontend/components/dashboard/DropZone.tsx +++ b/frontend/components/dashboard/DropZone.tsx @@ -1,12 +1,13 @@ -import { type ChangeEvent, type DragEvent, useState } from 'react'; -import Image from 'next/image'; -import { faUpload } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { type ChangeEvent, type DragEvent, useState } from "react"; +import Image from "next/image"; +import { useTranslation } from "next-i18next"; +import { faUpload } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import Button from '../basic/buttons/Button'; -import Error from '../basic/Error'; -import parse from '../utilities/file'; -import guidGenerator from '../utilities/randomId'; +import Button from "../basic/buttons/Button"; +import Error from "../basic/Error"; +import parse from "../utilities/file"; +import guidGenerator from "../utilities/randomId"; interface DropZoneProps { // TODO: change Data type from any @@ -26,8 +27,10 @@ const DropZone = ({ errorDragAndDrop, setButtonReady, keysExist, - numCurrentRows + numCurrentRows, }: DropZoneProps) => { + const { t } = useTranslation(); + const handleDragEnter = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); @@ -43,7 +46,7 @@ const DropZone = ({ e.stopPropagation(); // set dropEffect to copy i.e copy of the source item - e.dataTransfer.dropEffect = 'copy'; + e.dataTransfer.dropEffect = "copy"; }; const [loading, setLoading] = useState(false); @@ -54,7 +57,7 @@ const DropZone = ({ setTimeout(() => setLoading(false), 5000); e.preventDefault(); e.stopPropagation(); - e.dataTransfer.dropEffect = 'copy'; + e.dataTransfer.dropEffect = "copy"; const file = e.dataTransfer.files[0]; const reader = new FileReader(); @@ -69,7 +72,7 @@ const DropZone = ({ pos: numCurrentRows + index, key: key, value: keyPairs[key as keyof typeof keyPairs], - type: 'shared' + type: "shared", }; }); setData(newData); @@ -96,16 +99,16 @@ const DropZone = ({ reader.onload = (event) => { if (event.target === null || event.target.result === null) return; const { result } = event.target; - if (typeof result === 'string') { + if (typeof result === "string") { const newData = result - .split('\n') + .split("\n") .map((line: string, index: number) => { return { id: guidGenerator(), pos: numCurrentRows + index, - key: line.split('=')[0], - value: line.split('=').slice(1, line.split('=').length).join('='), - type: 'shared' + key: line.split("=")[0], + value: line.split("=").slice(1, line.split("=").length).join("="), + type: "shared", }; }); setData(newData); @@ -149,9 +152,7 @@ const DropZone = ({ icon={faUpload} className="text-bunker-300 text-3xl mr-6" /> -

    - Drag and drop your .env file here to add more keys. -

    +

    {t("common:drop-zone-keys")}

    {errorDragAndDrop ? (
    @@ -170,7 +171,7 @@ const DropZone = ({ onDrop={handleDrop} > -

    Drag and drop your .env file here.

    +

    {t("common:drop-zone")}

    [ [ , - 'Join Slack Forum', - 'https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g' + t("nav:support.slack"), + "https://join.slack.com/t/infisical/shared_invite/zt-1dgg63ln8-G7PCNJdCymAT9YF3j1ewVA", ], [ , - 'Read Docs', - 'https://infisical.com/docs/getting-started/introduction' + t("nav:support.docs"), + "https://infisical.com/docs/getting-started/introduction", ], [ , - 'Open a GitHub Issue', - 'https://github.com/Infisical/infisical-cli/issues' + t("nav:support.issue"), + "https://github.com/Infisical/infisical-cli/issues", ], [ , - 'Send us an Email', - 'mailto:support@infisical.com' - ] + t("nav:support.email"), + "mailto:support@infisical.com", + ], ]; export interface ICurrentOrg { @@ -68,6 +68,10 @@ export default function Navbar() { const [orgs, setOrgs] = useState([]); const [currentOrg, setCurrentOrg] = useState(); + const { t } = useTranslation(); + + const supportOptionsList = useMemo(() => supportOptions(t), [t]); + useEffect(() => { (async () => { const userData = await getUser(); @@ -75,16 +79,16 @@ export default function Navbar() { const orgsData = await getOrganizations(); setOrgs(orgsData); const currentOrg = await getOrganization({ - orgId: String(localStorage.getItem('orgData.id')) + orgId: String(localStorage.getItem("orgData.id")), }); setCurrentOrg(currentOrg); })(); }, []); const closeApp = async () => { - console.log('Logging out...'); + console.log("Logging out..."); await logout(); - router.push('/login'); + router.push("/login"); }; return ( @@ -119,18 +123,17 @@ export default function Navbar() { leaveTo="transform opacity-0 scale-95" > - {supportOptions.map((option) => ( - // eslint-disable-next-line react/jsx-no-target-blank + {supportOptionsList.map(([icon, text, url]) => (
    - {option[0]} -
    {option[1]}
    + {icon} +
    {text}
    ))} @@ -159,11 +162,11 @@ export default function Navbar() {
    - SIGNED IN AS + {t("nav:user.signed-in-as")}
    - router.push('/settings/personal/' + router.query.id) + router.push("/settings/personal/" + router.query.id) } className="flex flex-row items-center px-1 mx-1 my-1 hover:bg-white/5 cursor-pointer rounded-md" > @@ -173,11 +176,11 @@ export default function Navbar() {

    - {' '} + {" "} {user?.firstName} {user?.lastName}

    - {' '} + {" "} {user?.email}

    @@ -190,11 +193,11 @@ export default function Navbar() {
    - CURRENT ORGANIZATION + {t("nav:user.current-organization")}
    - router.push('/settings/org/' + router.query.id) + router.push("/settings/org/" + router.query.id) } className="flex flex-row items-center px-2 mt-2 py-1 hover:bg-white/5 cursor-pointer rounded-md" > @@ -217,7 +220,7 @@ export default function Navbar() { >
    - router.push('/settings/billing/' + router.query.id) + router.push("/settings/billing/" + router.query.id) } className="mt-1 relative flex justify-start cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/5 duration-200 hover:text-gray-200" > @@ -225,7 +228,7 @@ export default function Navbar() { className="text-lg pl-1.5 pr-3" icon={faCoins} /> -
    Usage & Billing
    +
    {t("nav:user.usage-billing")}
    {orgs?.length > 1 && (
    - OTHER ORGANIZATIONS + {t("nav:user.other-organizations")}
    {orgs .filter( (org: { _id: string }) => - org._id != localStorage.getItem('orgData.id') + org._id != localStorage.getItem("orgData.id") ) .map((org: { _id: string; name: string }) => (
    { - localStorage.setItem('orgData.id', org._id); + localStorage.setItem("orgData.id", org._id); router.reload(); }} className="flex flex-row justify-start items-center hover:bg-white/5 w-full p-1.5 cursor-pointer rounded-md" @@ -287,8 +290,8 @@ export default function Navbar() { onClick={closeApp} className={`${ active - ? 'bg-red font-semibold text-white' - : 'text-gray-400' + ? "bg-red font-semibold text-white" + : "text-gray-400" } group flex w-full items-center rounded-md px-2 py-2 text-sm`} >
    @@ -296,7 +299,7 @@ export default function Navbar() { className="text-lg ml-1.5 mr-3" icon={faRightFromBracket} /> - Log Out + {t("common:logout")}
    )} diff --git a/frontend/components/utilities/withTranslateProps.ts b/frontend/components/utilities/withTranslateProps.ts new file mode 100644 index 0000000000..6dc9abd108 --- /dev/null +++ b/frontend/components/utilities/withTranslateProps.ts @@ -0,0 +1,52 @@ +import { GetServerSideProps, GetStaticProps } from "next"; +import { serverSideTranslations } from "next-i18next/serverSideTranslations"; + +const DefaultNamespaces = ["common", "nav"]; + +type GetTranslatedStaticProps = ( + namespaces: string[], + getStaticProps?: GetStaticProps +) => GetStaticProps; + +type GetTranslatedServerSideProps = ( + namespaces: string[], + getStaticProps?: GetServerSideProps +) => GetServerSideProps; + +export const getTranslatedStaticProps: GetTranslatedStaticProps = + (namespaces, getStaticProps) => + async ({ locale, ...context }) => { + let staticProps = { props: {} }; + if (typeof getStaticProps === "function") { + staticProps = (await getStaticProps(context)) as any; + } + return { + ...staticProps, + props: { + ...(await serverSideTranslations(locale ?? "en", [ + ...DefaultNamespaces, + ...namespaces, + ])), + ...staticProps.props, + }, + }; + }; + +export const getTranslatedServerSideProps: GetTranslatedServerSideProps = + (namespaces, getServerSideProps) => + async ({ locale, ...context }) => { + let staticProps = { props: {} }; + if (typeof getServerSideProps === "function") { + staticProps = (await getServerSideProps(context)) as any; + } + return { + ...staticProps, + props: { + ...(await serverSideTranslations(locale ?? "en", [ + ...DefaultNamespaces, + ...namespaces, + ])), + ...staticProps.props, + }, + }; + }; diff --git a/frontend/const.js b/frontend/const.js index f3b285e438..195d54a6de 100644 --- a/frontend/const.js +++ b/frontend/const.js @@ -16,3 +16,8 @@ export const publicPaths = [ `/terms`, `/subprocessors`, ]; + +export const languageMap = { + en: "English", + ko: "한국어", +}; diff --git a/frontend/next-i18next.config.js b/frontend/next-i18next.config.js new file mode 100644 index 0000000000..5de9c97bc7 --- /dev/null +++ b/frontend/next-i18next.config.js @@ -0,0 +1,26 @@ +// @ts-check + +/** + * @type {import('next-i18next').UserConfig} + */ +module.exports = { + // https://www.i18next.com/overview/configuration-options#logging + debug: process.env.NODE_ENV === "development", + i18n: { + defaultLocale: "en", + locales: ["en", "ko"], + }, + fallbackLng: { + default: ["en"], + }, + + reloadOnPrerender: process.env.NODE_ENV === "development", + + /** + * @link https://github.com/i18next/next-i18next#6-advanced-configuration + */ + // saveMissing: false, + // strictMode: true, + // serializeConfig: false, + // react: { useSuspense: false } +}; diff --git a/frontend/next.config.js b/frontend/next.config.js index 2ae4b76413..0b87ee4c76 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -1,4 +1,4 @@ -// next.config.js +const { i18n } = require("./next-i18next.config.js"); const ContentSecurityPolicy = ` default-src 'self'; @@ -60,4 +60,8 @@ module.exports = { }, ]; }, + webpack: (config, { isServer, webpack }) => { + return config; + }, + i18n, }; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b65657f854..69a4ecbef6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -24,10 +24,12 @@ "fs": "^0.0.1-security", "gray-matter": "^4.0.3", "http-proxy": "^1.18.1", + "i18next": "^22.4.6", "jspdf": "^2.5.1", "jsrp": "^0.2.4", "markdown-it": "^13.0.1", "next": "^12.2.5", + "next-i18next": "^13.0.2", "posthog-js": "^1.34.0", "query-string": "^7.1.3", "react": "^17.0.2", @@ -36,6 +38,7 @@ "react-dom": "^17.0.2", "react-github-btn": "^1.4.0", "react-grid-layout": "^1.3.4", + "react-i18next": "^12.1.1", "react-mailchimp-subscribe": "^2.1.3", "react-markdown": "^8.0.3", "react-redux": "^8.0.2", @@ -393,11 +396,11 @@ } }, "node_modules/@babel/runtime": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.9.tgz", - "integrity": "sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==", + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz", + "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==", "dependencies": { - "regenerator-runtime": "^0.13.4" + "regenerator-runtime": "^0.13.11" }, "engines": { "node": ">=6.9.0" @@ -1368,14 +1371,14 @@ "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.45.0.tgz", - "integrity": "sha512-CXXHNlf0oL+Yg021cxgOdMHNTXD17rHkq7iW6RFHoybdFgQBjU3yIXhhcPpGwr1CjZlo6ET8C6tzX5juQoXeGA==", + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.47.1.tgz", + "integrity": "sha512-r4RZ2Jl9kcQN7K/dcOT+J7NAimbiis4sSM9spvWimsBvDegMhKLA5vri2jG19PmIPbDjPeWzfUPQ2hjEzA4Nmg==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.45.0", - "@typescript-eslint/type-utils": "5.45.0", - "@typescript-eslint/utils": "5.45.0", + "@typescript-eslint/scope-manager": "5.47.1", + "@typescript-eslint/type-utils": "5.47.1", + "@typescript-eslint/utils": "5.47.1", "debug": "^4.3.4", "ignore": "^5.2.0", "natural-compare-lite": "^1.4.0", @@ -1400,6 +1403,53 @@ } } }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.47.1.tgz", + "integrity": "sha512-9hsFDsgUwrdOoW1D97Ewog7DYSHaq4WKuNs0LHF9RiCmqB0Z+XRR4Pf7u7u9z/8CciHuJ6yxNws1XznI3ddjEw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.47.1", + "@typescript-eslint/visitor-keys": "5.47.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.47.1.tgz", + "integrity": "sha512-CmALY9YWXEpwuu6377ybJBZdtSAnzXLSQcxLSqSQSbC7VfpMu/HLVdrnVJj7ycI138EHqocW02LPJErE35cE9A==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.47.1.tgz", + "integrity": "sha512-rF3pmut2JCCjh6BLRhNKdYjULMb1brvoaiWDlHfLNVgmnZ0sBVJrs3SyaKE1XoDDnJuAx/hDQryHYmPUuNq0ig==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.47.1", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -1491,13 +1541,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.45.0.tgz", - "integrity": "sha512-DY7BXVFSIGRGFZ574hTEyLPRiQIvI/9oGcN8t1A7f6zIs6ftbrU0nhyV26ZW//6f85avkwrLag424n+fkuoJ1Q==", + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.47.1.tgz", + "integrity": "sha512-/UKOeo8ee80A7/GJA427oIrBi/Gd4osk/3auBUg4Rn9EahFpevVV1mUK8hjyQD5lHPqX397x6CwOk5WGh1E/1w==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.45.0", - "@typescript-eslint/utils": "5.45.0", + "@typescript-eslint/typescript-estree": "5.47.1", + "@typescript-eslint/utils": "5.47.1", "debug": "^4.3.4", "tsutils": "^3.21.0" }, @@ -1517,6 +1567,63 @@ } } }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.47.1.tgz", + "integrity": "sha512-CmALY9YWXEpwuu6377ybJBZdtSAnzXLSQcxLSqSQSbC7VfpMu/HLVdrnVJj7ycI138EHqocW02LPJErE35cE9A==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.47.1.tgz", + "integrity": "sha512-4+ZhFSuISAvRi2xUszEj0xXbNTHceV9GbH9S8oAD2a/F9SW57aJNQVOCxG8GPfSWH/X4eOPdMEU2jYVuWKEpWA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.47.1", + "@typescript-eslint/visitor-keys": "5.47.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.47.1.tgz", + "integrity": "sha512-rF3pmut2JCCjh6BLRhNKdYjULMb1brvoaiWDlHfLNVgmnZ0sBVJrs3SyaKE1XoDDnJuAx/hDQryHYmPUuNq0ig==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.47.1", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@typescript-eslint/type-utils/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -1604,16 +1711,16 @@ "dev": true }, "node_modules/@typescript-eslint/utils": { - "version": "5.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.45.0.tgz", - "integrity": "sha512-OUg2JvsVI1oIee/SwiejTot2OxwU8a7UfTFMOdlhD2y+Hl6memUSL4s98bpUTo8EpVEr0lmwlU7JSu/p2QpSvA==", + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.47.1.tgz", + "integrity": "sha512-l90SdwqfmkuIVaREZ2ykEfCezepCLxzWMo5gVfcJsJCaT4jHT+QjgSkYhs5BMQmWqE9k3AtIfk4g211z/sTMVw==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.45.0", - "@typescript-eslint/types": "5.45.0", - "@typescript-eslint/typescript-estree": "5.45.0", + "@typescript-eslint/scope-manager": "5.47.1", + "@typescript-eslint/types": "5.47.1", + "@typescript-eslint/typescript-estree": "5.47.1", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0", "semver": "^7.3.7" @@ -1629,6 +1736,97 @@ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.47.1.tgz", + "integrity": "sha512-9hsFDsgUwrdOoW1D97Ewog7DYSHaq4WKuNs0LHF9RiCmqB0Z+XRR4Pf7u7u9z/8CciHuJ6yxNws1XznI3ddjEw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.47.1", + "@typescript-eslint/visitor-keys": "5.47.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.47.1.tgz", + "integrity": "sha512-CmALY9YWXEpwuu6377ybJBZdtSAnzXLSQcxLSqSQSbC7VfpMu/HLVdrnVJj7ycI138EHqocW02LPJErE35cE9A==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.47.1.tgz", + "integrity": "sha512-4+ZhFSuISAvRi2xUszEj0xXbNTHceV9GbH9S8oAD2a/F9SW57aJNQVOCxG8GPfSWH/X4eOPdMEU2jYVuWKEpWA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.47.1", + "@typescript-eslint/visitor-keys": "5.47.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.47.1.tgz", + "integrity": "sha512-rF3pmut2JCCjh6BLRhNKdYjULMb1brvoaiWDlHfLNVgmnZ0sBVJrs3SyaKE1XoDDnJuAx/hDQryHYmPUuNq0ig==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.47.1", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -1651,6 +1849,12 @@ "node": ">=4.0" } }, + "node_modules/@typescript-eslint/utils/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, "node_modules/@typescript-eslint/visitor-keys": { "version": "5.45.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.45.0.tgz", @@ -2401,7 +2605,6 @@ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.0.tgz", "integrity": "sha512-+DkDrhoR4Y0PxDz6rurahuB+I45OsEUv8E1maPTB6OuHRohMMcznBq9TMpdpDMm/hUPob/mJJS3PqgbHpMTQgw==", "hasInstallScript": true, - "optional": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" @@ -4051,6 +4254,14 @@ "react-is": "^16.7.0" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/html-tokenize": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/html-tokenize/-/html-tokenize-2.0.1.tgz", @@ -4113,6 +4324,33 @@ "node": ">=8.0.0" } }, + "node_modules/i18next": { + "version": "22.4.6", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.4.6.tgz", + "integrity": "sha512-9Tm1ezxWyzV+306CIDMBbYBitC1jedQyYuuLtIv7oxjp2ohh8eyxP9xytIf+2bbQfhH784IQKPSYp+Zq9+YSbw==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.20.6" + } + }, + "node_modules/i18next-fs-backend": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.1.1.tgz", + "integrity": "sha512-FTnj+UmNgT3YRml5ruRv0jMZDG7odOL/OP5PF5mOqvXud2vHrPOOs68Zdk6iqzL47cnnM0ZVkK2BAvpFeDJToA==" + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -5500,6 +5738,45 @@ } } }, + "node_modules/next-i18next": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/next-i18next/-/next-i18next-13.0.2.tgz", + "integrity": "sha512-aUHyKT2kztMgEP44zDB5KoW8XZUQawIdOYWXcrMH6lxAcS0kBsKX0uKMzGS5XlgLW88gvOVc3D7NdfCznLgyyg==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://github.com/belgattitude" + } + ], + "dependencies": { + "@babel/runtime": "^7.20.6", + "@types/hoist-non-react-statics": "^3.3.1", + "core-js": "^3", + "hoist-non-react-statics": "^3.3.2", + "i18next-fs-backend": "^2.1.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "i18next": "^22.0.6", + "next": ">= 12.0.0", + "react": ">= 17.0.2", + "react-i18next": "^12.1.1" + } + }, "node_modules/node-abi": { "version": "3.30.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.30.0.tgz", @@ -6295,6 +6572,27 @@ "react-dom": ">= 16.3.0" } }, + "node_modules/react-i18next": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.1.1.tgz", + "integrity": "sha512-mFdieOI0LDy84q3JuZU6Aou1DoWW2fhapcTGeBS8+vWSJuViuoCLQAMYSb0QoHhXS8B0WKUOPpx4cffAP7r/aA==", + "dependencies": { + "@babel/runtime": "^7.14.5", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 19.0.0", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -7666,6 +7964,14 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -7986,11 +8292,11 @@ } }, "@babel/runtime": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.9.tgz", - "integrity": "sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==", + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz", + "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==", "requires": { - "regenerator-runtime": "^0.13.4" + "regenerator-runtime": "^0.13.11" } }, "@babel/runtime-corejs3": { @@ -8690,14 +8996,14 @@ "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==" }, "@typescript-eslint/eslint-plugin": { - "version": "5.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.45.0.tgz", - "integrity": "sha512-CXXHNlf0oL+Yg021cxgOdMHNTXD17rHkq7iW6RFHoybdFgQBjU3yIXhhcPpGwr1CjZlo6ET8C6tzX5juQoXeGA==", + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.47.1.tgz", + "integrity": "sha512-r4RZ2Jl9kcQN7K/dcOT+J7NAimbiis4sSM9spvWimsBvDegMhKLA5vri2jG19PmIPbDjPeWzfUPQ2hjEzA4Nmg==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.45.0", - "@typescript-eslint/type-utils": "5.45.0", - "@typescript-eslint/utils": "5.45.0", + "@typescript-eslint/scope-manager": "5.47.1", + "@typescript-eslint/type-utils": "5.47.1", + "@typescript-eslint/utils": "5.47.1", "debug": "^4.3.4", "ignore": "^5.2.0", "natural-compare-lite": "^1.4.0", @@ -8706,6 +9012,32 @@ "tsutils": "^3.21.0" }, "dependencies": { + "@typescript-eslint/scope-manager": { + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.47.1.tgz", + "integrity": "sha512-9hsFDsgUwrdOoW1D97Ewog7DYSHaq4WKuNs0LHF9RiCmqB0Z+XRR4Pf7u7u9z/8CciHuJ6yxNws1XznI3ddjEw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.47.1", + "@typescript-eslint/visitor-keys": "5.47.1" + } + }, + "@typescript-eslint/types": { + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.47.1.tgz", + "integrity": "sha512-CmALY9YWXEpwuu6377ybJBZdtSAnzXLSQcxLSqSQSbC7VfpMu/HLVdrnVJj7ycI138EHqocW02LPJErE35cE9A==", + "dev": true + }, + "@typescript-eslint/visitor-keys": { + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.47.1.tgz", + "integrity": "sha512-rF3pmut2JCCjh6BLRhNKdYjULMb1brvoaiWDlHfLNVgmnZ0sBVJrs3SyaKE1XoDDnJuAx/hDQryHYmPUuNq0ig==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.47.1", + "eslint-visitor-keys": "^3.3.0" + } + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -8763,17 +9095,48 @@ } }, "@typescript-eslint/type-utils": { - "version": "5.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.45.0.tgz", - "integrity": "sha512-DY7BXVFSIGRGFZ574hTEyLPRiQIvI/9oGcN8t1A7f6zIs6ftbrU0nhyV26ZW//6f85avkwrLag424n+fkuoJ1Q==", + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.47.1.tgz", + "integrity": "sha512-/UKOeo8ee80A7/GJA427oIrBi/Gd4osk/3auBUg4Rn9EahFpevVV1mUK8hjyQD5lHPqX397x6CwOk5WGh1E/1w==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "5.45.0", - "@typescript-eslint/utils": "5.45.0", + "@typescript-eslint/typescript-estree": "5.47.1", + "@typescript-eslint/utils": "5.47.1", "debug": "^4.3.4", "tsutils": "^3.21.0" }, "dependencies": { + "@typescript-eslint/types": { + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.47.1.tgz", + "integrity": "sha512-CmALY9YWXEpwuu6377ybJBZdtSAnzXLSQcxLSqSQSbC7VfpMu/HLVdrnVJj7ycI138EHqocW02LPJErE35cE9A==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.47.1.tgz", + "integrity": "sha512-4+ZhFSuISAvRi2xUszEj0xXbNTHceV9GbH9S8oAD2a/F9SW57aJNQVOCxG8GPfSWH/X4eOPdMEU2jYVuWKEpWA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.47.1", + "@typescript-eslint/visitor-keys": "5.47.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.47.1.tgz", + "integrity": "sha512-rF3pmut2JCCjh6BLRhNKdYjULMb1brvoaiWDlHfLNVgmnZ0sBVJrs3SyaKE1XoDDnJuAx/hDQryHYmPUuNq0ig==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.47.1", + "eslint-visitor-keys": "^3.3.0" + } + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -8830,21 +9193,71 @@ } }, "@typescript-eslint/utils": { - "version": "5.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.45.0.tgz", - "integrity": "sha512-OUg2JvsVI1oIee/SwiejTot2OxwU8a7UfTFMOdlhD2y+Hl6memUSL4s98bpUTo8EpVEr0lmwlU7JSu/p2QpSvA==", + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.47.1.tgz", + "integrity": "sha512-l90SdwqfmkuIVaREZ2ykEfCezepCLxzWMo5gVfcJsJCaT4jHT+QjgSkYhs5BMQmWqE9k3AtIfk4g211z/sTMVw==", "dev": true, "requires": { "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.45.0", - "@typescript-eslint/types": "5.45.0", - "@typescript-eslint/typescript-estree": "5.45.0", + "@typescript-eslint/scope-manager": "5.47.1", + "@typescript-eslint/types": "5.47.1", + "@typescript-eslint/typescript-estree": "5.47.1", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0", "semver": "^7.3.7" }, "dependencies": { + "@typescript-eslint/scope-manager": { + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.47.1.tgz", + "integrity": "sha512-9hsFDsgUwrdOoW1D97Ewog7DYSHaq4WKuNs0LHF9RiCmqB0Z+XRR4Pf7u7u9z/8CciHuJ6yxNws1XznI3ddjEw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.47.1", + "@typescript-eslint/visitor-keys": "5.47.1" + } + }, + "@typescript-eslint/types": { + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.47.1.tgz", + "integrity": "sha512-CmALY9YWXEpwuu6377ybJBZdtSAnzXLSQcxLSqSQSbC7VfpMu/HLVdrnVJj7ycI138EHqocW02LPJErE35cE9A==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.47.1.tgz", + "integrity": "sha512-4+ZhFSuISAvRi2xUszEj0xXbNTHceV9GbH9S8oAD2a/F9SW57aJNQVOCxG8GPfSWH/X4eOPdMEU2jYVuWKEpWA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.47.1", + "@typescript-eslint/visitor-keys": "5.47.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.47.1.tgz", + "integrity": "sha512-rF3pmut2JCCjh6BLRhNKdYjULMb1brvoaiWDlHfLNVgmnZ0sBVJrs3SyaKE1XoDDnJuAx/hDQryHYmPUuNq0ig==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.47.1", + "eslint-visitor-keys": "^3.3.0" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, "eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -8860,6 +9273,12 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true } } }, @@ -9400,8 +9819,7 @@ "core-js": { "version": "3.26.0", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.0.tgz", - "integrity": "sha512-+DkDrhoR4Y0PxDz6rurahuB+I45OsEUv8E1maPTB6OuHRohMMcznBq9TMpdpDMm/hUPob/mJJS3PqgbHpMTQgw==", - "optional": true + "integrity": "sha512-+DkDrhoR4Y0PxDz6rurahuB+I45OsEUv8E1maPTB6OuHRohMMcznBq9TMpdpDMm/hUPob/mJJS3PqgbHpMTQgw==" }, "core-js-pure": { "version": "3.26.1", @@ -10644,6 +11062,14 @@ "react-is": "^16.7.0" } }, + "html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "requires": { + "void-elements": "3.1.0" + } + }, "html-tokenize": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/html-tokenize/-/html-tokenize-2.0.1.tgz", @@ -10699,6 +11125,19 @@ "requires-port": "^1.0.0" } }, + "i18next": { + "version": "22.4.6", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.4.6.tgz", + "integrity": "sha512-9Tm1ezxWyzV+306CIDMBbYBitC1jedQyYuuLtIv7oxjp2ohh8eyxP9xytIf+2bbQfhH784IQKPSYp+Zq9+YSbw==", + "requires": { + "@babel/runtime": "^7.20.6" + } + }, + "i18next-fs-backend": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.1.1.tgz", + "integrity": "sha512-FTnj+UmNgT3YRml5ruRv0jMZDG7odOL/OP5PF5mOqvXud2vHrPOOs68Zdk6iqzL47cnnM0ZVkK2BAvpFeDJToA==" + }, "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -11600,6 +12039,18 @@ "use-sync-external-store": "1.2.0" } }, + "next-i18next": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/next-i18next/-/next-i18next-13.0.2.tgz", + "integrity": "sha512-aUHyKT2kztMgEP44zDB5KoW8XZUQawIdOYWXcrMH6lxAcS0kBsKX0uKMzGS5XlgLW88gvOVc3D7NdfCznLgyyg==", + "requires": { + "@babel/runtime": "^7.20.6", + "@types/hoist-non-react-statics": "^3.3.1", + "core-js": "^3", + "hoist-non-react-statics": "^3.3.2", + "i18next-fs-backend": "^2.1.0" + } + }, "node-abi": { "version": "3.30.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.30.0.tgz", @@ -12156,6 +12607,15 @@ "react-resizable": "^3.0.4" } }, + "react-i18next": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.1.1.tgz", + "integrity": "sha512-mFdieOI0LDy84q3JuZU6Aou1DoWW2fhapcTGeBS8+vWSJuViuoCLQAMYSb0QoHhXS8B0WKUOPpx4cffAP7r/aA==", + "requires": { + "@babel/runtime": "^7.14.5", + "html-parse-stringify": "^3.0.1" + } + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -13126,6 +13586,11 @@ "unist-util-stringify-position": "^3.0.0" } }, + "void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==" + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 12b2e9ec27..73a42518ca 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,10 +28,12 @@ "fs": "^0.0.1-security", "gray-matter": "^4.0.3", "http-proxy": "^1.18.1", + "i18next": "^22.4.6", "jspdf": "^2.5.1", "jsrp": "^0.2.4", "markdown-it": "^13.0.1", "next": "^12.2.5", + "next-i18next": "^13.0.2", "posthog-js": "^1.34.0", "query-string": "^7.1.3", "react": "^17.0.2", @@ -40,6 +42,7 @@ "react-dom": "^17.0.2", "react-github-btn": "^1.4.0", "react-grid-layout": "^1.3.4", + "react-i18next": "^12.1.1", "react-mailchimp-subscribe": "^2.1.3", "react-markdown": "^8.0.3", "react-redux": "^8.0.2", diff --git a/frontend/pages/_app.js b/frontend/pages/_app.js index aeb2050ccb..54ed7d5121 100644 --- a/frontend/pages/_app.js +++ b/frontend/pages/_app.js @@ -1,5 +1,6 @@ import { useEffect } from "react"; import { useRouter } from "next/router"; +import { appWithTranslation } from "next-i18next"; import { config } from "@fortawesome/fontawesome-svg-core"; import Layout from "~/components/basic/Layout"; @@ -16,6 +17,15 @@ config.autoAddCss = false; const App = ({ Component, pageProps, ...appProps }) => { const router = useRouter(); + useEffect(() => { + const storedLang = localStorage.getItem("lang"); + if (router.locale ?? "en" !== storedLang ?? "en") { + router.push(router.asPath, router.asPath, { + locale: storedLang ?? "en", + }); + } + }, [router.locale, router.pathname]); + useEffect(() => { // Init for auto capturing const telemetry = new Telemetry().getInstance(); @@ -52,7 +62,7 @@ const App = ({ Component, pageProps, ...appProps }) => { ); }; -export default App; +export default appWithTranslation(App); { /*