diff --git a/next-i18next.config.js b/next-i18next.config.js new file mode 100644 index 000000000..b36df55d7 --- /dev/null +++ b/next-i18next.config.js @@ -0,0 +1,143 @@ +// prettyBytes taken from https://github.com/sindresorhus/pretty-bytes + +/* eslint-disable no-param-reassign */ +const BYTE_UNITS = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; + +const BIBYTE_UNITS = ["B", "kiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]; + +const BIT_UNITS = ["b", "kbit", "Mbit", "Gbit", "Tbit", "Pbit", "Ebit", "Zbit", "Ybit"]; + +const BIBIT_UNITS = ["b", "kibit", "Mibit", "Gibit", "Tibit", "Pibit", "Eibit", "Zibit", "Yibit"]; + +/* +Formats the given number using `Number#toLocaleString`. +- If locale is a string, the value is expected to be a locale-key (for example: `de`). +- If locale is true, the system default locale is used for translation. +- If no value for locale is specified, the number is returned unmodified. +*/ +const toLocaleString = (number, locale, options) => { + let result = number; + if (typeof locale === "string" || Array.isArray(locale)) { + result = number.toLocaleString(locale, options); + } else if (locale === true || options !== undefined) { + result = number.toLocaleString(undefined, options); + } + + return result; +}; + +function prettyBytes(number, options) { + if (!Number.isFinite(number)) { + throw new TypeError(`Expected a finite number, got ${typeof number}: ${number}`); + } + + options = { + bits: false, + binary: false, + ...options, + }; + + // eslint-disable-next-line no-nested-ternary + const UNITS = options.bits ? (options.binary ? BIBIT_UNITS : BIT_UNITS) : options.binary ? BIBYTE_UNITS : BYTE_UNITS; + + if (options.signed && number === 0) { + return ` 0 ${UNITS[0]}`; + } + + const isNegative = number < 0; + // eslint-disable-next-line no-nested-ternary + const prefix = isNegative ? "-" : options.signed ? "+" : ""; + + if (isNegative) { + number = -number; + } + + let localeOptions; + + if (options.minimumFractionDigits !== undefined) { + localeOptions = { minimumFractionDigits: options.minimumFractionDigits }; + } + + if (options.maximumFractionDigits !== undefined) { + localeOptions = { maximumFractionDigits: options.maximumFractionDigits, ...localeOptions }; + } + + if (number < 1) { + const numberString = toLocaleString(number, options.locale, localeOptions); + return `${prefix + numberString} ${UNITS[0]}`; + } + + const exponent = Math.min( + Math.floor(options.binary ? Math.log(number) / Math.log(1024) : Math.log10(number) / 3), + UNITS.length - 1 + ); + number /= (options.binary ? 1024 : 1000) ** exponent; + + if (!localeOptions) { + number = number.toPrecision(3); + } + + const numberString = toLocaleString(Number(number), options.locale, localeOptions); + + const unit = UNITS[exponent]; + + return `${prefix + numberString} ${unit}`; +} + +module.exports = { + i18n: { + defaultLocale: "en", + locales: [ + "en", + "ca", + "de", + "es", + "fr", + "he", + "hr", + "hu", + "it", + "nb-NO", + "nl", + "pl", + "pt", + "ro", + "ru", + "sv", + "vi", + "zh-CN", + ], + }, + serializeConfig: false, + use: [ + { + init: (i18next) => { + i18next.services.formatter.add("bytes", (value, lng, options) => + prettyBytes(parseFloat(value), { locale: lng, ...options }) + ); + + i18next.services.formatter.add("rate", (value, lng, options) => { + if (value === 0) return "0 Bps"; + + const bits = options.bits ? value : value / 8; + const k = 1024; + const dm = options.decimals ? options.decimals : 0; + const sizes = ["Bps", "Kbps", "Mbps", "Gbps", "Tbps", "Pbps", "Ebps", "Zbps", "Ybps"]; + + const i = Math.floor(Math.log(bits) / Math.log(k)); + + const formatted = new Intl.NumberFormat(lng, { maximumFractionDigits: dm, minimumFractionDigits: dm }).format( + parseFloat(bits / k ** i) + ); + + return `${formatted} ${sizes[i]}`; + }); + + i18next.services.formatter.add("percent", (value, lng, options) => + new Intl.NumberFormat(lng, { style: "percent", ...options }).format(parseFloat(value) / 100.0) + ); + }, + type: "3rdParty", + }, + ], +}; diff --git a/next.config.js b/next.config.js index c06f75afa..dae78ca01 100644 --- a/next.config.js +++ b/next.config.js @@ -1,3 +1,5 @@ +const { i18n } = require("./next-i18next.config"); + /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, @@ -7,6 +9,7 @@ const nextConfig = { domains: ["cdn.jsdelivr.net"], unoptimized: true, }, + i18n, }; module.exports = nextConfig; diff --git a/package.json b/package.json index 51f169fd1..3dd2ed0d1 100644 --- a/package.json +++ b/package.json @@ -15,20 +15,19 @@ "compare-versions": "^5.0.1", "dockerode": "^3.3.4", "follow-redirects": "^1.15.2", - "i18next-browser-languagedetector": "^6.1.5", - "i18next-http-backend": "^1.4.4", "i18next": "^21.9.2", "js-yaml": "^4.1.0", "json-rpc-2.0": "^1.4.1", "memory-cache": "^0.2.0", "next": "^12.3.1", + "next-i18next": "^12.0.1", "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.6", "react-icons": "^4.4.0", - "react": "^18.2.0", "rutorrent-promise": "^2.0.0", "shvl": "^3.0.0", "swr": "^1.3.0", @@ -38,15 +37,15 @@ "devDependencies": { "@tailwindcss/forms": "^0.5.3", "autoprefixer": "^10.4.12", + "eslint": "^8.24.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-next": "^12.3.1", "eslint-config-prettier": "^8.5.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsx-a11y": "^6.6.1", "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react": "^7.31.8", - "eslint": "^8.24.0", + "eslint-plugin-react-hooks": "^4.6.0", "postcss": "^8.4.16", "prettier": "^2.7.1", "tailwind-scrollbar": "^2.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a01d92484..49394c8ea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,12 +18,11 @@ specifiers: eslint-plugin-react-hooks: ^4.6.0 follow-redirects: ^1.15.2 i18next: ^21.9.2 - i18next-browser-languagedetector: ^6.1.5 - i18next-http-backend: ^1.4.4 js-yaml: ^4.1.0 json-rpc-2.0: ^1.4.1 memory-cache: ^0.2.0 next: ^12.3.1 + next-i18next: ^12.0.1 node-os-utils: ^1.3.7 postcss: ^8.4.16 prettier: ^2.7.1 @@ -49,12 +48,11 @@ dependencies: dockerode: 3.3.4 follow-redirects: 1.15.2 i18next: 21.9.2 - i18next-browser-languagedetector: 6.1.5 - i18next-http-backend: 1.4.4 js-yaml: 4.1.0 json-rpc-2.0: 1.4.1 memory-cache: 0.2.0 next: 12.3.1_biqbaboplfbrettd7655fr4n2y + next-i18next: 12.0.1_azq6kxkn3od7qdylwkyksrwopy node-os-utils: 1.3.7 pretty-bytes: 6.0.0 raw-body: 2.5.1 @@ -338,10 +336,33 @@ packages: tailwindcss: 3.1.8_postcss@8.4.16 dev: true + /@types/hoist-non-react-statics/3.3.1: + resolution: {integrity: sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==} + dependencies: + '@types/react': 18.0.21 + hoist-non-react-statics: 3.3.2 + dev: false + /@types/json5/0.0.29: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} dev: true + /@types/prop-types/15.7.5: + resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} + dev: false + + /@types/react/18.0.21: + resolution: {integrity: sha512-7QUCOxvFgnD5Jk8ZKlUAhVcRj7GuJRjnjjiY/IUBWKgOlnvDvTMLD4RTF7NPyVmbRhNrbomZiOepg7M/2Kj1mA==} + dependencies: + '@types/prop-types': 15.7.5 + '@types/scheduler': 0.16.2 + csstype: 3.1.1 + dev: false + + /@types/scheduler/0.16.2: + resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==} + dev: false + /@typescript-eslint/parser/5.38.0_7ilbxdl5iguzcjriqqcg2m5cku: resolution: {integrity: sha512-/F63giJGLDr0ms1Cr8utDAxP2SPiglaD6V+pCOcG35P2jCqdfR7uuEhz1GIC3oy4hkUF8xA1XSXmd9hOh/a5EA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -746,6 +767,11 @@ packages: requiresBuild: true dev: true + /core-js/3.25.2: + resolution: {integrity: sha512-YB4IAT1bjEfxTJ1XYy11hJAKskO+qmhuDBM8/guIfMz4JvdsAQAqvyb97zXX7JgSrfPLG5mRGFWJwJD39ruq2A==} + requiresBuild: true + dev: false + /cpu-features/0.0.4: resolution: {integrity: sha512-fKiZ/zp1mUwQbnzb9IghXtHtDoTMtNeb8oYGx6kX2SYfhnG0HNdBEBIzB9b5KlXu5DQPhfy3mInbBxFcgwAr3A==} engines: {node: '>=10.0.0'} @@ -756,14 +782,6 @@ 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'} @@ -779,6 +797,10 @@ packages: hasBin: true dev: true + /csstype/3.1.1: + resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==} + dev: false + /damerau-levenshtein/1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} dev: true @@ -1613,6 +1635,12 @@ packages: function-bind: 1.1.1 dev: true + /hoist-non-react-statics/3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + dependencies: + react-is: 16.13.1 + dev: false + /html-parse-stringify/3.0.1: resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} dependencies: @@ -1630,18 +1658,8 @@ packages: toidentifier: 1.0.1 dev: false - /i18next-browser-languagedetector/6.1.5: - resolution: {integrity: sha512-11t7b39oKeZe4uyMxLSPnfw28BCPNLZgUk7zyufex0zKXZ+Bv+JnmJgoB+IfQLZwDt1d71PM8vwBX1NCgliY3g==} - dependencies: - '@babel/runtime': 7.19.0 - dev: false - - /i18next-http-backend/1.4.4: - resolution: {integrity: sha512-M4gLPe6JKZ2p1UmE6t4rzWV/sAxgrLThW7ztXAsTpFwFqXoyzhTzX8eYxVv9KjpCQh4K9nwxnEjEi+74C4Thbg==} - dependencies: - cross-fetch: 3.1.5 - transitivePeerDependencies: - - encoding + /i18next-fs-backend/1.1.5: + resolution: {integrity: sha512-raTel3EfshiUXxR0gvmIoqp75jhkj8+7R1LjB006VZKPTFBbXyx6TlUVhb8Z9+7ahgpFbcQg1QWVOdf/iNzI5A==} dev: false /i18next/21.9.2: @@ -1986,6 +2004,27 @@ packages: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true + /next-i18next/12.0.1_azq6kxkn3od7qdylwkyksrwopy: + resolution: {integrity: sha512-i1yLpOvokldjqnnkP4KfZtnbp2+DILlUvavzJ9rvCSu1Yk5w45IRAYrJfu20/0WDeWxea/ktYDmXw+z/2roo3g==} + engines: {node: '>=12'} + peerDependencies: + next: '>= 10.0.0' + react: '>= 16.8.0' + dependencies: + '@babel/runtime': 7.19.0 + '@types/hoist-non-react-statics': 3.3.1 + core-js: 3.25.2 + hoist-non-react-statics: 3.3.2 + i18next: 21.9.2 + i18next-fs-backend: 1.1.5 + next: 12.3.1_biqbaboplfbrettd7655fr4n2y + react: 18.2.0 + react-i18next: 11.18.6_ulhmqqxshznzmtuvahdi5nasbq + transitivePeerDependencies: + - react-dom + - react-native + dev: false + /next/12.3.1_biqbaboplfbrettd7655fr4n2y: resolution: {integrity: sha512-l7bvmSeIwX5lp07WtIiP9u2ytZMv7jIeB8iacR28PuUEFG5j0HGAPnMqyG5kbZNBG2H7tRsrQ4HCjuMOPnANZw==} engines: {node: '>=12.22.0'} @@ -2393,7 +2432,6 @@ packages: /react-is/16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - dev: true /react/18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} diff --git a/src/components/services/widget.jsx b/src/components/services/widget.jsx index 965dd6ec0..dd65b482f 100644 --- a/src/components/services/widget.jsx +++ b/src/components/services/widget.jsx @@ -1,5 +1,5 @@ -import { useTranslation } from "react-i18next"; import dynamic from "next/dynamic"; +import { useTranslation } from "next-i18next"; const Sonarr = dynamic(() => import("./widgets/service/sonarr")); const Radarr = dynamic(() => import("./widgets/service/radarr")); diff --git a/src/components/services/widgets/service/adguard.jsx b/src/components/services/widgets/service/adguard.jsx index c2eea2c47..a3ad75bbe 100644 --- a/src/components/services/widgets/service/adguard.jsx +++ b/src/components/services/widgets/service/adguard.jsx @@ -1,5 +1,5 @@ import useSWR from "swr"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; diff --git a/src/components/services/widgets/service/bazarr.jsx b/src/components/services/widgets/service/bazarr.jsx index 8d47a4439..ef2c6cca1 100644 --- a/src/components/services/widgets/service/bazarr.jsx +++ b/src/components/services/widgets/service/bazarr.jsx @@ -1,5 +1,5 @@ import useSWR from "swr"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; diff --git a/src/components/services/widgets/service/coinmarketcap.jsx b/src/components/services/widgets/service/coinmarketcap.jsx index a34e6e924..32b04713f 100644 --- a/src/components/services/widgets/service/coinmarketcap.jsx +++ b/src/components/services/widgets/service/coinmarketcap.jsx @@ -1,6 +1,6 @@ import useSWR from "swr"; import { useState } from "react"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import classNames from "classnames"; import Widget from "../widget"; diff --git a/src/components/services/widgets/service/docker.jsx b/src/components/services/widgets/service/docker.jsx index af2175abf..89ac73c8b 100644 --- a/src/components/services/widgets/service/docker.jsx +++ b/src/components/services/widgets/service/docker.jsx @@ -1,5 +1,5 @@ import useSWR from "swr"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; diff --git a/src/components/services/widgets/service/emby.jsx b/src/components/services/widgets/service/emby.jsx index 04db12ff9..63ae9c8e3 100644 --- a/src/components/services/widgets/service/emby.jsx +++ b/src/components/services/widgets/service/emby.jsx @@ -1,5 +1,5 @@ import useSWR from "swr"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import { BsVolumeMuteFill, BsFillPlayFill, BsPauseFill, BsCpu, BsFillCpuFill } from "react-icons/bs"; import { MdOutlineSmartDisplay } from "react-icons/md"; diff --git a/src/components/services/widgets/service/gotify.jsx b/src/components/services/widgets/service/gotify.jsx index 578d31cfb..4bd9f98b9 100644 --- a/src/components/services/widgets/service/gotify.jsx +++ b/src/components/services/widgets/service/gotify.jsx @@ -1,5 +1,5 @@ import useSWR from "swr"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; diff --git a/src/components/services/widgets/service/jackett.jsx b/src/components/services/widgets/service/jackett.jsx index b531f42c1..70de6c074 100644 --- a/src/components/services/widgets/service/jackett.jsx +++ b/src/components/services/widgets/service/jackett.jsx @@ -1,5 +1,5 @@ import useSWR from "swr"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; diff --git a/src/components/services/widgets/service/jellyseerr.jsx b/src/components/services/widgets/service/jellyseerr.jsx index f658b58a0..9bcc1e278 100644 --- a/src/components/services/widgets/service/jellyseerr.jsx +++ b/src/components/services/widgets/service/jellyseerr.jsx @@ -1,5 +1,5 @@ import useSWR from "swr"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; diff --git a/src/components/services/widgets/service/lidarr.jsx b/src/components/services/widgets/service/lidarr.jsx index 8818b7c11..2bf4cddb5 100644 --- a/src/components/services/widgets/service/lidarr.jsx +++ b/src/components/services/widgets/service/lidarr.jsx @@ -1,5 +1,5 @@ import useSWR from "swr"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; diff --git a/src/components/services/widgets/service/mastodon.jsx b/src/components/services/widgets/service/mastodon.jsx index 9d2ded4a1..20b14aca2 100644 --- a/src/components/services/widgets/service/mastodon.jsx +++ b/src/components/services/widgets/service/mastodon.jsx @@ -1,5 +1,5 @@ import useSWR from "swr"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; diff --git a/src/components/services/widgets/service/npm.jsx b/src/components/services/widgets/service/npm.jsx index 47b6e8bd1..563348c2c 100644 --- a/src/components/services/widgets/service/npm.jsx +++ b/src/components/services/widgets/service/npm.jsx @@ -1,5 +1,5 @@ import useSWR from "swr"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; diff --git a/src/components/services/widgets/service/nzbget.jsx b/src/components/services/widgets/service/nzbget.jsx index 10c6cd45f..b81258438 100644 --- a/src/components/services/widgets/service/nzbget.jsx +++ b/src/components/services/widgets/service/nzbget.jsx @@ -1,5 +1,5 @@ import useSWR from "swr"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; diff --git a/src/components/services/widgets/service/ombi.jsx b/src/components/services/widgets/service/ombi.jsx index 7b4bd0f9e..aa1a5f652 100644 --- a/src/components/services/widgets/service/ombi.jsx +++ b/src/components/services/widgets/service/ombi.jsx @@ -1,5 +1,5 @@ import useSWR from "swr"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; diff --git a/src/components/services/widgets/service/overseerr.jsx b/src/components/services/widgets/service/overseerr.jsx index 2e97d5a9e..59644e0a2 100644 --- a/src/components/services/widgets/service/overseerr.jsx +++ b/src/components/services/widgets/service/overseerr.jsx @@ -1,5 +1,5 @@ import useSWR from "swr"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; diff --git a/src/components/services/widgets/service/pihole.jsx b/src/components/services/widgets/service/pihole.jsx index 8b4bb0bd1..3468f5fc9 100644 --- a/src/components/services/widgets/service/pihole.jsx +++ b/src/components/services/widgets/service/pihole.jsx @@ -1,5 +1,5 @@ import useSWR from "swr"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; diff --git a/src/components/services/widgets/service/portainer.jsx b/src/components/services/widgets/service/portainer.jsx index c65c9d658..ab1c68951 100644 --- a/src/components/services/widgets/service/portainer.jsx +++ b/src/components/services/widgets/service/portainer.jsx @@ -1,5 +1,5 @@ import useSWR from "swr"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; diff --git a/src/components/services/widgets/service/prowlarr.jsx b/src/components/services/widgets/service/prowlarr.jsx index a86a61386..b46eb7a31 100644 --- a/src/components/services/widgets/service/prowlarr.jsx +++ b/src/components/services/widgets/service/prowlarr.jsx @@ -1,5 +1,5 @@ import useSWR from "swr"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; diff --git a/src/components/services/widgets/service/qbittorrent.jsx b/src/components/services/widgets/service/qbittorrent.jsx index 43999947f..954b9b05b 100644 --- a/src/components/services/widgets/service/qbittorrent.jsx +++ b/src/components/services/widgets/service/qbittorrent.jsx @@ -1,5 +1,5 @@ import useSWR from "swr"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; diff --git a/src/components/services/widgets/service/radarr.jsx b/src/components/services/widgets/service/radarr.jsx index 6a4401d3d..5fc44ff02 100644 --- a/src/components/services/widgets/service/radarr.jsx +++ b/src/components/services/widgets/service/radarr.jsx @@ -1,10 +1,9 @@ import useSWR from "swr"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; -import { formatApiUrl } from "utils/api-helpers"; export default function Radarr({ service }) { const { t } = useTranslation(); diff --git a/src/components/services/widgets/service/readarr.jsx b/src/components/services/widgets/service/readarr.jsx index c1ddea31e..2f4787683 100644 --- a/src/components/services/widgets/service/readarr.jsx +++ b/src/components/services/widgets/service/readarr.jsx @@ -1,5 +1,5 @@ import useSWR from "swr"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; diff --git a/src/components/services/widgets/service/rutorrent.jsx b/src/components/services/widgets/service/rutorrent.jsx index ddd22171d..ecbe24b5c 100644 --- a/src/components/services/widgets/service/rutorrent.jsx +++ b/src/components/services/widgets/service/rutorrent.jsx @@ -1,5 +1,5 @@ import useSWR from "swr"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; diff --git a/src/components/services/widgets/service/sabnzbd.jsx b/src/components/services/widgets/service/sabnzbd.jsx index b38747adb..14caa4dc4 100644 --- a/src/components/services/widgets/service/sabnzbd.jsx +++ b/src/components/services/widgets/service/sabnzbd.jsx @@ -1,5 +1,5 @@ import useSWR from "swr"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; diff --git a/src/components/services/widgets/service/sonarr.jsx b/src/components/services/widgets/service/sonarr.jsx index f95def973..584a3d78c 100644 --- a/src/components/services/widgets/service/sonarr.jsx +++ b/src/components/services/widgets/service/sonarr.jsx @@ -1,5 +1,5 @@ import useSWR from "swr"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; diff --git a/src/components/services/widgets/service/speedtest.jsx b/src/components/services/widgets/service/speedtest.jsx index 8e863876a..a4416b837 100644 --- a/src/components/services/widgets/service/speedtest.jsx +++ b/src/components/services/widgets/service/speedtest.jsx @@ -1,5 +1,5 @@ import useSWR from "swr"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; diff --git a/src/components/services/widgets/service/strelaysrv.jsx b/src/components/services/widgets/service/strelaysrv.jsx index aaf75daa8..5533d89a6 100644 --- a/src/components/services/widgets/service/strelaysrv.jsx +++ b/src/components/services/widgets/service/strelaysrv.jsx @@ -1,5 +1,5 @@ import useSWR from "swr"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; diff --git a/src/components/services/widgets/service/tautulli.jsx b/src/components/services/widgets/service/tautulli.jsx index 8ca6b01b4..54fc654b8 100644 --- a/src/components/services/widgets/service/tautulli.jsx +++ b/src/components/services/widgets/service/tautulli.jsx @@ -1,6 +1,6 @@ /* eslint-disable camelcase */ import useSWR from "swr"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import { BsFillPlayFill, BsPauseFill, BsCpu, BsFillCpuFill } from "react-icons/bs"; import { MdOutlineSmartDisplay, MdSmartDisplay } from "react-icons/md"; diff --git a/src/components/services/widgets/service/traefik.jsx b/src/components/services/widgets/service/traefik.jsx index fba946a72..816a40a6c 100644 --- a/src/components/services/widgets/service/traefik.jsx +++ b/src/components/services/widgets/service/traefik.jsx @@ -1,5 +1,5 @@ import useSWR from "swr"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; diff --git a/src/components/services/widgets/service/transmission.jsx b/src/components/services/widgets/service/transmission.jsx index 893bcc453..bb3eeea69 100644 --- a/src/components/services/widgets/service/transmission.jsx +++ b/src/components/services/widgets/service/transmission.jsx @@ -1,5 +1,5 @@ import useSWR from "swr"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import Widget from "../widget"; import Block from "../block"; diff --git a/src/components/version.jsx b/src/components/version.jsx index 187ad3273..15eb68b01 100644 --- a/src/components/version.jsx +++ b/src/components/version.jsx @@ -1,4 +1,4 @@ -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import useSWR from "swr"; import { compareVersions } from "compare-versions"; import { MdNewReleases } from "react-icons/md"; diff --git a/src/components/widgets/datetime/datetime.jsx b/src/components/widgets/datetime/datetime.jsx index af7fe97c3..f35b59394 100644 --- a/src/components/widgets/datetime/datetime.jsx +++ b/src/components/widgets/datetime/datetime.jsx @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; const textSizes = { "4xl": "text-4xl", diff --git a/src/components/widgets/openweathermap/weather.jsx b/src/components/widgets/openweathermap/weather.jsx index 4173871d7..5c78d74ca 100644 --- a/src/components/widgets/openweathermap/weather.jsx +++ b/src/components/widgets/openweathermap/weather.jsx @@ -3,7 +3,7 @@ import { useState } from "react"; import { BiError } from "react-icons/bi"; import { WiCloudDown } from "react-icons/wi"; import { MdLocationDisabled, MdLocationSearching } from "react-icons/md"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import Icon from "./icon"; diff --git a/src/components/widgets/resources/cpu.jsx b/src/components/widgets/resources/cpu.jsx index 89700d773..7bb443215 100644 --- a/src/components/widgets/resources/cpu.jsx +++ b/src/components/widgets/resources/cpu.jsx @@ -1,7 +1,7 @@ import useSWR from "swr"; import { FiCpu } from "react-icons/fi"; import { BiError } from "react-icons/bi"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import UsageBar from "./usage-bar"; diff --git a/src/components/widgets/resources/disk.jsx b/src/components/widgets/resources/disk.jsx index 5b80a8694..3d57bb19f 100644 --- a/src/components/widgets/resources/disk.jsx +++ b/src/components/widgets/resources/disk.jsx @@ -1,7 +1,7 @@ import useSWR from "swr"; import { FiHardDrive } from "react-icons/fi"; import { BiError } from "react-icons/bi"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import UsageBar from "./usage-bar"; diff --git a/src/components/widgets/resources/memory.jsx b/src/components/widgets/resources/memory.jsx index db530f1c5..4eb007e47 100644 --- a/src/components/widgets/resources/memory.jsx +++ b/src/components/widgets/resources/memory.jsx @@ -1,7 +1,7 @@ import useSWR from "swr"; import { FaMemory } from "react-icons/fa"; import { BiError } from "react-icons/bi"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import UsageBar from "./usage-bar"; diff --git a/src/components/widgets/search/search.jsx b/src/components/widgets/search/search.jsx index c651ba920..1ffd6bf69 100644 --- a/src/components/widgets/search/search.jsx +++ b/src/components/widgets/search/search.jsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import { FiSearch } from "react-icons/fi"; import { SiDuckduckgo, SiMicrosoftbing, SiGoogle } from "react-icons/si"; diff --git a/src/components/widgets/weather/weather.jsx b/src/components/widgets/weather/weather.jsx index dd5325740..9fe3b2421 100644 --- a/src/components/widgets/weather/weather.jsx +++ b/src/components/widgets/weather/weather.jsx @@ -3,7 +3,7 @@ import { useState } from "react"; import { BiError } from "react-icons/bi"; import { WiCloudDown } from "react-icons/wi"; import { MdLocationDisabled, MdLocationSearching } from "react-icons/md"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import Icon from "./icon"; diff --git a/src/pages/_app.jsx b/src/pages/_app.jsx index bfd01299d..6cdef8a0f 100644 --- a/src/pages/_app.jsx +++ b/src/pages/_app.jsx @@ -1,11 +1,12 @@ /* eslint-disable react/jsx-props-no-spreading */ import { SWRConfig } from "swr"; +import { appWithTranslation } from "next-i18next"; import "styles/globals.css"; import "styles/theme.css"; import "styles/manrope.css"; +import nextI18nextConfig from "../../next-i18next.config"; -import "utils/i18n"; import { ColorProvider } from "utils/color-context"; import { ThemeProvider } from "utils/theme-context"; import { SettingsProvider } from "utils/settings-context"; @@ -28,4 +29,4 @@ function MyApp({ Component, pageProps }) { ); } -export default MyApp; +export default appWithTranslation(MyApp, nextI18nextConfig); diff --git a/src/pages/index.jsx b/src/pages/index.jsx index c083d3379..614201c94 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -2,9 +2,10 @@ import useSWR from "swr"; import Head from "next/head"; import dynamic from "next/dynamic"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "next-i18next"; import { useEffect, useContext } from "react"; import { BiError } from "react-icons/bi"; +import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import ServicesGroup from "components/services/group"; import BookmarksGroup from "components/bookmarks/group"; @@ -30,7 +31,7 @@ const Version = dynamic(() => import("components/version"), { const rightAlignedWidgets = ["weatherapi", "openweathermap", "weather", "search", "datetime"]; -export function getStaticProps() { +export async function getStaticProps() { let logger; try { logger = createLogger("index"); @@ -39,6 +40,7 @@ export function getStaticProps() { return { props: { initialSettings: settings, + ...(await serverSideTranslations(settings.language ?? "en")), }, }; } catch (e) { @@ -48,6 +50,7 @@ export function getStaticProps() { return { props: { initialSettings: {}, + ...(await serverSideTranslations("en")), }, }; } diff --git a/src/utils/i18n.js b/src/utils/i18n.js deleted file mode 100644 index 98ff1c123..000000000 --- a/src/utils/i18n.js +++ /dev/null @@ -1,50 +0,0 @@ -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("rate", (value, lng, options) => { - if (value === 0) return "0 Bps"; - - const bits = options.bits ? value : value / 8; - const k = 1024; - const dm = options.decimals ? options.decimals : 0; - const sizes = ["Bps", "Kbps", "Mbps", "Gbps", "Tbps", "Pbps", "Ebps", "Zbps", "Ybps"]; - - const i = Math.floor(Math.log(bits) / Math.log(k)); - - const formatted = new Intl.NumberFormat(lng, { maximumFractionDigits: dm, minimumFractionDigits: dm }).format( - parseFloat(bits / k ** i) - ); - - return `${formatted} ${sizes[i]}`; -}); - -i18n.services.formatter.add("percent", (value, lng, options) => - new Intl.NumberFormat(lng, { style: "percent", ...options }).format(parseFloat(value) / 100.0) -); - -export default i18n;