diff --git a/client/package-lock.json b/client/package-lock.json index 288aa4e9..bc8c134a 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -13,8 +13,12 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@vitejs/plugin-react": "^2.2.0", "cron-parser": "^4.7.0", + "i18next": "^22.0.2", + "i18next-browser-languagedetector": "^6.1.8", + "i18next-http-backend": "^1.4.4", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-i18next": "^12.0.0", "sass": "^1.56.1", "vite": "^3.2.4", "vite-plugin-pwa": "^0.13.3" @@ -1566,9 +1570,9 @@ } }, "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.19.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.4.tgz", + "integrity": "sha512-EXpLCrk55f+cYqmHsSR+yD/0gAIMxxA9QK9lnQWzhMCvt+YmoBN7Zx94s++Kv0+unHk39vxNO8t+CMA2WSS3wA==", "dependencies": { "regenerator-runtime": "^0.13.4" }, @@ -2332,6 +2336,14 @@ "node": ">=12.0.0" } }, + "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/crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", @@ -3101,6 +3113,52 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/i18next": { + "version": "22.0.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.0.2.tgz", + "integrity": "sha512-rGXWILemhx0dpNE5PfudVU1g4SdW0hkh4WYHaP8Cl6gm1KkMdBckmNqj9WvYqp8fOSocqX5FFJeadwiOZHLSXQ==", + "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.17.2" + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-6.1.8.tgz", + "integrity": "sha512-Svm+MduCElO0Meqpj1kJAriTC6OhI41VhlT/A0UPjGoPZBhAHIaGE5EfsHlTpgdH09UVX7rcc72pSDDBeKSQQA==", + "dependencies": { + "@babel/runtime": "^7.19.0" + } + }, + "node_modules/i18next-http-backend": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-1.4.4.tgz", + "integrity": "sha512-M4gLPe6JKZ2p1UmE6t4rzWV/sAxgrLThW7ztXAsTpFwFqXoyzhTzX8eYxVv9KjpCQh4K9nwxnEjEi+74C4Thbg==", + "dependencies": { + "cross-fetch": "3.1.5" + } + }, "node_modules/idb": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.2.tgz", @@ -3567,6 +3625,44 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "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-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/node-fetch/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/node-fetch/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/node-releases": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", @@ -3765,6 +3861,27 @@ "react": "^18.2.0" } }, + "node_modules/react-i18next": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.0.0.tgz", + "integrity": "sha512-/O7N6aIEAl1FaWZBNvhdIo9itvF/MO/nRKr9pYqRc9LhuC1u21SlfwpiYQqvaeNSEW3g3qUXLREOWMt+gxrWbg==", + "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/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -4408,6 +4525,14 @@ "rollup": "^1.20.0 || ^2.0.0" } }, + "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/webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", @@ -5660,9 +5785,9 @@ } }, "@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.19.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.4.tgz", + "integrity": "sha512-EXpLCrk55f+cYqmHsSR+yD/0gAIMxxA9QK9lnQWzhMCvt+YmoBN7Zx94s++Kv0+unHk39vxNO8t+CMA2WSS3wA==", "requires": { "regenerator-runtime": "^0.13.4" } @@ -6213,6 +6338,14 @@ "luxon": "^3.1.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" + } + }, "crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", @@ -6674,6 +6807,38 @@ "has-symbols": "^1.0.2" } }, + "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" + } + }, + "i18next": { + "version": "22.0.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.0.2.tgz", + "integrity": "sha512-rGXWILemhx0dpNE5PfudVU1g4SdW0hkh4WYHaP8Cl6gm1KkMdBckmNqj9WvYqp8fOSocqX5FFJeadwiOZHLSXQ==", + "requires": { + "@babel/runtime": "^7.17.2" + } + }, + "i18next-browser-languagedetector": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-6.1.8.tgz", + "integrity": "sha512-Svm+MduCElO0Meqpj1kJAriTC6OhI41VhlT/A0UPjGoPZBhAHIaGE5EfsHlTpgdH09UVX7rcc72pSDDBeKSQQA==", + "requires": { + "@babel/runtime": "^7.19.0" + } + }, + "i18next-http-backend": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-1.4.4.tgz", + "integrity": "sha512-M4gLPe6JKZ2p1UmE6t4rzWV/sAxgrLThW7ztXAsTpFwFqXoyzhTzX8eYxVv9KjpCQh4K9nwxnEjEi+74C4Thbg==", + "requires": { + "cross-fetch": "3.1.5" + } + }, "idb": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.2.tgz", @@ -6999,6 +7164,35 @@ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" }, + "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" + }, + "dependencies": { + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "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==" + }, + "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" + } + } + } + }, "node-releases": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", @@ -7130,6 +7324,15 @@ "scheduler": "^0.23.0" } }, + "react-i18next": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.0.0.tgz", + "integrity": "sha512-/O7N6aIEAl1FaWZBNvhdIo9itvF/MO/nRKr9pYqRc9LhuC1u21SlfwpiYQqvaeNSEW3g3qUXLREOWMt+gxrWbg==", + "requires": { + "@babel/runtime": "^7.14.5", + "html-parse-stringify": "^3.0.1" + } + }, "readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -7564,6 +7767,11 @@ } } }, + "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==" + }, "webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", diff --git a/client/package.json b/client/package.json index c57df2ef..85a41fc6 100644 --- a/client/package.json +++ b/client/package.json @@ -12,6 +12,10 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@vitejs/plugin-react": "^2.2.0", "cron-parser": "^4.7.0", + "i18next": "^22.0.2", + "i18next-browser-languagedetector": "^6.1.8", + "i18next-http-backend": "^1.4.4", + "react-i18next": "^12.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.56.1", diff --git a/client/public/locales/de.json b/client/public/locales/de.json new file mode 100644 index 00000000..f2039897 --- /dev/null +++ b/client/public/locales/de.json @@ -0,0 +1,173 @@ +{ + "failed": "Fehlgeschlagen", + "dialog": { + "okay": "Okay", + "done": "Fertig", + "apply": "Ja, übernehmen", + "update": "Aktualisieren", + "close": "Schließen", + "unset": "Entfernen", + "password": { + "title": "Passwort erforderlich", + "placeholder": "Dein Passwort", + "wrong": "Das von dir eingegebene Passwort ist falsch" + }, + "accept": { + "title": "Nutzungsbedingungen akzeptieren", + "description": "Mit dem Klick auf Akzeptieren bestätigst du, dass du die EULA, Datenschutzerklärung und Nutzungsbedingungen von Ookla gelesen hast und diesen zustimmst.", + "button": "Akzeptieren" + } + }, + "dropdown": { + "settings": "Einstellungen", + "changes_applied": "Deine Änderungen wurden übernommen.", + "changes_unsaved": "Deine Änderungen wurden nicht übernommen. Überprüfe deine Eingabe.", + "invalid": "Eingabe ungültig", + "ping": "Optimaler Ping", + "upload": "Optimaler Up-Speed", + "download": "Optimaler Down-Speed", + "recommendations": "Optimale Werte", + "server": "Server wechseln", + "password": "Passwort ändern", + "cron": "Häufigkeit einstellen", + "time": "Zeitraum festlegen", + "export": "Tests exportieren", + "pause_tests": "Tests pausieren", + "resume_tests": "Tests fortsetzen", + "healthchecks": "Healthchecks", + "language": "Sprache ändern", + "info": "Info" + }, + "options": { + "time": { + "24hours": "24 Stunden (Standard)", + "2days": "2 Tage (Insgesamt)", + "7days": "7 Tage (Durchschnitt)", + "30days": "30 Tage (Durchschnitt)" + }, + "cron": { + "continuous": "Durchgehend (jede Minute)", + "frequent": "Sehr häufig (alle 30 Minuten)", + "default": "Standard (jede Stunde)", + "rare": "Selten (alle 3 Stunden)", + "really_rare": "Sehr selten (alle 6 Stunden)" + }, + "export": { + "json": "JSON-Datei", + "csv": "CSV-Datei" + } + }, + "update": { + "ping_title": "Optimalen Ping setzen (ms)", + "ping_placeholder": "Ping (ms)", + "upload_title": "Optimalen Up-Speed setzen (Mbit/s)", + "upload_placeholder": "Up-Speed (Mbit/s)", + "download_title": "Optimalen Down-Speed setzen (Mbit/s)", + "download_placeholder": "Down-Speed (Mbit/s)", + "recommendations_title": "Automatische Empfehlungen", + "recommendations_set": "Automatische Empfehlungen setzen?", + "server_title": "Speedtest-Server setzen", + "manually": "Manuell festlegen", + "manual_server_title": "Speedtest-Server setzen", + "manual_server_id": "Server-ID", + "new_password": "Neues Passwort festlegen", + "password_placeholder": "Neues Passwort", + "password_removed": "Die Passwortsperre wurde aufgehoben und das festgelegte Passwort wurde entfernt.", + "cron_title": "Test-Häufigkeit einstellen", + "cron_rules": "Cron-Regel", + "cron_next_test": "Nächster Test:", + "time_title": "Zeige Tests der letzten ...", + "export_title": "Speedtests exportieren", + "download": "Herunterladen", + "pause_title": "Speedtests pausieren für...", + "hours": "Stunden", + "pause": "Pausieren", + "release_manually": "Manuell freigeben", + "healthchecks": "HealthChecks Integration", + "healthchecks_url": "HealthChecks Server URL", + "healthchecks_activated": "Die Healthchecks wurden deaktiviert", + "language": "Sprache ändern" + }, + "header": { + "title": "Netzwerkanalyse", + "running_tooltip": "Speedtest läuft", + "start_tooltip": "Speedtest starten", + "new_update": "Update verfügbar", + "paused": "Speedtests sind aktuell pausiert. Bitte setze sie fort, wenn du einen machen möchtest.", + "running": "Es läuft bereits ein Speedtest. Bitte gedulde dich ein wenig, bis dieser fertig ist." + }, + "latest": { + "ping": "Ping", + "ping_unit": "ms", + "down": "Download", + "speed_unit": "Mbit/s", + "up": "Upload", + "latest": "Letzter Test", + "before": "vor" + }, + "info": { + "healthchecks": "MySpeed verwendet HealthChecks, um dich zu benachrichtigen, wenn dein Internet ausfällt. Um dies zu aktivieren, setze deine Ping URL in das Textfeld ein. Mehr dazu hier", + "credits": "MySpeed wird von GNMYT bereitgestellt und verwendet die Speedtest-CLI von Ookla.", + "recommendations_error": "Du musst mindestens 10 Tests machen, damit ein Durchschnitt ermittelt werden kann. Ob die Tests manuell oder automatisch durchgeführt wurden ist egal.", + "recommendations_info": "Anhand der letzten 10 Testergebnisse wurde festgestellt, dass der optimale Ping bei {{ping}} ms, der Download bei {{down}} Mbit/s und der Upload bei {{up}} Mbit/s liegt. Orientiere dich am besten an deinem Internetvertrag und übernehme es nur, wenn es mit dem übereinstimmt.", + "update": "Ein Update auf die Version {{version}} ist verfügbar. Sieh dir die Änderungen an und lade dir das Update herunter.", + "down": { + "title": "Download-Geschwindigkeit", + "description": "Die Downloadgeschwindigkeit wirkt sich auf dein Surferlebnis aus. Umso mehr du bekommst, desto schneller kann dein Computer Daten empfangen." + }, + "ping": { + "title": "Ping", + "description": "Der Ping zeigt dir, wie schnell der jeweilige Anbieter antwortet. Umso kürzer die Zeit, desto besser." + }, + "up": { + "title": "Upload-Geschwindigkeit", + "description": "Die Uploadgeschwindigkeit wirkt sich auf dein Surferlebnis aus. Umso mehr du bekommst, desto schneller kann dein Computer Daten senden." + }, + "latest": { + "title": "Letzter Test", + "description": "Dies ist die Zeit, die dir zeigt, wann der letzte Test ausgeführt wurde. In diesem Fall wurde der letzte Test am {{date}} um {{time}} ausgeführt." + } + }, + "time": { + "now": "Gerade eben", + "seconds": "{{seconds}} Sekunden", + "minute": "1 Minute", + "minutes": "{{minutes}} Minuten", + "hour": "1 Stunde", + "hours": "{{hours}} Stunden", + "on": "Am", + "at": "Um" + }, + "test": { + "not_available": "Es liegen aktuell keine Tests vor", + "unknown_error": "Unbekannter Fehler:", + "failed": "Test fehlgeschlagen", + "recheck": "Bitte überprüfe weitestgehend, ob das öfters passiert.", + "delete": "Test löschen", + "average": { + "title": "Durchschnittsgeschwindigkeit", + "description": "{{amount}} Tests haben ergeben, dass am {{date}} eine durchschnittliche Downloadgeschwindigkeit von {{down}} Mbit/s und eine Upload-geschwindigkeit von {{up}} Mbit/s bestand. Die Tests dauerten im Durchschnitt {{duration}} Sekunden." + }, + "result": { + "title": "Testergebnis", + "description": "Dieser Test erreichte eine maximale Downloadgeschwindigkeit von {{down}} Mbit/s und eine maximale Uploadgeschwindigkeit von {{up}} Mbit/s. Er wurde {{type}} angelegt und hat {{duration}} Sekunden gedauert.", + "from_you": "von dir", + "automatic": "automatisch" + }, + "tooltips": { + "custom": "Benutzerdefiniert", + "average": "Durchschnitt", + "auto": "Automatisiert" + } + }, + "errors": { + "network_unreachable": "Die Internetverbindung scheint unterbrochen gewesen zu sein", + "took_too_long": "Der Test hat zu lange gedauert und wurde abgebrochen", + "no_permission": "MySpeed hat keine Berechtigung, diesen Test zu starten", + "resource_unavailable": "Der Test konnte nicht durchgeführt werden, da die Ressource vorübergehend nicht verfügbar ist", + "no_route": "Der Test konnte nicht durchgeführt werden, da keine Route zum Host existiert", + "connection_refused": "Der Test konnte nicht durchgeführt werden, da die Verbindung abgelehnt wurde", + "timed_out": "Die Internetverbindung scheint unterbrochen gewesen zu sein", + "config": "Die Konfigurationsdatei konnte nicht geladen werden" + } +} \ No newline at end of file diff --git a/client/public/locales/en.json b/client/public/locales/en.json new file mode 100644 index 00000000..1cabbf91 --- /dev/null +++ b/client/public/locales/en.json @@ -0,0 +1,173 @@ +{ + "failed": "Failed", + "dialog": { + "okay": "Okay", + "done": "Done", + "apply": "Yes, apply", + "update": "Update", + "close": "Close", + "unset": "Remove", + "password": { + "title": "Password required", + "placeholder": "Your password", + "wrong": "The password you entered is incorrect" + }, + "accept": { + "title": "Accept Terms", + "description": "By clicking Accept, you acknowledge that you have read and agree to Ookla's EULA, Privacy Statement and Terms of Use.", + "button": "Accept" + } + }, + "dropdown": { + "settings": "Settings", + "changes_applied": "Your changes have been applied.", + "changes_unsaved": "Your changes were not applied. Check your input.", + "invalid": "Input invalid", + "ping": "Optimal ping", + "upload": "Optimal up-speed", + "download": "Optimal down-speed", + "recommendations": "Recommendations", + "server": "Change Server", + "password": "Change password", + "cron": "Set frequency", + "time": "Set period", + "export": "Export tests", + "pause_tests": "Pause tests", + "resume_tests": "Resume tests", + "healthchecks": "Healthchecks", + "language": "Change language", + "info": "Info" + }, + "options": { + "time": { + "24hours": "24 hours (default)", + "2days": "2 days (Total)", + "7days": "7 days (average)", + "30days": "30 days (average)" + }, + "cron": { + "continuous": "Continuous (every minute)", + "frequent": "Very often (every 30 minutes)", + "default": "Default (every hour)", + "rare": "Rarely (every 3 hours)", + "really_rare": "Very rarely (every 6 hours)" + }, + "export": { + "json": "JSON file", + "csv": "CSV file" + } + }, + "update": { + "ping_title": "Set optimal ping (ms)", + "ping_placeholder": "Ping (ms)", + "upload_title": "Set optimal up-speed (Mbps)", + "upload_placeholder": "Up speed (Mbps)", + "download_title": "Set optimal down-speed (Mbps)", + "download_placeholder": "Down speed (Mbps)", + "recommendations_title": "Automatic recommendations", + "recommendations_set": "Set automatic recommendations?", + "server_title": "Set speedtest server", + "manually": "Set manually", + "manual_server_title": "Set speedtest server", + "manual_server_id": "Server ID", + "new_password": "Set a new password", + "password_placeholder": "New password", + "password_removed": "The password lock has been removed and the set password has been removed.", + "cron_title": "Set test frequency", + "cron_rules": "Cron rule", + "cron_next_test": "Next Test:", + "time_title": "Show tests of the last ...", + "export_title": "Export speedtests", + "download": "Download", + "pause_title": "Pause speedtests for...", + "hours": "Hours", + "pause": "Pause", + "release_manually": "Release manually", + "healthchecks": "HealthChecks Integration", + "healthchecks_url": "HealthChecks Server URL", + "healthchecks_activated": "The health checks have been disabled", + "language": "Change language" + }, + "header": { + "title": "Network Analysis", + "running_tooltip": "Speedtest running", + "start_tooltip": "Start speedtest", + "new_update": "Update available", + "paused": "Speedtests are currently paused. Please continue them if you want to do one.", + "running": "A speed test is already running. Please be patient until it is finished." + }, + "latest": { + "ping": "Ping", + "ping_unit": "ms", + "down": "Download", + "speed_unit": "Mbps", + "up": "Upload", + "latest": "Last test", + "before": "before" + }, + "info": { + "healthchecks": "MySpeed uses HealthChecks to notify you when your internet is down. To enable this, put your ping URL in the text box. Read more here", + "credits": "MySpeed is provided by GNMYT and uses the Speedtest CLI from Ookla.", + "recommendations_error": "You have to do at least 10 tests to get an average. It doesn't matter if the tests were done manually or automatically.", + "recommendations_info": "Based on the last 10 test results, it was found that the optimal ping was {{ping}} ms, the download at {{down}} Mbit/s and the upload at {{up}} Mbit/s. It is best to orientate yourself on your internet contract and only adopt it if it matches that.", + "update": "An update to version {{version}} is available. See the changes and download the update.", + "down": { + "title": "Download speed", + "description": "Download speed affects your browsing experience. The more you get, the faster your computer can receive data." + }, + "ping": { + "title": "Ping", + "description": "The ping shows you how fast the respective provider responds. The shorter the time, the better." + }, + "up": { + "title": "Upload speed", + "description": "Upload speed affects your browsing experience. The more you get, the faster your computer can send data." + }, + "latest": { + "title": "Last test", + "description": "This is the time that shows you when the last test was run. In this case, the last test was run on {{date}} at {{time}}." + } + }, + "time": { + "now": "Just now", + "seconds": "{{seconds}} seconds", + "minute": "1 minute", + "minutes": "{{minutes}} minutes", + "hour": "1 hour", + "hours": "{{hours}} hours", + "on": "On", + "at": "At" + }, + "test": { + "not_available": "There are currently no tests available", + "unknown_error": "Unknown error:", + "failed": "Test failed", + "recheck": "Please check as far as possible if this happens often.", + "delete": "Delete Test", + "average": { + "title": "Average Speed", + "description": "{{amount}} Tests have shown that on {{date}} an average download speed of {{down}} Mbps and an upload speed of {{up}} Mbps was passed. The tests took an average of {{duration}} Seconds." + }, + "result": { + "title": "Test result", + "description": "This test achieved a maximum download speed of {{down}} Mbps and a maximum upload speed of {{up}} Mbps. It was created {{type}} and took {{duration}} Seconds.", + "from_you": "by you", + "automatic": "automatically" + }, + "tooltips": { + "custom": "Custom", + "average": "Average", + "auto": "Automated" + } + }, + "errors": { + "network_unreachable": "The Internet connection seems to have been interrupted", + "took_too_long": "The test took too long and was canceled", + "no_permission": "MySpeed has no permission to start this test", + "resource_unavailable": "The test could not be performed because the resource is temporarily unavailable", + "no_route": "The test could not be performed because there is no route to the host", + "connection_refused": "The test could not be performed because the connection was rejected", + "timed_out": "The Internet connection seems to have been interrupted", + "config": "The configuration file could not be loaded" + } +} \ No newline at end of file diff --git a/client/src/common/components/Dropdown/DropdownComponent.jsx b/client/src/common/components/Dropdown/DropdownComponent.jsx index f13b3079..e4b4d470 100644 --- a/client/src/common/components/Dropdown/DropdownComponent.jsx +++ b/client/src/common/components/Dropdown/DropdownComponent.jsx @@ -3,16 +3,17 @@ import "./styles.sass"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import { faArrowDown, faArrowUp, faCalendarDays, faCircleNodes, faClock, faClose, faFileExport, - faGear, faInfo, faKey, faPause, faPingPongPaddleBall, faPlay, faServer, faWandMagicSparkles + faGear, faGlobeEurope, faInfo, faKey, faPause, faPingPongPaddleBall, faPlay, faServer, faWandMagicSparkles } from "@fortawesome/free-solid-svg-icons"; import {ConfigContext} from "@/common/contexts/Config"; import {StatusContext} from "@/common/contexts/Status"; import {DialogContext} from "@/common/contexts/Dialog"; import {SpeedtestContext} from "@/common/contexts/Speedtests"; import {downloadRequest, jsonRequest, patchRequest, postRequest} from "@/common/utils/RequestUtil"; -import {creditsInfo, healthChecksInfo, recommendationsError, recommendationsInfo} from "@/common/components/Dropdown/utils/infos"; -import {exportOptions, selectOptions, timeOptions} from "@/common/components/Dropdown/utils/options"; +import {creditsInfo, healthChecksInfo, recommendationsInfo} from "@/common/components/Dropdown/utils/infos"; +import {exportOptions, languageOptions, selectOptions, timeOptions} from "@/common/components/Dropdown/utils/options"; import {parseCron, stringifyCron} from "@/common/components/Dropdown/utils/utils"; +import {changeLanguage, t} from "i18next"; let icon; @@ -57,7 +58,7 @@ function DropdownComponent() { }, []); const showFeedback = (customText, reload = true) => setDialog({ - title: "MySpeed", description: customText || <>Deine Änderungen wurden übernommen., buttonText: "Okay", + title: "MySpeed", description: customText || t('dropdown.changes_applied'), buttonText: t('dialog.okay'), onSuccess: () => reload ? reloadConfig() : "", onClose: () => reloadConfig() }); @@ -67,20 +68,20 @@ function DropdownComponent() { setDialog({ ...(await dialog(config[key])), onSuccess: value => patchRequest(`/config/${key}`, {value: postValue(value)}) - .then(res => showFeedback(!res.ok ? "Deine Änderungen wurden nicht übernommen. Überprüfe deine Eingabe." : undefined)) + .then(res => showFeedback(!res.ok ? t("dropdown.changes_unsaved") : undefined)) }) } const updatePing = async () => patchDialog("ping", (value) => ({ - title: "Optimalen Ping setzen (ms)", placeholder: "Ping (ms)", value + title: t("update.ping_title"), placeholder: t("update.ping_placeholder"), value })); const updateUpload = async () => patchDialog("upload", (value) => ({ - title: "Optimalen Up-Speed setzen (Mbit/s)", placeholder: "Up-Speed (Mbit/s)", value + title: t("update.upload_title"), placeholder: t("update.upload_placeholder"), value })); const updateDownload = async () => patchDialog("download", (value) => ({ - title: "Optimalen Down-Speed setzen (Mbit/s)", placeholder: "Down-Speed (Mbit/s)", value + title: t("update.download_title"), placeholder: t("update.download_placeholder"), value })); const recommendedSettings = async () => { @@ -89,9 +90,9 @@ function DropdownComponent() { if (!result.message) { setDialog({ - title: "Automatische Empfehlungen setzen?", + title: t("update.recommendations_set"), description: recommendationsInfo(result.ping, result.download, result.upload), - buttonText: "Ja, übernehmen", + buttonText: t("dialog.apply"), onSuccess: async () => { await patchRequest("/config/ping", {value: result.ping}); await patchRequest("/config/download", {value: result.download}); @@ -99,32 +100,31 @@ function DropdownComponent() { showFeedback(); } }); - } else setDialog({title: "Automatische Empfehlungen", description: recommendationsError, buttonText: "Okay"}); + } else setDialog({title: t("update.recommendations_title"), description: t("info.recommendations_error"), buttonText: t("dialog.okay")}); } const updateServer = () => patchDialog("serverId", async (value) => ({ - title: "Speedtest-Server setzen", + title: t("update.server_title"), select: true, selectOptions: await jsonRequest("/info/server"), - unsetButton: "Manuell festlegen", + unsetButton: t("update.manually"), onClear: updateServerManually, value })); const updateServerManually = () => patchDialog("serverId", (value) => ({ - title: "Speedtest-Server setzen", placeholder: "Server-ID", type: "number", value: value, + title: t("update.manual_server_title"), placeholder: t("update.manual_server_id"), type: "number", value: value, }), false); const updatePassword = async () => { toggleDropdown(); setDialog({ - title: "Neues Passwort festlegen", - placeholder: "Neues Passwort", + title: t("update.new_password"), + placeholder: t("update.password_placeholder"), type: "password", unsetButton: localStorage.getItem("password") != null ? "Sperre aufheben" : undefined, onClear: () => patchRequest("/config/password", {value: "none"}) - .then(() => showFeedback(<>Die Passwortsperre wurde aufgehoben und das festgelegte Passwort wurde - entfernt., false)) + .then(() => showFeedback(t("update.password_removed"), false)) .then(() => localStorage.removeItem("password")), onSuccess: (value) => patchRequest("/config/password", {value}) .then(() => showFeedback(undefined, false)) @@ -135,30 +135,30 @@ function DropdownComponent() { const updateCron = async () => { toggleDropdown(); setDialog({ - title: "Test-Häufigkeit einstellen", + title: t("update.cron_title"), select: true, - selectOptions: selectOptions, + selectOptions: selectOptions(), value: config.cron, - unsetButton: "Manuell festlegen", + unsetButton: t("update.manually"), onClear: updateCronManually, onSuccess: value => patchRequest("/config/cron", {value}).then(() => showFeedback()) }); } const updateCronManually = () => patchDialog("cron", (value) => ({ - title: <>Test-Häufigkeit einstellen ?, - placeholder: "Cron-Regel", + title: <>{t("update.cron_title")} ?, + placeholder: t("update.cron_rules"), value: value, - updateDescription: (val) => <>Nächster Test: {parseCron(val)}, - description: <>Nächster Test: {parseCron(value)}, + updateDescription: (val) => <>{t("update.cron_next_test")} {parseCron(val)}, + description: <>{t("update.cron_next_test")} {parseCron(value)}, }), false, (val) => stringifyCron(val)); const updateTime = async () => { toggleDropdown(); setDialog({ - title: "Zeige Tests der letzten ...", + title: t("update.time_title"), select: true, - selectOptions: timeOptions, + selectOptions: timeOptions(), value: localStorage.getItem("testTime") || 1, onSuccess: value => { localStorage.setItem("testTime", value); @@ -172,10 +172,10 @@ function DropdownComponent() { toggleDropdown(); setDialog({ select: true, - title: "Speedtests exportieren", - buttonText: "Herunterladen", + title: t("update.export_title"), + buttonText: t("update.download"), value: "json", - selectOptions: exportOptions, + selectOptions: exportOptions(), onSuccess: value => downloadRequest("/export/" + value) }); } @@ -184,11 +184,11 @@ function DropdownComponent() { toggleDropdown(); if (!status.paused) { setDialog({ - title: "Speedtests pausieren für...", - placeholder: "Stunden", + title: t("update.pause_title"), + placeholder: t("update.hours"), type: "number", - buttonText: "Pausieren", - unsetButton: "Manuell freigeben", + buttonText: t("update.pause"), + unsetButton: t("update.release_manually"), onClear: () => postRequest("/speedtests/pause", {resumeIn: -1}).then(updateStatus), onSuccess: (hours) => postRequest("/speedtests/pause", {resumeIn: hours}).then(updateStatus) }); @@ -196,45 +196,57 @@ function DropdownComponent() { } const updateIntegration = async () => patchDialog("healthChecksUrl", (value) => ({ - title: <>HealthChecks Integration ?, - placeholder: "HealthChecks Server URL", value, - buttonText: "Aktualisieren", + title: <>{t("update.healthchecks")} ?, + placeholder: t("update.healthchecks_url"), value, + buttonText: t("dialog.update"), unsetButton: !value.includes("") ? "Deaktivieren" : undefined, onClear: () => patchDialog("/config/healthChecksUrl", {value: "https://hc-ping.com/"}) - .then(() => showFeedback(<>Die Healthchecks wurden deaktiviert)) + .then(() => showFeedback(t("update.healthchecks_activated"))) })); const showIntegrationInfo = () => setDialog({ - title: "HealthChecks Integration", - description: healthChecksInfo, - buttonText: "Okay" + title: t("update.healthchecks"), + description: healthChecksInfo(), + buttonText: t("dialog.okay") }); + const updateLanguage = () => { + toggleDropdown(); + setDialog({ + title: t("update.language"), + select: true, + selectOptions: languageOptions, + value: localStorage.getItem("language") || "en", + onSuccess: value => changeLanguage(value, showFeedback()) + }); + } + const showCredits = () => { toggleDropdown(); - setDialog({title: "MySpeed", description: creditsInfo, buttonText: "Schließen"}); + setDialog({title: "MySpeed", description: creditsInfo(), buttonText: t("dialog.close")}); } const options = [ - {run: updatePing, icon: faPingPongPaddleBall, text: "Optimaler Ping"}, - {run: updateUpload, icon: faArrowUp, text: "Optimaler Up-Speed"}, - {run: updateDownload, icon: faArrowDown, text: "Optimaler Down-Speed"}, - {run: recommendedSettings, icon: faWandMagicSparkles, text: "Optimale Werte"}, + {run: updatePing, icon: faPingPongPaddleBall, text: t("dropdown.ping")}, + {run: updateUpload, icon: faArrowUp, text: t("dropdown.upload")}, + {run: updateDownload, icon: faArrowDown, text: t("dropdown.download")}, + {run: recommendedSettings, icon: faWandMagicSparkles, text: t("dropdown.recommendations")}, {hr: true, key: 1}, - {run: updateServer, icon: faServer, text: "Server wechseln"}, - {run: updatePassword, icon: faKey, text: "Passwort ändern"}, - {run: updateCron, icon: faClock, text: "Häufigkeit einstellen"}, - {run: updateTime, icon: faCalendarDays, text: "Zeitraum festlegen"}, - {run: exportDialog, icon: faFileExport, text: "Tests exportieren"}, - {run: togglePause, icon: status.paused ? faPlay : faPause, text: "Tests " + (status.paused ? "fortsetzen" : "pausieren")}, - {run: updateIntegration, icon: faCircleNodes, text: "Healthchecks"}, - {run: showCredits, icon: faInfo, text: "Info"} + {run: updateServer, icon: faServer, text: t("dropdown.server")}, + {run: updatePassword, icon: faKey, text: t("dropdown.password")}, + {run: updateCron, icon: faClock, text: t("dropdown.cron")}, + {run: updateTime, icon: faCalendarDays, text: t("dropdown.time")}, + {run: exportDialog, icon: faFileExport, text: t("dropdown.export")}, + {run: togglePause, icon: status.paused ? faPlay : faPause, text: t("dropdown." + (status.paused ? "resume_tests" : "pause_tests"))}, + {run: updateIntegration, icon: faCircleNodes, text: t("dropdown.healthchecks")}, + {run: updateLanguage, icon: faGlobeEurope, text: t("dropdown.language")}, + {run: showCredits, icon: faInfo, text: t("dropdown.info")} ]; return ( diff --git a/client/src/i18n.js b/client/src/i18n.js new file mode 100644 index 00000000..1466e0f9 --- /dev/null +++ b/client/src/i18n.js @@ -0,0 +1,18 @@ +import i18n from "i18next"; +import {initReactI18next} from "react-i18next"; +import LanguageDetector from 'i18next-browser-languagedetector'; +import HttpApi from 'i18next-http-backend'; + +i18n.use(initReactI18next).use(LanguageDetector).use(HttpApi).init({ + supportedLngs: ['en', 'de'], + fallbackLng: 'en', + backend: { + loadPath: '/locales/{{lng}}.json' + }, + detection: { + order: ['localStorage', 'htmlTag'], + lookupLocalStorage: 'language' + } +}); + +export default i18n; \ No newline at end of file diff --git a/client/src/index.jsx b/client/src/index.jsx index 2cd8ca3a..667ff26e 100644 --- a/client/src/index.jsx +++ b/client/src/index.jsx @@ -1,6 +1,10 @@ import React from 'react'; import ReactDOM from "react-dom/client"; import App from './App'; +import i18n from './i18n'; + +export const PROJECT_URL = "https://github.com/gnmyt/myspeed"; +export const PROJECT_WIKI = "https://myspeed.gnmyt.dev"; const root = ReactDOM.createRoot(document.getElementById('root')); diff --git a/client/src/pages/Home/components/LatestTest/LatestTestComponent.jsx b/client/src/pages/Home/components/LatestTest/LatestTestComponent.jsx index c9b6d9f4..6141405b 100644 --- a/client/src/pages/Home/components/LatestTest/LatestTestComponent.jsx +++ b/client/src/pages/Home/components/LatestTest/LatestTestComponent.jsx @@ -9,6 +9,7 @@ import {ConfigContext} from "@/common/contexts/Config"; import "./styles.sass"; import {getIconBySpeed} from "@/common/utils/TestUtil"; import {downloadInfo, latestTestInfo, pingInfo, uploadInfo} from "@/pages/Home/components/LatestTest/utils/dialogs"; +import {t} from "i18next"; function LatestTestComponent() { const status = useContext(StatusContext)[0]; @@ -35,24 +36,24 @@ function LatestTestComponent() { {/* Ping */}
- setDialog(pingInfo)} icon={faPingPongPaddleBall} + setDialog(pingInfo())} icon={faPingPongPaddleBall} className={"container-icon help-icon icon-" + getIconBySpeed(latest.ping, config.ping, false)}/> -

Pingms

+

{t("latest.ping")}{t("latest.ping_unit")}

-

{latest.ping === -1 ? "Test" : latest.ping}

+

{latest.ping === -1 ? "-" : latest.ping}

{/* Download */}
- setDialog(downloadInfo)} icon={faArrowDown} + setDialog(downloadInfo())} icon={faArrowDown} className={"container-icon help-icon icon-" + getIconBySpeed(latest.download, config.download, true)}/> -

DownloadMbit/s

+

{t("latest.down")}{t("latest.speed_unit")}

-

{latest.download === -1 ? "schlug" : latest.download}

+

{latest.download === -1 ? "-" : latest.download}

@@ -61,12 +62,12 @@ function LatestTestComponent() { {/* Upload */}
- setDialog(uploadInfo)} icon={faArrowUp} + setDialog(uploadInfo())} icon={faArrowUp} className={"container-icon help-icon icon-" + getIconBySpeed(latest.upload, config.upload, true)}/> -

UploadMbit/s

+

{t("latest.up")}{t("latest.speed_unit")}

-

{latest.upload === -1 ? "fehl!" : latest.upload}

+

{latest.upload === -1 ? "-" : latest.upload}

@@ -75,7 +76,7 @@ function LatestTestComponent() {
setDialog(latestTestInfo(latest))} icon={faClockRotateLeft} className="container-icon icon-blue help-icon"/> -

Letzter Testvor

+

{t("latest.latest")}{t("latest.before")}

{latestTestTime}

diff --git a/client/src/pages/Home/components/LatestTest/utils.js b/client/src/pages/Home/components/LatestTest/utils.js index 7662ce78..6ba2dc2c 100644 --- a/client/src/pages/Home/components/LatestTest/utils.js +++ b/client/src/pages/Home/components/LatestTest/utils.js @@ -1,3 +1,5 @@ +import {t} from "i18next"; + export function generateRelativeTime(created) { let currentDate = new Date().getTime(); let date = new Date(Date.parse(created)).getTime(); @@ -5,13 +7,13 @@ export function generateRelativeTime(created) { const diff = (currentDate - date) / 1000; if (diff < 5) { - return "Gerade eben" + return t("time.now"); } else if (diff < 60) { - return diff === 1 ? "1 Sekunde" : `${Math.floor(diff)} Sekunden` + return t("time.seconds", {replace: {seconds: Math.floor(diff)}}); } else if (diff < 3600) { - return Math.floor(diff / 60) === 1 ? "1 Minute" : `${Math.floor(diff / 60)} Minuten` + return Math.floor(diff / 60) === 1 ? t("time.minute") : t("time.minutes", {replace: {minutes: Math.floor(diff / 60)}}); } else if (diff < 86400) { - return Math.floor(diff / 3600) === 1 ? "1 Stunde" : `${Math.floor(diff / 3600)} Stunden` + return Math.floor(diff / 3600) === 1 ? t("time.hour") : t("time.hours", {replace: {hours: Math.floor(diff / 3600)}}); } return "N/A" diff --git a/client/src/pages/Home/components/LatestTest/utils/dialogs.jsx b/client/src/pages/Home/components/LatestTest/utils/dialogs.jsx index 2965e5c8..30208561 100644 --- a/client/src/pages/Home/components/LatestTest/utils/dialogs.jsx +++ b/client/src/pages/Home/components/LatestTest/utils/dialogs.jsx @@ -1,28 +1,16 @@ -export const downloadInfo = { - title: "Download-Geschwindigkeit", - description: "Die Downloadgeschwindigkeit wirkt sich auf dein Surferlebnis aus. Umso mehr du bekommst, desto schneller kann dein Computer Daten empfangen.", - buttonText: "Okay" -}; +import {t} from "i18next"; +import {Trans} from "react-i18next"; -export const pingInfo = { - title: "Ping", - description: "Der Ping zeigt dir, wie schnell der jeweilige Anbieter antwortet. Umso kürzer die Zeit, desto besser.", - buttonText: "Okay" -} +export const downloadInfo = () => ({title: t("info.down.title"), description: t("info.down.description"), buttonText: t("dialog.okay")}); -export const uploadInfo = { - title: "Upload-Geschwindigkeit", - description: "Die Uploadgeschwindigkeit wirkt sich auf dein Surferlebnis aus. Umso mehr du bekommst, desto schneller kann dein Computer Daten senden.", - buttonText: "Okay" -} +export const pingInfo = () => ({title: t("info.ping.title"), description: t("info.ping.description"), buttonText: t("dialog.okay")}); + +export const uploadInfo = () => ({title: t("info.up.title"), description: t("info.up.description"), buttonText: t("dialog.okay")}); export const latestTestInfo = (latest) => ({ - title: "Letzter Test", - description: <>Dies ist die Zeit, die dir zeigt, wann der letzte Test ausgeführt wurde. In - diesem Fall wurde der letzte Test am {new Date(latest.created).toLocaleDateString("de-DE")} um {new Date(latest.created).toLocaleTimeString("de-DE", { - hour: "2-digit", minute: "2-digit" - })} ausgeführt., - buttonText: "Okay" + title: t("info.latest.title"), + description: }} values={{date: new Date(latest.created).toLocaleDateString(), + time: new Date(latest.created).toLocaleTimeString(undefined, {hour: "2-digit", minute: "2-digit"})}}> + info.latest.description, + buttonText: t("dialog.okay") }); \ No newline at end of file diff --git a/client/src/pages/Home/components/Speedtest/SpeedtestComponent.jsx b/client/src/pages/Home/components/Speedtest/SpeedtestComponent.jsx index 95ca487e..7d935c3c 100644 --- a/client/src/pages/Home/components/Speedtest/SpeedtestComponent.jsx +++ b/client/src/pages/Home/components/Speedtest/SpeedtestComponent.jsx @@ -11,43 +11,40 @@ import "./styles.sass"; import {averageResultDialog, resultDialog} from "@/pages/Home/components/Speedtest/utils/infos"; import {errors} from "@/pages/Home/components/Speedtest/utils/errors"; import {tooltips} from "@/pages/Home/components/Speedtest/utils/tooltips"; +import {t} from "i18next"; function SpeedtestComponent(props) { const [setDialog] = useContext(DialogContext); const updateTests = useContext(SpeedtestContext)[1]; - let errorMessage = "Unbekannter Fehler: " + props.error; + let errorMessage = t("test.unknown_error") + " " + props.error; let isAverage = props.type === "average"; let timeString = (String(isAverage ? props.time.getDate() : props.time.getHours()).padStart(2, '0')) + (isAverage ? "." : ":") + (String(isAverage ? (props.time.getMonth() + 1) : props.time.getMinutes()).padStart(2, '0')); if (props.error) { - for (let errorsKey in errors) - if (props.error.includes(errorsKey)) errorMessage = errors[errorsKey]; + for (let errorsKey in errors()) + if (props.error.includes(errorsKey)) errorMessage = errors()[errorsKey]; } const showErrorDialog = () => setDialog({ - title: "Test fehlgeschlagen", - description: errorMessage + ". Bitte überprüfe weitestgehend, ob das öfters passiert.", - buttonText: "Okay", - unsetButton: "Test löschen", + title: t("test.failed"), + description: errorMessage + ". " + t("test.recheck"), + buttonText: t("dialog.okay"), + unsetButton: t("test.delete"), onClear: () => deleteRequest(`/speedtests/${props.id}`).then(updateTests) }); const showInfoDialog = () => { if (props.type === "average") { - setDialog({ - title: "Durchschnittsgeschwindigkeit", - buttonText: "Okay", - description: averageResultDialog(timeString, props) - }); + setDialog({title: t("test.average.title"), buttonText: t("dialog.okay"), description: averageResultDialog(timeString, props)}); } else { setDialog({ - title: "Testergebnis", + title: t("test.result.title"), description: resultDialog(props), - buttonText: "Okay", - unsetButton: "Test löschen", + buttonText: t("dialog.okay"), + unsetButton: t("test.delete"), onClear: () => deleteRequest(`/speedtests/${props.id}`).then(updateTests) }); } @@ -60,9 +57,9 @@ function SpeedtestComponent(props) { - {tooltips[props.type]} + {tooltips()[props.type]}
-

{(isAverage ? "Am " : "Um ") + timeString}

+

{(t("time." + (isAverage ? "on" : "at"))) + " " + timeString}

({ + "Network unreachable": t("errors.network_unreachable"), + "Timeout occurred in connect": t("errors.took_too_long"), + "permission denied": t("errors.no_permission"), + "Resource temporarily unavailable": t("errors.resource_unavailable"), + "No route to host": t("errors.no_route"), + "Connection refused": t("errors.connection_refused"), + "timed out": t("errors.timed_out"), + "Could not retrieve or read configuration": t("errors.config"), +}); \ No newline at end of file diff --git a/client/src/pages/Home/components/Speedtest/utils/infos.jsx b/client/src/pages/Home/components/Speedtest/utils/infos.jsx index 0d28a597..23f9d61a 100644 --- a/client/src/pages/Home/components/Speedtest/utils/infos.jsx +++ b/client/src/pages/Home/components/Speedtest/utils/infos.jsx @@ -1,13 +1,12 @@ import React from "react"; +import {Trans} from "react-i18next"; +import {t} from "i18next"; -export const averageResultDialog = (timeString, props) => <>{props.amount} Tests haben - ergeben, dass am {timeString} eine durchschnittliche Downloadgeschwindigkeit - von {props.down} Mbit/s und eine Upload-geschwindigkeit von {props.up} Mbit/s bestand. Die Tests dauerten im Durchschnitt {props.duration} Sekunden.; +export const averageResultDialog = (timeString, props) => }} + values={{amount: props.amount, date: timeString, down: props.down, + up: props.up, duration: props.duration}}>test.average.description -export const resultDialog = (props) => <>Dieser Test erreichte eine maximale Downloadgeschwindigkeit von {props.down} Mbit/s und eine maximale Uploadgeschwindigkeit von {props.up} Mbit/s. Er wurde {props.type === "custom" ? "von dir" : "automatisch"} angelegt und hat {props.duration} Sekunden gedauert. \ No newline at end of file +export const resultDialog = (props) => }} + values={{down: props.down, up: props.up, + type: t("test.result." + (props.type === "custom" ? "from_you" : "automatic")), + duration: props.duration}}>test.result.description \ No newline at end of file diff --git a/client/src/pages/Home/components/Speedtest/utils/tooltips.js b/client/src/pages/Home/components/Speedtest/utils/tooltips.js index 45046857..0df62abd 100644 --- a/client/src/pages/Home/components/Speedtest/utils/tooltips.js +++ b/client/src/pages/Home/components/Speedtest/utils/tooltips.js @@ -1,5 +1,7 @@ -export const tooltips = { - custom: "Benutzerdefiniert", - average: "Durchschnitt", - auto: "Automatisiert", -} \ No newline at end of file +import {t} from "i18next"; + +export const tooltips = () => ({ + custom: t("test.tooltips.custom"), + average: t("test.tooltips.average"), + auto: t("test.tooltips.auto"), +}); \ No newline at end of file diff --git a/client/src/pages/Home/components/TestArea/TestAreaComponent.jsx b/client/src/pages/Home/components/TestArea/TestAreaComponent.jsx index 10c5fe6c..15a85afb 100644 --- a/client/src/pages/Home/components/TestArea/TestAreaComponent.jsx +++ b/client/src/pages/Home/components/TestArea/TestAreaComponent.jsx @@ -4,6 +4,7 @@ import {SpeedtestContext} from "@/common/contexts/Speedtests"; import Speedtest from "../Speedtest"; import {getIconBySpeed} from "@/common/utils/TestUtil"; import "./styles.sass"; +import {t} from "i18next"; function TestArea() { const config = useContext(ConfigContext)[0]; @@ -11,7 +12,7 @@ function TestArea() { if (Object.entries(config).length === 0) return (<>); - if (speedtests.length === 0) return

Es liegen aktuell keine Tests vor

+ if (speedtests.length === 0) return

{t("test.not_available")}

return (