From c08d4b7b9c0e01e1d5f9497102958de6f528670b Mon Sep 17 00:00:00 2001 From: Ben Phelps Date: Thu, 8 Sep 2022 11:48:16 +0300 Subject: [PATCH] implement i18n --- package.json | 5 + pnpm-lock.yaml | 76 +++++++++++++- public/locales/en/common.json | 98 +++++++++++++++++++ src/components/services/widget.jsx | 8 +- .../services/widgets/service/docker.jsx | 25 ++--- .../services/widgets/service/emby.jsx | 20 ++-- .../services/widgets/service/jellyfin.jsx | 2 +- .../services/widgets/service/jellyseerr.jsx | 17 ++-- .../services/widgets/service/npm.jsx | 17 ++-- .../services/widgets/service/nzbget.jsx | 24 +++-- .../services/widgets/service/ombi.jsx | 17 ++-- .../services/widgets/service/pihole.jsx | 17 ++-- .../services/widgets/service/portainer.jsx | 19 ++-- .../services/widgets/service/radarr.jsx | 17 ++-- .../services/widgets/service/rutorrent.jsx | 18 ++-- .../services/widgets/service/sonarr.jsx | 17 ++-- .../services/widgets/service/speedtest.jsx | 27 +++-- .../services/widgets/service/tautulli.jsx | 18 ++-- .../services/widgets/service/traefik.jsx | 17 ++-- .../widgets/openweathermap/weather.jsx | 15 ++- src/components/widgets/resources/cpu.jsx | 11 ++- src/components/widgets/resources/disk.jsx | 11 ++- src/components/widgets/resources/memory.jsx | 11 ++- src/components/widgets/search/search.jsx | 5 +- src/components/widgets/weather/weather.jsx | 15 ++- src/pages/_app.jsx | 3 + src/pages/api/widgets/openweathermap.js | 4 +- src/pages/api/widgets/weather.js | 4 +- src/utils/i18n.js | 32 ++++++ 29 files changed, 431 insertions(+), 139 deletions(-) create mode 100644 public/locales/en/common.json create mode 100644 src/utils/i18n.js diff --git a/package.json b/package.json index 8db6e8f69..a6856c1cb 100644 --- a/package.json +++ b/package.json @@ -13,14 +13,19 @@ "@tailwindcss/forms": "^0.5.3", "classnames": "^2.3.1", "dockerode": "^3.3.4", + "i18next": "^21.9.1", + "i18next-browser-languagedetector": "^6.1.5", + "i18next-http-backend": "^1.4.1", "js-yaml": "^4.1.0", "json-rpc-2.0": "^1.4.1", "memory-cache": "^0.2.0", "next": "12.2.5", "node-os-utils": "^1.3.7", + "pretty-bytes": "^6.0.0", "raw-body": "^2.5.1", "react": "18.2.0", "react-dom": "18.2.0", + "react-i18next": "^11.18.5", "react-icons": "^4.4.0", "rutorrent-promise": "^2.0.0", "swr": "^1.3.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c58fee0ad..4bad1e81f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,9 @@ specifiers: eslint-plugin-prettier: ^4.2.1 eslint-plugin-react: ^7.30.1 eslint-plugin-react-hooks: ^4.6.0 + i18next: ^21.9.1 + i18next-browser-languagedetector: ^6.1.5 + i18next-http-backend: ^1.4.1 js-yaml: ^4.1.0 json-rpc-2.0: ^1.4.1 memory-cache: ^0.2.0 @@ -22,9 +25,11 @@ specifiers: node-os-utils: ^1.3.7 postcss: ^8.4.16 prettier: ^2.7.1 + pretty-bytes: ^6.0.0 raw-body: ^2.5.1 react: 18.2.0 react-dom: 18.2.0 + react-i18next: ^11.18.5 react-icons: ^4.4.0 rutorrent-promise: ^2.0.0 swr: ^1.3.0 @@ -36,14 +41,19 @@ dependencies: '@tailwindcss/forms': 0.5.3_tailwindcss@3.1.8 classnames: 2.3.1 dockerode: 3.3.4 + i18next: 21.9.1 + i18next-browser-languagedetector: 6.1.5 + i18next-http-backend: 1.4.1 js-yaml: 4.1.0 json-rpc-2.0: 1.4.1 memory-cache: 0.2.0 next: 12.2.5_biqbaboplfbrettd7655fr4n2y node-os-utils: 1.3.7 + pretty-bytes: 6.0.0 raw-body: 2.5.1 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 + react-i18next: 11.18.5_4sidbwfhen5r7txudrvpua6nty react-icons: 4.4.0_react@18.2.0 rutorrent-promise: 2.0.0 swr: 1.3.0_react@18.2.0 @@ -79,7 +89,6 @@ packages: engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.13.9 - dev: true /@balena/dockerignore/1.0.2: resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} @@ -666,6 +675,14 @@ packages: dev: false optional: true + /cross-fetch/3.1.5: + resolution: {integrity: sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==} + dependencies: + node-fetch: 2.6.7 + transitivePeerDependencies: + - encoding + dev: false + /cross-spawn/7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -1483,6 +1500,12 @@ packages: dependencies: function-bind: 1.1.1 + /html-parse-stringify/3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + dependencies: + void-elements: 3.1.0 + dev: false + /http-errors/2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -1494,6 +1517,26 @@ packages: toidentifier: 1.0.1 dev: false + /i18next-browser-languagedetector/6.1.5: + resolution: {integrity: sha512-11t7b39oKeZe4uyMxLSPnfw28BCPNLZgUk7zyufex0zKXZ+Bv+JnmJgoB+IfQLZwDt1d71PM8vwBX1NCgliY3g==} + dependencies: + '@babel/runtime': 7.18.9 + dev: false + + /i18next-http-backend/1.4.1: + resolution: {integrity: sha512-s4Q9hK2jS29iyhniMP82z+yYY8riGTrWbnyvsSzi5TaF7Le4E7b5deTmtuaRuab9fdDcYXtcwdBgawZG+JCEjA==} + dependencies: + cross-fetch: 3.1.5 + transitivePeerDependencies: + - encoding + dev: false + + /i18next/21.9.1: + resolution: {integrity: sha512-ITbDrAjbRR73spZAiu6+ex5WNlHRr1mY+acDi2ioTHuUiviJqSz269Le1xHAf0QaQ6GgIHResUhQNcxGwa/PhA==} + dependencies: + '@babel/runtime': 7.18.9 + dev: false + /iconv-lite/0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -2093,6 +2136,11 @@ packages: hasBin: true dev: true + /pretty-bytes/6.0.0: + resolution: {integrity: sha512-6UqkYefdogmzqAZWzJ7laYeJnaXDy2/J+ZqiiMtS7t7OfpXWTlaeGMwX8U6EFvPV/YWWEKRkS8hKS4k60WHTOg==} + engines: {node: ^14.13.1 || >=16.0.0} + dev: false + /prop-types/15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} dependencies: @@ -2140,6 +2188,26 @@ packages: scheduler: 0.23.0 dev: false + /react-i18next/11.18.5_4sidbwfhen5r7txudrvpua6nty: + resolution: {integrity: sha512-cKcyuuzIv0YUZ4l9WORflVNuhISPAqQShOAsxwFyYuJoCA7HlLmHm7XnvO6hfAGmGpDNRhJHoBX8hG49Cb2xZQ==} + peerDependencies: + i18next: '>= 19.0.0' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + dependencies: + '@babel/runtime': 7.18.9 + html-parse-stringify: 3.0.1 + i18next: 21.9.1 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + dev: false + /react-icons/4.4.0_react@18.2.0: resolution: {integrity: sha512-fSbvHeVYo/B5/L4VhB7sBA1i2tS8MkT0Hb9t2H1AVPkwGfVHLJCqyr2Py9dKMxsyM63Eng1GkdZfbWj+Fmv8Rg==} peerDependencies: @@ -2181,7 +2249,6 @@ packages: /regenerator-runtime/0.13.9: resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==} - dev: true /regexp.prototype.flags/1.4.3: resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==} @@ -2578,6 +2645,11 @@ packages: resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==} dev: true + /void-elements/3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + dev: false + /webidl-conversions/3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} dev: false diff --git a/public/locales/en/common.json b/public/locales/en/common.json new file mode 100644 index 000000000..00b79085d --- /dev/null +++ b/public/locales/en/common.json @@ -0,0 +1,98 @@ +{ + "common": { + "bytes": "{{value, bytes}}", + "bits": "{{value, bytes(bits: true)}}", + "bbytes": "{{value, bytes(binary: true)}}", + "bbits": "{{value, bytes(bits: true, binary: true)}}", + "byterate": "{{value, bytes}}", + "bitrate": "{{value, bytes(bits: true)}}", + "percent": "{{value, percent}}", + "number": "{{value, number}}", + "ms": "{{value, number}}" + }, + "widget": { + "missing_type": "Missing Widget Type: {{type}}", + "api_error": "API Error", + "status": "Status" + }, + "search": { + "placeholder": "Search..." + }, + "resources": { + "total": "Total", + "free": "Free", + "used": "Used" + }, + "docker": { + "rx": "RX", + "tx": "TX", + "mem": "MEM", + "cpu": "CPU", + "offline": "Offline" + }, + "emby": { + "playing": "Playing", + "transcoding": "Transcoding", + "bitrate": "Bitrate" + }, + "tautulli": { + "playing": "Playing", + "transcoding": "Transcoding", + "bitrate": "Bitrate" + }, + "nzbget": { + "rate": "Rate", + "remaining": "Remaining", + "downloaded": "Downloaded" + }, + "rutorrent": { + "active": "Active", + "upload": "Upload", + "download": "Download" + }, + "sonarr": { + "wanted": "Wanted", + "queued": "Queued", + "series": "Series" + }, + "radarr": { + "wanted": "Wanted", + "queued": "Queued", + "movies": "Movies" + }, + "ombi": { + "pending": "Pending", + "approved": "Approved", + "available": "Available" + }, + "jellyseerr": { + "pending": "Pending", + "approved": "Approved", + "available": "Available" + }, + "pihole": { + "queries": "Queries", + "blocked": "Blocked", + "gravity": "Gravity" + }, + "speedtest": { + "upload": "Upload", + "download": "Download", + "ping": "Ping" + }, + "portainer": { + "running": "Running", + "stopped": "Stopped", + "total": "Total" + }, + "traefik": { + "routers": "Routers", + "services": "Services", + "middleware": "Middleware" + }, + "npm": { + "enabled": "Enabled", + "disabled": "Disabled", + "total": "Total" + } +} diff --git a/src/components/services/widget.jsx b/src/components/services/widget.jsx index 61b31c0b7..199dbd5d4 100644 --- a/src/components/services/widget.jsx +++ b/src/components/services/widget.jsx @@ -1,3 +1,5 @@ +import { useTranslation } from "react-i18next"; + import Sonarr from "./widgets/service/sonarr"; import Radarr from "./widgets/service/radarr"; import Ombi from "./widgets/service/ombi"; @@ -33,6 +35,8 @@ const widgetMappings = { }; export default function Widget({ service }) { + const { t } = useTranslation("common"); + const ServiceWidget = widgetMappings[service.widget.type]; if (ServiceWidget) { @@ -41,9 +45,7 @@ export default function Widget({ service }) { return (
-
- Missing Widget Type: {service.widget.type} -
+
{t("widget.missing_type", { type: service.widget.type })}
); } diff --git a/src/components/services/widgets/service/docker.jsx b/src/components/services/widgets/service/docker.jsx index 7a6729cc4..88398a42e 100644 --- a/src/components/services/widgets/service/docker.jsx +++ b/src/components/services/widgets/service/docker.jsx @@ -1,11 +1,14 @@ import useSWR from "swr"; +import { useTranslation } from "react-i18next"; import Widget from "../widget"; import Block from "../block"; -import { calculateCPUPercent, formatBytes } from "utils/stats-helpers"; +import { calculateCPUPercent } from "utils/stats-helpers"; export default function Docker({ service }) { + const { t } = useTranslation(); + const config = service.widget; const { data: statusData, error: statusError } = useSWR( @@ -23,13 +26,13 @@ export default function Docker({ service }) { ); if (statsError || statusError) { - return ; + return ; } if (statusData && statusData.status !== "running") { return ( - + ); } @@ -37,22 +40,22 @@ export default function Docker({ service }) { if (!statsData || !statusData) { return ( - - - - + + + + ); } return ( - - + + {statsData.stats.networks && ( <> - - + + )} diff --git a/src/components/services/widgets/service/emby.jsx b/src/components/services/widgets/service/emby.jsx index c9e6e419c..8d2cc959f 100644 --- a/src/components/services/widgets/service/emby.jsx +++ b/src/components/services/widgets/service/emby.jsx @@ -1,25 +1,28 @@ import useSWR from "swr"; +import { useTranslation } from "react-i18next"; import Widget from "../widget"; import Block from "../block"; import { formatApiUrl } from "utils/api-helpers"; -export default function Emby({ service, title = "Emby" }) { +export default function Emby({ service }) { + const { t } = useTranslation(); + const config = service.widget; const { data: sessionsData, error: sessionsError } = useSWR(formatApiUrl(config, "Sessions")); if (sessionsError) { - return ; + return ; } if (!sessionsData) { return ( - - - + + + ); } @@ -28,13 +31,14 @@ export default function Emby({ service, title = "Emby" }) { const transcoding = sessionsData.filter( (session) => session?.PlayState && session.PlayState.PlayMethod === "Transcode" ); + const bitrate = playing.reduce((acc, session) => acc + session.NowPlayingItem.Bitrate, 0); return ( - - - + + + ); } diff --git a/src/components/services/widgets/service/jellyfin.jsx b/src/components/services/widgets/service/jellyfin.jsx index ab79335de..03a8840ab 100644 --- a/src/components/services/widgets/service/jellyfin.jsx +++ b/src/components/services/widgets/service/jellyfin.jsx @@ -2,5 +2,5 @@ import Emby from "./emby"; // Jellyfin and Emby share the same API, so proxy the Emby widget to Jellyfin. export default function Jellyfin({ service }) { - return ; + return ; } diff --git a/src/components/services/widgets/service/jellyseerr.jsx b/src/components/services/widgets/service/jellyseerr.jsx index 3820c2a16..f658b58a0 100644 --- a/src/components/services/widgets/service/jellyseerr.jsx +++ b/src/components/services/widgets/service/jellyseerr.jsx @@ -1,4 +1,5 @@ import useSWR from "swr"; +import { useTranslation } from "react-i18next"; import Widget from "../widget"; import Block from "../block"; @@ -6,29 +7,31 @@ import Block from "../block"; import { formatApiUrl } from "utils/api-helpers"; export default function Jellyseerr({ service }) { + const { t } = useTranslation(); + const config = service.widget; const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `request/count`)); if (statsError) { - return ; + return ; } if (!statsData) { return ( - - - + + + ); } return ( - - - + + + ); } diff --git a/src/components/services/widgets/service/npm.jsx b/src/components/services/widgets/service/npm.jsx index 916136e10..47b6e8bd1 100644 --- a/src/components/services/widgets/service/npm.jsx +++ b/src/components/services/widgets/service/npm.jsx @@ -1,4 +1,5 @@ import useSWR from "swr"; +import { useTranslation } from "react-i18next"; import Widget from "../widget"; import Block from "../block"; @@ -6,20 +7,22 @@ import Block from "../block"; import { formatApiUrl } from "utils/api-helpers"; export default function Npm({ service }) { + const { t } = useTranslation(); + const config = service.widget; const { data: infoData, error: infoError } = useSWR(formatApiUrl(config, "nginx/proxy-hosts")); if (infoError) { - return ; + return ; } if (!infoData) { return ( - - - + + + ); } @@ -30,9 +33,9 @@ export default function Npm({ service }) { return ( - - - + + + ); } diff --git a/src/components/services/widgets/service/nzbget.jsx b/src/components/services/widgets/service/nzbget.jsx index 56baa6312..10c6cd45f 100644 --- a/src/components/services/widgets/service/nzbget.jsx +++ b/src/components/services/widgets/service/nzbget.jsx @@ -1,35 +1,43 @@ import useSWR from "swr"; +import { useTranslation } from "react-i18next"; import Widget from "../widget"; import Block from "../block"; import { formatApiUrl } from "utils/api-helpers"; -import { formatBytes } from "utils/stats-helpers"; export default function Nzbget({ service }) { + const { t } = useTranslation("common"); + const config = service.widget; const { data: statusData, error: statusError } = useSWR(formatApiUrl(config, "status")); if (statusError) { - return ; + return ; } if (!statusData) { return ( - - - + + + ); } return ( - - - + + + ); } diff --git a/src/components/services/widgets/service/ombi.jsx b/src/components/services/widgets/service/ombi.jsx index 4bbf96fdb..7b4bd0f9e 100644 --- a/src/components/services/widgets/service/ombi.jsx +++ b/src/components/services/widgets/service/ombi.jsx @@ -1,4 +1,5 @@ import useSWR from "swr"; +import { useTranslation } from "react-i18next"; import Widget from "../widget"; import Block from "../block"; @@ -6,29 +7,31 @@ import Block from "../block"; import { formatApiUrl } from "utils/api-helpers"; export default function Ombi({ service }) { + const { t } = useTranslation(); + const config = service.widget; const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `Request/count`)); if (statsError) { - return ; + return ; } if (!statsData) { return ( - - - + + + ); } return ( - - - + + + ); } diff --git a/src/components/services/widgets/service/pihole.jsx b/src/components/services/widgets/service/pihole.jsx index 7c777ce76..8b4bb0bd1 100644 --- a/src/components/services/widgets/service/pihole.jsx +++ b/src/components/services/widgets/service/pihole.jsx @@ -1,4 +1,5 @@ import useSWR from "swr"; +import { useTranslation } from "react-i18next"; import Widget from "../widget"; import Block from "../block"; @@ -6,29 +7,31 @@ import Block from "../block"; import { formatApiUrl } from "utils/api-helpers"; export default function Pihole({ service }) { + const { t } = useTranslation(); + const config = service.widget; const { data: piholeData, error: piholeError } = useSWR(formatApiUrl(config, "api.php")); if (piholeError) { - return ; + return ; } if (!piholeData) { return ( - - - + + + ); } return ( - - - + + + ); } diff --git a/src/components/services/widgets/service/portainer.jsx b/src/components/services/widgets/service/portainer.jsx index 1e97052d8..c65c9d658 100644 --- a/src/components/services/widgets/service/portainer.jsx +++ b/src/components/services/widgets/service/portainer.jsx @@ -1,4 +1,5 @@ import useSWR from "swr"; +import { useTranslation } from "react-i18next"; import Widget from "../widget"; import Block from "../block"; @@ -6,26 +7,28 @@ import Block from "../block"; import { formatApiUrl } from "utils/api-helpers"; export default function Portainer({ service }) { + const { t } = useTranslation(); + const config = service.widget; const { data: containersData, error: containersError } = useSWR(formatApiUrl(config, `docker/containers/json?all=1`)); if (containersError) { - return ; + return ; } if (!containersData) { return ( - - - + + + ); } if (containersData.error) { - return ; + return ; } const running = containersData.filter((c) => c.State === "running").length; @@ -34,9 +37,9 @@ export default function Portainer({ service }) { return ( - - - + + + ); } diff --git a/src/components/services/widgets/service/radarr.jsx b/src/components/services/widgets/service/radarr.jsx index fec155c82..125f0f94a 100644 --- a/src/components/services/widgets/service/radarr.jsx +++ b/src/components/services/widgets/service/radarr.jsx @@ -1,4 +1,5 @@ import useSWR from "swr"; +import { useTranslation } from "react-i18next"; import Widget from "../widget"; import Block from "../block"; @@ -6,21 +7,23 @@ import Block from "../block"; import { formatApiUrl } from "utils/api-helpers"; export default function Radarr({ service }) { + const { t } = useTranslation(); + const config = service.widget; const { data: moviesData, error: moviesError } = useSWR(formatApiUrl(config, "movie")); const { data: queuedData, error: queuedError } = useSWR(formatApiUrl(config, "queue/status")); if (moviesError || queuedError) { - return ; + return ; } if (!moviesData || !queuedData) { return ( - - - + + + ); } @@ -30,9 +33,9 @@ export default function Radarr({ service }) { return ( - - - + + + ); } diff --git a/src/components/services/widgets/service/rutorrent.jsx b/src/components/services/widgets/service/rutorrent.jsx index ff9fb2a33..ddd22171d 100644 --- a/src/components/services/widgets/service/rutorrent.jsx +++ b/src/components/services/widgets/service/rutorrent.jsx @@ -1,26 +1,28 @@ import useSWR from "swr"; +import { useTranslation } from "react-i18next"; import Widget from "../widget"; import Block from "../block"; import { formatApiUrl } from "utils/api-helpers"; -import { formatBytes } from "utils/stats-helpers"; export default function Rutorrent({ service }) { + const { t } = useTranslation(); + const config = service.widget; const { data: statusData, error: statusError } = useSWR(formatApiUrl(config)); if (statusError) { - return ; + return ; } if (!statusData) { return ( - - - + + + ); } @@ -33,9 +35,9 @@ export default function Rutorrent({ service }) { return ( - - - + + + ); } diff --git a/src/components/services/widgets/service/sonarr.jsx b/src/components/services/widgets/service/sonarr.jsx index fe042a19d..a269bb5c1 100644 --- a/src/components/services/widgets/service/sonarr.jsx +++ b/src/components/services/widgets/service/sonarr.jsx @@ -1,4 +1,5 @@ import useSWR from "swr"; +import { useTranslation } from "react-i18next"; import Widget from "../widget"; import Block from "../block"; @@ -6,6 +7,8 @@ import Block from "../block"; import { formatApiUrl } from "utils/api-helpers"; export default function Sonarr({ service }) { + const { t } = useTranslation(); + const config = service.widget; const { data: wantedData, error: wantedError } = useSWR(formatApiUrl(config, "wanted/missing")); @@ -13,24 +16,24 @@ export default function Sonarr({ service }) { const { data: seriesData, error: seriesError } = useSWR(formatApiUrl(config, "series")); if (wantedError || queuedError || seriesError) { - return ; + return ; } if (!wantedData || !queuedData || !seriesData) { return ( - - - + + + ); } return ( - - - + + + ); } diff --git a/src/components/services/widgets/service/speedtest.jsx b/src/components/services/widgets/service/speedtest.jsx index 7bcf884d8..8e863876a 100644 --- a/src/components/services/widgets/service/speedtest.jsx +++ b/src/components/services/widgets/service/speedtest.jsx @@ -1,35 +1,46 @@ import useSWR from "swr"; +import { useTranslation } from "react-i18next"; import Widget from "../widget"; import Block from "../block"; -import { formatBits } from "utils/stats-helpers"; import { formatApiUrl } from "utils/api-helpers"; export default function Speedtest({ service }) { + const { t } = useTranslation(); + const config = service.widget; const { data: speedtestData, error: speedtestError } = useSWR(formatApiUrl(config, "speedtest/latest")); if (speedtestError) { - return ; + return ; } if (!speedtestData) { return ( - - - + + + ); } return ( - - - + + + ); } diff --git a/src/components/services/widgets/service/tautulli.jsx b/src/components/services/widgets/service/tautulli.jsx index a3dce1c15..d86646c33 100644 --- a/src/components/services/widgets/service/tautulli.jsx +++ b/src/components/services/widgets/service/tautulli.jsx @@ -1,4 +1,5 @@ import useSWR from "swr"; +import { useTranslation } from "react-i18next"; import Widget from "../widget"; import Block from "../block"; @@ -6,20 +7,22 @@ import Block from "../block"; import { formatApiUrl } from "utils/api-helpers"; export default function Tautulli({ service }) { + const { t } = useTranslation(); + const config = service.widget; const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, "get_activity")); if (statsError) { - return ; + return ; } if (!statsData) { return ( - - - + + + ); } @@ -28,10 +31,9 @@ export default function Tautulli({ service }) { return ( - - - {/* We divide by 1000 here because thats how Tautulli reports it on its own dashboard */} - + + + ); } diff --git a/src/components/services/widgets/service/traefik.jsx b/src/components/services/widgets/service/traefik.jsx index ce3fd4fe3..fba946a72 100644 --- a/src/components/services/widgets/service/traefik.jsx +++ b/src/components/services/widgets/service/traefik.jsx @@ -1,4 +1,5 @@ import useSWR from "swr"; +import { useTranslation } from "react-i18next"; import Widget from "../widget"; import Block from "../block"; @@ -6,29 +7,31 @@ import Block from "../block"; import { formatApiUrl } from "utils/api-helpers"; export default function Traefik({ service }) { + const { t } = useTranslation(); + const config = service.widget; const { data: traefikData, error: traefikError } = useSWR(formatApiUrl(config, "overview")); if (traefikError) { - return ; + return ; } if (!traefikData) { return ( - - - + + + ); } return ( - - - + + + ); } diff --git a/src/components/widgets/openweathermap/weather.jsx b/src/components/widgets/openweathermap/weather.jsx index c1ab9f057..d4c292f94 100644 --- a/src/components/widgets/openweathermap/weather.jsx +++ b/src/components/widgets/openweathermap/weather.jsx @@ -1,10 +1,15 @@ import useSWR from "swr"; import { BiError } from "react-icons/bi"; +import { useTranslation } from "react-i18next"; import Icon from "./icon"; export default function OpenWeatherMap({ options }) { - const { data, error } = useSWR(`/api/widgets/openweathermap?${new URLSearchParams(options).toString()}`); + const { t, i18n } = useTranslation(); + + const { data, error } = useSWR( + `/api/widgets/openweathermap?${new URLSearchParams({ lang: i18n.language, ...options }).toString()}` + ); if (error || data?.cod === 401) { return ( @@ -30,6 +35,8 @@ export default function OpenWeatherMap({ options }) { return
; } + const unit = options.units === "metric" ? "celsius" : "fahrenheit"; + return (
@@ -42,11 +49,9 @@ export default function OpenWeatherMap({ options }) {
{options.label && `${options.label}, `} - {data.main.temp.toFixed(1)}° - - - {data.weather[0].description.charAt(0).toUpperCase() + data.weather[0].description.slice(1)} + {t("common.number", { value: data.main.temp, style: "unit", unit })} + {data.weather[0].description}
diff --git a/src/components/widgets/resources/cpu.jsx b/src/components/widgets/resources/cpu.jsx index 1e6cd438c..87c1847c7 100644 --- a/src/components/widgets/resources/cpu.jsx +++ b/src/components/widgets/resources/cpu.jsx @@ -1,8 +1,11 @@ import useSWR from "swr"; import { FiCpu } from "react-icons/fi"; import { BiError } from "react-icons/bi"; +import { useTranslation } from "react-i18next"; export default function Cpu() { + const { t } = useTranslation(); + const { data, error } = useSWR(`/api/widgets/resources?type=cpu`, { refreshInterval: 1500, }); @@ -12,7 +15,7 @@ export default function Cpu() {
- API Error + {t("widget.api_error")}
); @@ -23,7 +26,7 @@ export default function Cpu() {
- - Usage + -
); @@ -35,7 +38,9 @@ export default function Cpu() {
-
{`${Math.round(data.cpu.usage)}%`}
+
+ {t("common.number", { value: data.cpu.usage, style: "unit", unit: "percent", maximumFractionDigits: 0 })} +
- API Error + {t("widget.api_error")}
); @@ -38,10 +39,10 @@ export default function Disk({ options }) {
- {formatBytes(data.drive.freeGb * 1024 * 1024 * 1024, 0)} Free + {t("common.bytes", { value: data.drive.freeGb * 1024 * 1024 * 1024 })} {t("resources.free")} - {formatBytes(data.drive.totalGb * 1024 * 1024 * 1024, 0)} Total + {t("common.bytes", { value: data.drive.totalGb * 1024 * 1024 * 1024 })} {t("resources.total")}
- API Error + {t("widget.api_error")}
); @@ -38,10 +39,10 @@ export default function Memory() {
- {formatBytes(data.memory.freeMemMb * 1024 * 1024)} Free + {t("common.bytes", { value: data.memory.freeMemMb * 1024 * 1024 })} {t("resources.free")} - {formatBytes(data.memory.usedMemMb * 1024 * 1024)} Used + {t("common.bytes", { value: data.memory.usedMemMb * 1024 * 1024 })} {t("resources.used")}
setQuery(s.currentTarget.value)} required /> diff --git a/src/components/widgets/weather/weather.jsx b/src/components/widgets/weather/weather.jsx index 36ca9cbd0..85d32303e 100644 --- a/src/components/widgets/weather/weather.jsx +++ b/src/components/widgets/weather/weather.jsx @@ -1,10 +1,15 @@ import useSWR from "swr"; import { BiError } from "react-icons/bi"; +import { useTranslation } from "react-i18next"; import Icon from "./icon"; export default function WeatherApi({ options }) { - const { data, error } = useSWR(`/api/widgets/weather?${new URLSearchParams(options).toString()}`); + const { t, i18n } = useTranslation(); + + const { data, error } = useSWR( + `/api/widgets/weather?${new URLSearchParams({ lang: i18n.language, ...options }).toString()}` + ); if (error) { return ( @@ -30,6 +35,8 @@ export default function WeatherApi({ options }) { return
; } + const unit = options.units === "metric" ? "celsius" : "fahrenheit"; + return (
@@ -39,7 +46,11 @@ export default function WeatherApi({ options }) {
{options.label && `${options.label}, `} - {options.units === "metric" ? data.current.temp_c : data.current.temp_f}° + {t("common.number", { + value: options.units === "metric" ? data.current.temp_c : data.current.temp_f, + style: "unit", + unit, + })} {data.current.condition.text}
diff --git a/src/pages/_app.jsx b/src/pages/_app.jsx index 7c12e38f7..d4a211313 100644 --- a/src/pages/_app.jsx +++ b/src/pages/_app.jsx @@ -1,9 +1,12 @@ /* eslint-disable react/jsx-props-no-spreading */ import { SWRConfig } from "swr"; + import "styles/globals.css"; import "styles/weather-icons.css"; import "styles/theme.css"; +import "utils/i18n"; + function MyApp({ Component, pageProps }) { return ( + prettyBytes(parseFloat(value), { locale: lng, ...options }) +); +i18n.services.formatter.add("percent", (value, lng, options) => + new Intl.NumberFormat(lng, { style: "percent", ...options }).format(parseFloat(value) / 100.0) +); + +export default i18n;