Add react-i18next integration

This commit is contained in:
Paul Bienkowski 2022-07-24 17:06:44 +02:00
parent ed9ed68d83
commit 5f3ac69f60
9 changed files with 423 additions and 12 deletions

View file

@ -16,6 +16,8 @@
"echarts": "^5.3.2",
"echarts-for-react": "^3.0.2",
"fomantic-ui-less": "^2.8.8",
"i18next-browser-languagedetector": "^6.1.4",
"i18next-http-backend": "^1.4.1",
"immer": "^9.0.7",
"luxon": "^1.28.0",
"maplibre-gl": "^1.15.2",
@ -30,6 +32,7 @@
"react-dom": "^17.0.2",
"react-helmet": "^6.1.0",
"react-hook-form": "^6.15.8",
"react-i18next": "^11.18.1",
"react-map-gl": "^6.1.17",
"react-markdown": "^5.0.3",
"react-redux": "^7.2.6",
@ -42,7 +45,8 @@
"sass": "^1.43.5",
"semantic-ui-react": "^2.0.4",
"ts-loader": "^9.2.6",
"typescript": "^4.7.4"
"typescript": "^4.7.4",
"yaml-loader": "^0.8.0"
},
"devDependencies": {
"@babel/core": "^7.16.0",
@ -4162,6 +4166,14 @@
"node": ">=10"
}
},
"node_modules/cross-fetch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
"integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
"dependencies": {
"node-fetch": "2.6.7"
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -6251,6 +6263,14 @@
"node": ">= 12"
}
},
"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-to-react": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/html-to-react/-/html-to-react-1.4.7.tgz",
@ -6385,6 +6405,45 @@
"node": ">=10.17.0"
}
},
"node_modules/i18next": {
"version": "21.8.14",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-21.8.14.tgz",
"integrity": "sha512-4Yi+DtexvMm/Yw3Q9fllzY12SgLk+Mcmar+rCAccsOPul/2UmnBzoHbTGn/L48IPkFcmrNaH7xTLboBWIbH6pw==",
"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"
}
],
"peer": true,
"dependencies": {
"@babel/runtime": "^7.17.2"
}
},
"node_modules/i18next-browser-languagedetector": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-6.1.4.tgz",
"integrity": "sha512-wukWnFeU7rKIWT66VU5i8I+3Zc4wReGcuDK2+kuFhtoxBRGWGdvYI9UQmqNL/yQH1KogWwh+xGEaIPH8V/i2Zg==",
"dependencies": {
"@babel/runtime": "^7.14.6"
}
},
"node_modules/i18next-http-backend": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-1.4.1.tgz",
"integrity": "sha512-s4Q9hK2jS29iyhniMP82z+yYY8riGTrWbnyvsSzi5TaF7Le4E7b5deTmtuaRuab9fdDcYXtcwdBgawZG+JCEjA==",
"dependencies": {
"cross-fetch": "3.1.5"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -7020,6 +7079,11 @@
"node": ">=0.10.0"
}
},
"node_modules/javascript-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz",
"integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg=="
},
"node_modules/jest-worker": {
"version": "27.3.1",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.3.1.tgz",
@ -7867,6 +7931,25 @@
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
},
"node_modules/node-fetch": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-forge": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
@ -9251,6 +9334,27 @@
"react": "^16.8.0 || ^17"
}
},
"node_modules/react-i18next": {
"version": "11.18.1",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.1.tgz",
"integrity": "sha512-S8cl4mvIOSA7OQCE5jNy2yhv705Vwi+7PinpqKIYcBmX/trJtHKqrf6CL67WJSA8crr2JU+oxE9jn9DQIrQezg==",
"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",
@ -10596,6 +10700,11 @@
"node": ">=0.6"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/trough": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz",
@ -10971,6 +11080,14 @@
"@math.gl/web-mercator": "^3.5.5"
}
},
"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/vt-pbf": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz",
@ -11010,6 +11127,11 @@
"minimalistic-assert": "^1.0.0"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/webpack": {
"version": "5.64.4",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.64.4.tgz",
@ -11275,6 +11397,15 @@
"node": ">=0.8.0"
}
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -11375,6 +11506,40 @@
"node": ">= 6"
}
},
"node_modules/yaml-loader": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/yaml-loader/-/yaml-loader-0.8.0.tgz",
"integrity": "sha512-LjeKnTzVBKWiQBeE2L9ssl6WprqaUIxCSNs5tle8PaDydgu3wVFXTbMfsvF2MSErpy9TDVa092n4q6adYwJaWg==",
"dependencies": {
"javascript-stringify": "^2.0.1",
"loader-utils": "^2.0.0",
"yaml": "^2.0.0"
},
"engines": {
"node": ">= 12.13"
}
},
"node_modules/yaml-loader/node_modules/loader-utils": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
"integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
"dependencies": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
},
"engines": {
"node": ">=8.9.0"
}
},
"node_modules/yaml-loader/node_modules/yaml": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.1.tgz",
"integrity": "sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw==",
"engines": {
"node": ">= 14"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
@ -14399,6 +14564,14 @@
"yaml": "^1.10.0"
}
},
"cross-fetch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
"integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
"requires": {
"node-fetch": "2.6.7"
}
},
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -16002,6 +16175,14 @@
}
}
},
"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-to-react": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/html-to-react/-/html-to-react-1.4.7.tgz",
@ -16100,6 +16281,31 @@
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
"dev": true
},
"i18next": {
"version": "21.8.14",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-21.8.14.tgz",
"integrity": "sha512-4Yi+DtexvMm/Yw3Q9fllzY12SgLk+Mcmar+rCAccsOPul/2UmnBzoHbTGn/L48IPkFcmrNaH7xTLboBWIbH6pw==",
"peer": true,
"requires": {
"@babel/runtime": "^7.17.2"
}
},
"i18next-browser-languagedetector": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-6.1.4.tgz",
"integrity": "sha512-wukWnFeU7rKIWT66VU5i8I+3Zc4wReGcuDK2+kuFhtoxBRGWGdvYI9UQmqNL/yQH1KogWwh+xGEaIPH8V/i2Zg==",
"requires": {
"@babel/runtime": "^7.14.6"
}
},
"i18next-http-backend": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-1.4.1.tgz",
"integrity": "sha512-s4Q9hK2jS29iyhniMP82z+yYY8riGTrWbnyvsSzi5TaF7Le4E7b5deTmtuaRuab9fdDcYXtcwdBgawZG+JCEjA==",
"requires": {
"cross-fetch": "3.1.5"
}
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -16509,6 +16715,11 @@
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
"dev": true
},
"javascript-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz",
"integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg=="
},
"jest-worker": {
"version": "27.3.1",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.3.1.tgz",
@ -17175,6 +17386,14 @@
}
}
},
"node-fetch": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"requires": {
"whatwg-url": "^5.0.0"
}
},
"node-forge": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
@ -18149,6 +18368,15 @@
"integrity": "sha512-prq82ofMbnRyj5wqDe8hsTRcdR25jQ+B8KtCS7BLCzjFHAwNuCjRwzPuP4eYLsEBjEIeYd6try+pdLdw0kPkpg==",
"requires": {}
},
"react-i18next": {
"version": "11.18.1",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.1.tgz",
"integrity": "sha512-S8cl4mvIOSA7OQCE5jNy2yhv705Vwi+7PinpqKIYcBmX/trJtHKqrf6CL67WJSA8crr2JU+oxE9jn9DQIrQezg==",
"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",
@ -19174,6 +19402,11 @@
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
"dev": true
},
"tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"trough": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz",
@ -19458,6 +19691,11 @@
"@math.gl/web-mercator": "^3.5.5"
}
},
"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=="
},
"vt-pbf": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz",
@ -19494,6 +19732,11 @@
"minimalistic-assert": "^1.0.0"
}
},
"webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"webpack": {
"version": "5.64.4",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.64.4.tgz",
@ -19672,6 +19915,15 @@
"integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==",
"dev": true
},
"whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"requires": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -19740,6 +19992,33 @@
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="
},
"yaml-loader": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/yaml-loader/-/yaml-loader-0.8.0.tgz",
"integrity": "sha512-LjeKnTzVBKWiQBeE2L9ssl6WprqaUIxCSNs5tle8PaDydgu3wVFXTbMfsvF2MSErpy9TDVa092n4q6adYwJaWg==",
"requires": {
"javascript-stringify": "^2.0.1",
"loader-utils": "^2.0.0",
"yaml": "^2.0.0"
},
"dependencies": {
"loader-utils": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
"integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
}
},
"yaml": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.1.tgz",
"integrity": "sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw=="
}
}
},
"yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View file

@ -15,6 +15,8 @@
"echarts": "^5.3.2",
"echarts-for-react": "^3.0.2",
"fomantic-ui-less": "^2.8.8",
"i18next-browser-languagedetector": "^6.1.4",
"i18next-http-backend": "^1.4.1",
"immer": "^9.0.7",
"luxon": "^1.28.0",
"maplibre-gl": "^1.15.2",
@ -29,6 +31,7 @@
"react-dom": "^17.0.2",
"react-helmet": "^6.1.0",
"react-hook-form": "^6.15.8",
"react-i18next": "^11.18.1",
"react-map-gl": "^6.1.17",
"react-markdown": "^5.0.3",
"react-redux": "^7.2.6",
@ -41,7 +44,8 @@
"sass": "^1.43.5",
"semantic-ui-react": "^2.0.4",
"ts-loader": "^9.2.6",
"typescript": "^4.7.4"
"typescript": "^4.7.4",
"yaml-loader": "^0.8.0"
},
"eslintConfig": {
"extends": [

View file

@ -7,9 +7,11 @@ import {useObservable} from 'rxjs-hooks'
import {from} from 'rxjs'
import {pluck} from 'rxjs/operators'
import {Helmet} from "react-helmet";
import {useTranslation} from 'react-i18next'
import {useConfig} from 'config'
import styles from './App.module.less'
import {AVAILABLE_LOCALES, setLocale} from 'i18n'
import {
ExportPage,
@ -58,6 +60,7 @@ function Banner({text, style = 'warning'}: {text: string; style: 'warning' | 'in
}
const App = connect((state) => ({login: state.login}))(function App({login}) {
const {t} = useTranslation()
const config = useConfig()
const apiVersion = useObservable(() => from(api.get('/info')).pipe(pluck('version')))
@ -210,12 +213,6 @@ const App = connect((state) => ({login: state.login}))(function App({login}) {
Imprint
</a>
</List.Item>
</List>
</Grid.Column>
<Grid.Column>
<Header as="h5">Info</Header>
<List>
<List.Item>
<a
href={`https://github.com/openbikesensor/portal${
@ -224,11 +221,18 @@ const App = connect((state) => ({login: state.login}))(function App({login}) {
target="_blank"
rel="noreferrer"
>
{apiVersion ? `v${apiVersion}` : 'Fetching version...'}
Version {apiVersion ? `v${apiVersion}` : 'Fetching version...'}
</a>
</List.Item>
</List>
</Grid.Column>
<Grid.Column>
<Header as="h5">{t('App.footer.changeLanguage')}</Header>
<List>
{AVAILABLE_LOCALES.map(locale => <List.Item key={locale}><a onClick={() => setLocale(locale)}>{t(`locales.${locale}`)}</a></List.Item>)}
</List>
</Grid.Column>
</Grid.Row>
</Grid>
</Container>

95
frontend/src/i18n.ts Normal file
View file

@ -0,0 +1,95 @@
import { useState, useEffect, useMemo } from "react";
import i18next, { TOptions } from "i18next";
import { BehaviorSubject, combineLatest } from "rxjs";
import { map, distinctUntilChanged } from "rxjs/operators";
import HttpBackend, {
BackendOptions,
RequestCallback,
} from "i18next-http-backend";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";
export type AvailableLocales = "en" | "de";
async function request(
_options: BackendOptions,
url: string,
_payload: any,
callback: RequestCallback
) {
try {
const [lng] = url.split("/");
const locale = await import(`translations/${lng}.yaml`);
callback(null, { status: 200, data: locale });
} catch (e) {
console.error(`Unable to load locale at ${url}\n`, e);
callback(null, { status: 404, data: String(e) });
}
}
export const AVAILABLE_LOCALES: AvailableLocales[] = ["en", "de"];
const i18n = i18next.createInstance();
const options: TOptions = {
fallbackLng: "en",
ns: ["common"],
defaultNS: "common",
whitelist: AVAILABLE_LOCALES,
// loading via webpack
backend: {
loadPath: "{{lng}}/{{ns}}",
parse: (data: any) => data,
request,
},
load: "languageOnly",
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
};
i18n
.use(HttpBackend)
.use(initReactI18next)
.use(LanguageDetector)
.init({ ...options });
const locale$ = new BehaviorSubject<AvailableLocales>("en");
export const translate = i18n.t.bind(i18n);
export const translate$ = (stringAndData$: [string, any]) =>
combineLatest([stringAndData$, locale$.pipe(distinctUntilChanged())]).pipe(
map(([stringAndData]) => {
if (typeof stringAndData === "string") {
return i18n.t(stringAndData);
} else {
const [string, data] = stringAndData;
return i18n.t(string, { data });
}
})
);
export const setLocale = (locale: AvailableLocales) => {
i18n.changeLanguage(locale);
locale$.next(locale);
};
export function useLocale() {
const [, reload] = useState();
useEffect(() => {
i18n.on("languageChanged", reload);
return () => {
i18n.off("languageChanged", reload);
};
}, []);
return i18n.language;
}
export default i18n;

View file

@ -1,4 +1,4 @@
import React from 'react'
import React, {Suspense} from 'react'
import {Settings} from 'luxon'
import ReactDOM from 'react-dom'
import 'fomantic-ui-less/semantic.less'
@ -11,13 +11,16 @@ import 'maplibre-gl/dist/maplibre-gl.css'
import {Provider} from 'react-redux'
import store from './store'
import './i18n'
// TODO: remove
Settings.defaultLocale = 'de-DE'
ReactDOM.render(
<Provider store={store}>
<Suspense fallback={null}>
<App />
</Suspense>
</Provider>,
document.getElementById('root')
)

View file

@ -4,6 +4,7 @@ import {Message, Grid, Loader, Header, Item} from 'semantic-ui-react'
import {useObservable} from 'rxjs-hooks'
import {of, from} from 'rxjs'
import {map, switchMap} from 'rxjs/operators'
import {useTranslation} from 'react-i18next'
import api from 'api'
import {Stats, Page, Map} from 'components'
@ -22,9 +23,10 @@ function MostRecentTrack() {
[]
)
const {t} = useTranslation()
return (
<>
<Header as="h2">Most recent track</Header>
<Header as="h2">{t('HomePage.mostRecentTrack')}</Header>
<Loader active={track === null} />
{track === undefined ? (
<Message>

View file

@ -0,0 +1,6 @@
HomePage:
mostRecentTrack: Neueste Aufzeichnung
App:
footer:
changeLanguage: Sprache wechseln

View file

@ -0,0 +1,10 @@
HomePage:
mostRecentTrack: Most recent track
locales:
en: English
de: Deutsch
App:
footer:
changeLanguage: Change language

View file

@ -207,6 +207,14 @@ module.exports = function (webpackEnv) {
},
},
},
{
test: /\.ya?ml$/,
type: 'json',
use: [{
loader: 'yaml-loader',
options: {asJSON: true},
}],
},
{
test: /\.css$/i,
use: getStyleLoaders(false),