mirror of
https://github.com/karl0ss/homepage.git
synced 2025-05-01 21:13:39 +01:00
implement i18n
This commit is contained in:
parent
d25148c8ae
commit
c08d4b7b9c
@ -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"
|
||||
|
76
pnpm-lock.yaml
generated
76
pnpm-lock.yaml
generated
@ -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
|
||||
|
98
public/locales/en/common.json
Normal file
98
public/locales/en/common.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
@ -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 (
|
||||
<div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
|
||||
<div className="font-thin text-sm">
|
||||
Missing Widget Type: <strong>{service.widget.type}</strong>
|
||||
</div>
|
||||
<div className="font-thin text-sm">{t("widget.missing_type", { type: service.widget.type })}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -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 <Widget error="Error Fetching Data" />;
|
||||
return <Widget error={t("docker.api_error")} />;
|
||||
}
|
||||
|
||||
if (statusData && statusData.status !== "running") {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Status" value="Offline" />
|
||||
<Block label={t("widget.status")} value={t("docker.offline")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
@ -37,22 +40,22 @@ export default function Docker({ service }) {
|
||||
if (!statsData || !statusData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="CPU" />
|
||||
<Block label="MEM" />
|
||||
<Block label="RX" />
|
||||
<Block label="TX" />
|
||||
<Block label={t("docker.cpu")} />
|
||||
<Block label={t("docker.mem")} />
|
||||
<Block label={t("docker.rx")} />
|
||||
<Block label={t("docker.tx")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="CPU" value={`${calculateCPUPercent(statsData.stats)}%`} />
|
||||
<Block label="MEM" value={formatBytes(statsData.stats.memory_stats.usage, 0)} />
|
||||
<Block label={t("docker.cpu")} value={t("common.percent", { value: calculateCPUPercent(statsData.stats) })} />
|
||||
<Block label={t("docker.mem")} value={t("common.bytes", { value: statsData.stats.memory_stats.usage })} />
|
||||
{statsData.stats.networks && (
|
||||
<>
|
||||
<Block label="RX" value={formatBytes(statsData.stats.networks.eth0.rx_bytes, 0)} />
|
||||
<Block label="TX" value={formatBytes(statsData.stats.networks.eth0.tx_bytes, 0)} />
|
||||
<Block label={t("docker.rx")} value={t("common.bytes", { value: statsData.stats.networks.eth0.rx_bytes })} />
|
||||
<Block label={t("docker.tx")} value={t("common.bytes", { value: statsData.stats.networks.eth0.tx_bytes })} />
|
||||
</>
|
||||
)}
|
||||
</Widget>
|
||||
|
@ -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 <Widget error={`${title} API Error`} />;
|
||||
return <Widget error={t("docker.api_error")} />;
|
||||
}
|
||||
|
||||
if (!sessionsData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Playing" />
|
||||
<Block label="Transcoding" />
|
||||
<Block label="Bitrate" />
|
||||
<Block label={t("emby.playing")} />
|
||||
<Block label={t("emby.transcoding")} />
|
||||
<Block label={t("emby.bitrate")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<Widget>
|
||||
<Block label="Playing" value={playing.length} />
|
||||
<Block label="Transcoding" value={transcoding.length} />
|
||||
<Block label="Bitrate" value={`${Math.round((bitrate / 1024 / 1024) * 100) / 100} Mbps`} />
|
||||
<Block label={t("emby.playing")} value={playing.length} />
|
||||
<Block label={t("emby.transcoding")} value={transcoding.length} />
|
||||
<Block label={t("emby.bitrate")} value={t("common.bitrate", { value: bitrate })} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
@ -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 <Emby service={service} title="Jellyfin" />;
|
||||
return <Emby service={service} />;
|
||||
}
|
||||
|
@ -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 <Widget error="Jellyseerr API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!statsData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Pending" />
|
||||
<Block label="Approved" />
|
||||
<Block label="Available" />
|
||||
<Block label={t("jellyseerr.pending")} />
|
||||
<Block label={t("jellyseerr.approved")} />
|
||||
<Block label={t("jellyseerr.available")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Pending" value={statsData.pending} />
|
||||
<Block label="Approved" value={statsData.approved} />
|
||||
<Block label="Available" value={statsData.available} />
|
||||
<Block label={t("jellyseerr.pending")} value={statsData.pending} />
|
||||
<Block label={t("jellyseerr.approved")} value={statsData.approved} />
|
||||
<Block label={t("jellyseerr.available")} value={statsData.available} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
@ -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 <Widget error="NGINX Proxy Manager API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!infoData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Enabled" />
|
||||
<Block label="Disabled" />
|
||||
<Block label="Total" />
|
||||
<Block label={t("npm.enabled")} />
|
||||
<Block label={t("npm.disabled")} />
|
||||
<Block label={t("npm.total")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
@ -30,9 +33,9 @@ export default function Npm({ service }) {
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Enabled" value={enabled} />
|
||||
<Block label="Disabled" value={disabled} />
|
||||
<Block label="Total" value={total} />
|
||||
<Block label={t("npm.enabled")} value={enabled} />
|
||||
<Block label={t("npm.disabled")} value={disabled} />
|
||||
<Block label={t("npm.total")} value={total} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
@ -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 <Widget error="Nzbget API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!statusData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Rate" />
|
||||
<Block label="Remaining" />
|
||||
<Block label="Downloaded" />
|
||||
<Block label={t("nzbget.rate")} />
|
||||
<Block label={t("nzbget.remaining")} />
|
||||
<Block label={t("nzbget.downloaded")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Rate" value={`${formatBytes(statusData.DownloadRate)}/s`} />
|
||||
<Block label="Remaining" value={`${Math.round((statusData.RemainingSizeMB / 1024) * 100) / 100} GB`} />
|
||||
<Block label="Downloaded" value={`${Math.round((statusData.DownloadedSizeMB / 1024) * 100) / 100} GB`} />
|
||||
<Block label={t("nzbget.rate")} value={t("common.bitrate", { value: statusData.DownloadRate })} />
|
||||
<Block
|
||||
label={t("nzbget.remaining")}
|
||||
value={t("common.bytes", { value: statusData.RemainingSizeMB * 1024 * 1024 })}
|
||||
/>
|
||||
<Block
|
||||
label={t("nzbget.downloaded")}
|
||||
value={t("common.bytes", { value: statusData.DownloadedSizeMB * 1024 * 1024 })}
|
||||
/>
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
@ -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 <Widget error="Ombi API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!statsData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Pending" />
|
||||
<Block label="Approved" />
|
||||
<Block label="Available" />
|
||||
<Block label={t("ombi.pending")} />
|
||||
<Block label={t("ombi.approved")} />
|
||||
<Block label={t("ombi.available")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Pending" value={statsData.pending} />
|
||||
<Block label="Approved" value={statsData.approved} />
|
||||
<Block label="Available" value={statsData.available} />
|
||||
<Block label={t("ombi.pending")} value={statsData.pending} />
|
||||
<Block label={t("ombi.approved")} value={statsData.approved} />
|
||||
<Block label={t("ombi.available")} value={statsData.available} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
@ -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 <Widget error="PiHole API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!piholeData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Queries" />
|
||||
<Block label="Blocked" />
|
||||
<Block label="Gravity" />
|
||||
<Block label={t("pihole.queries")} />
|
||||
<Block label={t("pihole.blocked")} />
|
||||
<Block label={t("pihole.gravity")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Queries" value={piholeData.dns_queries_today.toLocaleString()} />
|
||||
<Block label="Blocked" value={piholeData.ads_blocked_today.toLocaleString()} />
|
||||
<Block label="Gravity" value={piholeData.domains_being_blocked.toLocaleString()} />
|
||||
<Block label={t("pihole.queries")} value={t("common.number", { value: piholeData.dns_queries_today })} />
|
||||
<Block label={t("pihole.blocked")} value={t("common.number", { value: piholeData.ads_blocked_today })} />
|
||||
<Block label={t("pihole.gravity")} value={t("common.number", { value: piholeData.domains_being_blocked })} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
@ -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 <Widget error="Portainer API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!containersData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Running" />
|
||||
<Block label="Stopped" />
|
||||
<Block label="Total" />
|
||||
<Block label={t("portainer.running")} />
|
||||
<Block label={t("portainer.stopped")} />
|
||||
<Block label={t("portainer.total")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
if (containersData.error) {
|
||||
return <Widget error="Portainer API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
const running = containersData.filter((c) => c.State === "running").length;
|
||||
@ -34,9 +37,9 @@ export default function Portainer({ service }) {
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Running" value={running} />
|
||||
<Block label="Stopped" value={stopped} />
|
||||
<Block label="Total" value={total} />
|
||||
<Block label={t("portainer.running")} value={running} />
|
||||
<Block label={t("portainer.stopped")} value={stopped} />
|
||||
<Block label={t("portainer.total")} value={total} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
@ -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 <Widget error="Radarr API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!moviesData || !queuedData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Wanted" />
|
||||
<Block label="Queued" />
|
||||
<Block label="Movies" />
|
||||
<Block label={t("radarr.wanted")} />
|
||||
<Block label={t("radarr.queued")} />
|
||||
<Block label={t("radarr.movies")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
@ -30,9 +33,9 @@ export default function Radarr({ service }) {
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Wanted" value={wanted.length} />
|
||||
<Block label="Queued" value={queuedData.totalCount} />
|
||||
<Block label="Movies" value={have.length} />
|
||||
<Block label={t("radarr.wanted")} value={wanted.length} />
|
||||
<Block label={t("radarr.queued")} value={queuedData.totalCount} />
|
||||
<Block label={t("radarr.movies")} value={have.length} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
@ -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 <Widget error="Nzbget API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!statusData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Active" />
|
||||
<Block label="Upload" />
|
||||
<Block label="Download" />
|
||||
<Block label={t("rutorrent.active")} />
|
||||
<Block label={t("rutorrent.upload")} />
|
||||
<Block label={t("rutorrent.download")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
@ -33,9 +35,9 @@ export default function Rutorrent({ service }) {
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Active" value={active.length} />
|
||||
<Block label="Upload" value={`${formatBytes(upload)}/s`} />
|
||||
<Block label="Download" value={`${formatBytes(download)}/s`} />
|
||||
<Block label={t("rutorrent.active")} value={active.length} />
|
||||
<Block label={t("rutorrent.upload")} value={t("common.bitrate", { value: upload })} />
|
||||
<Block label={t("rutorrent.download")} value={t("common.bitrate", { value: download })} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
@ -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 <Widget error="Sonar API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!wantedData || !queuedData || !seriesData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Wanted" />
|
||||
<Block label="Queued" />
|
||||
<Block label="Series" />
|
||||
<Block label={t("sonarr.wanted")} />
|
||||
<Block label={t("sonarr.queued")} />
|
||||
<Block label={t("sonarr.series")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Wanted" value={wantedData.totalRecords} />
|
||||
<Block label="Queued" value={queuedData.totalRecords} />
|
||||
<Block label="Series" value={seriesData.length} />
|
||||
<Block label={t("sonarr.wanted")} value={wantedData.totalRecords} />
|
||||
<Block label={t("sonarr.queued")} value={queuedData.totalRecords} />
|
||||
<Block label={t("sonarr.series")} value={seriesData.length} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
@ -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 <Widget error="Speedtest API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!speedtestData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Download" />
|
||||
<Block label="Upload" />
|
||||
<Block label="Ping" />
|
||||
<Block label={t("speedtest.download")} />
|
||||
<Block label={t("speedtest.upload")} />
|
||||
<Block label={t("speedtest.ping")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Download" value={`${formatBits(speedtestData.data.download * 1024 * 1024, 0)}ps`} />
|
||||
<Block label="Upload" value={`${formatBits(speedtestData.data.upload * 1024 * 1024, 0)}ps`} />
|
||||
<Block label="Ping" value={`${speedtestData.data.ping} ms`} />
|
||||
<Block
|
||||
label={t("speedtest.download")}
|
||||
value={t("common.bitrate", { value: speedtestData.data.download * 1024 * 1024 })}
|
||||
/>
|
||||
<Block
|
||||
label={t("speedtest.upload")}
|
||||
value={t("common.bitrate", { value: speedtestData.data.upload * 1024 * 1024 })}
|
||||
/>
|
||||
<Block
|
||||
label={t("speedtest.ping")}
|
||||
value={t("common.ms", { value: speedtestData.data.ping, style: "unit", unit: "millisecond" })}
|
||||
/>
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
@ -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 <Widget error="Tautulli API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!statsData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Playing" />
|
||||
<Block label="Transcoding" />
|
||||
<Block label="Bitrate" />
|
||||
<Block label={t("tautulli.playing")} />
|
||||
<Block label={t("tautulli.transcoding")} />
|
||||
<Block label={t("tautulli.bitrate")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
@ -28,10 +31,9 @@ export default function Tautulli({ service }) {
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Playing" value={data.stream_count} />
|
||||
<Block label="Transcoding" value={data.stream_count_transcode} />
|
||||
{/* We divide by 1000 here because thats how Tautulli reports it on its own dashboard */}
|
||||
<Block label="Bitrate" value={`${Math.round((data.total_bandwidth / 1000) * 100) / 100} Mbps`} />
|
||||
<Block label={t("tautulli.playing")} value={data.stream_count} />
|
||||
<Block label={t("tautulli.transcoding")} value={data.stream_count_transcode} />
|
||||
<Block label={t("tautulli.bitrate")} value={t("common.bitrate", { value: data.total_bandwidth })} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
@ -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 <Widget error="Traefik API Error" />;
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!traefikData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Routers" />
|
||||
<Block label="Services" />
|
||||
<Block label="Middleware" />
|
||||
<Block label={t("traefik.routers")} />
|
||||
<Block label={t("traefik.services")} />
|
||||
<Block label={t("traefik.middleware")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label="Routers" value={traefikData.http.routers.total} />
|
||||
<Block label="Services" value={traefikData.http.services.total} />
|
||||
<Block label="Middleware" value={traefikData.http.middlewares.total} />
|
||||
<Block label={t("traefik.routers")} value={traefikData.http.routers.total} />
|
||||
<Block label={t("traefik.services")} value={traefikData.http.services.total} />
|
||||
<Block label={t("traefik.middleware")} value={traefikData.http.middlewares.total} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
@ -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 <div className="flex flex-row items-center" />;
|
||||
}
|
||||
|
||||
const unit = options.units === "metric" ? "celsius" : "fahrenheit";
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-center">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
@ -42,11 +49,9 @@ export default function OpenWeatherMap({ options }) {
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">
|
||||
{options.label && `${options.label}, `}
|
||||
{data.main.temp.toFixed(1)}°
|
||||
</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">
|
||||
{data.weather[0].description.charAt(0).toUpperCase() + data.weather[0].description.slice(1)}
|
||||
{t("common.number", { value: data.main.temp, style: "unit", unit })}
|
||||
</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{data.weather[0].description}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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() {
|
||||
<div className="flex-none flex flex-row items-center justify-center">
|
||||
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">API Error</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("widget.api_error")}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -23,7 +26,7 @@ export default function Cpu() {
|
||||
<div className="flex-none flex flex-row items-center justify-center">
|
||||
<FiCpu className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">- Usage</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">-</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -35,7 +38,9 @@ export default function Cpu() {
|
||||
<div className="flex-none flex flex-row items-center justify-center">
|
||||
<FiCpu className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left font-mono min-w-[50px]">
|
||||
<div className="text-theme-800 dark:text-theme-200 text-xs">{`${Math.round(data.cpu.usage)}%`}</div>
|
||||
<div className="text-theme-800 dark:text-theme-200 text-xs">
|
||||
{t("common.number", { value: data.cpu.usage, style: "unit", unit: "percent", maximumFractionDigits: 0 })}
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-1 dark:bg-gray-700">
|
||||
<div
|
||||
className="bg-theme-600 h-1 rounded-full dark:bg-theme-500"
|
||||
|
@ -1,10 +1,11 @@
|
||||
import useSWR from "swr";
|
||||
import { FiHardDrive } from "react-icons/fi";
|
||||
import { BiError } from "react-icons/bi";
|
||||
|
||||
import { formatBytes } from "utils/stats-helpers";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function Disk({ options }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data, error } = useSWR(`/api/widgets/resources?type=disk&target=${options.disk}`, {
|
||||
refreshInterval: 1500,
|
||||
});
|
||||
@ -14,7 +15,7 @@ export default function Disk({ options }) {
|
||||
<div className="flex-none flex flex-row items-center justify-center">
|
||||
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left font-mono">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">API Error</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("widget.api_error")}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -38,10 +39,10 @@ export default function Disk({ options }) {
|
||||
<FiHardDrive className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left font-mono ">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs group-hover:hidden">
|
||||
{formatBytes(data.drive.freeGb * 1024 * 1024 * 1024, 0)} Free
|
||||
{t("common.bytes", { value: data.drive.freeGb * 1024 * 1024 * 1024 })} {t("resources.free")}
|
||||
</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs hidden group-hover:block">
|
||||
{formatBytes(data.drive.totalGb * 1024 * 1024 * 1024, 0)} Total
|
||||
{t("common.bytes", { value: data.drive.totalGb * 1024 * 1024 * 1024 })} {t("resources.total")}
|
||||
</span>
|
||||
<div className="w-full bg-gray-200 rounded-full h-1 dark:bg-gray-700">
|
||||
<div
|
||||
|
@ -1,10 +1,11 @@
|
||||
import useSWR from "swr";
|
||||
import { FaMemory } from "react-icons/fa";
|
||||
import { BiError } from "react-icons/bi";
|
||||
|
||||
import { formatBytes } from "utils/stats-helpers";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function Memory() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data, error } = useSWR(`/api/widgets/resources?type=memory`, {
|
||||
refreshInterval: 1500,
|
||||
});
|
||||
@ -14,7 +15,7 @@ export default function Memory() {
|
||||
<div className="flex-none flex flex-row items-center justify-center">
|
||||
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left font-mono">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">API Error</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("widget.api_error")}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -38,10 +39,10 @@ export default function Memory() {
|
||||
<FaMemory className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left font-mono">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs group-hover:hidden">
|
||||
{formatBytes(data.memory.freeMemMb * 1024 * 1024)} Free
|
||||
{t("common.bytes", { value: data.memory.freeMemMb * 1024 * 1024 })} {t("resources.free")}
|
||||
</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs hidden group-hover:block">
|
||||
{formatBytes(data.memory.usedMemMb * 1024 * 1024)} Used
|
||||
{t("common.bytes", { value: data.memory.usedMemMb * 1024 * 1024 })} {t("resources.used")}
|
||||
</span>
|
||||
<div className="w-full bg-gray-200 rounded-full h-1 dark:bg-gray-700">
|
||||
<div
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FiSearch } from "react-icons/fi";
|
||||
import { SiDuckduckgo, SiMicrosoftbing, SiGoogle } from "react-icons/si";
|
||||
|
||||
@ -26,6 +27,8 @@ const providers = {
|
||||
};
|
||||
|
||||
export default function Search({ options }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const provider = providers[options.provider];
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
@ -53,7 +56,7 @@ export default function Search({ options }) {
|
||||
<input
|
||||
type="search"
|
||||
className="overflow-hidden w-full placeholder-theme-900 text-xs text-theme-900 bg-theme-50 rounded-md border border-theme-300 focus:ring-theme-500 focus:border-theme-500 dark:bg-theme-800 dark:border-theme-600 dark:placeholder-theme-400 dark:text-white dark:focus:ring-theme-500 dark:focus:border-theme-500 h-full"
|
||||
placeholder="Search..."
|
||||
placeholder={t("search.placeholder")}
|
||||
onChange={(s) => setQuery(s.currentTarget.value)}
|
||||
required
|
||||
/>
|
||||
|
@ -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 <div className="flex flex-row items-center justify-end" />;
|
||||
}
|
||||
|
||||
const unit = options.units === "metric" ? "celsius" : "fahrenheit";
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-center">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
@ -39,7 +46,11 @@ export default function WeatherApi({ options }) {
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">
|
||||
{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,
|
||||
})}
|
||||
</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{data.current.condition.text}</span>
|
||||
</div>
|
||||
|
@ -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 (
|
||||
<SWRConfig
|
||||
|
@ -2,7 +2,7 @@ import cachedFetch from "utils/cached-fetch";
|
||||
import { getSettings } from "utils/config";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { latitude, longitude, units, provider, cache } = req.query;
|
||||
const { latitude, longitude, units, provider, cache, lang } = req.query;
|
||||
let { apiKey } = req.query;
|
||||
|
||||
if (!apiKey && !provider) {
|
||||
@ -22,7 +22,7 @@ export default async function handler(req, res) {
|
||||
return res.status(400).json({ error: "Missing API key" });
|
||||
}
|
||||
|
||||
const apiUrl = `https://api.openweathermap.org/data/2.5/weather?lat=${latitude}&lon=${longitude}&appid=${apiKey}&units=${units}`;
|
||||
const apiUrl = `https://api.openweathermap.org/data/2.5/weather?lat=${latitude}&lon=${longitude}&appid=${apiKey}&units=${units}&lang=${lang}`;
|
||||
|
||||
return res.send(await cachedFetch(apiUrl, cache));
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import cachedFetch from "utils/cached-fetch";
|
||||
import { getSettings } from "utils/config";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { latitude, longitude, provider, cache } = req.query;
|
||||
const { latitude, longitude, provider, cache, lang } = req.query;
|
||||
let { apiKey } = req.query;
|
||||
|
||||
if (!apiKey && !provider) {
|
||||
@ -22,7 +22,7 @@ export default async function handler(req, res) {
|
||||
return res.status(400).json({ error: "Missing API key" });
|
||||
}
|
||||
|
||||
const apiUrl = `http://api.weatherapi.com/v1/current.json?q=${latitude},${longitude}&key=${apiKey}`;
|
||||
const apiUrl = `http://api.weatherapi.com/v1/current.json?q=${latitude},${longitude}&key=${apiKey}&lang=${lang}`;
|
||||
|
||||
return res.send(await cachedFetch(apiUrl, cache));
|
||||
}
|
||||
|
32
src/utils/i18n.js
Normal file
32
src/utils/i18n.js
Normal file
@ -0,0 +1,32 @@
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import Backend from "i18next-http-backend";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
|
||||
i18n
|
||||
.use(Backend)
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
fallbackLng: "en",
|
||||
ns: ["common"],
|
||||
debug: process.env.NODE_ENV === "development",
|
||||
defaultNS: "common",
|
||||
nonExplicitSupportedLngs: true,
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
backend: {
|
||||
loadPath: "/locales/{{lng}}/{{ns}}.json",
|
||||
},
|
||||
});
|
||||
|
||||
i18n.services.formatter.add("bytes", (value, lng, options) =>
|
||||
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;
|
Loading…
x
Reference in New Issue
Block a user