diff --git a/package.json b/package.json
index 8db6e8f6..a6856c1c 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 c58fee0a..4bad1e81 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 00000000..00b79085
--- /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 61b31c0b..199dbd5d 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 7a6729cc..88398a42 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 c9e6e419..8d2cc959 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 ab79335d..03a8840a 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 3820c2a1..f658b58a 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 916136e1..47b6e8bd 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 56baa631..10c6cd45 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 4bbf96fd..7b4bd0f9 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 7c777ce7..8b4bb0bd 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 1e97052d..c65c9d65 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 fec155c8..125f0f94 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 ff9fb2a3..ddd22171 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 fe042a19..a269bb5c 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 7bcf884d..8e863876 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 a3dce1c1..d86646c3 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 ce3fd4fe..fba946a7 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 c1ab9f05..d4c292f9 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 1e6cd438..87c1847c 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() {
);
@@ -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 36ca9cbd..85d32303 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 7c12e38f..d4a21131 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;