From c5f85d143b3d0262e844017cd1f2d6b0123c8cfa Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Thu, 9 Mar 2023 21:20:39 +0000 Subject: [PATCH 001/125] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 92b9080..28bb38e 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ The roadmap is still being expanded. * Forum support * Better profiles (avatar, bio etc.) * Premoderation option +* Anti-cheat ## Configuration From 6cc2fd1c0ba39f1f02572d57d44ec54440fb1dec Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Sun, 12 Mar 2023 11:37:06 +0000 Subject: [PATCH 002/125] fix error on home page with no announcements --- api/src/controllers/announcement.js | 3 ++- client/pages/index.js | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/api/src/controllers/announcement.js b/api/src/controllers/announcement.js index c33fb42..0abb5ca 100644 --- a/api/src/controllers/announcement.js +++ b/api/src/controllers/announcement.js @@ -258,7 +258,8 @@ export const getLatestAnnouncement = async (req, res, next) => { }, }, ]); - res.json(announcement); + if (announcement) res.json(announcement); + else res.sendStatus(404); } catch (e) { next(e); } diff --git a/client/pages/index.js b/client/pages/index.js index dd504e8..2e6ac2f 100644 --- a/client/pages/index.js +++ b/client/pages/index.js @@ -173,7 +173,10 @@ export const getServerSideProps = withAuthServerSideProps( headers: fetchHeaders, } ); - const latestAnnouncement = await latestAnnouncementRes.json(); + let latestAnnouncement = null; + if (latestAnnouncementRes.status === 200) { + latestAnnouncement = await latestAnnouncementRes.json(); + } const verifiedRes = await fetch(`${SQ_API_URL}/account/get-verified`, { headers: fetchHeaders, @@ -184,6 +187,7 @@ export const getServerSideProps = withAuthServerSideProps( props: { latestTorrents, latestAnnouncement, emailVerified, token }, }; } catch (e) { + console.error(e); if (e === "banned") throw "banned"; return { props: {} }; } From 0645138d3700dbcf8c54b9aaa5e397be43ed0ee6 Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Sun, 12 Mar 2023 11:37:54 +0000 Subject: [PATCH 003/125] no api rate limit in dev --- api/src/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/api/src/index.js b/api/src/index.js index e6e3895..6c77288 100644 --- a/api/src/index.js +++ b/api/src/index.js @@ -127,6 +127,7 @@ validateConfig(config).then(() => { return req.headers["x-forwarded-for"].split(",")[0]; return req.ip; }, + skip: () => process.env.NODE_ENV !== "production", }); app.use(limiter); From 4ec621fa1ffd8efb8b51534e9e97badd44e2da3f Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Sun, 12 Mar 2023 11:38:52 +0000 Subject: [PATCH 004/125] upload do not error on wrong announce / not private, just set them on backend --- api/src/controllers/torrent.js | 22 ++++------------------ client/pages/upload.js | 5 ++--- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/api/src/controllers/torrent.js b/api/src/controllers/torrent.js index 9d61c81..cf47847 100644 --- a/api/src/controllers/torrent.js +++ b/api/src/controllers/torrent.js @@ -33,16 +33,6 @@ export const uploadTorrent = async (req, res, next) => { const torrent = Buffer.from(req.body.torrent, "base64"); const parsed = bencode.decode(torrent); - if (parsed.info.private !== 1) { - res.status(400).send("Torrent must be set to private"); - return; - } - - if (!parsed.announce || parsed["announce-list"]) { - res.status(400).send("One and only one announce URL must be set"); - return; - } - if (process.env.SQ_TORRENT_CATEGORIES.length && !req.body.type) { res.status(400).send("Torrent must have a category"); return; @@ -67,14 +57,6 @@ export const uploadTorrent = async (req, res, next) => { const user = await User.findOne({ _id: req.userId }).lean(); - if ( - parsed.announce.toString() !== - `${process.env.SQ_BASE_URL}/sq/${user.uid}/announce` - ) { - res.status(400).send("Announce URL is invalid"); - return; - } - const infoHash = crypto .createHash("sha1") .update(bencode.encode(parsed.info)) @@ -87,6 +69,10 @@ export const uploadTorrent = async (req, res, next) => { return; } + parsed.info.private = 1; + parsed.announce = `${process.env.SQ_BASE_URL}/sq/${user.uid}/announce`; + delete parsed["announce-list"]; + let files; if (parsed.info.files) { files = parsed.info.files.map((file) => ({ diff --git a/client/pages/upload.js b/client/pages/upload.js index 42a6a54..1361ea3 100644 --- a/client/pages/upload.js +++ b/client/pages/upload.js @@ -269,15 +269,14 @@ const Upload = ({ token, userId }) => { - Announce URL must be set to{" "} + Announce URL:{" "} {SQ_BASE_URL}/sq/{userId}/announce - {" "} - or upload will be rejected +
From b73e22675bbd1593f45fa173cdf5c9fc4eda115c Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Sun, 12 Mar 2023 11:39:04 +0000 Subject: [PATCH 005/125] fix ssr notFound not working --- client/utils/withAuth.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/utils/withAuth.js b/client/utils/withAuth.js index 41f9eba..0649aa1 100644 --- a/client/utils/withAuth.js +++ b/client/utils/withAuth.js @@ -71,14 +71,14 @@ export const withAuthServerSideProps = ( fetchHeaders["Authorization"] = `Bearer ${token}`; } - const { props: ssProps } = await getServerSideProps({ + const { props: ssProps, notFound } = await getServerSideProps({ ...ctx, token, userId, fetchHeaders, isPublicAccess, }); - return { props: { ...ssProps, token } }; + return { props: { ...ssProps, token }, notFound }; } catch (e) { if (e === "banned") return { From 0774c924ec34fb2259c7ad87ff0844c4a12997b1 Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Sun, 12 Mar 2023 11:52:34 +0000 Subject: [PATCH 006/125] makes folder items in torrent file list expandable/collapsible --- client/pages/torrent/[infoHash].js | 78 ++++++++++++++++++------------ 1 file changed, 47 insertions(+), 31 deletions(-) diff --git a/client/pages/torrent/[infoHash].js b/client/pages/torrent/[infoHash].js index baa7553..ee897da 100644 --- a/client/pages/torrent/[infoHash].js +++ b/client/pages/torrent/[infoHash].js @@ -77,38 +77,54 @@ export const Info = ({ title, items }) => ( ); -const FileItem = ({ file, depth = 0 }) => ( - - - {file.name} - {file.size !== undefined ? ( - <> - {" "} - - ({prettyBytes(file.size)}) +const WrapExpandable = ({ wrap, children }) => + wrap ?
{children}
: children; + +const FileItem = ({ file, depth = 0 }) => { + return ( + + + + + {file.name} + {file.size !== undefined ? ( + <> + {" "} + + ({prettyBytes(file.size)}) + + + ) : ( + "/" + )} - - ) : ( - "/" - )} -
- {!!file.children.length && ( - - {file.children.map((child) => ( - - ))} - - )} -
-); + + {!!file.children.length && ( + + {file.children.map((child) => ( + + ))} + + )} + + + ); +}; const Torrent = ({ token, torrent = {}, userId, userRole, uid }) => { const [showReportModal, setShowReportModal] = useState(false); From efed118108d8ec50b0e70b3814e4e8278b53ad1a Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Sun, 12 Mar 2023 11:54:42 +0000 Subject: [PATCH 007/125] fix nav bar z-index --- client/pages/_app.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/pages/_app.js b/client/pages/_app.js index 469c83c..0f4d8a8 100644 --- a/client/pages/_app.js +++ b/client/pages/_app.js @@ -275,6 +275,7 @@ const SqTracker = ({ Component, pageProps, initialTheme }) => { borderColor="border" position="fixed" top={0} + zIndex={9} > Date: Sun, 12 Mar 2023 11:57:42 +0000 Subject: [PATCH 008/125] ver 1.1.0 --- api/package.json | 2 +- client/package.json | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/package.json b/api/package.json index a5bf965..74b7adf 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "@sqtracker/api", - "version": "1.0.0", + "version": "1.1.0", "private": true, "license": "GPL-3.0-only", "scripts": { diff --git a/client/package.json b/client/package.json index e2a5fd4..5a3ae75 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@sqtracker/client", - "version": "1.0.0", + "version": "1.1.0", "private": true, "license": "GPL-3.0-only", "scripts": { diff --git a/package.json b/package.json index c8453c1..0619da0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sqtracker", - "version": "1.0.0", + "version": "1.1.0", "private": true, "license": "GPL-3.0-only", "workspaces": [ From e4479fcd9d27e0fb99a286d6d70b31cf92e3bdad Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Sun, 12 Mar 2023 12:14:39 +0000 Subject: [PATCH 009/125] meta: upgrade github action versions --- .github/workflows/main.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 470de79..d44fe11 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,7 +20,7 @@ jobs: uses: actions/checkout@v3 - name: Log in to the Container registry - uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + uses: docker/login-action@v2 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -28,18 +28,18 @@ jobs: - name: Extract metadata (tags, labels) for Docker (api) id: meta-api - uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + uses: docker/metadata-action@v4 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_BASE }}-api - name: Extract metadata (tags, labels) for Docker (client) id: meta-client - uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + uses: docker/metadata-action@v4 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_BASE }}-client - name: Build and push Docker image (api) - uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc + uses: docker/build-push-action@v4 with: context: ./api push: true @@ -47,7 +47,7 @@ jobs: labels: ${{ steps.meta-api.outputs.labels }} - name: Build and push Docker image (client) - uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc + uses: docker/build-push-action@v4 with: context: ./client push: true From 08aa8e3041ed99f86716c9526311b18cf2f09ad6 Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Sun, 12 Mar 2023 13:59:16 +0000 Subject: [PATCH 010/125] fix typo in config.example.js --- config.example.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config.example.js b/config.example.js index c3e582f..d280459 100644 --- a/config.example.js +++ b/config.example.js @@ -51,10 +51,10 @@ module.exports = { // They get double if they also the uploader of the accepted torrent SQ_BP_EARNED_PER_FILLED_REQUEST: 1, - // Number of bonus it costs a user to buy 1 invite (set to 0 to disable buying invites). + // Number of bonus points it costs a user to buy 1 invite (set to 0 to disable buying invites). SQ_BP_COST_PER_INVITE: 3, - // Number of bonus it costs a user to buy 1 GB of upload (set to 0 to disable buying upload). + // Number of bonus points it costs a user to buy 1 GB of upload (set to 0 to disable buying upload). SQ_BP_COST_PER_GB: 3, // Whether to enable freeleech on all torrents. From 0fef96b4dc050315c16511d91f0fdd4da37fe0b3 Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Tue, 14 Mar 2023 18:02:05 +0000 Subject: [PATCH 011/125] add/remove tags individually in ui --- client/pages/upload.js | 57 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/client/pages/upload.js b/client/pages/upload.js index 1361ea3..67f196a 100644 --- a/client/pages/upload.js +++ b/client/pages/upload.js @@ -8,6 +8,8 @@ import slugify from "slugify"; import { Link as LinkIcon } from "@styled-icons/boxicons-regular/Link"; import { Check } from "@styled-icons/boxicons-regular/Check"; import { InfoCircle } from "@styled-icons/boxicons-regular/InfoCircle"; +import { Plus } from "@styled-icons/boxicons-regular/Plus"; +import { X } from "@styled-icons/boxicons-regular/X"; import { withAuth } from "../utils/withAuth"; import SEO from "../components/SEO"; import Box from "../components/Box"; @@ -48,6 +50,7 @@ export const TorrentFields = ({ values?.type ?? slugify(Object.keys(categories)[0], { lower: true }) ); const [sources, setSources] = useState([]); + const [tags, setTags] = useState(values?.tags?.split(",") ?? []); useEffect(() => { setSources( @@ -126,13 +129,53 @@ export const TorrentFields = ({ fontFamily="mono" mb={4} /> - + + + {tags.map((tag, i) => ( + + { + setTags((t) => { + const curTags = [...t]; + curTags.splice(i, 1, e.target.value); + return curTags; + }); + }} + width="138px" + mr={2} + /> + + + ))} + + + + ); }; From 0cb5368f38ce6e6ef07de1489b85b6a94a105e57 Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Tue, 14 Mar 2023 18:35:54 +0000 Subject: [PATCH 012/125] do not slugify tags so any locale can be used --- api/src/controllers/torrent.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/api/src/controllers/torrent.js b/api/src/controllers/torrent.js index cf47847..b5e0518 100644 --- a/api/src/controllers/torrent.js +++ b/api/src/controllers/torrent.js @@ -9,6 +9,15 @@ import Comment from "../schema/comment"; import Group from "../schema/group"; import { createGroup, addToGroup, removeFromGroup } from "./group"; +const urlReservedCharRegex = /[&$+,/:;=?@#<>\[\]{}|\\\^%]/g; + +const formatTag = (tag) => + tag + .trim() + .toLowerCase() + .replaceAll(urlReservedCharRegex, "") + .replaceAll(" ", "-"); + export const embellishTorrentsWithTrackerScrape = async (tracker, torrents) => { if (!torrents.length) return []; @@ -127,9 +136,7 @@ export const uploadTorrent = async (req, res, next) => { upvotes: [], downvotes: [], freeleech: false, - tags: (req.body.tags ?? "") - .split(",") - .map((t) => slugify(t.trim(), { lower: true })), + tags: (req.body.tags ?? "").split(",").map((t) => formatTag(t)), group: groupId, mediaInfo: req.body.mediaInfo, }); @@ -200,9 +207,7 @@ export const editTorrent = async (req, res, next) => { type: req.body.type, source: req.body.source, description: req.body.description, - tags: (req.body.tags ?? "") - .split(",") - .map((t) => slugify(t.trim(), { lower: true })), + tags: (req.body.tags ?? "").split(",").map((t) => formatTag(t)), }, mediaInfo: req.body.mediaInfo, } @@ -684,7 +689,7 @@ export const searchTorrents = (tracker) => async (req, res, next) => { query: query ? decodeURIComponent(query) : undefined, category, source, - tag, + tag: tag ? decodeURIComponent(tag) : undefined, userId: req.userId, tracker, }); From c73d3bafb4dc97c45f93fcb9832fb12359d0fed7 Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Wed, 15 Mar 2023 13:58:50 +0000 Subject: [PATCH 013/125] basic localisation implementation --- client/components/Navigation.js | 33 ++++- client/locales.json | 16 ++ client/pages/_app.js | 253 +++++++++++++++++--------------- client/utils/LocaleContext.js | 8 + 4 files changed, 190 insertions(+), 120 deletions(-) create mode 100644 client/locales.json create mode 100644 client/utils/LocaleContext.js diff --git a/client/components/Navigation.js b/client/components/Navigation.js index 94acd87..67d8ce5 100644 --- a/client/components/Navigation.js +++ b/client/components/Navigation.js @@ -24,6 +24,7 @@ import { UserPlus } from "@styled-icons/boxicons-regular/UserPlus"; import Box from "./Box"; import Text from "./Text"; import Button from "./Button"; +import LocaleContext from "../utils/LocaleContext"; const NavLink = styled.a(({ theme, href, highlights = [], mt = 0 }) => { const router = useRouter(); @@ -56,6 +57,18 @@ const NavLink = styled.a(({ theme, href, highlights = [], mt = 0 }) => { }); }); +const LocaleSelector = styled.select(() => + css({ + background: "none", + color: "text", + border: 0, + fontSize: 0, + fontFamily: "body", + cursor: "pointer", + p: 0, + }) +); + const Navigation = ({ isMobile, menuIsOpen, setMenuIsOpen }) => { const [cookies] = useCookies(); const [role, setRole] = useState("user"); @@ -63,6 +76,9 @@ const Navigation = ({ isMobile, menuIsOpen, setMenuIsOpen }) => { const theme = useContext(ThemeContext); + const { locale, setLocale, locales, getLocaleString } = + useContext(LocaleContext); + const { asPath } = useRouter(); const { username, token } = cookies; @@ -73,7 +89,6 @@ const Navigation = ({ isMobile, menuIsOpen, setMenuIsOpen }) => { SQ_API_URL, SQ_ALLOW_REGISTER, SQ_VERSION, - SQ_TORRENT_CATEGORIES, }, } = getConfig(); @@ -234,7 +249,7 @@ const Navigation = ({ isMobile, menuIsOpen, setMenuIsOpen }) => { - Log in + {getLocaleString("logIn")} @@ -242,7 +257,7 @@ const Navigation = ({ isMobile, menuIsOpen, setMenuIsOpen }) => { SQ_ALLOW_REGISTER === "invite") && ( - Register + {getLocaleString("register")} @@ -270,10 +285,18 @@ const Navigation = ({ isMobile, menuIsOpen, setMenuIsOpen }) => { > ■ sqtracker {" "} - - v{SQ_VERSION} + setLocale(e.target.value)} + > + {locales.map((l) => ( + + ))} + ); diff --git a/client/locales.json b/client/locales.json new file mode 100644 index 0000000..e5dc877 --- /dev/null +++ b/client/locales.json @@ -0,0 +1,16 @@ +{ + "en": { + "poweredBy": "Powered by", + "logIn": "Log in", + "register": "Register", + "email": "Email", + "username": "Username", + "usernameRules": "Can only consist of letters, numbers, and “.”", + "password": "Password", + "resetPassword": "Reset password", + "newPassword": "New password" + }, + "fr": { + "logIn": "Se connecter" + } +} diff --git a/client/pages/_app.js b/client/pages/_app.js index 0f4d8a8..a5dc21c 100644 --- a/client/pages/_app.js +++ b/client/pages/_app.js @@ -26,6 +26,8 @@ import Input from "../components/Input"; import { NotificationsProvider } from "../components/Notifications"; import Text from "../components/Text"; import LoadingContext from "../utils/LoadingContext"; +import LocaleContext from "../utils/LocaleContext"; +import locales from "../locales.json"; const getThemeColours = (themeName, customTheme = {}) => { switch (themeName) { @@ -151,6 +153,9 @@ const Loading = styled(LoaderAlt)` animation: ${spin} 1s linear infinite; `; +const getLocaleString = (locale) => (key) => + locales[locale][key] ?? locales.en[key]; + const SqTracker = ({ Component, pageProps, initialTheme }) => { const [isMobile, setIsMobile] = useState(false); const [menuIsOpen, setMenuIsOpen] = useState(false); @@ -158,6 +163,7 @@ const SqTracker = ({ Component, pageProps, initialTheme }) => { const [isServer, setIsServer] = useState(true); const [loading, setLoading] = useState(false); const [userStats, setUserStats] = useState(); + const [locale, setLocale] = useState("en"); const router = useRouter(); @@ -206,6 +212,9 @@ const SqTracker = ({ Component, pageProps, initialTheme }) => { }); } + const { locale: localeCookie } = cookies; + if (Object.keys(locales).includes(localeCookie)) setLocale(localeCookie); + Router.events.on("routeChangeStart", () => setLoading(true)); Router.events.on("routeChangeComplete", () => setLoading(false)); Router.events.on("routeChangeError", () => setLoading(false)); @@ -261,131 +270,145 @@ const SqTracker = ({ Component, pageProps, initialTheme }) => { - - - + { + setLocale(l); + setCookie("locale", l, { path: "/" }); + }, + locales: Object.keys(locales), + getLocaleString: getLocaleString(locale), + }} + > + + - - - {loading && ( - - - - )} - {SQ_SITE_WIDE_FREELEECH === true && ( - - Site-wide freeleech enabled! - - )} - - {!isServer && token && ( + - {userStats && ( - - - - {userStats.ratio === -1 ? "N/A" : userStats.ratio} - - - - {prettyBytes(userStats.up ?? 0)} - - - - {prettyBytes(userStats.down ?? 0)} - - - - {userStats.bp ?? 0} BP - + + {loading && ( + + )} - - - - {allowThemeToggle && ( - + Site-wide freeleech enabled! + )} - )} + {!isServer && token && ( + + {userStats && ( + + + + {userStats.ratio === -1 ? "N/A" : userStats.ratio} + + + + {prettyBytes(userStats.up ?? 0)} + + + + {prettyBytes(userStats.down ?? 0)} + + + + {userStats.bp ?? 0} BP + + + )} + + + + {allowThemeToggle && ( + + )} + + )} + + + + - - - - - + + diff --git a/client/utils/LocaleContext.js b/client/utils/LocaleContext.js new file mode 100644 index 0000000..d6695fc --- /dev/null +++ b/client/utils/LocaleContext.js @@ -0,0 +1,8 @@ +import { createContext } from "react"; + +export default createContext({ + locale: "en", + setLocale: () => {}, + locales: [], + getLocaleString: () => {}, +}); From 3a2d2d905072c8034cb9a0146a450456761f9d13 Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Wed, 15 Mar 2023 14:05:48 +0000 Subject: [PATCH 014/125] update roadmap --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 28bb38e..64aba4b 100644 --- a/README.md +++ b/README.md @@ -51,10 +51,11 @@ Please join the [Discord server](https://discord.gg/BEGXEk29Up) for support and The roadmap is still being expanded. -* Forum support +* Forum support & direct messages * Better profiles (avatar, bio etc.) * Premoderation option * Anti-cheat +* Localisation (ongoing) ## Configuration From 8f97e3555a6583b4285ed864e4517244b0e2aab2 Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Wed, 15 Mar 2023 20:51:02 +0000 Subject: [PATCH 015/125] move strings to locales file for splash, log in, register, password reset --- api/src/controllers/user.js | 4 +- client/locales.json | 13 +++++- client/pages/index.js | 54 +++++++++++++------------ client/pages/login.js | 26 +++++++----- client/pages/register.js | 42 +++++++++++++------ client/pages/reset-password/finalise.js | 26 ++++++++---- client/pages/reset-password/initiate.js | 20 ++++++--- 7 files changed, 121 insertions(+), 64 deletions(-) diff --git a/api/src/controllers/user.js b/api/src/controllers/user.js index 48c94c2..c281b0c 100644 --- a/api/src/controllers/user.js +++ b/api/src/controllers/user.js @@ -371,7 +371,7 @@ export const initiatePasswordReset = (mail) => async (req, res, next) => { const user = await User.findOne({ email: req.body.email }).lean(); if (!user) { - res.status(404).send("User does not exist"); + res.sendStatus(200); return; } @@ -397,7 +397,7 @@ export const initiatePasswordReset = (mail) => async (req, res, next) => { ${process.env.SQ_BASE_URL}/reset-password/finalise?token=${token}`, }); - res.send(token); + res.sendStatus(200); } catch (e) { next(e); } diff --git a/client/locales.json b/client/locales.json index e5dc877..6be7d48 100644 --- a/client/locales.json +++ b/client/locales.json @@ -7,8 +7,19 @@ "username": "Username", "usernameRules": "Can only consist of letters, numbers, and “.”", "password": "Password", + "totp": "One-time code", "resetPassword": "Reset password", - "newPassword": "New password" + "newPassword": "New password", + "welcomeBack": "Welcome back", + "logInFailed": "Could not log in", + "welcome": "Welcome", + "registerFailed": "Could not register", + "registrationClosed": "Registration closed", + "passwordResetRequestSuccess": "If an account with that email address exists, you will receive an email shortly", + "passwordResetRequestFailed": "Could not initiate password reset", + "passwordResetSuccess": "Password was reset successfully", + "passwordResetFailed": "Could not complete password reset", + "tokenError": "Token error" }, "fr": { "logIn": "Se connecter" diff --git a/client/pages/index.js b/client/pages/index.js index 2e6ac2f..108d672 100644 --- a/client/pages/index.js +++ b/client/pages/index.js @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useContext } from "react"; import getConfig from "next/config"; import Link from "next/link"; import { useRouter } from "next/router"; @@ -13,34 +13,38 @@ import Infobox from "../components/Infobox"; import { ErrorCircle } from "@styled-icons/boxicons-regular/ErrorCircle"; import { News } from "@styled-icons/boxicons-regular/News"; import moment from "moment/moment"; +import LocaleContext from "../utils/LocaleContext"; -const PublicLanding = ({ name, allowRegister }) => ( - - - {name} - - - - - Log in - - - {allowRegister && ( - - - Register +const PublicLanding = ({ name, allowRegister }) => { + const { getLocaleString } = useContext(LocaleContext); + return ( + + + {name} + + + + + {getLocaleString("logIn")} - )} + {allowRegister && ( + + + {getLocaleString("register")} + + + )} + - -); + ); +}; const Index = ({ token, diff --git a/client/pages/login.js b/client/pages/login.js index f1a3925..5fa8684 100644 --- a/client/pages/login.js +++ b/client/pages/login.js @@ -9,6 +9,7 @@ import Input from "../components/Input"; import Button from "../components/Button"; import { NotificationContext } from "../components/Notifications"; import LoadingContext from "../utils/LoadingContext"; +import LocaleContext from "../utils/LocaleContext"; import { usernamePattern } from "./register"; const Login = () => { @@ -18,6 +19,7 @@ const Login = () => { const { addNotification } = useContext(NotificationContext); const { setLoading } = useContext(LoadingContext); + const { getLocaleString } = useContext(LocaleContext); const router = useRouter(); @@ -57,11 +59,17 @@ const Login = () => { setCookie("userId", uid, { path: "/", expires }); setCookie("username", username, { path: "/", expires }); - addNotification("success", `Welcome back ${form.get("username")}!`); + addNotification( + "success", + `${getLocaleString("welcomeBack")} ${form.get("username")}!` + ); router.push("/"); } catch (e) { - addNotification("error", `Could not log in: ${e.message}`); + addNotification( + "error", + `${getLocaleString("logInFailed")}: ${e.message}` + ); console.error(e); } @@ -70,14 +78,14 @@ const Login = () => { return ( <> - + - Log in + {getLocaleString("logIn")} { {totpRequired && ( - + )} - + - Reset password + {getLocaleString("resetPassword")} diff --git a/client/pages/register.js b/client/pages/register.js index dad030a..d7d56f3 100644 --- a/client/pages/register.js +++ b/client/pages/register.js @@ -12,6 +12,7 @@ import Button from "../components/Button"; import Box from "../components/Box"; import { NotificationContext } from "../components/Notifications"; import LoadingContext from "../utils/LoadingContext"; +import LocaleContext from "../utils/LocaleContext"; export const usernamePattern = "[A-Za-z0-9.]+"; @@ -21,6 +22,7 @@ const Register = ({ token: inviteToken, tokenError }) => { const { colors } = useContext(ThemeContext); const { addNotification } = useContext(NotificationContext); const { setLoading } = useContext(LoadingContext); + const { getLocaleString } = useContext(LocaleContext); const router = useRouter(); @@ -60,11 +62,17 @@ const Register = ({ token: inviteToken, tokenError }) => { setCookie("userId", uid, { path: "/", expires }); setCookie("username", username, { path: "/", expires }); - addNotification("success", `Welcome ${form.get("username")}!`); + addNotification( + "success", + `${getLocaleString("welcome")} ${form.get("username")}!` + ); router.push("/"); } catch (e) { - addNotification("error", `Could not register: ${e.message}`); + addNotification( + "error", + `${getLocaleString("registerFailed")}: ${e.message}` + ); console.error(e); } @@ -74,28 +82,34 @@ const Register = ({ token: inviteToken, tokenError }) => { if (SQ_ALLOW_REGISTER !== "open" && SQ_ALLOW_REGISTER !== "invite") { return ( <> - + - Register + {getLocaleString("register")} -

Registration is closed.

+

{getLocaleString("registrationClosed")}.

); } return ( <> - + - Register + {getLocaleString("register")} {!tokenError ? (
- + { - +
) : ( { borderRadius={1} p={4} > - Could not register: {tokenError} + + {getLocaleString("registerFailed")}: {tokenError} + )} diff --git a/client/pages/reset-password/finalise.js b/client/pages/reset-password/finalise.js index 618de05..e1b55e3 100644 --- a/client/pages/reset-password/finalise.js +++ b/client/pages/reset-password/finalise.js @@ -8,10 +8,12 @@ import Input from "../../components/Input"; import Button from "../../components/Button"; import { NotificationContext } from "../../components/Notifications"; import LoadingContext from "../../utils/LoadingContext"; +import LocaleContext from "../../utils/LocaleContext"; const FinalisePasswordReset = ({ token, email, tokenError }) => { const { addNotification } = useContext(NotificationContext); const { setLoading } = useContext(LoadingContext); + const { getLocaleString } = useContext(LocaleContext); const router = useRouter(); @@ -42,13 +44,13 @@ const FinalisePasswordReset = ({ token, email, tokenError }) => { throw new Error(reason); } - addNotification("success", "Password was reset successfully"); + addNotification("success", getLocaleString("passwordResetSuccess")); router.push("/login"); } catch (e) { addNotification( "error", - `Could not complete password reset: ${e.message}` + `${getLocaleString("passwordResetFailed")}: ${e.message}` ); console.error(e); } @@ -58,24 +60,32 @@ const FinalisePasswordReset = ({ token, email, tokenError }) => { return ( <> - + - Reset password + {getLocaleString("resetPassword")} - + {!tokenError ? (
- +
) : ( -

Token error: {tokenError}

+

+ {getLocaleString("tokenError")}: {tokenError} +

)} ); diff --git a/client/pages/reset-password/initiate.js b/client/pages/reset-password/initiate.js index 4a6f41f..5367353 100644 --- a/client/pages/reset-password/initiate.js +++ b/client/pages/reset-password/initiate.js @@ -6,10 +6,12 @@ import Input from "../../components/Input"; import Button from "../../components/Button"; import { NotificationContext } from "../../components/Notifications"; import LoadingContext from "../../utils/LoadingContext"; +import LocaleContext from "../../utils/LocaleContext"; const InitiatePasswordReset = () => { const { addNotification } = useContext(NotificationContext); const { setLoading } = useContext(LoadingContext); + const { getLocaleString } = useContext(LocaleContext); const { publicRuntimeConfig: { SQ_API_URL }, @@ -38,12 +40,12 @@ const InitiatePasswordReset = () => { addNotification( "success", - "If an account with that email address exists, you will receive an email shortly" + getLocaleString("passwordResetRequestSuccess") ); } catch (e) { addNotification( "error", - `Could not initiate password reset: ${e.message}` + `${getLocaleString("passwordResetRequestFailed")}: ${e.message}` ); console.error(e); } @@ -53,13 +55,19 @@ const InitiatePasswordReset = () => { return ( <> - + - Reset password + {getLocaleString("resetPassword")}
- - + +
); From b6ae74fce7d17fbe73fbbf7a9a4caed46285d8ce Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Sun, 19 Mar 2023 11:36:14 +0000 Subject: [PATCH 016/125] improved wiki path handling --- api/src/controllers/wiki.js | 31 +++++++++++++++++++++++-------- client/pages/wiki/new.js | 11 +++++++++++ 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/api/src/controllers/wiki.js b/api/src/controllers/wiki.js index 197db62..491190b 100644 --- a/api/src/controllers/wiki.js +++ b/api/src/controllers/wiki.js @@ -1,7 +1,16 @@ +import slugify from "slugify"; import Wiki from "../schema/wiki"; const slugRegex = /^\/([a-z0-9-_\/])*/i; +const formatSlug = (slug) => { + if (!slug.startsWith("/")) slug = `/${slug}`; + if (slug.endsWith("/")) slug = slug.slice(0, -1); + const split = slug.split("/"); + const slugified = split.map((token) => slugify(token, { lower: true })); + return slugified.join("/"); +}; + export const createWiki = async (req, res, next) => { if (req.body.slug && req.body.title && req.body.body) { try { @@ -12,14 +21,17 @@ export const createWiki = async (req, res, next) => { return; } - const validSlug = slugRegex.test(req.body.slug); + let { slug } = req.body; + slug = formatSlug(slug); + + const validSlug = slugRegex.test(slug); if (!validSlug) { res.status(400).send("That is not a valid path"); return; } - const existing = await Wiki.findOne({ slug: req.body.slug }).lean(); + const existing = await Wiki.findOne({ slug }).lean(); if (existing) { res @@ -31,7 +43,7 @@ export const createWiki = async (req, res, next) => { } const wiki = new Wiki({ - slug: req.body.slug, + slug, title: req.body.title, body: req.body.body, createdBy: req.userId, @@ -39,7 +51,7 @@ export const createWiki = async (req, res, next) => { }); await wiki.save(); - res.send(req.body.slug); + res.send(slug); } catch (e) { next(e); } @@ -130,15 +142,18 @@ export const updateWiki = async (req, res, next) => { return; } - const validSlug = slugRegex.test(req.body.slug); + let { slug } = req.body; + slug = formatSlug(slug); + + const validSlug = slugRegex.test(slug); if (!validSlug) { res.status(400).send("That is not a valid path"); return; } - if (req.body.slug !== existing.slug) { - const existingSlug = await Wiki.findOne({ slug: req.body.slug }).lean(); + if (slug !== existing.slug) { + const existingSlug = await Wiki.findOne({ slug }).lean(); if (existingSlug) { res @@ -154,7 +169,7 @@ export const updateWiki = async (req, res, next) => { { _id: req.params.wikiId }, { $set: { - slug: req.body.slug, + slug, title: req.body.title, body: req.body.body, updated: Date.now(), diff --git a/client/pages/wiki/new.js b/client/pages/wiki/new.js index 420af67..6e69e98 100644 --- a/client/pages/wiki/new.js +++ b/client/pages/wiki/new.js @@ -2,6 +2,7 @@ import React, { useContext, useState } from "react"; import getConfig from "next/config"; import { useRouter } from "next/router"; import jwt from "jsonwebtoken"; +import slugify from "slugify"; import SEO from "../../components/SEO"; import Text from "../../components/Text"; import Input from "../../components/Input"; @@ -25,6 +26,16 @@ export const WikiFields = ({ values }) => { label="Path" value={slugValue} onChange={(e) => setSlugValue(e.target.value)} + onBlur={(e) => { + let { value } = e.target; + if (!value.startsWith("/")) value = `/${value}`; + if (value.endsWith("/")) value = value.slice(0, -1); + const split = value.split("/"); + const slugified = split.map((token) => + slugify(token, { lower: true }) + ); + setSlugValue(slugified.join("/")); + }} disabled={values?.slug === "/"} mb={2} required From 5eb4dd312f4b2eb7380d1e51253c8c6f0d1d22bc Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Sun, 19 Mar 2023 11:41:36 +0000 Subject: [PATCH 017/125] add download count to torrent list --- api/src/tracker/announce.js | 10 ---------- client/components/TorrentList.js | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/api/src/tracker/announce.js b/api/src/tracker/announce.js index a9da65a..f5befe5 100644 --- a/api/src/tracker/announce.js +++ b/api/src/tracker/announce.js @@ -122,16 +122,6 @@ const handleAnnounce = async (req, res) => { ); } - console.log({ - bytes, - uploaded, - alreadyUploadedSession, - uploadDeltaSession, - downloaded, - alreadyDownloadedSession, - downloadDeltaSession, - }); - await Progress.findOneAndUpdate( { userId: user._id, infoHash }, { diff --git a/client/components/TorrentList.js b/client/components/TorrentList.js index 0ff0ff7..dd649df 100644 --- a/client/components/TorrentList.js +++ b/client/components/TorrentList.js @@ -6,6 +6,7 @@ import slugify from "slugify"; import { ListUl } from "@styled-icons/boxicons-regular/ListUl"; import { Upload } from "@styled-icons/boxicons-regular/Upload"; import { Download } from "@styled-icons/boxicons-regular/Download"; +import { File } from "@styled-icons/boxicons-regular/File"; import { Chat } from "@styled-icons/boxicons-solid/Chat"; import { ChevronsLeft } from "@styled-icons/boxicons-solid/ChevronsLeft"; import { ChevronLeft } from "@styled-icons/boxicons-solid/ChevronLeft"; @@ -109,6 +110,20 @@ const TorrentList = ({ torrents = [], categories, total }) => { gridWidth: "100px", rightAlign: true, }, + { + header: "Downloads", + accessor: "downloads", + cell: ({ value }) => ( + + {value || 0} + + ), + gridWidth: "100px", + rightAlign: true, + }, { header: "Comments", accessor: "comments.count", From edc5e8275f6f86b1240f94d591749f1ee7e5489b Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Sun, 19 Mar 2023 13:02:03 +0000 Subject: [PATCH 018/125] add sorting torrent list on categories, tags, search --- api/src/controllers/torrent.js | 32 +++++++++------ client/components/List.js | 53 +++++++++++++++++++++++- client/components/TorrentList.js | 58 ++++++++++++++++++++++----- client/pages/categories/[category].js | 17 +++++--- client/pages/search/[[...query]].js | 17 +++++--- client/pages/tags/[tag].js | 17 +++++--- 6 files changed, 153 insertions(+), 41 deletions(-) diff --git a/api/src/controllers/torrent.js b/api/src/controllers/torrent.js index b5e0518..f3026c2 100644 --- a/api/src/controllers/torrent.js +++ b/api/src/controllers/torrent.js @@ -452,10 +452,19 @@ export const getTorrentsPage = async ({ tag, uploadedBy, userId, + sort, tracker, }) => { const queryNGrams = nGrams(query, false, 2, false).join(" "); + const [sortField, sortDirString] = sort?.split(":") ?? []; + const sortDir = sortDirString === "asc" ? 1 : -1; + + const combinedSort = {}; + if (sortField) combinedSort[sortField] = sortDir; + if (query) combinedSort.confidenceScore = { $meta: "textScore" }; + combinedSort.created = -1; + const torrents = await Torrent.aggregate([ ...(query ? [ @@ -529,17 +538,6 @@ export const getTorrentsPage = async ({ }, ] : []), - { - $sort: query - ? { confidenceScore: { $meta: "textScore" } } - : { created: -1 }, - }, - { - $skip: skip, - }, - { - $limit: limit, - }, { $lookup: { from: "comments", @@ -588,6 +586,15 @@ export const getTorrentsPage = async ({ }, }, { $unwind: { path: "$fetchedBy", preserveNullAndEmptyArrays: true } }, + { + $sort: combinedSort, + }, + { + $skip: skip, + }, + { + $limit: limit, + }, ]); const [count] = await Torrent.aggregate([ @@ -682,7 +689,7 @@ export const listAll = async (req, res, next) => { }; export const searchTorrents = (tracker) => async (req, res, next) => { - const { query, category, source, tag, page } = req.query; + const { query, category, source, tag, page, sort } = req.query; try { const torrents = await getTorrentsPage({ skip: page ? parseInt(page) : 0, @@ -691,6 +698,7 @@ export const searchTorrents = (tracker) => async (req, res, next) => { source, tag: tag ? decodeURIComponent(tag) : undefined, userId: req.userId, + sort: sort ? decodeURIComponent(sort) : undefined, tracker, }); res.json(torrents); diff --git a/client/components/List.js b/client/components/List.js index 9d9620f..ec9c607 100644 --- a/client/components/List.js +++ b/client/components/List.js @@ -1,6 +1,10 @@ -import React from "react"; +import React, { useState } from "react"; import Link from "next/link"; +import { useRouter } from "next/router"; import { toPath } from "lodash"; +import qs from "qs"; +import { CaretUp } from "@styled-icons/boxicons-regular/CaretUp"; +import { CaretDown } from "@styled-icons/boxicons-regular/CaretDown"; import Box from "../components/Box"; import Text from "../components/Text"; @@ -47,7 +51,17 @@ const ListItem = ({ children }) => { ); }; +const getSortIcon = (accessor, sort = "") => { + const [sortAccessor, sortDirection] = sort.split(":"); + if (accessor !== sortAccessor) return null; + if (sortDirection === "asc") return CaretUp; + if (sortDirection === "desc") return CaretDown; + return null; +}; + const List = ({ data = [], columns = [], ...rest }) => { + const router = useRouter(); + const { sort } = router.query; return ( @@ -65,7 +79,42 @@ const List = ({ data = [], columns = [], ...rest }) => { fontWeight={600} fontSize={1} textAlign={col.rightAlign ? "right" : "left"} - _css={{ textTransform: "uppercase" }} + _css={{ + textTransform: "uppercase", + cursor: col.sortable ? "pointer" : "text", + userSelect: col.sortable ? "none" : "auto", + }} + onClick={ + col.sortable + ? () => { + const query = window.location.search; + const parsed = qs.parse(query.replace("?", "")); + if (parsed.sort) { + const [accessor, direction] = parsed.sort.split(":"); + if (accessor === col.accessor) { + if (direction === "asc") + parsed.sort = `${col.accessor}:desc`; + else if (direction === "desc") delete parsed.sort; + } else { + parsed.sort = `${col.accessor}:asc`; + } + } else { + parsed.sort = `${col.accessor}:asc`; + } + router.replace( + Object.keys(parsed).length + ? `${window.location.pathname}?${qs.stringify( + parsed + )}` + : window.location.pathname + ); + } + : undefined + } + icon={getSortIcon(col.accessor, sort)} + iconTextWrapperProps={{ + justifyContent: col.rightAlign ? "flex-end" : "flex-start", + }} > {col.header} diff --git a/client/components/TorrentList.js b/client/components/TorrentList.js index dd649df..053c183 100644 --- a/client/components/TorrentList.js +++ b/client/components/TorrentList.js @@ -1,8 +1,9 @@ -import React from "react"; +import React, { useEffect } from "react"; import getConfig from "next/config"; import { useRouter } from "next/router"; import moment from "moment"; import slugify from "slugify"; +import qs from "qs"; import { ListUl } from "@styled-icons/boxicons-regular/ListUl"; import { Upload } from "@styled-icons/boxicons-regular/Upload"; import { Download } from "@styled-icons/boxicons-regular/Download"; @@ -18,28 +19,62 @@ import Text from "./Text"; import Box from "./Box"; import Button from "./Button"; -const TorrentList = ({ torrents = [], categories, total }) => { +const pageSize = 25; + +const TorrentList = ({ + torrents = [], + setTorrents, + categories, + total, + fetchPath, + token, +}) => { const { publicRuntimeConfig: { SQ_SITE_WIDE_FREELEECH }, } = getConfig(); const router = useRouter(); const { - asPath, - query: { page: pageParam }, + query: { page: pageParam, sort }, } = router; const page = pageParam ? parseInt(pageParam) - 1 : 0; - const maxPage = Math.floor(total / 25); + const maxPage = total > pageSize ? Math.floor(total / pageSize) : 0; const canPrevPage = page > 0; const canNextPage = page < maxPage; const setPage = (number) => { - if (number === 0) router.push(asPath.split("?")[0]); - else router.push(`${asPath.split("?")[0]}?page=${number + 1}`); + const query = qs.parse(window.location.search.replace("?", "")); + if (number === 0) delete query.page; + else query.page = number + 1; + router.push( + Object.keys(query).length + ? `${window.location.pathname}?${qs.stringify(query)}` + : window.location.pathname + ); }; + useEffect(() => { + const fetchTorrents = async () => { + try { + const searchRes = await fetch( + `${fetchPath}?${qs.stringify(router.query)}`, + { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + } + ); + + const results = await searchRes.json(); + setTorrents(results.torrents); + } catch (e) {} + }; + if (fetchPath && token) fetchTorrents(); + }, [sort, page]); + return ( <> { ), gridWidth: "100px", rightAlign: true, + sortable: !!token, }, { header: "Leechers", @@ -109,6 +145,7 @@ const TorrentList = ({ torrents = [], categories, total }) => { ), gridWidth: "100px", rightAlign: true, + sortable: !!token, }, { header: "Downloads", @@ -121,8 +158,9 @@ const TorrentList = ({ torrents = [], categories, total }) => { {value || 0} ), - gridWidth: "100px", + gridWidth: "115px", rightAlign: true, + sortable: !!token, }, { header: "Comments", @@ -135,8 +173,9 @@ const TorrentList = ({ torrents = [], categories, total }) => { {value || 0} ), - gridWidth: "100px", + gridWidth: "110px", rightAlign: true, + sortable: !!token, }, { header: "Uploaded", @@ -146,6 +185,7 @@ const TorrentList = ({ torrents = [], categories, total }) => { ), gridWidth: "140px", rightAlign: true, + sortable: !!token, }, ]} /> diff --git a/client/pages/categories/[category].js b/client/pages/categories/[category].js index 139972c..6bfad32 100644 --- a/client/pages/categories/[category].js +++ b/client/pages/categories/[category].js @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from "react"; import { useRouter } from "next/router"; import getConfig from "next/config"; import qs from "qs"; @@ -8,14 +8,16 @@ import SEO from "../../components/SEO"; import Text from "../../components/Text"; import TorrentList from "../../components/TorrentList"; -const Category = ({ results }) => { +const Category = ({ results, token }) => { + const [torrents, setTorrents] = useState(results?.torrents ?? []); + const router = useRouter(); const { query: { category: categorySlug }, } = router; const { - publicRuntimeConfig: { SQ_TORRENT_CATEGORIES }, + publicRuntimeConfig: { SQ_TORRENT_CATEGORIES, SQ_API_URL }, } = getConfig(); const category = Object.keys(SQ_TORRENT_CATEGORIES).find( @@ -28,11 +30,14 @@ const Category = ({ results }) => { Browse {category} - {results?.torrents.length ? ( + {torrents.length ? ( ) : ( No results. @@ -74,7 +79,7 @@ export const getServerSideProps = withAuthServerSideProps( throw "banned"; } const results = await searchRes.json(); - return { props: { results } }; + return { props: { results, token } }; } catch (e) { if (e === "banned") throw "banned"; return { props: {} }; diff --git a/client/pages/search/[[...query]].js b/client/pages/search/[[...query]].js index 3f5f089..271ad51 100644 --- a/client/pages/search/[[...query]].js +++ b/client/pages/search/[[...query]].js @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from "react"; import getConfig from "next/config"; import { useRouter } from "next/router"; import qs from "qs"; @@ -10,7 +10,9 @@ import Button from "../../components/Button"; import Box from "../../components/Box"; import TorrentList from "../../components/TorrentList"; -const Search = ({ results, error }) => { +const Search = ({ results, error, token }) => { + const [torrents, setTorrents] = useState(results?.torrents ?? []); + const router = useRouter(); let { query: { query }, @@ -18,7 +20,7 @@ const Search = ({ results, error }) => { query = query ? decodeURIComponent(query) : ""; const { - publicRuntimeConfig: { SQ_TORRENT_CATEGORIES }, + publicRuntimeConfig: { SQ_TORRENT_CATEGORIES, SQ_API_URL }, } = getConfig(); const handleSearch = (e) => { @@ -44,11 +46,14 @@ const Search = ({ results, error }) => { <> {query && ( <> - {results.torrents.length ? ( + {torrents.length ? ( ) : ( No results. @@ -92,7 +97,7 @@ export const getServerSideProps = withAuthServerSideProps( return { props: { error: message } }; } else { const results = await searchRes.json(); - return { props: { results } }; + return { props: { results, token } }; } } catch (e) { if (e === "banned") throw "banned"; diff --git a/client/pages/tags/[tag].js b/client/pages/tags/[tag].js index 7154d5c..15554b8 100644 --- a/client/pages/tags/[tag].js +++ b/client/pages/tags/[tag].js @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from "react"; import { useRouter } from "next/router"; import getConfig from "next/config"; import qs from "qs"; @@ -7,14 +7,16 @@ import SEO from "../../components/SEO"; import Text from "../../components/Text"; import TorrentList from "../../components/TorrentList"; -const Tag = ({ results }) => { +const Tag = ({ results, token }) => { + const [torrents, setTorrents] = useState(results?.torrents ?? []); + const router = useRouter(); const { query: { tag }, } = router; const { - publicRuntimeConfig: { SQ_TORRENT_CATEGORIES }, + publicRuntimeConfig: { SQ_TORRENT_CATEGORIES, SQ_API_URL }, } = getConfig(); return ( @@ -23,11 +25,14 @@ const Tag = ({ results }) => { Tagged with “{tag}” - {results?.torrents.length ? ( + {torrents.length ? ( ) : ( No results. @@ -64,7 +69,7 @@ export const getServerSideProps = withAuthServerSideProps( throw "banned"; } const results = await searchRes.json(); - return { props: { results } }; + return { props: { results, token } }; } catch (e) { if (e === "banned") throw "banned"; return { props: {} }; From b7231dd87f4ad0e12321b836b4a7573f6e2e97c7 Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Sat, 25 Mar 2023 11:30:42 +0000 Subject: [PATCH 019/125] fix sorting by created timestamp --- api/src/controllers/torrent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/controllers/torrent.js b/api/src/controllers/torrent.js index f3026c2..e639368 100644 --- a/api/src/controllers/torrent.js +++ b/api/src/controllers/torrent.js @@ -463,7 +463,7 @@ export const getTorrentsPage = async ({ const combinedSort = {}; if (sortField) combinedSort[sortField] = sortDir; if (query) combinedSort.confidenceScore = { $meta: "textScore" }; - combinedSort.created = -1; + if (!combinedSort.created) combinedSort.created = -1; const torrents = await Torrent.aggregate([ ...(query From fc684de18a0fdeb5f41654ec9febee522e9bba99 Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Sat, 25 Mar 2023 11:54:52 +0000 Subject: [PATCH 020/125] check for rate limit on client error page --- api/src/index.js | 7 +++- client/components/Navigation.js | 17 ++++---- client/pages/_error.js | 74 ++++++++++++++++++++++----------- 3 files changed, 64 insertions(+), 34 deletions(-) diff --git a/api/src/index.js b/api/src/index.js index 6c77288..4c27291 100644 --- a/api/src/index.js +++ b/api/src/index.js @@ -111,6 +111,8 @@ validateConfig(config).then(() => { }) ); + app.use(cors()); + // rate limit all API routes. if the request comes from Next SSR rather than // the client browser, we need to make use of the forwarded IP rather than // the origin of the request, as this will be the same for all users. to @@ -127,7 +129,9 @@ validateConfig(config).then(() => { return req.headers["x-forwarded-for"].split(",")[0]; return req.ip; }, - skip: () => process.env.NODE_ENV !== "production", + skip: (req) => { + return process.env.NODE_ENV !== "production" || req.method === "OPTIONS"; + }, }); app.use(limiter); @@ -142,7 +146,6 @@ validateConfig(config).then(() => { app.use(bodyParser.json({ limit: "5mb" })); app.use(cookieParser()); - app.use(cors()); app.get("/", (req, res) => { res.setHeader("Content-Type", "text/plain"); diff --git a/client/components/Navigation.js b/client/components/Navigation.js index 94acd87..040c313 100644 --- a/client/components/Navigation.js +++ b/client/components/Navigation.js @@ -73,19 +73,20 @@ const Navigation = ({ isMobile, menuIsOpen, setMenuIsOpen }) => { SQ_API_URL, SQ_ALLOW_REGISTER, SQ_VERSION, - SQ_TORRENT_CATEGORIES, }, } = getConfig(); useEffect(() => { const getUserRole = async () => { - const roleRes = await fetch(`${SQ_API_URL}/account/get-role`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - const role = await roleRes.text(); - setRole(role); + try { + const roleRes = await fetch(`${SQ_API_URL}/account/get-role`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + const role = await roleRes.text(); + setRole(role); + } catch (e) {} }; if (token) getUserRole(); setIsServer(false); diff --git a/client/pages/_error.js b/client/pages/_error.js index f52eb2e..457d147 100644 --- a/client/pages/_error.js +++ b/client/pages/_error.js @@ -1,33 +1,59 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import Link from "next/link"; +import getConfig from "next/config"; import NextErrorComponent from "next/error"; import * as Sentry from "@sentry/nextjs"; import SEO from "../components/SEO"; import Text from "../components/Text"; -const ErrorPage = () => ( - <> - - - Something went wrong :( - - - If the error persists, please{" "} - - report it - - . For now,{" "} - - return home - - . - - -); +const ErrorPage = () => { + const [rateLimited, setRateLimited] = useState(false); + + const { + publicRuntimeConfig: { SQ_API_URL }, + } = getConfig(); + + useEffect(() => { + const checkRateLimit = async () => { + try { + const res = await fetch(SQ_API_URL); + if (res.status === 429) setRateLimited(true); + } catch (e) {} + }; + checkRateLimit(); + }, []); + + return ( + <> + + + Something went wrong :( + + {rateLimited ? ( + + Too many requests! You have been rate limited. Please wait a while + before trying again. + + ) : ( + + If the error persists, please{" "} + + report it + + . For now,{" "} + + return home + + . + + )} + + ); +}; ErrorPage.getInitialProps = async (contextData) => { await Sentry.captureUnderscoreErrorException(contextData); From 2e938d9eb09a21a311d32ef87846c6f3c8adf601 Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Sat, 25 Mar 2023 11:55:33 +0000 Subject: [PATCH 021/125] ver 1.2.1 --- api/package.json | 2 +- client/package.json | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/package.json b/api/package.json index 74b7adf..b91777c 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "@sqtracker/api", - "version": "1.1.0", + "version": "1.2.1", "private": true, "license": "GPL-3.0-only", "scripts": { diff --git a/client/package.json b/client/package.json index 5a3ae75..eee5681 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@sqtracker/client", - "version": "1.1.0", + "version": "1.2.1", "private": true, "license": "GPL-3.0-only", "scripts": { diff --git a/package.json b/package.json index 0619da0..bd2aed7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sqtracker", - "version": "1.1.0", + "version": "1.2.1", "private": true, "license": "GPL-3.0-only", "workspaces": [ From 00d6eafee10a17a5397c970d28b70c01b87cae37 Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Sat, 25 Mar 2023 12:50:37 +0000 Subject: [PATCH 022/125] adds wiki pages list, fix creation of root wiki page --- api/src/controllers/wiki.js | 6 ++- client/pages/wiki/[[...slug]].js | 81 ++++++++++++++++++++++++-------- client/pages/wiki/new.js | 2 +- 3 files changed, 66 insertions(+), 23 deletions(-) diff --git a/api/src/controllers/wiki.js b/api/src/controllers/wiki.js index 491190b..de30989 100644 --- a/api/src/controllers/wiki.js +++ b/api/src/controllers/wiki.js @@ -5,7 +5,7 @@ const slugRegex = /^\/([a-z0-9-_\/])*/i; const formatSlug = (slug) => { if (!slug.startsWith("/")) slug = `/${slug}`; - if (slug.endsWith("/")) slug = slug.slice(0, -1); + if (slug.endsWith("/") && slug !== "/") slug = slug.slice(0, -1); const split = slug.split("/"); const slugified = split.map((token) => slugify(token, { lower: true })); return slugified.join("/"); @@ -98,7 +98,9 @@ export const getWiki = async (req, res, next) => { return; } - res.json(page); + const allPages = await Wiki.find({}, { slug: 1, title: 1 }).lean(); + + res.json({ page, allPages }); } catch (e) { next(e); } diff --git a/client/pages/wiki/[[...slug]].js b/client/pages/wiki/[[...slug]].js index 82e1929..f993199 100644 --- a/client/pages/wiki/[[...slug]].js +++ b/client/pages/wiki/[[...slug]].js @@ -17,7 +17,13 @@ import LoadingContext from "../../utils/LoadingContext"; import Modal from "../../components/Modal"; import { WikiFields } from "./new"; -const Wiki = ({ page, token, userRole, slug }) => { +const sortSlug = (a, b) => { + if (a.slug > b.slug) return 1; + if (a.slug < b.slug) return -1; + return 0; +}; + +const Wiki = ({ page, allPages, token, userRole, slug }) => { const [editing, setEditing] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); @@ -161,24 +167,57 @@ const Wiki = ({ page, token, userRole, slug }) => { {!editing ? ( - - - ) : ( - - - - ); - }, - }} + + - {page.body} - - + + Pages + + {allPages.sort(sortSlug).map((p) => ( + + + {p.title} + + + ))} + + + + ) : ( + + + + ); + }, + }} + > + {page.body} + + +
) : (
@@ -246,8 +285,10 @@ export const getServerSideProps = withAuthServerSideProps( ) { throw "banned"; } - const page = await wikiRes.json(); - return { props: { page, token, userRole: role, slug: parsedSlug } }; + const { page, allPages } = await wikiRes.json(); + return { + props: { page, allPages, token, userRole: role, slug: parsedSlug }, + }; } catch (e) { if (e === "banned") throw "banned"; return { props: { token, userRole: role, slug: parsedSlug } }; diff --git a/client/pages/wiki/new.js b/client/pages/wiki/new.js index 6e69e98..d015b2a 100644 --- a/client/pages/wiki/new.js +++ b/client/pages/wiki/new.js @@ -29,7 +29,7 @@ export const WikiFields = ({ values }) => { onBlur={(e) => { let { value } = e.target; if (!value.startsWith("/")) value = `/${value}`; - if (value.endsWith("/")) value = value.slice(0, -1); + if (value.endsWith("/") && value !== "/") value = value.slice(0, -1); const split = value.split("/"); const slugified = split.map((token) => slugify(token, { lower: true }) From 55606bb086608cbaab82ba18972dcd101bccb805 Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Sat, 25 Mar 2023 13:24:13 +0000 Subject: [PATCH 023/125] torrent file list alphabetical sort --- client/pages/torrent/[infoHash].js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/client/pages/torrent/[infoHash].js b/client/pages/torrent/[infoHash].js index ee897da..b5259db 100644 --- a/client/pages/torrent/[infoHash].js +++ b/client/pages/torrent/[infoHash].js @@ -80,6 +80,12 @@ export const Info = ({ title, items }) => ( const WrapExpandable = ({ wrap, children }) => wrap ?
{children}
: children; +const sortName = (a, b) => { + if (a.name > b.name) return 1; + if (a.name < b.name) return -1; + return 0; +}; + const FileItem = ({ file, depth = 0 }) => { return ( @@ -112,7 +118,7 @@ const FileItem = ({ file, depth = 0 }) => { {!!file.children.length && ( - {file.children.map((child) => ( + {file.children.sort(sortName).map((child) => ( { Files - {parsedFiles.map((file, i) => ( + {parsedFiles.sort(sortName).map((file, i) => ( ))} From 73dda4cdca8b7fa4379e0e6864914040710c0e4e Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Tue, 28 Mar 2023 18:50:02 +0100 Subject: [PATCH 024/125] SQ_ALLOW_UNREGISTERED_VIEW allow public access to category/tag pages, and wiki pages if opted in --- api/src/controllers/wiki.js | 16 +++++++- api/src/schema/wiki.js | 1 + client/components/Navigation.js | 17 ++++++++ client/pages/categories/[category].js | 6 ++- client/pages/categories/index.js | 7 ++-- client/pages/tags/[tag].js | 12 ++++-- client/pages/wiki/[[...slug]].js | 58 +++++++++++++-------------- client/pages/wiki/new.js | 14 ++++++- config.example.js | 1 + 9 files changed, 92 insertions(+), 40 deletions(-) diff --git a/api/src/controllers/wiki.js b/api/src/controllers/wiki.js index de30989..445f24a 100644 --- a/api/src/controllers/wiki.js +++ b/api/src/controllers/wiki.js @@ -47,6 +47,7 @@ export const createWiki = async (req, res, next) => { title: req.body.title, body: req.body.body, createdBy: req.userId, + public: !!req.body.public, created: Date.now(), }); @@ -64,7 +65,7 @@ export const getWiki = async (req, res, next) => { try { const slug = req.params[0]; - const [page] = await Wiki.aggregate([ + let [page] = await Wiki.aggregate([ { $match: { slug }, }, @@ -98,7 +99,17 @@ export const getWiki = async (req, res, next) => { return; } - const allPages = await Wiki.find({}, { slug: 1, title: 1 }).lean(); + if (process.env.SQ_ALLOW_UNREGISTERED_VIEW && !req.userId && !page.public) { + page = null; + } + + const query = {}; + + if (process.env.SQ_ALLOW_UNREGISTERED_VIEW && !req.userId) { + query.public = true; + } + + const allPages = await Wiki.find(query, { slug: 1, title: 1 }).lean(); res.json({ page, allPages }); } catch (e) { @@ -174,6 +185,7 @@ export const updateWiki = async (req, res, next) => { slug, title: req.body.title, body: req.body.body, + public: !!req.body.public, updated: Date.now(), }, } diff --git a/api/src/schema/wiki.js b/api/src/schema/wiki.js index ac8efcf..79662c3 100644 --- a/api/src/schema/wiki.js +++ b/api/src/schema/wiki.js @@ -5,6 +5,7 @@ const Wiki = new mongoose.Schema({ slug: String, body: String, createdBy: mongoose.Schema.ObjectId, + public: Boolean, created: Number, updated: Number, }); diff --git a/client/components/Navigation.js b/client/components/Navigation.js index 040c313..4c4feff 100644 --- a/client/components/Navigation.js +++ b/client/components/Navigation.js @@ -73,6 +73,7 @@ const Navigation = ({ isMobile, menuIsOpen, setMenuIsOpen }) => { SQ_API_URL, SQ_ALLOW_REGISTER, SQ_VERSION, + SQ_ALLOW_UNREGISTERED_VIEW, }, } = getConfig(); @@ -248,6 +249,22 @@ const Navigation = ({ isMobile, menuIsOpen, setMenuIsOpen }) => { )} + {SQ_ALLOW_UNREGISTERED_VIEW && ( + <> + + + Browse + + + + + + Wiki + + + + + )} )} diff --git a/client/pages/categories/[category].js b/client/pages/categories/[category].js index 6bfad32..6a86be0 100644 --- a/client/pages/categories/[category].js +++ b/client/pages/categories/[category].js @@ -50,9 +50,10 @@ export const getServerSideProps = withAuthServerSideProps( async ({ token, fetchHeaders, + isPublicAccess, query: { category, source, page: pageParam }, }) => { - if (!token) return { props: {} }; + if (!token && !isPublicAccess) return { props: {} }; const { publicRuntimeConfig: { SQ_API_URL }, @@ -84,7 +85,8 @@ export const getServerSideProps = withAuthServerSideProps( if (e === "banned") throw "banned"; return { props: {} }; } - } + }, + true ); export default Category; diff --git a/client/pages/categories/index.js b/client/pages/categories/index.js index c21a75f..6ca5696 100644 --- a/client/pages/categories/index.js +++ b/client/pages/categories/index.js @@ -97,8 +97,8 @@ const Categories = ({ tags }) => { }; export const getServerSideProps = withAuthServerSideProps( - async ({ token, fetchHeaders }) => { - if (!token) return { props: {} }; + async ({ token, fetchHeaders, isPublicAccess }) => { + if (!token && !isPublicAccess) return { props: {} }; const { publicRuntimeConfig: { SQ_API_URL }, @@ -120,7 +120,8 @@ export const getServerSideProps = withAuthServerSideProps( if (e === "banned") throw "banned"; return { props: {} }; } - } + }, + true ); export default Categories; diff --git a/client/pages/tags/[tag].js b/client/pages/tags/[tag].js index 15554b8..73d2d89 100644 --- a/client/pages/tags/[tag].js +++ b/client/pages/tags/[tag].js @@ -42,8 +42,13 @@ const Tag = ({ results, token }) => { }; export const getServerSideProps = withAuthServerSideProps( - async ({ token, fetchHeaders, query: { tag, page: pageParam } }) => { - if (!token) return { props: {} }; + async ({ + token, + fetchHeaders, + isPublicAccess, + query: { tag, page: pageParam }, + }) => { + if (!token && !isPublicAccess) return { props: {} }; const { publicRuntimeConfig: { SQ_API_URL }, @@ -74,7 +79,8 @@ export const getServerSideProps = withAuthServerSideProps( if (e === "banned") throw "banned"; return { props: {} }; } - } + }, + true ); export default Tag; diff --git a/client/pages/wiki/[[...slug]].js b/client/pages/wiki/[[...slug]].js index f993199..a498481 100644 --- a/client/pages/wiki/[[...slug]].js +++ b/client/pages/wiki/[[...slug]].js @@ -87,6 +87,7 @@ const Wiki = ({ page, allPages, token, userRole, slug }) => { slug: page.slug === "/" ? "/" : form.get("slug"), title: form.get("title"), body: form.get("body"), + public: !!form.get("public"), }), } ); @@ -169,15 +170,35 @@ const Wiki = ({ page, allPages, token, userRole, slug }) => { {!editing ? ( + + + ) : ( + +
+ + ); + }, + }} + > + {page.body} + + { Pages {allPages.sort(sortSlug).map((p) => ( - + {p.title} ))} - - - ) : ( - - - - ); - }, - }} - > - {page.body} - - ) : ( @@ -263,8 +262,8 @@ const Wiki = ({ page, allPages, token, userRole, slug }) => { }; export const getServerSideProps = withAuthServerSideProps( - async ({ token, fetchHeaders, query: { slug } }) => { - if (!token) return { props: {} }; + async ({ token, fetchHeaders, isPublicAccess, query: { slug } }) => { + if (!token && !isPublicAccess) return { props: {} }; const parsedSlug = slug?.length ? slug.join("/") : ""; @@ -273,7 +272,7 @@ export const getServerSideProps = withAuthServerSideProps( serverRuntimeConfig: { SQ_JWT_SECRET }, } = getConfig(); - const { role } = jwt.verify(token, SQ_JWT_SECRET); + const { role } = token ? jwt.verify(token, SQ_JWT_SECRET) : { role: null }; try { const wikiRes = await fetch(`${SQ_API_URL}/wiki/${parsedSlug}`, { @@ -293,7 +292,8 @@ export const getServerSideProps = withAuthServerSideProps( if (e === "banned") throw "banned"; return { props: { token, userRole: role, slug: parsedSlug } }; } - } + }, + true ); export default Wiki; diff --git a/client/pages/wiki/new.js b/client/pages/wiki/new.js index d015b2a..18a53b1 100644 --- a/client/pages/wiki/new.js +++ b/client/pages/wiki/new.js @@ -11,14 +11,17 @@ import MarkdownInput from "../../components/MarkdownInput"; import { withAuthServerSideProps } from "../../utils/withAuth"; import { NotificationContext } from "../../components/Notifications"; import LoadingContext from "../../utils/LoadingContext"; +import Checkbox from "../../components/Checkbox"; export const WikiFields = ({ values }) => { const [slugValue, setSlugValue] = useState(values?.slug); const { - publicRuntimeConfig: { SQ_BASE_URL }, + publicRuntimeConfig: { SQ_BASE_URL, SQ_ALLOW_UNREGISTERED_VIEW }, } = getConfig(); + console.log(values); + return ( <> { mb={4} required /> + {SQ_ALLOW_UNREGISTERED_VIEW && ( + + )} ); }; @@ -93,6 +104,7 @@ const NewWiki = ({ token, userRole }) => { slug: form.get("slug"), title: form.get("title"), body: form.get("body"), + public: !!form.get("public"), }), }); diff --git a/config.example.js b/config.example.js index d280459..c06e57c 100644 --- a/config.example.js +++ b/config.example.js @@ -62,6 +62,7 @@ module.exports = { // Whether torrent pages can be viewed by unregistered users. // If true, only logged-in users will be able to download/interact, but anyone (search engines included) will be able to view/read torrent info. + // Non-logged-in users will also be able to browse category/tag pages and wiki pages that have been set to public. // Enable if you want torrents to be indexed to help search traffic. SQ_ALLOW_UNREGISTERED_VIEW: false, From 7c806e009136b8452424a2aa862f2df9b7fc0906 Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Mon, 3 Apr 2023 18:47:55 +0100 Subject: [PATCH 025/125] track hit n runs, set max allowed value in config before preventing download --- README.md | 3 +- api/src/controllers/user.js | 8 ++- api/src/tracker/announce.js | 22 +++++- api/src/utils/hitnrun.js | 8 +++ api/src/utils/validateConfig.js | 3 +- client/pages/_app.js | 51 ++++++++++---- client/pages/torrent/[infoHash].js | 31 +++++++-- client/pages/user/[username].js | 106 ++++++++++++++++++++--------- config.example.js | 6 +- 9 files changed, 181 insertions(+), 57 deletions(-) create mode 100644 api/src/utils/hitnrun.js diff --git a/README.md b/README.md index 64aba4b..549d05f 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,8 @@ Please join the [Discord server](https://discord.gg/BEGXEk29Up) for support and * Upload / download tracking * Track how much content each user has uploaded / downloaded * Track ratios - * Limit up / downloading per user based on ratio + * Track hit'n'runs + * Limit downloading per user based on ratio, HnRs, or both * Award bonus points based on upload * User interaction * Commenting on torrents and announcements diff --git a/api/src/controllers/user.js b/api/src/controllers/user.js index 48c94c2..ee3914b 100644 --- a/api/src/controllers/user.js +++ b/api/src/controllers/user.js @@ -8,8 +8,8 @@ import Invite from "../schema/invite"; import Progress from "../schema/progress"; import { getTorrentsPage } from "./torrent"; import { getUserRatio } from "../utils/ratio"; +import { getUserHitNRuns } from "../utils/hitnrun"; import { BYTES_GB } from "../tracker/announce"; -import Torrent from "../schema/torrent"; export const sendVerificationEmail = async (mail, address, token) => { await mail.sendMail({ @@ -684,6 +684,8 @@ export const fetchUser = (tracker) => async (req, res, next) => { const { ratio } = await getUserRatio(user._id); user.ratio = ratio; + user.hitnruns = await getUserHitNRuns(user._id); + const { torrents } = await getTorrentsPage({ uploadedBy: user._id, userId: req.userId, @@ -707,7 +709,9 @@ export const getUserStats = async (req, res, next) => { } const ratioStats = await getUserRatio(user._id); - res.json({ ...ratioStats, bp: user.bonusPoints }); + const hitnruns = await getUserHitNRuns(user._id); + + res.json({ ...ratioStats, bp: user.bonusPoints, hitnruns }); } catch (e) { next(e); } diff --git a/api/src/tracker/announce.js b/api/src/tracker/announce.js index f5befe5..55a1cce 100644 --- a/api/src/tracker/announce.js +++ b/api/src/tracker/announce.js @@ -4,6 +4,7 @@ import User from "../schema/user"; import Torrent from "../schema/torrent"; import Progress from "../schema/progress"; import { getUserRatio } from "../utils/ratio"; +import { getUserHitNRuns } from "../utils/hitnrun"; export const BYTES_GB = 1e9; @@ -11,7 +12,6 @@ export const binaryToHex = (b) => Buffer.from(b, "binary").toString("hex"); export const hexToBinary = (h) => Buffer.from(h, "hex").toString("binary"); const handleAnnounce = async (req, res) => { - //console.log(req) const userId = req.originalUrl.split("/")[2]; req.userId = userId; @@ -58,11 +58,14 @@ const handleAnnounce = async (req, res) => { } const { ratio } = await getUserRatio(user._id); + const hitnruns = await getUserHitNRuns(user._id); console.log(`[DEBUG] user ratio: ${ratio}`); + console.log(`[DEBUG] user hit'n'runs: ${hitnruns}`); - // if users ratio is below the minimum threshold and they are trying to download, deny announce + // if users ratio is below the minimum threshold, and they are trying to download, deny announce if ( + Number(process.env.SQ_MINIMUM_RATIO) !== -1 && ratio < Number(process.env.SQ_MINIMUM_RATIO) && ratio !== -1 && Number(params.left > 0) @@ -76,6 +79,21 @@ const handleAnnounce = async (req, res) => { return; } + // if user has committed more than the allowed number of hit'n'runs, and they are trying to download, deny announce + if ( + Number(process.env.SQ_MAXIMUM_HIT_N_RUNS) !== -1 && + hitnruns >= Number(process.env.SQ_MAXIMUM_HIT_N_RUNS) && + Number(params.left > 0) + ) { + const response = bencode.encode({ + "failure reason": `Announce denied: You have committed ${process.env.SQ_MAXIMUM_HIT_N_RUNS} or more hit'n'runs.`, + peers: [], + peers6: [], + }); + res.send(response); + return; + } + const uploaded = Number(params.uploaded); const downloaded = params.event === "started" ? 0 : Number(params.downloaded); diff --git a/api/src/utils/hitnrun.js b/api/src/utils/hitnrun.js new file mode 100644 index 0000000..6cc114a --- /dev/null +++ b/api/src/utils/hitnrun.js @@ -0,0 +1,8 @@ +import Progress from "../schema/progress"; + +export const getUserHitNRuns = async (_id) => { + const progressRecords = + (await Progress.find({ userId: _id, left: 0 }).lean()) ?? []; + return progressRecords.filter((p) => p.uploaded.total < p.downloaded.total) + .length; +}; diff --git a/api/src/utils/validateConfig.js b/api/src/utils/validateConfig.js index 3fe629e..c5dff40 100644 --- a/api/src/utils/validateConfig.js +++ b/api/src/utils/validateConfig.js @@ -15,7 +15,8 @@ const configSchema = yup .oneOf(["open", "invite", "closed"]) .required(), SQ_ALLOW_ANONYMOUS_UPLOADS: yup.boolean().required(), - SQ_MINIMUM_RATIO: yup.number().min(0).required(), + SQ_MINIMUM_RATIO: yup.number().min(-1).required(), + SQ_MAXIMUM_HIT_N_RUNS: yup.number().integer().min(-1).required(), SQ_BP_EARNED_PER_GB: yup.number().min(0).required(), SQ_BP_EARNED_PER_FILLED_REQUEST: yup.number().min(0).required(), SQ_BP_COST_PER_INVITE: yup.number().min(0).required(), diff --git a/client/pages/_app.js b/client/pages/_app.js index 0f4d8a8..c14af9a 100644 --- a/client/pages/_app.js +++ b/client/pages/_app.js @@ -18,6 +18,7 @@ import { LoaderAlt } from "@styled-icons/boxicons-regular/LoaderAlt"; import { Sort } from "@styled-icons/boxicons-regular/Sort"; import { CaretUp } from "@styled-icons/boxicons-regular/CaretUp"; import { CaretDown } from "@styled-icons/boxicons-regular/CaretDown"; +import { Run } from "@styled-icons/boxicons-regular/Run"; import { Award } from "@styled-icons/boxicons-regular/Award"; import Navigation from "../components/Navigation"; import Box from "../components/Box"; @@ -173,6 +174,7 @@ const SqTracker = ({ Component, pageProps, initialTheme }) => { SQ_SITE_WIDE_FREELEECH, SQ_API_URL, SQ_MINIMUM_RATIO, + SQ_MAXIMUM_HIT_N_RUNS, }, } = getConfig(); @@ -324,20 +326,24 @@ const SqTracker = ({ Component, pageProps, initialTheme }) => { alignItems="center" color="grey" > - - - {userStats.ratio === -1 ? "N/A" : userStats.ratio} - + {Number(SQ_MINIMUM_RATIO) !== -1 && ( + <> + + + {userStats.ratio === -1 ? "N/A" : userStats.ratio} + + + )} {prettyBytes(userStats.up ?? 0)} @@ -346,6 +352,23 @@ const SqTracker = ({ Component, pageProps, initialTheme }) => { {prettyBytes(userStats.down ?? 0)} + {Number(SQ_MAXIMUM_HIT_N_RUNS) !== -1 && ( + <> + + SQ_MAXIMUM_HIT_N_RUNS + ? "error" + : "grey" + } + fontSize={0} + ml={1} + mr={2} + > + {userStats.hitnruns ?? 0} + + + )} {userStats.bp ?? 0} BP diff --git a/client/pages/torrent/[infoHash].js b/client/pages/torrent/[infoHash].js index b5259db..f2bc8fc 100644 --- a/client/pages/torrent/[infoHash].js +++ b/client/pages/torrent/[infoHash].js @@ -132,7 +132,7 @@ const FileItem = ({ file, depth = 0 }) => { ); }; -const Torrent = ({ token, torrent = {}, userId, userRole, uid }) => { +const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { const [showReportModal, setShowReportModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false); @@ -160,6 +160,8 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid }) => { SQ_API_URL, SQ_TORRENT_CATEGORIES, SQ_SITE_WIDE_FREELEECH, + SQ_MINIMUM_RATIO, + SQ_MAXIMUM_HIT_N_RUNS, }, } = getConfig(); @@ -481,6 +483,12 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid }) => { .map(({ path, size }) => ({ path: path.split("/"), size })) .reduce((children, { path, size }) => insert(children, path, size), []); + const downloadDisabled = + (Number(SQ_MINIMUM_RATIO) !== -1 && + userStats.ratio < Number(SQ_MINIMUM_RATIO)) || + (Number(SQ_MAXIMUM_HIT_N_RUNS) !== -1 && + userStats.hitnruns > Number(SQ_MAXIMUM_HIT_N_RUNS)); + return ( <> @@ -537,11 +545,16 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid }) => { {isFreeleech ? "Unset" : "Set"} freeleech )} - {!!userId ? ( + {userId ? ( @@ -893,7 +906,17 @@ export const getServerSideProps = withAuthServerSideProps( if (torrentRes.status === 404) return { notFound: true }; const torrent = await torrentRes.json(); - return { props: { torrent, userId: id, userRole: role, uid: userId } }; + + const userStatsRes = await fetch(`${SQ_API_URL}/account/get-stats`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + const userStats = await userStatsRes.json(); + + return { + props: { torrent, userId: id, userRole: role, uid: userId, userStats }, + }; } catch (e) { if (e === "banned") throw "banned"; return { props: {} }; diff --git a/client/pages/user/[username].js b/client/pages/user/[username].js index ebff7d9..7c322a1 100644 --- a/client/pages/user/[username].js +++ b/client/pages/user/[username].js @@ -1,11 +1,12 @@ -import React, { useState, useContext } from "react"; +import React, { useState, useContext, useMemo } from "react"; import getConfig from "next/config"; import Link from "next/link"; import { useCookies } from "react-cookie"; import moment from "moment"; import prettyBytes from "pretty-bytes"; import jwt from "jsonwebtoken"; -import { BarChartSquare } from "@styled-icons/boxicons-regular/BarChartSquare"; +import { Sort } from "@styled-icons/boxicons-regular/Sort"; +import { Run } from "@styled-icons/boxicons-regular/Run"; import { Upload } from "@styled-icons/boxicons-regular/Upload"; import { Download } from "@styled-icons/boxicons-regular/Download"; import { UserCircle } from "@styled-icons/boxicons-regular/UserCircle"; @@ -35,6 +36,7 @@ const User = ({ token, user, userRole }) => { publicRuntimeConfig: { SQ_TORRENT_CATEGORIES, SQ_MINIMUM_RATIO, + SQ_MAXIMUM_HIT_N_RUNS, SQ_API_URL, }, } = getConfig(); @@ -79,6 +81,13 @@ const User = ({ token, user, userRole }) => { setLoading(false); }; + const cards = useMemo(() => { + let c = 2; + if (SQ_MINIMUM_RATIO !== -1) c++; + if (SQ_MAXIMUM_HIT_N_RUNS !== -1) c++; + return c; + }, [SQ_MINIMUM_RATIO, SQ_MAXIMUM_HIT_N_RUNS]); + return ( <> @@ -151,39 +160,72 @@ const User = ({ token, user, userRole }) => { )} - - - Ratio - - - {typeof user.ratio === "number" - ? user.ratio === -1 - ? "N/A" - : user.ratio.toFixed(2) - : "?"} - {typeof user.ratio === "number" && user.ratio !== -1 && ( - = SQ_MINIMUM_RATIO ? "success" : "error"} - > - {" "} - {user.ratio >= SQ_MINIMUM_RATIO ? ">" : "<"} {SQ_MINIMUM_RATIO} - - )} - - + {SQ_MINIMUM_RATIO !== -1 && ( + + + Ratio + + + {typeof user.ratio === "number" + ? user.ratio === -1 + ? "N/A" + : user.ratio.toFixed(2) + : "?"} + {typeof user.ratio === "number" && user.ratio !== -1 && ( + = SQ_MINIMUM_RATIO ? "success" : "error"} + > + {" "} + {user.ratio >= SQ_MINIMUM_RATIO ? ">" : "<"}{" "} + {SQ_MINIMUM_RATIO} + + )} + + + )} + {SQ_MAXIMUM_HIT_N_RUNS !== -1 && ( + + + Hit'n'runs + + + {typeof user.hitnruns === "number" ? user.hitnruns : "?"} + {typeof user.hitnruns === "number" && ( + + {" "} + {user.hitnruns <= SQ_MAXIMUM_HIT_N_RUNS ? "<" : ">"}{" "} + {SQ_MAXIMUM_HIT_N_RUNS} + + )} + + + )} Date: Tue, 4 Apr 2023 18:05:20 +0100 Subject: [PATCH 026/125] adds file extension blacklist logic --- api/src/controllers/torrent.js | 13 +++++++++++++ api/src/utils/validateConfig.js | 1 + client/pages/upload.js | 23 ++++++++++++++++++++++- config.example.js | 3 +++ 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/api/src/controllers/torrent.js b/api/src/controllers/torrent.js index e639368..4118230 100644 --- a/api/src/controllers/torrent.js +++ b/api/src/controllers/torrent.js @@ -97,6 +97,19 @@ export const uploadTorrent = async (req, res, next) => { ]; } + const hasBlackListedFiles = files.some((file) => + (process.env.SQ_EXTENSION_BLACKLIST ?? []).some((ext) => + file.path.endsWith(`.${ext}`) + ) + ); + + if (hasBlackListedFiles) { + res + .status(403) + .send("One or more files have blacklisted file extensions"); + return; + } + let groupId; if (req.body.groupWith) { diff --git a/api/src/utils/validateConfig.js b/api/src/utils/validateConfig.js index c5dff40..096a15a 100644 --- a/api/src/utils/validateConfig.js +++ b/api/src/utils/validateConfig.js @@ -49,6 +49,7 @@ const configSchema = yup text: yup.string().matches(hexRegex), grey: yup.string().matches(hexRegex), }), + SQ_EXTENSION_BLACKLIST: yup.array().of(yup.string()).min(0), SQ_BASE_URL: yup.string().matches(httpRegex).required(), SQ_API_URL: yup.string().matches(httpRegex).required(), SQ_MONGO_URL: yup.string().matches(mongoRegex).required(), diff --git a/client/pages/upload.js b/client/pages/upload.js index 67f196a..fc34abe 100644 --- a/client/pages/upload.js +++ b/client/pages/upload.js @@ -33,7 +33,7 @@ const FileUpload = styled(Box)(() => borderColor: "border", borderRadius: 2, cursor: "pointer", - p: 5, + p: 6, "&:hover": { bg: "sidebar", }, @@ -191,6 +191,7 @@ const Upload = ({ token, userId }) => { SQ_API_URL, SQ_TORRENT_CATEGORIES, SQ_ALLOW_ANONYMOUS_UPLOAD, + SQ_EXTENSION_BLACKLIST, }, } = getConfig(); @@ -322,6 +323,26 @@ const Upload = ({ token, userId }) => { + {!!SQ_EXTENSION_BLACKLIST.length && ( + + + The following file extensions are blacklisted. Any torrent + containing files of these types will not be uploaded. + + + {SQ_EXTENSION_BLACKLIST.join(", ")} + + + )} diff --git a/config.example.js b/config.example.js index 0c8240e..a4344f2 100644 --- a/config.example.js +++ b/config.example.js @@ -70,6 +70,9 @@ module.exports = { // Enable if you want torrents to be indexed to help search traffic. SQ_ALLOW_UNREGISTERED_VIEW: false, + // An array of blacklisted file extensions. Torrents containing files with these extensions will fail to upload. + SQ_EXTENSION_BLACKLIST: ["exe"], + // The URL of your tracker site. SQ_BASE_URL: "https://sqtracker.dev", From d2764e2ef85cf0952fa184f6a844d98a0df95308 Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Tue, 4 Apr 2023 20:48:00 +0100 Subject: [PATCH 027/125] bug fixes: correct infohash on upload when torrent manipulated, handle undefined ext blacklist, fix disabled download with no ratio --- api/src/controllers/torrent.js | 8 ++++---- client/pages/torrent/[infoHash].js | 1 + client/pages/upload.js | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/api/src/controllers/torrent.js b/api/src/controllers/torrent.js index 4118230..b4ee336 100644 --- a/api/src/controllers/torrent.js +++ b/api/src/controllers/torrent.js @@ -66,6 +66,10 @@ export const uploadTorrent = async (req, res, next) => { const user = await User.findOne({ _id: req.userId }).lean(); + parsed.info.private = 1; + parsed.announce = `${process.env.SQ_BASE_URL}/sq/${user.uid}/announce`; + delete parsed["announce-list"]; + const infoHash = crypto .createHash("sha1") .update(bencode.encode(parsed.info)) @@ -78,10 +82,6 @@ export const uploadTorrent = async (req, res, next) => { return; } - parsed.info.private = 1; - parsed.announce = `${process.env.SQ_BASE_URL}/sq/${user.uid}/announce`; - delete parsed["announce-list"]; - let files; if (parsed.info.files) { files = parsed.info.files.map((file) => ({ diff --git a/client/pages/torrent/[infoHash].js b/client/pages/torrent/[infoHash].js index f2bc8fc..b64b24c 100644 --- a/client/pages/torrent/[infoHash].js +++ b/client/pages/torrent/[infoHash].js @@ -485,6 +485,7 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { const downloadDisabled = (Number(SQ_MINIMUM_RATIO) !== -1 && + userStats.ratio !== -1 && userStats.ratio < Number(SQ_MINIMUM_RATIO)) || (Number(SQ_MAXIMUM_HIT_N_RUNS) !== -1 && userStats.hitnruns > Number(SQ_MAXIMUM_HIT_N_RUNS)); diff --git a/client/pages/upload.js b/client/pages/upload.js index fc34abe..87ff254 100644 --- a/client/pages/upload.js +++ b/client/pages/upload.js @@ -191,7 +191,7 @@ const Upload = ({ token, userId }) => { SQ_API_URL, SQ_TORRENT_CATEGORIES, SQ_ALLOW_ANONYMOUS_UPLOAD, - SQ_EXTENSION_BLACKLIST, + SQ_EXTENSION_BLACKLIST = [], }, } = getConfig(); From 35cf824b704a671c937627726a4dc7df7ee53ab9 Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Tue, 4 Apr 2023 20:54:56 +0100 Subject: [PATCH 028/125] ver 1.3.0 --- api/package.json | 2 +- client/package.json | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/package.json b/api/package.json index b91777c..c7366fc 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "@sqtracker/api", - "version": "1.2.1", + "version": "1.3.0", "private": true, "license": "GPL-3.0-only", "scripts": { diff --git a/client/package.json b/client/package.json index eee5681..8107699 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@sqtracker/client", - "version": "1.2.1", + "version": "1.3.0", "private": true, "license": "GPL-3.0-only", "scripts": { diff --git a/package.json b/package.json index bd2aed7..01dd13f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sqtracker", - "version": "1.2.1", + "version": "1.3.0", "private": true, "license": "GPL-3.0-only", "workspaces": [ From b1e55c31acf7928fd1aff353b2103cb1a727dbfc Mon Sep 17 00:00:00 2001 From: doomtop Date: Wed, 17 May 2023 20:58:34 -0400 Subject: [PATCH 029/125] Fixes #30 Delete multiple trackers `announce-list` from downloads --- api/src/controllers/torrent.js | 1 + 1 file changed, 1 insertion(+) diff --git a/api/src/controllers/torrent.js b/api/src/controllers/torrent.js index b4ee336..a636df1 100644 --- a/api/src/controllers/torrent.js +++ b/api/src/controllers/torrent.js @@ -251,6 +251,7 @@ export const downloadTorrent = async (req, res, next) => { const parsed = bencode.decode(Buffer.from(binary, "base64")); parsed.announce = `${process.env.SQ_BASE_URL}/sq/${user.uid}/announce`; + delete parsed["announce-list"]; parsed.info.private = 1; res.setHeader("Content-Type", "application/x-bittorrent"); From 673181b9029b33e8aabcd2b23e086cca1153898e Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Thu, 29 Jun 2023 10:39:35 +0100 Subject: [PATCH 030/125] fix empty search results bug --- client/pages/search/[[...query]].js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/client/pages/search/[[...query]].js b/client/pages/search/[[...query]].js index 271ad51..01bf139 100644 --- a/client/pages/search/[[...query]].js +++ b/client/pages/search/[[...query]].js @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import getConfig from "next/config"; import { useRouter } from "next/router"; import qs from "qs"; @@ -11,7 +11,11 @@ import Box from "../../components/Box"; import TorrentList from "../../components/TorrentList"; const Search = ({ results, error, token }) => { - const [torrents, setTorrents] = useState(results?.torrents ?? []); + const [torrents, setTorrents] = useState([]); + + useEffect(() => { + setTorrents(results?.torrents ?? []); + }, [results?.total]); const router = useRouter(); let { From aa674a2d0f5332cbfad8de3d09386fbbe0c192e0 Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Thu, 29 Jun 2023 10:43:06 +0100 Subject: [PATCH 031/125] more friendly upload missing torrent error message --- client/pages/upload.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/pages/upload.js b/client/pages/upload.js index 87ff254..d5aa6df 100644 --- a/client/pages/upload.js +++ b/client/pages/upload.js @@ -243,6 +243,8 @@ const Upload = ({ token, userId }) => { const form = new FormData(e.target); try { + if (!torrentFile) throw new Error("No .torrent file added"); + const uploadRes = await fetch(`${SQ_API_URL}/torrent/upload`, { method: "POST", headers: { From b2d541ef5656eb1885fc94594dd151176fdbe1e2 Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Thu, 29 Jun 2023 18:08:30 +0100 Subject: [PATCH 032/125] increase api rate limit --- api/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/index.js b/api/src/index.js index 4c27291..ad9fc33 100644 --- a/api/src/index.js +++ b/api/src/index.js @@ -120,7 +120,7 @@ validateConfig(config).then(() => { // a secret only available to the server const limiter = ratelimit({ windowMs: 1000 * 60, - max: 30, + max: 120, keyGenerator: (req) => { if ( req.headers["x-forwarded-for"] && From 515a2a0061f9e793a525211dfc1492109a56c6c4 Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Thu, 29 Jun 2023 18:09:24 +0100 Subject: [PATCH 033/125] ver 1.3.1 --- api/package.json | 2 +- client/package.json | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/package.json b/api/package.json index c7366fc..c7e9d68 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "@sqtracker/api", - "version": "1.3.0", + "version": "1.3.1", "private": true, "license": "GPL-3.0-only", "scripts": { diff --git a/client/package.json b/client/package.json index 8107699..3a690bd 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@sqtracker/client", - "version": "1.3.0", + "version": "1.3.1", "private": true, "license": "GPL-3.0-only", "scripts": { diff --git a/package.json b/package.json index 01dd13f..b15907a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sqtracker", - "version": "1.3.0", + "version": "1.3.1", "private": true, "license": "GPL-3.0-only", "workspaces": [ From b224e4c8697d052671b1e3b2d346306d56c9e68b Mon Sep 17 00:00:00 2001 From: Oleg Omelchenko Date: Sat, 19 Aug 2023 21:10:03 +0300 Subject: [PATCH 034/125] incorrect work with non latin letters in torrent file or folder name https://github.com/tdjsnelling/sqtracker/issues/39 --- api/package.json | 1 + api/src/controllers/torrent.js | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/api/package.json b/api/package.json index c7e9d68..7c51868 100644 --- a/api/package.json +++ b/api/package.json @@ -19,6 +19,7 @@ "bittorrent-tracker": "9.19.0", "body-parser": "^1.19.0", "chalk": "^4.1.1", + "content-disposition": "^0.5.4", "cookie-parser": "^1.4.6", "cors": "^2.8.5", "dotenv": "^10.0.0", diff --git a/api/src/controllers/torrent.js b/api/src/controllers/torrent.js index a636df1..111311e 100644 --- a/api/src/controllers/torrent.js +++ b/api/src/controllers/torrent.js @@ -8,6 +8,7 @@ import User from "../schema/user"; import Comment from "../schema/comment"; import Group from "../schema/group"; import { createGroup, addToGroup, removeFromGroup } from "./group"; +import contentDisposition from "content-disposition"; const urlReservedCharRegex = /[&$+,/:;=?@#<>\[\]{}|\\\^%]/g; @@ -254,13 +255,16 @@ export const downloadTorrent = async (req, res, next) => { delete parsed["announce-list"]; parsed.info.private = 1; + const fileName = `${parsed.info.name.toString()} - ${ + process.env.SQ_SITE_NAME + }.torrent`; + res.setHeader("Content-Type", "application/x-bittorrent"); res.setHeader( "Content-Disposition", - `attachment;filename=${parsed.info.name.toString()} - ${ - process.env.SQ_SITE_NAME - }.torrent` + `attachment;filename=${contentDisposition(fileName)}` ); + res.write(bencode.encode(parsed)); res.end(); } catch (e) { From a072d94a7ce10d63ee0792a85cb7ab14e8bb63d0 Mon Sep 17 00:00:00 2001 From: Oleg Omelchenko Date: Mon, 21 Aug 2023 23:28:20 +0300 Subject: [PATCH 035/125] incorrect work with non latin letters in torrent file or folder name https://github.com/tdjsnelling/sqtracker/issues/39 --- api/src/controllers/torrent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/controllers/torrent.js b/api/src/controllers/torrent.js index 111311e..3e039b5 100644 --- a/api/src/controllers/torrent.js +++ b/api/src/controllers/torrent.js @@ -3,12 +3,12 @@ import crypto from "crypto"; import mongoose from "mongoose"; import { createNGrams, nGrams } from "mongoose-fuzzy-searching/helpers"; import slugify from "slugify"; +import contentDisposition from "content-disposition"; import Torrent from "../schema/torrent"; import User from "../schema/user"; import Comment from "../schema/comment"; import Group from "../schema/group"; import { createGroup, addToGroup, removeFromGroup } from "./group"; -import contentDisposition from "content-disposition"; const urlReservedCharRegex = /[&$+,/:;=?@#<>\[\]{}|\\\^%]/g; From feee90990157b62410587f790f169b2ce13566a6 Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Tue, 22 Aug 2023 20:24:01 +0100 Subject: [PATCH 036/125] ver 1.3.3 --- api/package.json | 2 +- api/src/controllers/torrent.js | 5 +---- client/package.json | 2 +- package.json | 2 +- yarn.lock | 2 +- 5 files changed, 5 insertions(+), 8 deletions(-) diff --git a/api/package.json b/api/package.json index 7c51868..eb9dea5 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "@sqtracker/api", - "version": "1.3.1", + "version": "1.3.3", "private": true, "license": "GPL-3.0-only", "scripts": { diff --git a/api/src/controllers/torrent.js b/api/src/controllers/torrent.js index 3e039b5..baa5e25 100644 --- a/api/src/controllers/torrent.js +++ b/api/src/controllers/torrent.js @@ -260,10 +260,7 @@ export const downloadTorrent = async (req, res, next) => { }.torrent`; res.setHeader("Content-Type", "application/x-bittorrent"); - res.setHeader( - "Content-Disposition", - `attachment;filename=${contentDisposition(fileName)}` - ); + res.setHeader("Content-Disposition", contentDisposition(fileName)); res.write(bencode.encode(parsed)); res.end(); diff --git a/client/package.json b/client/package.json index 3a690bd..71a9ff1 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@sqtracker/client", - "version": "1.3.1", + "version": "1.3.3", "private": true, "license": "GPL-3.0-only", "scripts": { diff --git a/package.json b/package.json index b15907a..a14342f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sqtracker", - "version": "1.3.1", + "version": "1.3.3", "private": true, "license": "GPL-3.0-only", "workspaces": [ diff --git a/yarn.lock b/yarn.lock index 73c8eec..bbbdf8b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2228,7 +2228,7 @@ console-control-strings@^1.0.0, console-control-strings@^1.1.0: resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== -content-disposition@0.5.4: +content-disposition@0.5.4, content-disposition@^0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== From 1062097b732761c02798993e984421a48f0af80b Mon Sep 17 00:00:00 2001 From: smlinux Date: Sat, 9 Sep 2023 07:02:16 +0200 Subject: [PATCH 037/125] docker-compose: MongoDB without AVX --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 0a7cdb6..f242a10 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,7 +25,7 @@ services: - ./traefik.yml:/config/traefik.yml database: container_name: sq_mongodb - image: mongo:6.0 + image: nwzz/mongo-without-avx:6.0.5-jammy ports: - "127.0.0.1:27017:27017" volumes: From 696d627f3a5afe55c6117caf54c2f47d9fb99af9 Mon Sep 17 00:00:00 2001 From: smlinux Date: Sat, 9 Sep 2023 07:14:31 +0200 Subject: [PATCH 038/125] gitignore: add more files --- .gitignore | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a7ae3e7..3394cc7 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,9 @@ data/ config.js .DS_Store dist/ -letsencrypt/ \ No newline at end of file +letsencrypt/ +*~ +*.bak +*.orig +*.diff +*.patch From 8e0229822a3adc77346363f1ebd88091ab4d7bd9 Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Wed, 15 Mar 2023 13:58:50 +0000 Subject: [PATCH 039/125] basic localisation implementation --- client/components/Navigation.js | 32 ++++++++++++++++++++++++++++---- client/locales.json | 16 ++++++++++++++++ client/pages/_app.js | 21 +++++++++++++++++++++ client/utils/LocaleContext.js | 8 ++++++++ 4 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 client/locales.json create mode 100644 client/utils/LocaleContext.js diff --git a/client/components/Navigation.js b/client/components/Navigation.js index 4c4feff..17dd928 100644 --- a/client/components/Navigation.js +++ b/client/components/Navigation.js @@ -24,6 +24,7 @@ import { UserPlus } from "@styled-icons/boxicons-regular/UserPlus"; import Box from "./Box"; import Text from "./Text"; import Button from "./Button"; +import LocaleContext from "../utils/LocaleContext"; const NavLink = styled.a(({ theme, href, highlights = [], mt = 0 }) => { const router = useRouter(); @@ -56,6 +57,18 @@ const NavLink = styled.a(({ theme, href, highlights = [], mt = 0 }) => { }); }); +const LocaleSelector = styled.select(() => + css({ + background: "none", + color: "text", + border: 0, + fontSize: 0, + fontFamily: "body", + cursor: "pointer", + p: 0, + }) +); + const Navigation = ({ isMobile, menuIsOpen, setMenuIsOpen }) => { const [cookies] = useCookies(); const [role, setRole] = useState("user"); @@ -63,6 +76,9 @@ const Navigation = ({ isMobile, menuIsOpen, setMenuIsOpen }) => { const theme = useContext(ThemeContext); + const { locale, setLocale, locales, getLocaleString } = + useContext(LocaleContext); + const { asPath } = useRouter(); const { username, token } = cookies; @@ -236,7 +252,7 @@ const Navigation = ({ isMobile, menuIsOpen, setMenuIsOpen }) => { - Log in + {getLocaleString("logIn")} @@ -244,7 +260,7 @@ const Navigation = ({ isMobile, menuIsOpen, setMenuIsOpen }) => { SQ_ALLOW_REGISTER === "invite") && ( - Register + {getLocaleString("register")} @@ -288,10 +304,18 @@ const Navigation = ({ isMobile, menuIsOpen, setMenuIsOpen }) => { > ■ sqtracker {" "} - - v{SQ_VERSION} + setLocale(e.target.value)} + > + {locales.map((l) => ( + + ))} + ); diff --git a/client/locales.json b/client/locales.json new file mode 100644 index 0000000..e5dc877 --- /dev/null +++ b/client/locales.json @@ -0,0 +1,16 @@ +{ + "en": { + "poweredBy": "Powered by", + "logIn": "Log in", + "register": "Register", + "email": "Email", + "username": "Username", + "usernameRules": "Can only consist of letters, numbers, and “.”", + "password": "Password", + "resetPassword": "Reset password", + "newPassword": "New password" + }, + "fr": { + "logIn": "Se connecter" + } +} diff --git a/client/pages/_app.js b/client/pages/_app.js index c14af9a..8f1a3b7 100644 --- a/client/pages/_app.js +++ b/client/pages/_app.js @@ -27,6 +27,8 @@ import Input from "../components/Input"; import { NotificationsProvider } from "../components/Notifications"; import Text from "../components/Text"; import LoadingContext from "../utils/LoadingContext"; +import LocaleContext from "../utils/LocaleContext"; +import locales from "../locales.json"; const getThemeColours = (themeName, customTheme = {}) => { switch (themeName) { @@ -152,6 +154,9 @@ const Loading = styled(LoaderAlt)` animation: ${spin} 1s linear infinite; `; +const getLocaleString = (locale) => (key) => + locales[locale][key] ?? locales.en[key]; + const SqTracker = ({ Component, pageProps, initialTheme }) => { const [isMobile, setIsMobile] = useState(false); const [menuIsOpen, setMenuIsOpen] = useState(false); @@ -159,6 +164,7 @@ const SqTracker = ({ Component, pageProps, initialTheme }) => { const [isServer, setIsServer] = useState(true); const [loading, setLoading] = useState(false); const [userStats, setUserStats] = useState(); + const [locale, setLocale] = useState("en"); const router = useRouter(); @@ -208,6 +214,9 @@ const SqTracker = ({ Component, pageProps, initialTheme }) => { }); } + const { locale: localeCookie } = cookies; + if (Object.keys(locales).includes(localeCookie)) setLocale(localeCookie); + Router.events.on("routeChangeStart", () => setLoading(true)); Router.events.on("routeChangeComplete", () => setLoading(false)); Router.events.on("routeChangeError", () => setLoading(false)); @@ -263,6 +272,17 @@ const SqTracker = ({ Component, pageProps, initialTheme }) => { + { + setLocale(l); + setCookie("locale", l, { path: "/" }); + }, + locales: Object.keys(locales), + getLocaleString: getLocaleString(locale), + }} + > { + diff --git a/client/utils/LocaleContext.js b/client/utils/LocaleContext.js new file mode 100644 index 0000000..d6695fc --- /dev/null +++ b/client/utils/LocaleContext.js @@ -0,0 +1,8 @@ +import { createContext } from "react"; + +export default createContext({ + locale: "en", + setLocale: () => {}, + locales: [], + getLocaleString: () => {}, +}); From 60637be69d1eecc4c8e348f7da5d2a9ebbe17462 Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Wed, 15 Mar 2023 20:51:02 +0000 Subject: [PATCH 040/125] move strings to locales file for splash, log in, register, password reset --- api/src/controllers/user.js | 4 +- client/locales.json | 13 +++++- client/pages/index.js | 54 +++++++++++++------------ client/pages/login.js | 26 +++++++----- client/pages/register.js | 42 +++++++++++++------ client/pages/reset-password/finalise.js | 26 ++++++++---- client/pages/reset-password/initiate.js | 20 ++++++--- 7 files changed, 121 insertions(+), 64 deletions(-) diff --git a/api/src/controllers/user.js b/api/src/controllers/user.js index ee3914b..fed1be5 100644 --- a/api/src/controllers/user.js +++ b/api/src/controllers/user.js @@ -371,7 +371,7 @@ export const initiatePasswordReset = (mail) => async (req, res, next) => { const user = await User.findOne({ email: req.body.email }).lean(); if (!user) { - res.status(404).send("User does not exist"); + res.sendStatus(200); return; } @@ -397,7 +397,7 @@ export const initiatePasswordReset = (mail) => async (req, res, next) => { ${process.env.SQ_BASE_URL}/reset-password/finalise?token=${token}`, }); - res.send(token); + res.sendStatus(200); } catch (e) { next(e); } diff --git a/client/locales.json b/client/locales.json index e5dc877..6be7d48 100644 --- a/client/locales.json +++ b/client/locales.json @@ -7,8 +7,19 @@ "username": "Username", "usernameRules": "Can only consist of letters, numbers, and “.”", "password": "Password", + "totp": "One-time code", "resetPassword": "Reset password", - "newPassword": "New password" + "newPassword": "New password", + "welcomeBack": "Welcome back", + "logInFailed": "Could not log in", + "welcome": "Welcome", + "registerFailed": "Could not register", + "registrationClosed": "Registration closed", + "passwordResetRequestSuccess": "If an account with that email address exists, you will receive an email shortly", + "passwordResetRequestFailed": "Could not initiate password reset", + "passwordResetSuccess": "Password was reset successfully", + "passwordResetFailed": "Could not complete password reset", + "tokenError": "Token error" }, "fr": { "logIn": "Se connecter" diff --git a/client/pages/index.js b/client/pages/index.js index 2e6ac2f..108d672 100644 --- a/client/pages/index.js +++ b/client/pages/index.js @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useContext } from "react"; import getConfig from "next/config"; import Link from "next/link"; import { useRouter } from "next/router"; @@ -13,34 +13,38 @@ import Infobox from "../components/Infobox"; import { ErrorCircle } from "@styled-icons/boxicons-regular/ErrorCircle"; import { News } from "@styled-icons/boxicons-regular/News"; import moment from "moment/moment"; +import LocaleContext from "../utils/LocaleContext"; -const PublicLanding = ({ name, allowRegister }) => ( - - - {name} - - - - - Log in - - - {allowRegister && ( - - - Register +const PublicLanding = ({ name, allowRegister }) => { + const { getLocaleString } = useContext(LocaleContext); + return ( + + + {name} + + + + + {getLocaleString("logIn")} - )} + {allowRegister && ( + + + {getLocaleString("register")} + + + )} + - -); + ); +}; const Index = ({ token, diff --git a/client/pages/login.js b/client/pages/login.js index f1a3925..5fa8684 100644 --- a/client/pages/login.js +++ b/client/pages/login.js @@ -9,6 +9,7 @@ import Input from "../components/Input"; import Button from "../components/Button"; import { NotificationContext } from "../components/Notifications"; import LoadingContext from "../utils/LoadingContext"; +import LocaleContext from "../utils/LocaleContext"; import { usernamePattern } from "./register"; const Login = () => { @@ -18,6 +19,7 @@ const Login = () => { const { addNotification } = useContext(NotificationContext); const { setLoading } = useContext(LoadingContext); + const { getLocaleString } = useContext(LocaleContext); const router = useRouter(); @@ -57,11 +59,17 @@ const Login = () => { setCookie("userId", uid, { path: "/", expires }); setCookie("username", username, { path: "/", expires }); - addNotification("success", `Welcome back ${form.get("username")}!`); + addNotification( + "success", + `${getLocaleString("welcomeBack")} ${form.get("username")}!` + ); router.push("/"); } catch (e) { - addNotification("error", `Could not log in: ${e.message}`); + addNotification( + "error", + `${getLocaleString("logInFailed")}: ${e.message}` + ); console.error(e); } @@ -70,14 +78,14 @@ const Login = () => { return ( <> - + - Log in + {getLocaleString("logIn")} { {totpRequired && ( - + )} - + - Reset password + {getLocaleString("resetPassword")} diff --git a/client/pages/register.js b/client/pages/register.js index dad030a..d7d56f3 100644 --- a/client/pages/register.js +++ b/client/pages/register.js @@ -12,6 +12,7 @@ import Button from "../components/Button"; import Box from "../components/Box"; import { NotificationContext } from "../components/Notifications"; import LoadingContext from "../utils/LoadingContext"; +import LocaleContext from "../utils/LocaleContext"; export const usernamePattern = "[A-Za-z0-9.]+"; @@ -21,6 +22,7 @@ const Register = ({ token: inviteToken, tokenError }) => { const { colors } = useContext(ThemeContext); const { addNotification } = useContext(NotificationContext); const { setLoading } = useContext(LoadingContext); + const { getLocaleString } = useContext(LocaleContext); const router = useRouter(); @@ -60,11 +62,17 @@ const Register = ({ token: inviteToken, tokenError }) => { setCookie("userId", uid, { path: "/", expires }); setCookie("username", username, { path: "/", expires }); - addNotification("success", `Welcome ${form.get("username")}!`); + addNotification( + "success", + `${getLocaleString("welcome")} ${form.get("username")}!` + ); router.push("/"); } catch (e) { - addNotification("error", `Could not register: ${e.message}`); + addNotification( + "error", + `${getLocaleString("registerFailed")}: ${e.message}` + ); console.error(e); } @@ -74,28 +82,34 @@ const Register = ({ token: inviteToken, tokenError }) => { if (SQ_ALLOW_REGISTER !== "open" && SQ_ALLOW_REGISTER !== "invite") { return ( <> - + - Register + {getLocaleString("register")} -

Registration is closed.

+

{getLocaleString("registrationClosed")}.

); } return ( <> - + - Register + {getLocaleString("register")} {!tokenError ? (
- + { - +
) : ( { borderRadius={1} p={4} > - Could not register: {tokenError} + + {getLocaleString("registerFailed")}: {tokenError} + )} diff --git a/client/pages/reset-password/finalise.js b/client/pages/reset-password/finalise.js index 618de05..e1b55e3 100644 --- a/client/pages/reset-password/finalise.js +++ b/client/pages/reset-password/finalise.js @@ -8,10 +8,12 @@ import Input from "../../components/Input"; import Button from "../../components/Button"; import { NotificationContext } from "../../components/Notifications"; import LoadingContext from "../../utils/LoadingContext"; +import LocaleContext from "../../utils/LocaleContext"; const FinalisePasswordReset = ({ token, email, tokenError }) => { const { addNotification } = useContext(NotificationContext); const { setLoading } = useContext(LoadingContext); + const { getLocaleString } = useContext(LocaleContext); const router = useRouter(); @@ -42,13 +44,13 @@ const FinalisePasswordReset = ({ token, email, tokenError }) => { throw new Error(reason); } - addNotification("success", "Password was reset successfully"); + addNotification("success", getLocaleString("passwordResetSuccess")); router.push("/login"); } catch (e) { addNotification( "error", - `Could not complete password reset: ${e.message}` + `${getLocaleString("passwordResetFailed")}: ${e.message}` ); console.error(e); } @@ -58,24 +60,32 @@ const FinalisePasswordReset = ({ token, email, tokenError }) => { return ( <> - + - Reset password + {getLocaleString("resetPassword")} - + {!tokenError ? (
- +
) : ( -

Token error: {tokenError}

+

+ {getLocaleString("tokenError")}: {tokenError} +

)} ); diff --git a/client/pages/reset-password/initiate.js b/client/pages/reset-password/initiate.js index 4a6f41f..5367353 100644 --- a/client/pages/reset-password/initiate.js +++ b/client/pages/reset-password/initiate.js @@ -6,10 +6,12 @@ import Input from "../../components/Input"; import Button from "../../components/Button"; import { NotificationContext } from "../../components/Notifications"; import LoadingContext from "../../utils/LoadingContext"; +import LocaleContext from "../../utils/LocaleContext"; const InitiatePasswordReset = () => { const { addNotification } = useContext(NotificationContext); const { setLoading } = useContext(LoadingContext); + const { getLocaleString } = useContext(LocaleContext); const { publicRuntimeConfig: { SQ_API_URL }, @@ -38,12 +40,12 @@ const InitiatePasswordReset = () => { addNotification( "success", - "If an account with that email address exists, you will receive an email shortly" + getLocaleString("passwordResetRequestSuccess") ); } catch (e) { addNotification( "error", - `Could not initiate password reset: ${e.message}` + `${getLocaleString("passwordResetRequestFailed")}: ${e.message}` ); console.error(e); } @@ -53,13 +55,19 @@ const InitiatePasswordReset = () => { return ( <> - + - Reset password + {getLocaleString("resetPassword")}
- - + +
); From f4f07bb2b8da05a3634d1c53b84f61e4e01e6acf Mon Sep 17 00:00:00 2001 From: smlinux Date: Sat, 9 Sep 2023 16:32:08 +0200 Subject: [PATCH 041/125] locales: more strings for upload (en) --- client/locales.json | 25 ++++++++++++++- client/pages/upload.js | 69 +++++++++++++++++++++++------------------- 2 files changed, 62 insertions(+), 32 deletions(-) diff --git a/client/locales.json b/client/locales.json index 6be7d48..f57b00d 100644 --- a/client/locales.json +++ b/client/locales.json @@ -19,7 +19,30 @@ "passwordResetRequestFailed": "Could not initiate password reset", "passwordResetSuccess": "Password was reset successfully", "passwordResetFailed": "Could not complete password reset", - "tokenError": "Token error" + "tokenError": "Token error", + "uploadTorrentUploadSuccess": "Torrent uploaded successfully", + "uploadCouldNotUploadFile": "Could not upload file", + "uploadCouldNotGetGroupSuggestions": "Could not get group suggestions", + "uploadName": "Name", + "uploadCategory": "Category", + "uploadSource": "Source", + "uploadDescription": "Description", + "uploadMarkdownSupport": "Markdown supported", + "uploadMediaInfo": "MediaInfo", + "uploadAddTag": "Add tag", + "uploadTags": "Tags", + "uploadTorrentFile": "Torrent file", + "uploadGroupWith": "Group with", + "uploadAnonymousUpload": "Anonymous upload", + "uploadGroupWithThisTorrent": "Group with this torrent", + "uploadUpload": "Upload", + "uploadAnnounceURL": "Announce URL", + "uploadInfoBox1": "The following file extensions are blacklisted. Any torrent containing files of these types will not be uploaded.", + "uploadDropFileHere": "Drop the file here...", + "uploadDragDropClickSelect": "Drag and drop .torrent file here, or click to select", + "uploadCouldNotUploadTorrent": "Could not upload .torrent", + "uploadInfoBox2": "It looks like these existing torrents have similar names. Would you like to group your upload with any of them? Groups should only contain very similar content, e.g. the same movie in different formats.", + "uploadInfoBox3": "Note: if you have started seeding a torrent before uploading, you may need to refresh trackers in your torrent client once the upload is complete." }, "fr": { "logIn": "Se connecter" diff --git a/client/pages/upload.js b/client/pages/upload.js index d5aa6df..99b7a91 100644 --- a/client/pages/upload.js +++ b/client/pages/upload.js @@ -22,6 +22,7 @@ import Infobox from "../components/Infobox"; import List from "../components/List"; import { NotificationContext } from "../components/Notifications"; import LoadingContext from "../utils/LoadingContext"; +import LocaleContext from "../utils/LocaleContext"; import MarkdownInput from "../components/MarkdownInput"; const FileUpload = styled(Box)(() => @@ -52,6 +53,8 @@ export const TorrentFields = ({ const [sources, setSources] = useState([]); const [tags, setTags] = useState(values?.tags?.split(",") ?? []); + const { getLocaleString } = useContext(LocaleContext); + useEffect(() => { setSources( category @@ -68,7 +71,7 @@ export const TorrentFields = ({ <> { setCategory(e.target.value); @@ -100,7 +103,7 @@ export const TorrentFields = ({ {!!sources.length && ( - + {tags.map((tag, i) => ( @@ -170,7 +173,7 @@ export const TorrentFields = ({ > - Add tag + {getLocaleString("uploadAddTag")} @@ -197,6 +200,7 @@ const Upload = ({ token, userId }) => { const { addNotification } = useContext(NotificationContext); const { setLoading } = useContext(LoadingContext); + const { getLocaleString } = useContext(LocaleContext); const router = useRouter(); @@ -269,12 +273,18 @@ const Upload = ({ token, userId }) => { throw new Error(reason); } - addNotification("success", "Torrent uploaded successfully"); + addNotification( + "success", + `${getLocaleString("uploadTorrentUploadSuccess")}` + ); const infoHash = await uploadRes.text(); router.push(`/torrent/${infoHash}`); } catch (e) { - addNotification("error", `Could not upload file: ${e.message}`); + addNotification( + "error", + `${getLocaleString("uploadCouldNotUploadFile")}: ${e.message}` + ); console.error(e); } @@ -302,20 +312,23 @@ const Upload = ({ token, userId }) => { const { results } = await suggestionsRes.json(); setGroupSuggestions(results); } catch (e) { - addNotification("error", `Could not get group suggestions: ${e.message}`); + addNotification( + "error", + `${getLocaleString("uploadCouldNotGetGroupSuggestions")}: ${e.message}` + ); console.error(e); } }; return ( <> - + - Upload + {getLocaleString("uploadUpload")} - Announce URL:{" "} + {getLocaleString("uploadAnnounceURL")}:{" "} { {!!SQ_EXTENSION_BLACKLIST.length && ( - The following file extensions are blacklisted. Any torrent - containing files of these types will not be uploaded. + {getLocaleString("uploadInfoBox1")} { )}
- + {torrentFile ? ( @@ -355,17 +367,17 @@ const Upload = ({ token, userId }) => { {torrentFile.name} ) : isDragActive ? ( - Drop the file here... + {getLocaleString("uploadDropFileHere")} ) : ( - Drag and drop .torrent file here, or click to select + {getLocaleString("uploadDragDropClickSelect")} )} {dropError && ( - Could not upload .torrent: {dropError} + {getLocaleString("uploadCouldNotUploadTorrent")}: {dropError} )} @@ -376,10 +388,7 @@ const Upload = ({ token, userId }) => { groupSuggestions.length ? ( - It looks like these existing torrents have similar names. - Would you like to group your upload with any of them? Groups - should only contain very similar content, e.g. the same movie - in different formats. + {getLocaleString("uploadInfoBox2")} ({ @@ -423,7 +432,7 @@ const Upload = ({ token, userId }) => { disabled={groupWith === row.infoHash} small > - Group with this torrent + {getLocaleString("uploadGroupWithThisTorrent")} ), @@ -440,7 +449,7 @@ const Upload = ({ token, userId }) => { { )} {SQ_ALLOW_ANONYMOUS_UPLOAD && ( - + )} - Note: if you have started seeding a torrent before uploading, you may - need to refresh trackers in your torrent client once the upload is - complete. + {getLocaleString("uploadInfoBox3")} From 56e8ec46ed7e733ef3169c2ebc23c476b378ca28 Mon Sep 17 00:00:00 2001 From: smlinux Date: Sun, 10 Sep 2023 11:27:01 +0200 Subject: [PATCH 042/125] locales: more strings (en, ru) --- client/components/Navigation.js | 30 ++-- client/locales.json | 263 ++++++++++++++++++++++++++------ client/pages/account.js | 139 +++++++++-------- client/pages/index.js | 19 +-- client/pages/verify-email.js | 16 +- 5 files changed, 328 insertions(+), 139 deletions(-) diff --git a/client/components/Navigation.js b/client/components/Navigation.js index 17dd928..bc028ea 100644 --- a/client/components/Navigation.js +++ b/client/components/Navigation.js @@ -167,55 +167,55 @@ const Navigation = ({ isMobile, menuIsOpen, setMenuIsOpen }) => { - Home + {getLocaleString("navHome")} - Browse + {getLocaleString("navBrowse")} - Search + {getLocaleString("navSearch")} - Upload + {getLocaleString("navUpload")} - Requests + {getLocaleString("navRequests")} - Announcements + {getLocaleString("navAnnouncements")} - Wiki + {getLocaleString("navWiki")} - RSS + {getLocaleString("navRSS")} - Bookmarks + {getLocaleString("navBookmarks")} @@ -229,13 +229,13 @@ const Navigation = ({ isMobile, menuIsOpen, setMenuIsOpen }) => { <> - Reports + {getLocaleString("navReports")} - Stats + {getLocaleString("navStats")} @@ -243,7 +243,7 @@ const Navigation = ({ isMobile, menuIsOpen, setMenuIsOpen }) => { )} - Log out + {getLocaleString("navLogOut")} @@ -269,13 +269,13 @@ const Navigation = ({ isMobile, menuIsOpen, setMenuIsOpen }) => { <> - Browse + {getLocaleString("navBrowse")} - Wiki + {getLocaleString("navWiki")} @@ -296,7 +296,7 @@ const Navigation = ({ isMobile, menuIsOpen, setMenuIsOpen }) => { p={3} > - Powered by{" "} + {getLocaleString("poweredBy")}{" "} { const [amount, setAmount] = useState(1); @@ -82,6 +83,8 @@ const Account = ({ token, invites = [], user, userRole }) => { const { addNotification } = useContext(NotificationContext); const { setLoading } = useContext(LoadingContext); + const { getLocaleString } = useContext(LocaleContext); + const theme = useContext(ThemeContext); const { @@ -127,13 +130,17 @@ const Account = ({ token, invites = [], user, userRole }) => { return currentInvitesList; }); - addNotification("success", "Invite sent successfully"); + addNotification("success", + `${getLocaleString("accInviteSentSuccess")}` + ); setRemainingInvites((r) => r - 1); setShowInviteModal(false); } catch (e) { - addNotification("error", `Could not send invite: ${e.message}`); + addNotification("error", + `${getLocaleString("accCouldNotSendInvite")}: ${e.message}` + ); console.error(e); } @@ -166,7 +173,9 @@ const Account = ({ token, invites = [], user, userRole }) => { throw new Error(reason); } - addNotification("success", "Password changed successfully"); + addNotification("success", + `${getLocaleString("accPassChangedSuccess")}` + ); const fields = e.target.querySelectorAll("input"); for (const field of fields) { @@ -174,7 +183,9 @@ const Account = ({ token, invites = [], user, userRole }) => { field.blur(); } } catch (e) { - addNotification("error", `Could not change password: ${e.message}`); + addNotification("error", + `${getLocaleString("accCouldNotChangePass")}: ${e.message}` + ); console.error(e); } @@ -206,7 +217,9 @@ const Account = ({ token, invites = [], user, userRole }) => { throw new Error(reason); } - addNotification("success", "Items purchased successfully"); + addNotification("success", + `${getLocaleString("accItemsPurchasedSuccess")}` + ); const pointsRemaining = await buyRes.text(); setBonusPoints(parseInt(pointsRemaining)); @@ -219,7 +232,9 @@ const Account = ({ token, invites = [], user, userRole }) => { field.blur(); } } catch (e) { - addNotification("error", `Could not buy items: ${e.message}`); + addNotification("error", + `${getLocaleString("accCouldNotBuyItems")}: ${e.message}` + ); console.error(e); } @@ -248,7 +263,9 @@ const Account = ({ token, invites = [], user, userRole }) => { setTotpBackupCodes(backupCodes); setTotpQrData(undefined); setTotpEnabled(true); - addNotification("success", "2FA enabled"); + addNotification("success", + `${getLocaleString("acc2FAEnabled")}` + ); } else { const message = await enableRes.text(); addNotification("error", message); @@ -280,14 +297,18 @@ const Account = ({ token, invites = [], user, userRole }) => { if (disableRes.status === 200) { setTotpEnabled(false); - addNotification("success", "2FA disabled"); + addNotification("success", + `${getLocaleString("acc2FADisabled")}` + ); } else { const message = await disableRes.text(); addNotification("error", message); } } } catch (e) { - addNotification("error", `Could not toggle 2FA: ${e.message}`); + addNotification("error", + `${getLocaleString("accCouldNotToggle2FA")}: ${e.message}` + ); console.error(e); } }; @@ -315,52 +336,52 @@ const Account = ({ token, invites = [], user, userRole }) => { await router.push("/logout"); } catch (e) { - addNotification("error", `Could not delete account: ${e.message}`); + addNotification("error", + `${getLocaleString("accCouldNotDelAcc")}: ${e.message}` + ); console.error(e); } }; return ( <> - + - My account + {getLocaleString("accMyAccount")} {userRole === "admin" && ( - This is an admin account. + {getLocaleString("accThisIsAdminAcc")} )} - Bonus points + {getLocaleString("accBonusPoints")} - You currently have {bonusPoints} bonus points. + {getLocaleString("accYouCurrentlyHave")} {bonusPoints} {getLocaleString("accBonusPointsHave")}. - You will earn {SQ_BP_EARNED_PER_GB}{" "} - {pluralize("point", SQ_BP_EARNED_PER_GB)} for every GB you upload. + {getLocaleString("accYouWillEarn")} {SQ_BP_EARNED_PER_GB}{" "} + {pluralize(`${getLocaleString("accBonusPointsHave")}`, SQ_BP_EARNED_PER_GB)} {getLocaleString("accForEveryGBYouUpload")}. - You will earn {SQ_BP_EARNED_PER_FILLED_REQUEST}{" "} - {pluralize("point", SQ_BP_EARNED_PER_FILLED_REQUEST)} for every - request you fulfill, or{" "} - {SQ_BP_EARNED_PER_FILLED_REQUEST * 2} if you are also - the uploader of the accepted torrent. + {getLocaleString("accYouWillEarn")} {SQ_BP_EARNED_PER_FILLED_REQUEST}{" "} + {pluralize(`${getLocaleString("accBonusPointsHave")}`, SQ_BP_EARNED_PER_FILLED_REQUEST)} {getLocaleString("accEveryRequestYouFulfill")}{" "} + {SQ_BP_EARNED_PER_FILLED_REQUEST * 2} {getLocaleString("accIfYouAreAlsUploaderAcceptTorrent")}. * + *": { mt: 3 } }} mb={5}> {SQ_ALLOW_REGISTER === "invite" && ( handleBuy(e, "invite")} /> )} handleBuy(e, "upload")} @@ -375,7 +396,7 @@ const Account = ({ token, invites = [], user, userRole }) => { justifyContent="space-between" mb={4} > - Invites + {getLocaleString("accInvites")} { onClick={() => setShowInviteModal(true)} disabled={remainingInvites < 1} > - Send invite + {getLocaleString("accSendInvite")} @@ -399,13 +420,13 @@ const Account = ({ token, invites = [], user, userRole }) => { data={invitesList} columns={[ { - header: "Email", + header: `${getLocaleString("email")}`, accessor: "email", cell: ({ value }) => {value}, gridWidth: "1.75fr", }, { - header: "Claimed", + header: `${getLocaleString("accClaimed")}`, accessor: "claimed", cell: ({ value }) => ( @@ -415,27 +436,27 @@ const Account = ({ token, invites = [], user, userRole }) => { gridWidth: "0.5fr", }, { - header: "Valid until", + header: `${getLocaleString("accValidUntil")}`, accessor: "validUntil", cell: ({ value }) => ( - {moment(value).format("HH:mm Do MMM YYYY")} + {moment(value).format(`${getLocaleString("indexTime")}`)} ), gridWidth: "175px", }, { - header: "Created", + header: `${getLocaleString("accCreated")}`, accessor: "created", cell: ({ value }) => ( - {moment(value).format("HH:mm Do MMM YYYY")} + {moment(value).format(`${getLocaleString("indexTime")}`)} ), gridWidth: "175px", }, ...(userRole === "admin" ? [ { - header: "Role", + header: `${getLocaleString("accRole")}`, accessor: "role", cell: ({ value }) => ( @@ -447,7 +468,7 @@ const Account = ({ token, invites = [], user, userRole }) => { ] : []), { - header: "Copy link", + header: `${getLocaleString("accCopyLink")}`, cell: ({ row }) => { return ( + @@ -539,9 +558,7 @@ const Account = ({ token, invites = [], user, userRole }) => { {totpBackupCodes ? ( <> - 2FA enabled successfully. These backup codes can be used to - log in if you lose access to your authenticator app. Save - them now, they will not be visible again. + {getLocaleString("acc2FAText1")} {totpBackupCodes.split(",").map((code) => ( @@ -568,7 +585,7 @@ const Account = ({ token, invites = [], user, userRole }) => { mb={4} /> )} - + )} @@ -577,25 +594,25 @@ const Account = ({ token, invites = [], user, userRole }) => { - Change password + {getLocaleString("accChangePass")} - + {user.username !== "admin" && ( @@ -605,22 +622,20 @@ const Account = ({ token, invites = [], user, userRole }) => { p={4} > - Danger zone + {getLocaleString("accDangerZone")}
)} {showInviteModal && ( setShowInviteModal(false)}> - Enter an email address to send an invite. The invited user will need - to sign up with the same email address. Once the invite is - generated, you can also copy a direct invite link. + {getLocaleString("accInviteText1")}
@@ -637,9 +652,9 @@ const Account = ({ token, invites = [], user, userRole }) => { variant="secondary" mr={3} > - Cancel + {getLocaleString("accCancel")} - +
@@ -647,15 +662,13 @@ const Account = ({ token, invites = [], user, userRole }) => { {showDeleteAccountModal && ( setShowDeleteAccountModal(false)}> - Are you sure you want to delete your account? This action cannot be - undone, and you may not be able to register again. Your personal - information will be deleted but your uploaded torrents will remain. + {getLocaleString("accDelAccText1")}
@@ -666,9 +679,9 @@ const Account = ({ token, invites = [], user, userRole }) => { variant="secondary" mr={3} > - Cancel + {getLocaleString("accCancel")} - +
diff --git a/client/pages/index.js b/client/pages/index.js index 108d672..2b802c4 100644 --- a/client/pages/index.js +++ b/client/pages/index.js @@ -77,17 +77,18 @@ const Index = ({ if (query) router.push(`/search/${encodeURIComponent(query)}`); }; + const { getLocaleString } = useContext(LocaleContext); + return ( <> - + - Home + {getLocaleString("navHome")} {!emailVerified && ( - Your email address is not yet verified. You will not be able to - upload or download any data until this is done. + {getLocaleString("indexText1")} )} @@ -112,14 +113,14 @@ const Index = ({ _css={{ textTransform: "uppercase" }} mb={3} > - Latest announcement + {getLocaleString("indexLatestAnnounce")} {latestAnnouncement.title} Posted{" "} - {moment(latestAnnouncement.created).format("HH:mm Do MMM YYYY")}{" "} + {moment(latestAnnouncement.created).format(`${getLocaleString("indexTime")}`)}{" "} by{" "} {latestAnnouncement.createdBy?.username ? ( )} - - + + - Latest torrents + {getLocaleString("indexLatestTorrents")} { const [tokenError, setTokenError] = useState(); @@ -20,6 +21,8 @@ const VerifyEmail = () => { publicRuntimeConfig: { SQ_API_URL }, } = getConfig(); + const { getLocaleString } = useContext(LocaleContext); + useEffect(() => { const verify = async () => { if (token) { @@ -44,7 +47,7 @@ const VerifyEmail = () => { setTokenError(e.message); } } else { - setTokenError("No verification token provided"); + setTokenError(`${getLocaleString("veNoVerificationTokenProvided")}`); } }; verify(); @@ -52,18 +55,17 @@ const VerifyEmail = () => { return ( <> - + - Verify email + {getLocaleString("veVerifyEmail")} {!tokenError ? ( <> - Email address verified successfully.{" "} + {getLocaleString("veEmailAddressVerifiedSuccess")}{" "} -
Log in + {getLocaleString("logIn")} - . ) : ( @@ -74,7 +76,7 @@ const VerifyEmail = () => { borderRadius={1} p={4} > - Could not verify email address: {tokenError} + {getLocaleString("veCouldNotVerifyEmailAddress")} {tokenError}
)} From 7f1d8f8eec385bf528c8f687c6b3b3c5951277f0 Mon Sep 17 00:00:00 2001 From: smlinux Date: Sun, 10 Sep 2023 16:13:28 +0200 Subject: [PATCH 043/125] locales: more strings Categories (en, ru) --- client/locales.json | 12 ++++++++++++ client/pages/categories/[category].js | 11 +++++++---- client/pages/categories/index.js | 14 ++++++++------ client/pages/stats.js | 10 ++++++---- 4 files changed, 33 insertions(+), 14 deletions(-) diff --git a/client/locales.json b/client/locales.json index 5376875..370749c 100644 --- a/client/locales.json +++ b/client/locales.json @@ -45,6 +45,10 @@ "accValidUntil": "Valid until", "accYouCurrentlyHave": "You currently have", "accYouWillEarn": "You will earn", + "catCategories": "Categories", + "catNoCategoryHaveBeenDefined": "No categories have been defined.", + "catNoTagsHaveBeenDefined": "No tags have been defined.", + "catNoResults": "No results.", "email": "Email", "indexLatestAnnounce": "Latest announcement", "indexLatestTorrents": "Latest torrents", @@ -77,6 +81,8 @@ "registerFailed": "Could not register", "registrationClosed": "Registration closed", "resetPassword": "Reset password", + "statYouNotPermission": "You do not have permission to do that.", + "statTrackerStat": "Tracker statistics", "tokenError": "Token error", "totp": "One-time code", "uploadAddTag": "Add tag", @@ -157,6 +163,10 @@ "accValidUntil": "Годен до", "accYouCurrentlyHave": "В настоящее время у вас есть", "accYouWillEarn": "Вы получите", + "catCategories": "Категории", + "catNoCategoryHaveBeenDefined": "Категории не определены.", + "catNoTagsHaveBeenDefined": "Метки не определены.", + "catNoResults": "Нет результатов.", "email": "Эл. почта", "indexLatestAnnounce": "Последний анонс", "indexLatestTorrents": "Последние торренты", @@ -186,6 +196,8 @@ "registerFailed": "Не удалось зарегистрироваться", "registrationClosed": "Регистрация закрыта", "resetPassword": "Сброс пароля", + "statYouNotPermission": "У вас нет прав для просмотра.", + "statTrackerStat": "Статистика трекера", "tokenError": "Ошибка токена", "totp": "Одноразовый код", "uploadAddTag": "Добавить метку", diff --git a/client/pages/categories/[category].js b/client/pages/categories/[category].js index 6a86be0..84db304 100644 --- a/client/pages/categories/[category].js +++ b/client/pages/categories/[category].js @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useContext, useState } from "react"; import { useRouter } from "next/router"; import getConfig from "next/config"; import qs from "qs"; @@ -7,6 +7,7 @@ import { withAuthServerSideProps } from "../../utils/withAuth"; import SEO from "../../components/SEO"; import Text from "../../components/Text"; import TorrentList from "../../components/TorrentList"; +import LocaleContext from "../../utils/LocaleContext"; const Category = ({ results, token }) => { const [torrents, setTorrents] = useState(results?.torrents ?? []); @@ -24,11 +25,13 @@ const Category = ({ results, token }) => { (c) => slugify(c, { lower: true }) === categorySlug ); + const { getLocaleString } = useContext(LocaleContext); + return ( <> - + - Browse {category} + {getLocaleString("navBrowse")} {category} {torrents.length ? ( { token={token} /> ) : ( - No results. + {getLocaleString("catNoResults")} )} ); diff --git a/client/pages/categories/index.js b/client/pages/categories/index.js index 6ca5696..aa18c80 100644 --- a/client/pages/categories/index.js +++ b/client/pages/categories/index.js @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useContext } from "react"; import getConfig from "next/config"; import Link from "next/link"; import styled from "styled-components"; @@ -8,6 +8,7 @@ import { withAuth, withAuthServerSideProps } from "../../utils/withAuth"; import SEO from "../../components/SEO"; import Box from "../../components/Box"; import Text from "../../components/Text"; +import LocaleContext from "../../utils/LocaleContext"; import qs from "qs"; const CategoryItem = styled.li(() => @@ -29,12 +30,13 @@ const Categories = ({ tags }) => { const { publicRuntimeConfig: { SQ_TORRENT_CATEGORIES }, } = getConfig(); + const { getLocaleString } = useContext(LocaleContext); return ( <> - + - Categories + {getLocaleString("catCategories")} {Object.keys(SQ_TORRENT_CATEGORIES).length ? ( @@ -57,11 +59,11 @@ const Categories = ({ tags }) => { ))} ) : ( - No categories have been defined. + {getLocaleString("catNoCategoryHaveBeenDefined")} )}
- Tags + {getLocaleString("uploadTags")} {tags.length ? ( @@ -90,7 +92,7 @@ const Categories = ({ tags }) => { ))} ) : ( - No tags have been defined. + {getLocaleString("catNoTagsHaveBeenDefined")} )} ); diff --git a/client/pages/stats.js b/client/pages/stats.js index e7bb88d..a67090b 100644 --- a/client/pages/stats.js +++ b/client/pages/stats.js @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useContext } from "react"; import getConfig from "next/config"; import jwt from "jsonwebtoken"; import styled from "styled-components"; @@ -6,6 +6,7 @@ import css from "@styled-system/css"; import SEO from "../components/SEO"; import Text from "../components/Text"; import { withAuthServerSideProps } from "../utils/withAuth"; +import LocaleContext from "../utils/LocaleContext"; const StyledTable = styled.table(() => css({ @@ -22,15 +23,16 @@ const StyledTable = styled.table(() => ); const Stats = ({ stats, userRole }) => { + const { getLocaleString } = useContext(LocaleContext); if (userRole !== "admin") { - return You do not have permission to do that.; + return {getLocaleString("statYouNotPermission")}; } return ( <> - + - Tracker statistics + {getLocaleString("statTrackerStat")} {Object.entries(stats).map(([key, value]) => { From c80ea1c0a4f5400cef811a601734a37cee7070d6 Mon Sep 17 00:00:00 2001 From: smlinux Date: Sun, 10 Sep 2023 19:52:51 +0200 Subject: [PATCH 044/125] locales: more strings user (en, ru) --- client/locales.json | 34 ++++++++++++++++++++++++++ client/pages/user/[username].js | 43 ++++++++++++++++++--------------- 2 files changed, 57 insertions(+), 20 deletions(-) diff --git a/client/locales.json b/client/locales.json index 370749c..00805b5 100644 --- a/client/locales.json +++ b/client/locales.json @@ -109,6 +109,23 @@ "uploadTorrentUploadSuccess": "Torrent uploaded successfully", "uploadUpload": "Upload", "username": "Username", + "userUser": "User", + "userSuccessfully": "successfully", + "userCouldNot": "Could not", + "userProfile": "profile", + "userUserSince": "User since", + "userOnlyAdminsSee": "Only admins can see this", + "userInvitedBy": "Invited by", + "userEmailVerified": "Email verified", + "userRemainingInvites": "Remaining invites", + "userRatio": "Ratio", + "userDownloaded": "Downloaded", + "userComments": "Comments", + "userNoComments": "No comments.", + "userYouSureWant": "Are you sure that you want to", + "userThisUserQ": "this user?", + "userUploaded": "Uploaded", + "userMyUploads": "My uploads", "usernameRules": "Can only consist of letters, numbers, and “.”", "veCouldNotVerifyEmailAddress": "Could not verify email address:", "veEmailAddressVerifiedSuccess": "Email address verified successfully.", @@ -224,6 +241,23 @@ "uploadTorrentUploadSuccess": "Торрент успешно загружен", "uploadUpload": "Загрузить", "username": "Имя", + "userUser": "Пользователь", + "userSuccessfully": "успешно", + "userCouldNot": "Не удалось", + "userProfile": "профиль", + "userUserSince": "Пользователь с", + "userOnlyAdminsSee": "Это могут видеть только администраторы", + "userInvitedBy": "По приглашению", + "userEmailVerified": "Эл. почта подтверждена", + "userRemainingInvites": "Оставшиеся приглашения", + "userRatio": "Ratio(соотношение)", + "userDownloaded": "Скачано", + "userComments": "Комментарии", + "userNoComments": "Комментариев нет.", + "userYouSureWant": "Вы уверены, что хотите", + "userThisUserQ": "этого пользователя?", + "userUploaded": "Роздано", + "userMyUploads": "Мои раздачи", "usernameRules": "Может состоять только из букв, цифр и “.”", "veCouldNotVerifyEmailAddress": "Не удалось подтвердить адрес эл. почты:", "veEmailAddressVerifiedSuccess": "Адрес эл. почты успешно подтвержден.", diff --git a/client/pages/user/[username].js b/client/pages/user/[username].js index 7c322a1..9406db3 100644 --- a/client/pages/user/[username].js +++ b/client/pages/user/[username].js @@ -22,6 +22,7 @@ import Comment from "../../components/Comment"; import Modal from "../../components/Modal"; import { NotificationContext } from "../../components/Notifications"; import LoadingContext from "../../utils/LoadingContext"; +import LocaleContext from "../../utils/LocaleContext"; const User = ({ token, user, userRole }) => { const [banned, setBanned] = useState(!!user.banned); @@ -44,6 +45,8 @@ const User = ({ token, user, userRole }) => { const downloadedBytes = prettyBytes(user.downloaded?.bytes || 0).split(" "); const uploadedBytes = prettyBytes(user.uploaded?.bytes || 0).split(" "); + const { getLocaleString } = useContext(LocaleContext); + const handleBanUser = async () => { setLoading(true); @@ -65,7 +68,7 @@ const User = ({ token, user, userRole }) => { addNotification( "success", - `User ${banned ? "unbanned" : "banned"} successfully` + `${getLocaleString("User")} ${banned ? "unbanned" : "banned"} ${getLocaleString("userSuccessfully")}` ); setBanned((b) => !b); @@ -73,7 +76,7 @@ const User = ({ token, user, userRole }) => { } catch (e) { addNotification( "error", - `Could not ${banned ? "unban" : "ban"} user: ${e.message}` + `${getLocaleString("userCouldNot")} ${banned ? "unban" : "ban"} ${getLocaleString("User")}: ${e.message}` ); console.error(e); } @@ -90,7 +93,7 @@ const User = ({ token, user, userRole }) => { return ( <> - + { mb={3} > - {user.username}’s profile + {user.username} {getLocaleString("userProfile")} {user.role === "admin" && ( Admin @@ -113,7 +116,7 @@ const User = ({ token, user, userRole }) => { {cookies.username === user.username && ( - + )} @@ -124,7 +127,7 @@ const User = ({ token, user, userRole }) => { )} - User since {moment(user.created).format("Do MMM YYYY")} + {getLocaleString("userUserSince")} {moment(user.created).format("Do MMM YYYY")} {userRole === "admin" && ( @@ -134,26 +137,26 @@ const User = ({ token, user, userRole }) => { mb={3} _css={{ textTransform: "uppercase" }} > - Only admins can see this + {getLocaleString("userOnlyAdminsSee")}
    - {user.email &&
  • Email: {user.email}
  • } + {user.email &&
  • {getLocaleString("email")}: {user.email}
  • } {typeof user.emailVerified === "boolean" && ( -
  • Email verified: {user.emailVerified ? "yes" : "no"}
  • +
  • {getLocaleString("userEmailVerified")}: {user.emailVerified ? "yes" : "no"}
  • )} {user.invitedBy && (
  • - Invited by:{" "} + {getLocaleString("userInvitedBy")}:{" "} {user.invitedBy.username}
  • )} {typeof user.remainingInvites === "number" && ( -
  • Remaining invites: {user.remainingInvites}
  • +
  • {getLocaleString("userRemainingInvites")}: {user.remainingInvites}
  • )} {typeof user.bonusPoints === "number" && ( -
  • Bonus points: {user.bonusPoints}
  • +
  • {getLocaleString("accBonusPoints")}: {user.bonusPoints}
  • )}
@@ -174,7 +177,7 @@ const User = ({ token, user, userRole }) => { icon={Sort} iconColor="text" > - Ratio + {getLocaleString("userRatio")} {typeof user.ratio === "number" @@ -235,7 +238,7 @@ const User = ({ token, user, userRole }) => { icon={Download} iconColor="text" > - Downloaded + {getLocaleString("userDownloaded")} {downloadedBytes[0]} @@ -254,7 +257,7 @@ const User = ({ token, user, userRole }) => { icon={Upload} iconColor="text" > - Uploaded + {getLocaleString("userUploaded")} {uploadedBytes[0]} @@ -266,7 +269,7 @@ const User = ({ token, user, userRole }) => {
- Uploaded + {getLocaleString("userMyUploads")} { /> - Comments + {getLocaleString("userComments")} {user.comments?.length ? ( @@ -287,12 +290,12 @@ const User = ({ token, user, userRole }) => { ))} ) : ( - No comments. + {getLocaleString("userNoComments")} )} {showBanModal && ( setShowBanModal(false)}> - Are you sure that you want to {banned ? "unban" : "ban"} this user? + {getLocaleString("userYouSureWant")} {banned ? "unban" : "ban"} {getLocaleString("userThisUserQ")} From 9611d029323696146dfc67f49f8f16d8d8fc15f2 Mon Sep 17 00:00:00 2001 From: smlinux Date: Sun, 10 Sep 2023 20:34:32 +0200 Subject: [PATCH 045/125] locales: more strings search, tags (en, ru) --- client/locales.json | 6 ++++++ client/pages/search/[[...query]].js | 15 +++++++++------ client/pages/tags/[tag].js | 11 +++++++---- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/client/locales.json b/client/locales.json index 00805b5..4b53dd8 100644 --- a/client/locales.json +++ b/client/locales.json @@ -81,8 +81,11 @@ "registerFailed": "Could not register", "registrationClosed": "Registration closed", "resetPassword": "Reset password", + "searchSearchResults": "Search results for", + "searchSearchError": "Search error", "statYouNotPermission": "You do not have permission to do that.", "statTrackerStat": "Tracker statistics", + "tagTaggedWith": "Tagged with", "tokenError": "Token error", "totp": "One-time code", "uploadAddTag": "Add tag", @@ -213,8 +216,11 @@ "registerFailed": "Не удалось зарегистрироваться", "registrationClosed": "Регистрация закрыта", "resetPassword": "Сброс пароля", + "searchSearchResults": "Результат поиска для", + "searchSearchError": "Ошибка поиска", "statYouNotPermission": "У вас нет прав для просмотра.", "statTrackerStat": "Статистика трекера", + "tagTaggedWith": "С метками", "tokenError": "Ошибка токена", "totp": "Одноразовый код", "uploadAddTag": "Добавить метку", diff --git a/client/pages/search/[[...query]].js b/client/pages/search/[[...query]].js index 01bf139..4e4e098 100644 --- a/client/pages/search/[[...query]].js +++ b/client/pages/search/[[...query]].js @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useContext, useEffect } from "react"; import getConfig from "next/config"; import { useRouter } from "next/router"; import qs from "qs"; @@ -8,6 +8,7 @@ import Text from "../../components/Text"; import Input from "../../components/Input"; import Button from "../../components/Button"; import Box from "../../components/Box"; +import LocaleContext from "../../utils/LocaleContext"; import TorrentList from "../../components/TorrentList"; const Search = ({ results, error, token }) => { @@ -34,18 +35,20 @@ const Search = ({ results, error, token }) => { if (query) router.push(`/search/${encodeURIComponent(query)}`); }; + const { getLocaleString } = useContext(LocaleContext); + return ( <> - + - {query ? `Search results for “${query}”` : "Search"} + {query ? `${getLocaleString("searchSearchResults")} “${query}”` : `${getLocaleString("indexSearch")}`} - + {error ? ( - Search error: {error} + {getLocaleString("searchSearchError")}: {error} ) : ( <> {query && ( @@ -60,7 +63,7 @@ const Search = ({ results, error, token }) => { token={token} /> ) : ( - No results. + {getLocaleString("catNoResults")} )} )} diff --git a/client/pages/tags/[tag].js b/client/pages/tags/[tag].js index 73d2d89..2201346 100644 --- a/client/pages/tags/[tag].js +++ b/client/pages/tags/[tag].js @@ -1,10 +1,11 @@ -import React, { useState } from "react"; +import React, { useState, useContext } from "react"; import { useRouter } from "next/router"; import getConfig from "next/config"; import qs from "qs"; import { withAuthServerSideProps } from "../../utils/withAuth"; import SEO from "../../components/SEO"; import Text from "../../components/Text"; +import LocaleContext from "../../utils/LocaleContext"; import TorrentList from "../../components/TorrentList"; const Tag = ({ results, token }) => { @@ -19,11 +20,13 @@ const Tag = ({ results, token }) => { publicRuntimeConfig: { SQ_TORRENT_CATEGORIES, SQ_API_URL }, } = getConfig(); + const { getLocaleString } = useContext(LocaleContext); + return ( <> - + - Tagged with “{tag}” + {getLocaleString("tagTaggedWith")} “{tag}” {torrents.length ? ( { token={token} /> ) : ( - No results. + {getLocaleString("catNoResults")} )} ); From 10698406c7536f1090776fb11226b3718f3211c1 Mon Sep 17 00:00:00 2001 From: smlinux Date: Mon, 11 Sep 2023 07:10:36 +0200 Subject: [PATCH 046/125] locales: more strings requests (en, ru) --- client/components/List.js | 8 +++- client/locales.json | 63 +++++++++++++++++++++++++++++ client/pages/requests/[index].js | 68 ++++++++++++++++++++------------ client/pages/requests/index.js | 22 ++++++----- client/pages/requests/new.js | 23 +++++++---- client/pages/user/[username].js | 2 +- 6 files changed, 141 insertions(+), 45 deletions(-) diff --git a/client/components/List.js b/client/components/List.js index ec9c607..008d47f 100644 --- a/client/components/List.js +++ b/client/components/List.js @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useContext, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/router"; import { toPath } from "lodash"; @@ -7,6 +7,7 @@ import { CaretUp } from "@styled-icons/boxicons-regular/CaretUp"; import { CaretDown } from "@styled-icons/boxicons-regular/CaretDown"; import Box from "../components/Box"; import Text from "../components/Text"; +import LocaleContext from "../utils/LocaleContext"; const getIn = (obj, key, p = 0) => { const path = toPath(key); @@ -62,6 +63,9 @@ const getSortIcon = (accessor, sort = "") => { const List = ({ data = [], columns = [], ...rest }) => { const router = useRouter(); const { sort } = router.query; + + const { getLocaleString } = useContext(LocaleContext); + return ( @@ -165,7 +169,7 @@ const List = ({ data = [], columns = [], ...rest }) => { ) : ( - No items to show. + {getLocaleString("listNoItemShow")} )} diff --git a/client/locales.json b/client/locales.json index 4b53dd8..ac3cb4a 100644 --- a/client/locales.json +++ b/client/locales.json @@ -56,6 +56,7 @@ "indexSearchTorrents": "Search torrents", "indexText1": "Your email address is not yet verified. You will not be able to upload or download any data until this is done.", "indexTime": "HH:mm Do MMM YYYY", + "listNoItemShow": "No items to show.", "logIn": "Log in", "logInFailed": "Could not log in", "navAnnouncements": "Announcements", @@ -80,6 +81,35 @@ "register": "Register", "registerFailed": "Could not register", "registrationClosed": "Registration closed", + "reqCreateNew": "Create new", + "reqTitle": "Title", + "reqPostedBy": "Posted by", + "reqPost": "Post", + "reqRequestCreatedSuccess": "Request created successfully", + "reqCouldNotCreateReq": "Could not create request", + "reqNewRequest": "New request", + "reqWhatYouLookForQ": "What are you looking for?", + "reqRequestDelSuccess": "Request deleted successfully", + "reqCouldNotDelReq": "Could not delete request", + "reqCommentPostSuccess": "Comment posted successfully", + "reqCommentNotPost": "Could not post comment", + "reqSuggestionAddSuccess": "Suggestion added successfully", + "reqSuggestionNotAdded": "Could not add suggestion", + "reqSuggestionAcceptSuccess": "Suggestion accepted successfully", + "reqCouldNotAcceptSuggestion": "Could not accept suggestion", + "reqDelete": "Delete", + "reqSuggestedTorrents": "Suggested torrents", + "reqSuggestATorrent": "Suggest a torrent", + "reqAccepted": "Accepted", + "reqAccept": "Accept", + "reqNoTorrentsHaveBeenSuggestedYet": "No torrents have been suggested yet.", + "reqPostAComment": "Post a comment", + "reqEnterInfohashTorrentBelow": "Enter the infohash of a torrent below.", + "reqFulfilled": "Fulfilled", + "reqPosted": "Posted", + "reqBy": "by", + "reqInfohash": "Infohash", + "reqSuggest": "Suggest", "resetPassword": "Reset password", "searchSearchResults": "Search results for", "searchSearchError": "Search error", @@ -117,6 +147,7 @@ "userCouldNot": "Could not", "userProfile": "profile", "userUserSince": "User since", + "userUserSinceTime": "Do MMM YYYY", "userOnlyAdminsSee": "Only admins can see this", "userInvitedBy": "Invited by", "userEmailVerified": "Email verified", @@ -194,6 +225,7 @@ "indexSearchTorrents": "Поиск торрентов", "indexText1": "Ваш адрес эл. почты еще не подтвержден. Пока это не будет сделано, вы не сможете загружать или скачивать.", "indexTime": "DD.MM.YYYY HH:mm", + "listNoItemShow": "Нет элементов для отображения.", "logIn": "Вход", "logInFailed": "Не удалось войти", "navAnnouncements": "Анонсы", @@ -215,6 +247,36 @@ "register": "Регистрация", "registerFailed": "Не удалось зарегистрироваться", "registrationClosed": "Регистрация закрыта", + "reqCreateNew": "Создать новый", + "reqTitle": "Заголовок", + "reqPostedBy": "Сообщение от", + "reqPost": "Опубликовать", + "reqTitle": "Заголовок", + "reqRequestCreatedSuccess": "Запрос успешно создан", + "reqCouldNotCreateReq": "Не удалось создать запрос", + "reqNewRequest": "Новый запрос", + "reqWhatYouLookForQ": "Что Вы ищете?", + "reqRequestDelSuccess": "Запрос успешно удален", + "reqCouldNotDelReq": "Не удалось удалить запрос", + "reqCommentPostSuccess": "Комментарий успешно опубликован", + "reqCommentNotPost": "Не удалось опубликовать комментарий", + "reqSuggestionAddSuccess": "Предложение успешно добавлено", + "reqSuggestionNotAdded": "Не удалось добавить предложение", + "reqSuggestionAcceptSuccess": "Предложение успешно принято", + "reqCouldNotAcceptSuggestion": "Не удалось принять предложение", + "reqDelete": "Удалить", + "reqSuggestedTorrents": "Предлагаемые торренты", + "reqSuggestATorrent": "Предложить торрент", + "reqAccepted": "Принято", + "reqAccept": "Принять", + "reqNoTorrentsHaveBeenSuggestedYet": "Торренты пока не предложены.", + "reqPostAComment": "Оставить комментарий", + "reqEnterInfohashTorrentBelow": "Введите информационный хэш торрента ниже.", + "reqFulfilled": "Выполнено", + "reqPosted": "Опубликовано", + "reqBy": "пользователем", + "reqInfohash": "Инфохеш", + "reqSuggest": "Предложить", "resetPassword": "Сброс пароля", "searchSearchResults": "Результат поиска для", "searchSearchError": "Ошибка поиска", @@ -252,6 +314,7 @@ "userCouldNot": "Не удалось", "userProfile": "профиль", "userUserSince": "Пользователь с", + "userUserSinceTime": "DD.MM.YYYY", "userOnlyAdminsSee": "Это могут видеть только администраторы", "userInvitedBy": "По приглашению", "userEmailVerified": "Эл. почта подтверждена", diff --git a/client/pages/requests/[index].js b/client/pages/requests/[index].js index 2d3939b..b0e3401 100644 --- a/client/pages/requests/[index].js +++ b/client/pages/requests/[index].js @@ -23,6 +23,7 @@ import slugify from "slugify"; import { Check } from "@styled-icons/boxicons-regular/Check"; import { X } from "@styled-icons/boxicons-regular/X"; import LoadingContext from "../../utils/LoadingContext"; +import LocaleContext from "../../utils/LocaleContext"; const Request = ({ request, token, user }) => { const [comments, setComments] = useState(request.comments); @@ -32,6 +33,7 @@ const Request = ({ request, token, user }) => { const { addNotification } = useContext(NotificationContext); const { setLoading } = useContext(LoadingContext); + const { getLocaleString } = useContext(LocaleContext); const commentInputRef = useRef(); @@ -63,11 +65,15 @@ const Request = ({ request, token, user }) => { throw new Error(reason); } - addNotification("success", "Request deleted successfully"); + addNotification("success", + `${getLocaleString("reqRequestDelSuccess")}` + ); router.push("/requests"); } catch (e) { - addNotification("error", `Could not delete request: ${e.message}`); + addNotification("error", + `${getLocaleString("reqCouldNotDelReq")}: ${e.message}` + ); console.error(e); } @@ -99,7 +105,9 @@ const Request = ({ request, token, user }) => { throw new Error(reason); } - addNotification("success", "Comment posted successfully"); + addNotification("success", + `${getLocaleString("reqCommentPostSuccess")}` + ); setComments((c) => { const newComment = { @@ -114,7 +122,9 @@ const Request = ({ request, token, user }) => { commentInputRef.current.value = ""; } catch (e) { - addNotification("error", `Could not post comment: ${e.message}`); + addNotification("error", + `${getLocaleString("reqCommentNotPost")}: ${e.message}` + ); console.error(e); } @@ -146,14 +156,18 @@ const Request = ({ request, token, user }) => { throw new Error(reason); } - addNotification("success", "Suggestion added successfully"); + addNotification("success", + `${getLocaleString("reqSuggestionAddSuccess")}` + ); const { torrent } = await suggestRes.json(); setCandidates((existing) => [torrent, ...existing]); setShowSuggestModal(false); } catch (e) { - addNotification("error", `Could not add suggestion: ${e.message}`); + addNotification("error", + `${getLocaleString("reqSuggestionNotAdded")}: ${e.message}` + ); console.error(e); } @@ -183,12 +197,16 @@ const Request = ({ request, token, user }) => { throw new Error(reason); } - addNotification("success", "Suggestion accepted successfully"); + addNotification("success", + `${getLocaleString("reqSuggestionAcceptSuccess")}` + ); const { torrent } = await acceptRes.json(); setFulfilledBy(torrent); } catch (e) { - addNotification("error", `Could not accept suggestion: ${e.message}`); + addNotification("error", + `${getLocaleString("reqCouldNotAcceptSuggestion")}: ${e.message}` + ); console.error(e); } @@ -209,13 +227,13 @@ const Request = ({ request, token, user }) => { {user === request.createdBy?._id && ( )} - Posted {moment(request.created).format("HH:mm Do MMM YYYY")} by{" "} + {getLocaleString("reqPosted")} {moment(request.created).format(`${getLocaleString("indexTime")}`)} {getLocaleString("reqBy")}{" "} {request.createdBy?.username ? ( {request.createdBy.username} @@ -239,12 +257,12 @@ const Request = ({ request, token, user }) => { justifyContent="space-between" mb={4} > - Suggested torrents + {getLocaleString("reqSuggestedTorrents")} {candidates.length ? ( @@ -255,7 +273,7 @@ const Request = ({ request, token, user }) => { }))} columns={[ { - header: "Name", + header: `${getLocaleString("uploadName")}`, accessor: "name", cell: ({ value, row }) => ( @@ -270,7 +288,7 @@ const Request = ({ request, token, user }) => { gridWidth: "2fr", }, { - header: "Category", + header: `${getLocaleString("uploadCategory")}`, accessor: "type", cell: ({ value }) => ( @@ -282,7 +300,7 @@ const Request = ({ request, token, user }) => { gridWidth: "1fr", }, { - header: "Accepted", + header: `${getLocaleString("reqAccepted")}`, accessor: "_id", cell: ({ value }) => ( @@ -296,10 +314,10 @@ const Request = ({ request, token, user }) => { gridWidth: "1fr", }, { - header: "Uploaded", + header: `${getLocaleString("userUploaded")}`, accessor: "created", cell: ({ value }) => ( - {moment(value).format("Do MMM YYYY")} + {moment(value).format(`${getLocaleString("indexTime")}`)} ), gridWidth: "175px", rightAlign: true, @@ -316,7 +334,7 @@ const Request = ({ request, token, user }) => { disabled={!!fulfilledBy} small > - Accept + {getLocaleString("reqAccept")} ), gridWidth: "90px", @@ -327,23 +345,23 @@ const Request = ({ request, token, user }) => { ]} /> ) : ( - No torrents have been suggested yet. + {getLocaleString("reqNoTorrentsHaveBeenSuggestedYet")} )} - Comments + {getLocaleString("userComments")}
{!!comments?.length && ( @@ -359,7 +377,7 @@ const Request = ({ request, token, user }) => {
{showSuggestModal && ( setShowSuggestModal(false)}> - Enter the infohash of a torrent below. + {getLocaleString("reqEnterInfohashTorrentBelow")}
@@ -369,9 +387,9 @@ const Request = ({ request, token, user }) => { variant="secondary" mr={3} > - Cancel + {getLocaleString("accCancel")} - +
diff --git a/client/pages/requests/index.js b/client/pages/requests/index.js index 1dc1099..dbd566a 100644 --- a/client/pages/requests/index.js +++ b/client/pages/requests/index.js @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useContext } from "react"; import getConfig from "next/config"; import Link from "next/link"; import jwt from "jsonwebtoken"; @@ -11,21 +11,25 @@ import Text from "../../components/Text"; import { withAuthServerSideProps } from "../../utils/withAuth"; import Button from "../../components/Button"; import List from "../../components/List"; +import LocaleContext from "../../utils/LocaleContext"; const Requests = ({ requests }) => { + + const { getLocaleString } = useContext(LocaleContext); + return ( <> - + - Requests + {getLocaleString("navRequests")} - + @@ -36,19 +40,19 @@ const Requests = ({ requests }) => { }))} columns={[ { - header: "Title", + header: `${getLocaleString("reqTitle")}`, accessor: "title", cell: ({ value }) => {value}, gridWidth: "1fr", }, { - header: "Posted by", + header: `${getLocaleString("reqPostedBy")}`, accessor: "createdBy.username", cell: ({ value }) => {value ?? "deleted user"}, gridWidth: "0.5fr", }, { - header: "Fulfilled", + header: `${getLocaleString("reqFulfilled")}`, accessor: "fulfilledBy", cell: ({ value }) => ( @@ -58,10 +62,10 @@ const Requests = ({ requests }) => { gridWidth: "100px", }, { - header: "Created", + header: `${getLocaleString("accCreated")}`, accessor: "created", cell: ({ value }) => ( - {moment(value).format("HH:mm Do MMM YYYY")} + {moment(value).format(`${getLocaleString("indexTime")}`)} ), rightAlign: true, gridWidth: "175px", diff --git a/client/pages/requests/new.js b/client/pages/requests/new.js index d57581d..1554bc2 100644 --- a/client/pages/requests/new.js +++ b/client/pages/requests/new.js @@ -10,6 +10,7 @@ import { withAuthServerSideProps } from "../../utils/withAuth"; import { NotificationContext } from "../../components/Notifications"; import LoadingContext from "../../utils/LoadingContext"; import MarkdownInput from "../../components/MarkdownInput"; +import LocaleContext from "../../utils/LocaleContext"; const NewRequest = ({ token }) => { const { addNotification } = useContext(NotificationContext); @@ -21,6 +22,8 @@ const NewRequest = ({ token }) => { publicRuntimeConfig: { SQ_API_URL }, } = getConfig(); + const { getLocaleString } = useContext(LocaleContext); + const handleCreate = async (e) => { e.preventDefault(); setLoading(true); @@ -44,12 +47,16 @@ const NewRequest = ({ token }) => { throw new Error(reason); } - addNotification("success", "Request created successfully"); + addNotification("success", + `${getLocaleString("reqRequestCreatedSuccess")}` + ); const { index } = await createRequestRes.json(); router.push(`/requests/${index}`); } catch (e) { - addNotification("error", `Could not create request: ${e.message}`); + addNotification("error", + `${getLocaleString("reqCouldNotCreateReq")}: ${e.message}` + ); console.error(e); } @@ -58,22 +65,22 @@ const NewRequest = ({ token }) => { return ( <> - + - New request + {getLocaleString("reqNewRequest")}
{ )} - {getLocaleString("userUserSince")} {moment(user.created).format("Do MMM YYYY")} + {getLocaleString("userUserSince")} {moment(user.created).format(`${getLocaleString("userUserSinceTime")}`)} {userRole === "admin" && ( From b1775896983b9dbacf9815fb6255c5582eec47ad Mon Sep 17 00:00:00 2001 From: smlinux Date: Mon, 11 Sep 2023 07:37:39 +0200 Subject: [PATCH 047/125] locales: more strings torrentlist (en, ru) --- client/components/TorrentList.js | 21 ++++++++++++--------- client/locales.json | 6 ++++++ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/client/components/TorrentList.js b/client/components/TorrentList.js index 053c183..83ce0c9 100644 --- a/client/components/TorrentList.js +++ b/client/components/TorrentList.js @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import React, { useContext, useEffect } from "react"; import getConfig from "next/config"; import { useRouter } from "next/router"; import moment from "moment"; @@ -18,6 +18,7 @@ import List from "./List"; import Text from "./Text"; import Box from "./Box"; import Button from "./Button"; +import LocaleContext from "../utils/LocaleContext"; const pageSize = 25; @@ -75,6 +76,8 @@ const TorrentList = ({ if (fetchPath && token) fetchTorrents(); }, [sort, page]); + const { getLocaleString } = useContext(LocaleContext); + return ( <> ( @@ -102,7 +105,7 @@ const TorrentList = ({ gridWidth: "2fr", }, { - header: "Category", + header: `${getLocaleString("uploadCategory")}`, accessor: "type", cell: ({ value }) => { const category = @@ -118,7 +121,7 @@ const TorrentList = ({ gridWidth: "1fr", }, { - header: "Seeders", + header: `${getLocaleString("torrSeeders")}`, accessor: "seeders", cell: ({ value }) => ( ( ( ( ( - {moment(value).format("Do MMM YYYY")} + {moment(value).format(`${getLocaleString("userUserSinceTime")}`)} ), gridWidth: "140px", rightAlign: true, diff --git a/client/locales.json b/client/locales.json index ac3cb4a..329644e 100644 --- a/client/locales.json +++ b/client/locales.json @@ -117,6 +117,9 @@ "statTrackerStat": "Tracker statistics", "tagTaggedWith": "Tagged with", "tokenError": "Token error", + "torrSeeders": "Seeders", + "torrLeechers": "Leechers", + "torrDownloads": "Downloads", "totp": "One-time code", "uploadAddTag": "Add tag", "uploadAnnounceURL": "Announce URL", @@ -284,6 +287,9 @@ "statTrackerStat": "Статистика трекера", "tagTaggedWith": "С метками", "tokenError": "Ошибка токена", + "torrSeeders": "Сидеры", + "torrLeechers": "Личеры", + "torrDownloads": "Загрузки", "totp": "Одноразовый код", "uploadAddTag": "Добавить метку", "uploadAnnounceURL": "Адрес трекера", From 871bb561361182e473ff9da27d16148bd51bb501 Mon Sep 17 00:00:00 2001 From: smlinux Date: Mon, 11 Sep 2023 07:47:18 +0200 Subject: [PATCH 048/125] locales: fix uploaded torrentlist (en, ru) --- client/components/TorrentList.js | 2 +- client/locales.json | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/client/components/TorrentList.js b/client/components/TorrentList.js index 83ce0c9..06a5644 100644 --- a/client/components/TorrentList.js +++ b/client/components/TorrentList.js @@ -181,7 +181,7 @@ const TorrentList = ({ sortable: !!token, }, { - header: `${getLocaleString("userUploaded")}`, + header: `${getLocaleString("torrUploaded")}`, accessor: "created", cell: ({ value }) => ( {moment(value).format(`${getLocaleString("userUserSinceTime")}`)} diff --git a/client/locales.json b/client/locales.json index 329644e..5866699 100644 --- a/client/locales.json +++ b/client/locales.json @@ -120,6 +120,7 @@ "torrSeeders": "Seeders", "torrLeechers": "Leechers", "torrDownloads": "Downloads", + "torrUploaded": "Uploaded", "totp": "One-time code", "uploadAddTag": "Add tag", "uploadAnnounceURL": "Announce URL", @@ -290,6 +291,7 @@ "torrSeeders": "Сидеры", "torrLeechers": "Личеры", "torrDownloads": "Загрузки", + "torrUploaded": "Дата загрузки", "totp": "Одноразовый код", "uploadAddTag": "Добавить метку", "uploadAnnounceURL": "Адрес трекера", From a928869b1b22751149916b6a2fadb4f3962dad61 Mon Sep 17 00:00:00 2001 From: smlinux Date: Tue, 12 Sep 2023 05:57:55 +0200 Subject: [PATCH 049/125] locales: more strings comment, markdown (en, ru) --- client/components/Comment.js | 12 ++++++++---- client/components/MarkdownInput.js | 8 ++++++-- client/locales.json | 6 ++++++ 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/client/components/Comment.js b/client/components/Comment.js index 200bac6..f04a9f9 100644 --- a/client/components/Comment.js +++ b/client/components/Comment.js @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useContext } from "react"; import Link from "next/link"; import moment from "moment"; import { Comment as CommentIcon } from "@styled-icons/boxicons-regular/Comment"; @@ -7,8 +7,12 @@ import { News } from "@styled-icons/boxicons-regular/News"; import { CommentAdd } from "@styled-icons/boxicons-regular/CommentAdd"; import Box from "./Box"; import Text from "./Text"; +import LocaleContext from "../utils/LocaleContext"; const Comment = ({ comment }) => { + + const { getLocaleString } = useContext(LocaleContext); + return ( { ) : ( - Comment by{" "} + {getLocaleString("comCommentBy")}{" "} - deleted user + {getLocaleString("comDelUser")} )} - Posted {moment(comment.created).format("HH:mm Do MMM YYYY")} + {getLocaleString("reqPosted")} {moment(comment.created).format(`${getLocaleString("indexTime")}`)} {comment.comment} diff --git a/client/components/MarkdownInput.js b/client/components/MarkdownInput.js index e5fb42c..7231af5 100644 --- a/client/components/MarkdownInput.js +++ b/client/components/MarkdownInput.js @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useContext, useState } from "react"; import Link from "next/link"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; @@ -7,9 +7,13 @@ import Box from "./Box"; import Text from "./Text"; import Infobox from "./Infobox"; import MarkdownBody from "./MarkdownBody"; +import LocaleContext from "../utils/LocaleContext"; const MarkdownInput = ({ defaultValue, mb, ...rest }) => { const [bodyValue, setBodyValue] = useState(defaultValue); + + const { getLocaleString } = useContext(LocaleContext); + return ( { fontSize={1} _css={{ textTransform: "uppercase" }} > - Markdown preview + {getLocaleString("mdMarkdownPreview")} diff --git a/client/locales.json b/client/locales.json index 5866699..4ed30c0 100644 --- a/client/locales.json +++ b/client/locales.json @@ -49,6 +49,8 @@ "catNoCategoryHaveBeenDefined": "No categories have been defined.", "catNoTagsHaveBeenDefined": "No tags have been defined.", "catNoResults": "No results.", + "comCommentBy": "Comment by", + "comDelUser": "deleted user", "email": "Email", "indexLatestAnnounce": "Latest announcement", "indexLatestTorrents": "Latest torrents", @@ -59,6 +61,7 @@ "listNoItemShow": "No items to show.", "logIn": "Log in", "logInFailed": "Could not log in", + "mdMarkdownPreview": "Markdown preview", "navAnnouncements": "Announcements", "navBookmarks": "Bookmarks", "navBrowse": "Browse", @@ -222,6 +225,8 @@ "catNoCategoryHaveBeenDefined": "Категории не определены.", "catNoTagsHaveBeenDefined": "Метки не определены.", "catNoResults": "Нет результатов.", + "comCommentBy": "Комментарий от", + "comDelUser": "удаленный пользователь", "email": "Эл. почта", "indexLatestAnnounce": "Последний анонс", "indexLatestTorrents": "Последние торренты", @@ -232,6 +237,7 @@ "listNoItemShow": "Нет элементов для отображения.", "logIn": "Вход", "logInFailed": "Не удалось войти", + "mdMarkdownPreview": "Markdown предпросмотр", "navAnnouncements": "Анонсы", "navBookmarks": "Закладки", "navBrowse": "Категории", From 0ea4619a622ac9a609ae57375c3b87147c7d5a68 Mon Sep 17 00:00:00 2001 From: smlinux Date: Tue, 12 Sep 2023 06:53:34 +0200 Subject: [PATCH 050/125] locales: more strings torrent (en, ru) --- client/locales.json | 49 ++++++++++++- client/pages/torrent/[infoHash].js | 109 ++++++++++++++++++----------- 2 files changed, 118 insertions(+), 40 deletions(-) diff --git a/client/locales.json b/client/locales.json index 4ed30c0..c68c0db 100644 --- a/client/locales.json +++ b/client/locales.json @@ -124,6 +124,30 @@ "torrLeechers": "Leechers", "torrDownloads": "Downloads", "torrUploaded": "Uploaded", + "torrTorrEditSuccess": "Torrent edited successfully", + "torrCouldEditTorr": "Could edit torrent", + "torrTorrDelSuccess": "Torrent deleted successfully", + "torrCouldNotDelTorr": "Could not delete torrent", + "torrVoteSubmitSuccess": "Vote submitted successfully", + "torrCouldNotSubmitVote": "Could not submit vote", + "torrReportSubmitSuccess": "Report submitted successfully", + "torrCouldNotSubmitReport": "Could not submit report", + "torrFLToggleSuccess": "Freeleech toggled successfully", + "torrCouldNotToggleFL": "Could not toggle freeleech", + "torrTorrRemFromGroupSuccess": "Torrent removed from group successfully", + "torrCouldNotRemTorrFromGroup": "Could not remove torrent from group", + "torrTorrent": "Torrent", + "torrCouldNotBookmarkTorr": "Could not bookmark torrent", + "torrFL": "FL!", + "torrEdit": "Edit", + "torrDownload": "Download", + "torrLogInDownload": "Log in to download", + "torrFiles": "Files", + "torrReport": "Report", + "torrRemTorr": "Remove this torrent", + "torrAddTorr": "Add a torrent", + "torrReasonForReport": "Reason for report", + "torrSureDeleteTorr": "Are you sure you want to delete this torrent? This cannot be undone.", "totp": "One-time code", "uploadAddTag": "Add tag", "uploadAnnounceURL": "Announce URL", @@ -298,6 +322,29 @@ "torrLeechers": "Личеры", "torrDownloads": "Загрузки", "torrUploaded": "Дата загрузки", + "torrTorrEditSuccess": "Торрент успешно отредактирован", + "torrCouldEditTorr": "Можно редактировать торрент", + "torrTorrDelSuccess": "Торрент успешно удален", + "torrCouldNotDelTorr": "Не удалось удалить торрент", + "torrVoteSubmitSuccess": "Голосование отправлено успешно", + "torrCouldNotSubmitVote": "Не удалось проголосовать", + "torrReportSubmitSuccess": "Отчет успешно отправлен", + "torrCouldNotSubmitReport": "Не удалось отправить отчет", + "torrFLToggleSuccess": "Фрилич успешно переключен", + "torrCouldNotToggleFL": "Не удалось переключить фрилич", + "torrTorrRemFromGroupSuccess": "Торрент успешно удален из группы", + "torrCouldNotRemTorrFromGroup": "Не удалось удалить торрент из группы", + "torrTorrent": "Торрент", + "torrCouldNotBookmarkTorr": "Не удалось добавить торрент в закладки", + "torrEdit": "Изменить", + "torrDownload": "Загрузить", + "torrLogInDownload": "Войдите, чтобы скачать", + "torrFiles": "Файлы", + "torrReport": "Отчет", + "torrRemTorr": "Удалить этот торрент", + "torrAddTorr": "Добавить торрент", + "torrReasonForReport": "Причина отчета", + "torrSureDeleteTorr": "Вы уверены, что хотите удалить этот торрент? Это действие нельзя отменить.", "totp": "Одноразовый код", "uploadAddTag": "Добавить метку", "uploadAnnounceURL": "Адрес трекера", @@ -315,7 +362,7 @@ "uploadInfoBox2": "Похоже, что эти существующие торренты имеют схожие названия. Хотите сгруппировать загрузку с каким-либо из них? Группы должны содержать только очень похожий контент, например один и тот же фильм в разных форматах.", "uploadInfoBox3": "Примечание. Если вы начали раздачу торрента перед загрузкой, вам может потребоваться обновить трекеры в вашем торрент-клиенте после завершения загрузки.", "uploadMarkdownSupport": "Поддержка Markdown", - "uploadMediaInfo": "Медиаинформация", + "uploadMediaInfo": "Медиаинфо", "uploadName": "Имя", "uploadSource": "Источник", "uploadTags": "Метки", diff --git a/client/pages/torrent/[infoHash].js b/client/pages/torrent/[infoHash].js index b64b24c..32490d9 100644 --- a/client/pages/torrent/[infoHash].js +++ b/client/pages/torrent/[infoHash].js @@ -30,6 +30,7 @@ import { NotificationContext } from "../../components/Notifications"; import LoadingContext from "../../utils/LoadingContext"; import { TorrentFields } from "../upload"; import MarkdownInput from "../../components/MarkdownInput"; +import LocaleContext from "../utils/LocaleContext"; // from https://stackoverflow.com/a/44681235/7739519 const insert = (children = [], [head, ...tail], size) => { @@ -174,6 +175,8 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { setLoading(true); const form = new FormData(e.target); + const { getLocaleString } = useContext(LocaleContext); + try { const uploadRes = await fetch( `${SQ_API_URL}/torrent/edit/${torrent.infoHash}`, @@ -199,11 +202,15 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { throw new Error(reason); } - addNotification("success", "Torrent edited successfully"); + addNotification("success", + `${getLocaleString("torrTorrEditSuccess")}` + ); window.location.reload(); } catch (e) { - addNotification("error", `Could edit torrent: ${e.message}`); + addNotification("error", + `${getLocaleString("torrCouldEditTorr")}: ${e.message}` + ); console.error(e); } @@ -229,11 +236,15 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { throw new Error(reason); } - addNotification("success", "Torrent deleted successfully"); + addNotification("success", + `${getLocaleString("torrTorrDelSuccess")}` + ); router.push("/"); } catch (e) { - addNotification("error", `Could not delete torrent: ${e.message}`); + addNotification("error", + `${getLocaleString("torrCouldNotDelTorr")}: ${e.message}` + ); console.error(e); } @@ -265,7 +276,9 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { throw new Error(reason); } - addNotification("success", "Comment posted successfully"); + addNotification("success", + `${getLocaleString("reqCommentPostSuccess")}` + ); setComments((c) => { const newComment = { @@ -280,7 +293,9 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { commentInputRef.current.value = ""; } catch (e) { - addNotification("error", `Could not post comment: ${e.message}`); + addNotification("error", + `${getLocaleString("reqCommentNotPost")}: ${e.message}` + ); console.error(e); } @@ -330,9 +345,13 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { throw new Error(reason); } - addNotification("success", "Vote submitted successfully"); + addNotification("success", + `${getLocaleString("torrVoteSubmitSuccess")}` + ); } catch (e) { - addNotification("error", `Could not submit vote: ${e.message}`); + addNotification("error", + `${getLocaleString("torrCouldNotSubmitVote")}: ${e.message}` + ); console.error(e); } @@ -364,11 +383,15 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { throw new Error(reason); } - addNotification("success", "Report submitted successfully"); + addNotification("success", + `${getLocaleString("torrReportSubmitSuccess")}` + ); setShowReportModal(false); } catch (e) { - addNotification("error", `Could not submit report: ${e.message}`); + addNotification("error", + `${getLocaleString("torrCouldNotSubmitReport")}: ${e.message}` + ); console.error(e); } @@ -394,11 +417,15 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { throw new Error(reason); } - addNotification("success", "Freeleech toggled successfully"); + addNotification("success", + `${getLocaleString("torrFLToggleSuccess")}` + ); setIsFreeleech((f) => !f); } catch (e) { - addNotification("error", `Could not toggle freeleech: ${e.message}`); + addNotification("error", + `${getLocaleString("torrCouldNotToggleFL")}: ${e.message}` + ); console.error(e); } @@ -424,13 +451,15 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { throw new Error(reason); } - addNotification("success", "Torrent removed from group successfully"); + addNotification("success", + `${getLocaleString("torrTorrRemFromGroupSuccess")}` + ); setHasGroup(false); } catch (e) { addNotification( "error", - `Could not remove torrent from group: ${e.message}` + `${getLocaleString("torrCouldNotRemTorrFromGroup")}: ${e.message}` ); console.error(e); } @@ -459,12 +488,14 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { addNotification( "success", - `Torrent ${bookmarked ? "removed from" : "added to"} bookmarks` + `${getLocaleString("torrTorrent")} ${bookmarked ? "removed from" : "added to"} bookmarks` ); setBookmarked((b) => !b); } catch (e) { - addNotification("error", `Could not bookmark torrent: ${e.message}`); + addNotification("error", + `${getLocaleString("torrCouldNotBookmarkTorr")}: ${e.message}` + ); console.error(e); } @@ -504,7 +535,7 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { {torrent.name} {(torrent.freeleech || SQ_SITE_WIDE_FREELEECH === true) && ( - FL! + {getLocaleString("torrFL")} )} @@ -530,14 +561,14 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { variant="secondary" mr={3} > - Edit + {getLocaleString("torrEdit")} )} @@ -557,11 +588,11 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { target="_blank" disabled={downloadDisabled} > - Download .torrent + {getLocaleString("torrDownload")} .torrent ) : ( - + )} @@ -581,7 +612,7 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { )} ), - Category: category ? ( + {getLocaleString("uploadCategory")}: category ? ( { {source} ) : undefined, - Date: moment(torrent.created).format("HH:mm Do MMM YYYY"), + Date: moment(torrent.created).format(`${getLocaleString("indexTime")}`), "Info hash": ( { _css={{ textTransform: "uppercase" }} mb={3} > - Description + {getLocaleString("uploadDescription")} @@ -640,7 +671,7 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { _css={{ textTransform: "uppercase" }} mb={3} > - MediaInfo + {getLocaleString("uploadMediaInfo")} {torrent.mediaInfo} @@ -654,7 +685,7 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { _css={{ textTransform: "uppercase" }} mb={3} > - Tags + {getLocaleString("uploadTags")} {torrent.tags.filter((t) => !!t).length ? ( @@ -693,7 +724,7 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { _css={{ textTransform: "uppercase" }} mb={3} > - Files + {getLocaleString("torrFiles")} {parsedFiles.sort(sortName).map((file, i) => ( @@ -738,7 +769,7 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { onClick={() => setShowReportModal(true)} variant="noBackground" > - Report + {getLocaleString("torrReport")} )} @@ -761,13 +792,13 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { variant="secondary" ml={3} > - Remove this torrent + {getLocaleString("torrRemTorr")} )} {!!userId && ( )} @@ -783,19 +814,19 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { )} - Comments + {getLocaleString("userComments")} {!!comments?.length && ( @@ -813,8 +844,8 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => {
{ variant="secondary" mr={3} > - Cancel + {getLocaleString("accCancel")} @@ -853,7 +884,7 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { {showDeleteModal && ( setShowDeleteModal(false)}> - Are you sure you want to delete this torrent? This cannot be undone. + {getLocaleString("torrSureDeleteTorr")} From 6b509b605b9fde3368b7b75510f1e203934bcdaf Mon Sep 17 00:00:00 2001 From: smlinux Date: Tue, 12 Sep 2023 07:15:27 +0200 Subject: [PATCH 051/125] locales: fix torrent --- client/pages/torrent/[infoHash].js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/pages/torrent/[infoHash].js b/client/pages/torrent/[infoHash].js index 32490d9..95ef806 100644 --- a/client/pages/torrent/[infoHash].js +++ b/client/pages/torrent/[infoHash].js @@ -30,7 +30,7 @@ import { NotificationContext } from "../../components/Notifications"; import LoadingContext from "../../utils/LoadingContext"; import { TorrentFields } from "../upload"; import MarkdownInput from "../../components/MarkdownInput"; -import LocaleContext from "../utils/LocaleContext"; +import LocaleContext from "../../utils/LocaleContext"; // from https://stackoverflow.com/a/44681235/7739519 const insert = (children = [], [head, ...tail], size) => { @@ -612,7 +612,7 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { )} ), - {getLocaleString("uploadCategory")}: category ? ( + Category: category ? ( Date: Tue, 12 Sep 2023 07:58:55 +0200 Subject: [PATCH 052/125] locales: fix2 torrent --- client/locales.json | 2 ++ client/pages/requests/new.js | 2 +- client/pages/torrent/[infoHash].js | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/client/locales.json b/client/locales.json index c68c0db..d54122b 100644 --- a/client/locales.json +++ b/client/locales.json @@ -85,6 +85,7 @@ "registerFailed": "Could not register", "registrationClosed": "Registration closed", "reqCreateNew": "Create new", + "reqCreateReq": "Create request", "reqTitle": "Title", "reqPostedBy": "Posted by", "reqPost": "Post", @@ -289,6 +290,7 @@ "reqRequestCreatedSuccess": "Запрос успешно создан", "reqCouldNotCreateReq": "Не удалось создать запрос", "reqNewRequest": "Новый запрос", + "reqCreateReq": "Создать запрос", "reqWhatYouLookForQ": "Что Вы ищете?", "reqRequestDelSuccess": "Запрос успешно удален", "reqCouldNotDelReq": "Не удалось удалить запрос", diff --git a/client/pages/requests/new.js b/client/pages/requests/new.js index 1554bc2..660bbb7 100644 --- a/client/pages/requests/new.js +++ b/client/pages/requests/new.js @@ -86,7 +86,7 @@ const NewRequest = ({ token }) => { required /> diff --git a/client/pages/torrent/[infoHash].js b/client/pages/torrent/[infoHash].js index 95ef806..1a07851 100644 --- a/client/pages/torrent/[infoHash].js +++ b/client/pages/torrent/[infoHash].js @@ -170,13 +170,13 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { const [cookies] = useCookies(); + const { getLocaleString } = useContext(LocaleContext); + const handleEdit = async (e) => { e.preventDefault(); setLoading(true); const form = new FormData(e.target); - const { getLocaleString } = useContext(LocaleContext); - try { const uploadRes = await fetch( `${SQ_API_URL}/torrent/edit/${torrent.infoHash}`, From a2d8a04ba591bfe9168b2bfd96ea56364749d9cf Mon Sep 17 00:00:00 2001 From: smlinux Date: Tue, 12 Sep 2023 08:24:13 +0200 Subject: [PATCH 053/125] locales: more strings torrent (en) --- client/locales.json | 10 ++++++++++ client/pages/torrent/[infoHash].js | 26 +++++++++++++------------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/client/locales.json b/client/locales.json index d54122b..8b77335 100644 --- a/client/locales.json +++ b/client/locales.json @@ -149,6 +149,16 @@ "torrAddTorr": "Add a torrent", "torrReasonForReport": "Reason for report", "torrSureDeleteTorr": "Are you sure you want to delete this torrent? This cannot be undone.", + "torrFreeleech": "Freeleech", + "torrUnset": "Unset", + "torrSet": "Set", + "torrUploadedBy": "Uploaded by", + "torrDate": "Date", + "torrSize": "Size", + "torrYes": "Yes", + "torrNo": "No", + "torrGroupTorr": "Grouped torrents", + "torrThereAreNoOtherTorrGroup": "There are no other torrents in this group.", "totp": "One-time code", "uploadAddTag": "Add tag", "uploadAnnounceURL": "Announce URL", diff --git a/client/pages/torrent/[infoHash].js b/client/pages/torrent/[infoHash].js index 1a07851..b17470c 100644 --- a/client/pages/torrent/[infoHash].js +++ b/client/pages/torrent/[infoHash].js @@ -574,7 +574,7 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { )} {userRole === "admin" && ( )} {userId ? ( @@ -599,7 +599,7 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => {
@@ -612,7 +612,7 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { )} ), - Category: category ? ( + `${getLocaleString("uploadCategory")}`: category ? ( { {category} ) : undefined, - Source: source ? ( + `${getLocaleString("uploadSource")}`: source ? ( { {source} ) : undefined, - Date: moment(torrent.created).format(`${getLocaleString("indexTime")}`), + `${getLocaleString("torrDate")}` moment(torrent.created).format(`${getLocaleString("indexTime")}`), "Info hash": ( { {torrent.infoHash} ), - Size: prettyBytes(torrent.size), - Downloads: torrent.downloads, - Seeders: torrent.seeders !== undefined ? torrent.seeders : "?", - Leechers: torrent.leechers !== undefined ? torrent.leechers : "?", - Freeleech: - torrent.freeleech || SQ_SITE_WIDE_FREELEECH === true ? "Yes" : "No", + `${getLocaleString("torrDate")}`: prettyBytes(torrent.size), + `${getLocaleString("torrDownloads")}`: torrent.downloads, + `${getLocaleString("torrSeeders")}`: torrent.seeders !== undefined ? torrent.seeders : "?", + `${getLocaleString("torrLeechers")}`: torrent.leechers !== undefined ? torrent.leechers : "?", + `${getLocaleString("torrFreeleech")}`: + torrent.freeleech || SQ_SITE_WIDE_FREELEECH === true ? `${getLocaleString("torrYes")}` : `${getLocaleString("torrNo")}`, }} /> @@ -782,7 +782,7 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { width="100%" mb={4} > - Grouped torrents + {getLocaleString("torrGroupTorr")} {!!torrent.groupTorrents.length && (userRole === "admin" || userId === torrent.uploadedBy._id) && @@ -810,7 +810,7 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { categories={SQ_TORRENT_CATEGORIES} /> ) : ( - There are no other torrents in this group. + {getLocaleString("torrThereAreNoOtherTorrGroup")} )} From be2035df230f6f25fe2e1a8f1aa7e9e48353a648 Mon Sep 17 00:00:00 2001 From: technik Date: Tue, 12 Sep 2023 09:33:19 +0200 Subject: [PATCH 054/125] locales: more strings torrent (ru) --- client/locales.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/client/locales.json b/client/locales.json index 8b77335..c9acd24 100644 --- a/client/locales.json +++ b/client/locales.json @@ -357,6 +357,16 @@ "torrAddTorr": "Добавить торрент", "torrReasonForReport": "Причина отчета", "torrSureDeleteTorr": "Вы уверены, что хотите удалить этот торрент? Это действие нельзя отменить.", + "torrFreeleech": "Фрилич", + "torrUnset": "Отключить", + "torrSet": "Включить", + "torrUploadedBy": "Загружено пользователем", + "torrDate": "Дата", + "torrSize": "Размер", + "torrYes": "Да", + "torrNo": "Нет", + "torrGroupTorr": "Сгруппированные торренты", + "torrThereAreNoOtherTorrGroup": "Других торрентов в этой группе нет.", "totp": "Одноразовый код", "uploadAddTag": "Добавить метку", "uploadAnnounceURL": "Адрес трекера", From cc273f3e6b51ba175831f6781501c1ee8a30831d Mon Sep 17 00:00:00 2001 From: technik Date: Tue, 12 Sep 2023 12:09:08 +0200 Subject: [PATCH 055/125] locales: fix backtick --- client/pages/torrent/[infoHash].js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/client/pages/torrent/[infoHash].js b/client/pages/torrent/[infoHash].js index b17470c..a836a73 100644 --- a/client/pages/torrent/[infoHash].js +++ b/client/pages/torrent/[infoHash].js @@ -599,7 +599,7 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => {
@@ -612,7 +612,7 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { )} ), - `${getLocaleString("uploadCategory")}`: category ? ( + [getLocaleString("uploadCategory")]: category ? ( { {category} ) : undefined, - `${getLocaleString("uploadSource")}`: source ? ( + [getLocaleString("uploadSource")]: source ? ( { {source} ) : undefined, - `${getLocaleString("torrDate")}` moment(torrent.created).format(`${getLocaleString("indexTime")}`), - "Info hash": ( + [getLocaleString("torrDate")]: moment(torrent.created).format(`${getLocaleString("indexTime")}`), + [getLocaleString("reqInfohash")]: ( { {torrent.infoHash} ), - `${getLocaleString("torrDate")}`: prettyBytes(torrent.size), - `${getLocaleString("torrDownloads")}`: torrent.downloads, - `${getLocaleString("torrSeeders")}`: torrent.seeders !== undefined ? torrent.seeders : "?", - `${getLocaleString("torrLeechers")}`: torrent.leechers !== undefined ? torrent.leechers : "?", - `${getLocaleString("torrFreeleech")}`: - torrent.freeleech || SQ_SITE_WIDE_FREELEECH === true ? `${getLocaleString("torrYes")}` : `${getLocaleString("torrNo")}`, + [getLocaleString("torrSize")]: prettyBytes(torrent.size), + [getLocaleString("torrDownloads")]: torrent.downloads, + [getLocaleString("torrSeeders")]: torrent.seeders !== undefined ? torrent.seeders : "?", + [getLocaleString("torrLeechers")]: torrent.leechers !== undefined ? torrent.leechers : "?", + [getLocaleString("torrFreeleech")]: + torrent.freeleech || SQ_SITE_WIDE_FREELEECH === true ? [getLocaleString("torrYes")] : [getLocaleString("torrNo")], }} /> From 1e21172d308d9288fa5f8bfbd3c91a6e07fdf167 Mon Sep 17 00:00:00 2001 From: technik Date: Tue, 12 Sep 2023 12:57:10 +0200 Subject: [PATCH 056/125] change demo site --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 549d05f..c4f6925 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ sqtracker is a modern private BitTorrent tracker platform. It implements all of the features required to run a private (or public) tracker and does not focus on any one specific type of content. It is suitable for running a tracker site of any kind. -Check out the demo at [demo.sqtracker.dev](https://demo.sqtracker.dev). +Check out the demo at [bt.dansh.ru](https://bt.dansh.ru). Please join the [Discord server](https://discord.gg/BEGXEk29Up) for support and general chat. From c72673ba70b61ee497641695dc754dc5bde485fc Mon Sep 17 00:00:00 2001 From: smlinux Date: Wed, 13 Sep 2023 04:55:05 +0200 Subject: [PATCH 057/125] locales: more strings announcement (en, ru) --- client/components/Comment.js | 4 +- client/locales.json | 56 ++++++++++++++++++++-- client/pages/announcements/[slug]/edit.js | 27 +++++++---- client/pages/announcements/[slug]/index.js | 54 ++++++++++++--------- client/pages/announcements/index.js | 32 +++++++------ client/pages/announcements/new.js | 27 +++++++---- 6 files changed, 137 insertions(+), 63 deletions(-) diff --git a/client/components/Comment.js b/client/components/Comment.js index f04a9f9..d78fb81 100644 --- a/client/components/Comment.js +++ b/client/components/Comment.js @@ -34,11 +34,11 @@ const Comment = ({ comment }) => { > {comment.user?.username ? ( - Comment by{" "} + {getLocaleString("comCommentBy")}{" "} {comment.user.username} {" "} - on{" "} + {getLocaleString("comOn")}{" "} {comment.type === "torrent" && ( <> {comment.torrent ? ( diff --git a/client/locales.json b/client/locales.json index c9acd24..7b2ac3a 100644 --- a/client/locales.json +++ b/client/locales.json @@ -45,12 +45,35 @@ "accValidUntil": "Valid until", "accYouCurrentlyHave": "You currently have", "accYouWillEarn": "You will earn", + "annPinnedAnnounce": "Pinned announcements", + "annOtherAnnounce": "Other announcements", + "annAnnounceCreatSuccess": "Announcement created successfully", + "annCouldNotCreateAnnounce": "Could not create announcement", + "annNewAnnounce": "New announcement", + "annBody": "Body", + "annPinThisAnnounceQ": "Pin this announcement?", + "annAllowCommentsQ": "Allow comments?", + "annCreateAnnounce": "Create announcement", + "annAnnounceDelSuccess": "Announcement deleted successfully", + "annCouldNotDelAnnounce": "Could not delete announcement", + "annUnpin": "unpin", + "annPin": "pin", + "annUnpinned": "unpinned", + "annPinned": "pinned", + "annLastUpdated": "Last updated", + "annCommentsDisabled": "Comments disabled.", + "annAreYouSureYouWantToDelThisannounceQ": "Are you sure you want to delete this announcement? This cannot be undone.", + "annAnnounceUpdatedSuccess": "Announcement updated successfully", + "annCouldNotUpdateAnnounce": "Could not update announcement", + "annEditAnnounce": "Edit announcement", + "annUpdateAnnounce": "Update announcement", "catCategories": "Categories", "catNoCategoryHaveBeenDefined": "No categories have been defined.", "catNoTagsHaveBeenDefined": "No tags have been defined.", "catNoResults": "No results.", "comCommentBy": "Comment by", "comDelUser": "deleted user", + "comOn": "on", "email": "Email", "indexLatestAnnounce": "Latest announcement", "indexLatestTorrents": "Latest torrents", @@ -256,12 +279,35 @@ "accValidUntil": "Годен до", "accYouCurrentlyHave": "В настоящее время у вас есть", "accYouWillEarn": "Вы получите", + "annPinnedAnnounce": "Закрепленные анонсы", + "annOtherAnnounce": "Другие анонсы", + "annAnnounceCreatSuccess": "Анонс создан успешно", + "annCouldNotCreateAnnounce": "Не удалось создать анонс", + "annNewAnnounce": "Новый анонс", + "annBody": "Описание", + "annPinThisAnnounceQ": "Закрепить этот анонс?", + "annAllowCommentsQ": "Разрешить комментарии?", + "annCreateAnnounce": "Создать анонс", + "annAnnounceDelSuccess": "Анонс успешно удален", + "annCouldNotDelAnnounce": "Не удалось удалить анонс", + "annUnpin": "Открепить", + "annPin": "Прикрепить", + "annUnpinned": "откреплен", + "annPinned": "прикреплен", + "annLastUpdated": "Последнее обновление", + "annCommentsDisabled": "Комментарии отключены.", + "annAreYouSureYouWantToDelThisannounceQ": "Вы уверены, что хотите удалить этот анонс? Его потом не восстановить.", + "annAnnounceUpdatedSuccess": "Анонс успешно обновлен", + "annCouldNotUpdateAnnounce": "Не удалось обновить анонс", + "annEditAnnounce": "Изменить анонс", + "annUpdateAnnounce": "Обновить анонс", "catCategories": "Категории", "catNoCategoryHaveBeenDefined": "Категории не определены.", "catNoTagsHaveBeenDefined": "Метки не определены.", "catNoResults": "Нет результатов.", "comCommentBy": "Комментарий от", "comDelUser": "удаленный пользователь", + "comOn": "на", "email": "Эл. почта", "indexLatestAnnounce": "Последний анонс", "indexLatestTorrents": "Последние торренты", @@ -278,7 +324,7 @@ "navBrowse": "Категории", "navHome": "Главная", "navLogOut": "Выход", - "navReports": "Отчеты", + "navReports": "Жалобы", "navRequests": "Запросы", "navSearch": "Поиск", "navStats": "Статистика", @@ -340,8 +386,8 @@ "torrCouldNotDelTorr": "Не удалось удалить торрент", "torrVoteSubmitSuccess": "Голосование отправлено успешно", "torrCouldNotSubmitVote": "Не удалось проголосовать", - "torrReportSubmitSuccess": "Отчет успешно отправлен", - "torrCouldNotSubmitReport": "Не удалось отправить отчет", + "torrReportSubmitSuccess": "Жалоба успешно отправлен", + "torrCouldNotSubmitReport": "Не удалось отправить жалобу", "torrFLToggleSuccess": "Фрилич успешно переключен", "torrCouldNotToggleFL": "Не удалось переключить фрилич", "torrTorrRemFromGroupSuccess": "Торрент успешно удален из группы", @@ -352,10 +398,10 @@ "torrDownload": "Загрузить", "torrLogInDownload": "Войдите, чтобы скачать", "torrFiles": "Файлы", - "torrReport": "Отчет", + "torrReport": "Жалоба", "torrRemTorr": "Удалить этот торрент", "torrAddTorr": "Добавить торрент", - "torrReasonForReport": "Причина отчета", + "torrReasonForReport": "Причина жалобы", "torrSureDeleteTorr": "Вы уверены, что хотите удалить этот торрент? Это действие нельзя отменить.", "torrFreeleech": "Фрилич", "torrUnset": "Отключить", diff --git a/client/pages/announcements/[slug]/edit.js b/client/pages/announcements/[slug]/edit.js index e3d4f7c..143b45f 100644 --- a/client/pages/announcements/[slug]/edit.js +++ b/client/pages/announcements/[slug]/edit.js @@ -11,6 +11,7 @@ import { withAuthServerSideProps } from "../../../utils/withAuth"; import { NotificationContext } from "../../../components/Notifications"; import LoadingContext from "../../../utils/LoadingContext"; import MarkdownInput from "../../../components/MarkdownInput"; +import LocaleContext from "../../../utils/LocaleContext"; const EditAnnouncement = ({ announcement, token, userRole }) => { if (userRole !== "admin") { @@ -26,6 +27,8 @@ const EditAnnouncement = ({ announcement, token, userRole }) => { publicRuntimeConfig: { SQ_API_URL }, } = getConfig(); + const { getLocaleString } = useContext(LocaleContext); + const handleCreate = async (e) => { e.preventDefault(); setLoading(true); @@ -54,12 +57,16 @@ const EditAnnouncement = ({ announcement, token, userRole }) => { throw new Error(reason); } - addNotification("success", "Announcement updated successfully"); + addNotification("success", + `${getLocaleString("annAnnounceUpdatedSuccess")}` + ); const slug = await updateAnnouncementRes.text(); router.push(`/announcements/${slug}`); } catch (e) { - addNotification("error", `Could not update announcement: ${e.message}`); + addNotification("error", + `${getLocaleString("annCouldNotUpdateAnnounce")}: ${e.message}` + ); console.error(e); } @@ -68,41 +75,41 @@ const EditAnnouncement = ({ announcement, token, userRole }) => { return ( <> - + - Edit announcement + {getLocaleString("annEditAnnounce")}
diff --git a/client/pages/announcements/[slug]/index.js b/client/pages/announcements/[slug]/index.js index ab64861..b5d09a7 100644 --- a/client/pages/announcements/[slug]/index.js +++ b/client/pages/announcements/[slug]/index.js @@ -19,6 +19,7 @@ import Input from "../../../components/Input"; import Comment from "../../../components/Comment"; import LoadingContext from "../../../utils/LoadingContext"; import Modal from "../../../components/Modal"; +import LocaleContext from "../../../utils/LocaleContext"; const Announcement = ({ announcement, token, userRole }) => { const [pinned, setPinned] = useState(announcement.pinned); @@ -38,6 +39,8 @@ const Announcement = ({ announcement, token, userRole }) => { const [cookies] = useCookies(); + const { getLocaleString } = useContext(LocaleContext); + const handleDelete = async () => { setLoading(true); @@ -57,11 +60,15 @@ const Announcement = ({ announcement, token, userRole }) => { throw new Error(reason); } - addNotification("success", "Announcement deleted successfully"); + addNotification("success" + `${getLocaleString("annAnnounceDelSuccess")}` + ); router.push("/announcements"); } catch (e) { - addNotification("error", `Could not delete announcement: ${e.message}`); + addNotification("error", + `${getLocaleString("annCouldNotDelAnnounce")}: ${e.message}` + ); console.error(e); } @@ -74,7 +81,7 @@ const Announcement = ({ announcement, token, userRole }) => { try { const pinRes = await fetch( `${SQ_API_URL}/announcements/pin/${announcement._id}/${ - pinned ? "unpin" : "pin" + pinned ? [getLocaleString("annUnpin")] : [getLocaleString("annPin")] }`, { method: "POST", @@ -91,14 +98,14 @@ const Announcement = ({ announcement, token, userRole }) => { addNotification( "success", - `Announcement ${pinned ? "unpinned" : "pinned"} successfully` + `Announcement ${pinned ? [getLocaleString("annUnpinned")] : [getLocaleString("annPinned")]} successfully` ); setPinned((p) => !p); } catch (e) { addNotification( "error", - `Could not ${pinned ? "unpin" : "pin"} announcement: ${e.message}` + `${getLocaleString("userCouldNot")} ${pinned ? '${getLocaleString("annUnpin")}' : '${getLocaleString("annPin")}'} announcement: ${e.message}` ); console.error(e); } @@ -131,7 +138,9 @@ const Announcement = ({ announcement, token, userRole }) => { throw new Error(reason); } - addNotification("success", "Comment posted successfully"); + addNotification("success", + `${getLocaleString("reqCommentPostSuccess")}` + ); setComments((c) => { const newComment = { @@ -146,7 +155,9 @@ const Announcement = ({ announcement, token, userRole }) => { commentInputRef.current.value = ""; } catch (e) { - addNotification("error", `Could not post comment: ${e.message}`); + addNotification("error", + `${getLocaleString("reqCommentNotPost")}: ${e.message}` + ); console.error(e); } @@ -155,7 +166,7 @@ const Announcement = ({ announcement, token, userRole }) => { return ( <> - + { {userRole === "admin" && ( @@ -186,14 +197,14 @@ const Announcement = ({ announcement, token, userRole }) => { onClick={() => setShowDeleteModal(true)} variant="secondary" > - Delete + {getLocaleString("reqDelete")} )} - Posted {moment(announcement.created).format("HH:mm Do MMM YYYY")} by{" "} + {getLocaleString("reqPosted")} {moment(announcement.created).format(`${getLocaleString("indexTime")}`)} {getLocaleString("reqBy")}{" "} {announcement.createdBy?.username ? ( {announcement.createdBy.username} @@ -204,8 +215,8 @@ const Announcement = ({ announcement, token, userRole }) => { {announcement.updated && ( - Last updated{" "} - {moment(announcement.updated).format("HH:mm Do MMM YYYY")} + {getLocaleString("annLastUpdated")}{" "} + {moment(announcement.updated).format(`${getLocaleString("indexTime")}`)} )} @@ -218,16 +229,16 @@ const Announcement = ({ announcement, token, userRole }) => {
- Comments + {getLocaleString("userComments")}
{ disabled={!announcement.allowComments} ml="auto" > - Post + {getLocaleString("reqPost")}
{!!comments?.length && ( @@ -254,8 +265,7 @@ const Announcement = ({ announcement, token, userRole }) => { {showDeleteModal && ( setShowDeleteModal(false)}> - Are you sure you want to delete this announcement? This cannot be - undone. + {getLocaleString("annAreYouSureYouWantToDelThisannounceQ")} diff --git a/client/pages/announcements/index.js b/client/pages/announcements/index.js index 0721078..4837d99 100644 --- a/client/pages/announcements/index.js +++ b/client/pages/announcements/index.js @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useContext } from "react"; import getConfig from "next/config"; import Link from "next/link"; import jwt from "jsonwebtoken"; @@ -9,22 +9,26 @@ import Text from "../../components/Text"; import { withAuthServerSideProps } from "../../utils/withAuth"; import Button from "../../components/Button"; import List from "../../components/List"; +import LocaleContext from "../../utils/LocaleContext"; const Announcements = ({ announcements, pinnedAnnouncements, userRole }) => { + + const { getLocaleString } = useContext(LocaleContext); + return ( <> - + - Announcements + {getLocaleString("navAnnouncements")} {userRole === "admin" && ( - + )} @@ -33,7 +37,7 @@ const Announcements = ({ announcements, pinnedAnnouncements, userRole }) => { <> - Pinned announcements + {getLocaleString("annPinnedAnnounce")} ({ @@ -42,22 +46,22 @@ const Announcements = ({ announcements, pinnedAnnouncements, userRole }) => { }))} columns={[ { - header: "Title", + header: `${getLocaleString("reqTitle")}`, accessor: "title", cell: ({ value }) => {value}, gridWidth: "1fr", }, { - header: "Posted by", + header: `${getLocaleString("reqPostedBy")}`, accessor: "createdBy.username", cell: ({ value }) => {value ?? "deleted user"}, gridWidth: "1fr", }, { - header: "Created", + header: `${getLocaleString("accCreated")}`, accessor: "created", cell: ({ value }) => ( - {moment(value).format("HH:mm Do MMM YYYY")} + {moment(value).format(`${getLocaleString("indexTime")}`)} ), rightAlign: true, gridWidth: "175px", @@ -66,7 +70,7 @@ const Announcements = ({ announcements, pinnedAnnouncements, userRole }) => { /> - Other announcements + {getLocaleString("annOtherAnnounce")} )} @@ -77,22 +81,22 @@ const Announcements = ({ announcements, pinnedAnnouncements, userRole }) => { }))} columns={[ { - header: "Title", + header: `${getLocaleString("reqTitle")}`, accessor: "title", cell: ({ value }) => {value}, gridWidth: "1fr", }, { - header: "Posted by", + header: `${getLocaleString("reqPostedBy")}`, accessor: "createdBy.username", cell: ({ value }) => {value ?? "deleted user"}, gridWidth: "1fr", }, { - header: "Created", + header: `${getLocaleString("accCreated")}`, accessor: "created", cell: ({ value }) => ( - {moment(value).format("HH:mm Do MMM YYYY")} + {moment(value).format(`${getLocaleString("indexTime")}`)} ), rightAlign: true, gridWidth: "175px", diff --git a/client/pages/announcements/new.js b/client/pages/announcements/new.js index ede00d2..2fd23e9 100644 --- a/client/pages/announcements/new.js +++ b/client/pages/announcements/new.js @@ -11,6 +11,7 @@ import { withAuthServerSideProps } from "../../utils/withAuth"; import { NotificationContext } from "../../components/Notifications"; import LoadingContext from "../../utils/LoadingContext"; import MarkdownInput from "../../components/MarkdownInput"; +import LocaleContext from "../../utils/LocaleContext"; const NewAnnouncement = ({ token, userRole }) => { if (userRole !== "admin") { @@ -26,6 +27,8 @@ const NewAnnouncement = ({ token, userRole }) => { publicRuntimeConfig: { SQ_API_URL }, } = getConfig(); + const { getLocaleString } = useContext(LocaleContext); + const handleCreate = async (e) => { e.preventDefault(); setLoading(true); @@ -54,12 +57,16 @@ const NewAnnouncement = ({ token, userRole }) => { throw new Error(reason); } - addNotification("success", "Announcement created successfully"); + addNotification("success", + `${getLocaleString("annAnnounceCreatSuccess")}` + ); const slug = await createAnnouncementRes.text(); router.push(`/announcements/${slug}`); } catch (e) { - addNotification("error", `Could not create announcement: ${e.message}`); + addNotification("error", + `${getLocaleString("annCouldNotCreateAnnounce")}: ${e.message}` + ); console.error(e); } @@ -68,24 +75,24 @@ const NewAnnouncement = ({ token, userRole }) => { return ( <> - + - New announcement + {getLocaleString("annNewAnnounce")}
- + - - + + From 120680841fefaa5ae087e4a3f1482ad3713046c6 Mon Sep 17 00:00:00 2001 From: smlinux Date: Wed, 13 Sep 2023 07:13:24 +0200 Subject: [PATCH 058/125] locales: more strings wiki, 404 (en, ru) --- client/locales.json | 44 ++++++++++++++++++++++++++++-- client/pages/404.js | 38 +++++++++++++++----------- client/pages/wiki/[[...slug]].js | 46 +++++++++++++++++++------------- client/pages/wiki/new.js | 34 ++++++++++++++--------- 4 files changed, 114 insertions(+), 48 deletions(-) diff --git a/client/locales.json b/client/locales.json index 7b2ac3a..c9ede58 100644 --- a/client/locales.json +++ b/client/locales.json @@ -1,5 +1,8 @@ { "en": { + "404NotFound": "Not found", + "404PageDoesNotExist": "That page does not exist.", + "404ReturnHome": "Return home", "acc2FADisabled": "2FA disabled", "acc2FAEnabled": "2FA enabled", "acc2FAScanQR": "Scan the QR code with your authenticator app and enter the one-time code", @@ -231,9 +234,29 @@ "veNoVerificationTokenProvided": "No verification token provided", "veVerifyEmail": "Verify email", "welcome": "Welcome", - "welcomeBack": "Welcome back" + "welcomeBack": "Welcome back", + "wikiPath": "Path", + "wikiPageWillBeVisibleAt": "Page will be visible at", + "wikiAllowUnregisteredView": "Allow unregistered view", + "wikiPageCreateSuccess": "Wiki page created successfully", + "wikiCouldNotCreatePage": "Could not create wiki page", + "wikiNewPage": "New wiki page", + "wikiCreatePage": "Create wiki page", + "wikiPageDelSuccess": "Wiki page deleted successfully", + "wikiCouldNotDelPage": "Could not delete wiki page", + "wikiPageUpdateSuccess": "Wiki page updated successfully", + "wikiCouldNotUpdatePage": "Could not update wiki page", + "wikiAddPage": "Add page", + "wikiLastEdited": "Last edited", + "wikiPages": "Pages", + "wikiSaveChanges": "Save changes", + "wikiThereNothingHereYet": "There is nothing here yet.", + "wikiDelThisPageQ": "Are you sure you want to delete this wiki page? This cannot be undone." }, "ru": { + "404NotFound": "Не найдено", + "404PageDoesNotExist": "Эта страница не существует.", + "404ReturnHome": "Вернуться на главную страницу", "acc2FADisabled": "2FA отключена", "acc2FAEnabled": "2FA включена", "acc2FAScanQR": "Отсканируйте QR-код с помощью приложения для аутентификации и введите одноразовый код", @@ -462,6 +485,23 @@ "veNoVerificationTokenProvided": "Токен подтверждения не предоставлен", "veVerifyEmail": "Подтвердить эл. почту", "welcome": "Добро пожаловать", - "welcomeBack": "С возвращением" + "welcomeBack": "С возвращением", + "wikiPath": "Путь", + "wikiPageWillBeVisibleAt": "Страница будет видна по адресу", + "wikiAllowUnregisteredView": "Разрешить просмотр без авторизации", + "wikiPageCreateSuccess": "Wiki страница успешно создана", + "wikiCouldNotCreatePage": "Не удалось создать wiki страницу", + "wikiNewPage": "Новая wiki страница", + "wikiCreatePage": "Создать wiki страницу", + "wikiPageDelSuccess": "Wiki страница успешно удалена", + "wikiCouldNotDelPage": "Не удалось удалить wiki страницу", + "wikiPageUpdateSuccess": "Wiki страница успешно обновлена", + "wikiCouldNotUpdatePage": "Не удалось обновить wiki страницу", + "wikiAddPage": "Добавить страницу", + "wikiLastEdited": "Последнее редактирование", + "wikiPages": "Страницы", + "wikiSaveChanges": "Сохранить изменения", + "wikiThereNothingHereYet": "Здесь пока ничего нет.", + "wikiDelThisPageQ": "Вы уверены, что хотите удалить эту wiki страницу? Действие необратимо." } } diff --git a/client/pages/404.js b/client/pages/404.js index 3c0ea5a..09edabd 100644 --- a/client/pages/404.js +++ b/client/pages/404.js @@ -1,22 +1,28 @@ -import React from "react"; +import React, { useContext } from "react"; import Link from "next/link"; import SEO from "../components/SEO"; import Text from "../components/Text"; +import LocaleContext from "../utils/LocaleContext"; -const NotFound = () => ( - <> - - - 404: Not found - - - That page does not exist.{" "} - - Return home - - . - - -); +const NotFound = () => { + + const { getLocaleString } = useContext(LocaleContext); + + return ( + <> + + + 404: {getLocaleString("404NotFound")} + + + {getLocaleString("404PageDoesNotExist")}{" "} + + {getLocaleString("404ReturnHome")} + + . + + + ); +}; export default NotFound; diff --git a/client/pages/wiki/[[...slug]].js b/client/pages/wiki/[[...slug]].js index a498481..885f72e 100644 --- a/client/pages/wiki/[[...slug]].js +++ b/client/pages/wiki/[[...slug]].js @@ -16,6 +16,7 @@ import { NotificationContext } from "../../components/Notifications"; import LoadingContext from "../../utils/LoadingContext"; import Modal from "../../components/Modal"; import { WikiFields } from "./new"; +import LocaleContext from "../../utils/LocaleContext"; const sortSlug = (a, b) => { if (a.slug > b.slug) return 1; @@ -40,6 +41,8 @@ const Wiki = ({ page, allPages, token, userRole, slug }) => { setEditing(false); }, [router.asPath]); + const { getLocaleString } = useContext(LocaleContext); + const handleDelete = async () => { setLoading(true); @@ -56,13 +59,17 @@ const Wiki = ({ page, allPages, token, userRole, slug }) => { throw new Error(reason); } - addNotification("success", "Wiki page deleted successfully"); + addNotification("success", + `${getLocaleString("wikiPageDelSuccess")}` + ); setShowDeleteModal(false); await router.push("/wiki"); } catch (e) { - addNotification("error", `Could not delete wiki page: ${e.message}`); + addNotification("error", + `${getLocaleString("wikiCouldNotDelPage")}: ${e.message}` + ); console.error(e); } @@ -97,14 +104,18 @@ const Wiki = ({ page, allPages, token, userRole, slug }) => { throw new Error(reason); } - addNotification("success", "Wiki page updated successfully"); + addNotification("success", + `${getLocaleString("wikiPageUpdateSuccess")}` + ); if (form.get("slug") === page.slug) window.location.reload(); else window.location.href = "/wiki" + (page.slug === "/" ? "" : form.get("slug")); } catch (e) { - addNotification("error", `Could not update wiki page: ${e.message}`); + addNotification("error", + `${getLocaleString("wikiCouldNotUpdatePage")}: ${e.message}` + ); console.error(e); } @@ -125,7 +136,7 @@ const Wiki = ({ page, allPages, token, userRole, slug }) => { {!!page && ( @@ -135,7 +146,7 @@ const Wiki = ({ page, allPages, token, userRole, slug }) => { variant="secondary" ml={3} > - Edit + {getLocaleString("torrEdit")} {!!slug && ( )} @@ -155,9 +166,9 @@ const Wiki = ({ page, allPages, token, userRole, slug }) => { <> - Last edited{" "} - {moment(page.updated ?? page.created).format("HH:mm Do MMM YYYY")}{" "} - by{" "} + {getLocaleString("wikiLastEdited")}{" "} + {moment(page.updated ?? page.created).format(`${getLocaleString("indexTime")}`)}{" "} + {getLocaleString("reqBy")}{" "} {page.createdBy?.username ? ( {page.createdBy.username} @@ -206,7 +217,7 @@ const Wiki = ({ page, allPages, token, userRole, slug }) => { mb={3} _css={{ textTransform: "uppercase" }} > - Pages + {getLocaleString("wikiPages")} {allPages.sort(sortSlug).map((p) => ( @@ -227,21 +238,20 @@ const Wiki = ({ page, allPages, token, userRole, slug }) => { variant="secondary" mr={3} > - Cancel + {getLocaleString("accCancel")} - + )} ) : ( - There is nothing here yet. + {getLocaleString("wikiThereNothingHereYet")} )} {showDeleteModal && ( setShowDeleteModal(false)}> - Are you sure you want to delete this wiki page? This cannot be - undone. + {getLocaleString("wikiDelThisPageQ")} diff --git a/client/pages/wiki/new.js b/client/pages/wiki/new.js index 18a53b1..83c6917 100644 --- a/client/pages/wiki/new.js +++ b/client/pages/wiki/new.js @@ -12,6 +12,7 @@ import { withAuthServerSideProps } from "../../utils/withAuth"; import { NotificationContext } from "../../components/Notifications"; import LoadingContext from "../../utils/LoadingContext"; import Checkbox from "../../components/Checkbox"; +import LocaleContext from "../../utils/LocaleContext"; export const WikiFields = ({ values }) => { const [slugValue, setSlugValue] = useState(values?.slug); @@ -20,13 +21,15 @@ export const WikiFields = ({ values }) => { publicRuntimeConfig: { SQ_BASE_URL, SQ_ALLOW_UNREGISTERED_VIEW }, } = getConfig(); + const { getLocaleString } = useContext(LocaleContext); + console.log(values); return ( <> setSlugValue(e.target.value)} onBlur={(e) => { @@ -44,19 +47,19 @@ export const WikiFields = ({ values }) => { required /> - Page will be visible at {SQ_BASE_URL}/wiki{slugValue} + {getLocaleString("wikiPageWillBeVisibleAt")} {SQ_BASE_URL}/wiki{slugValue} { {SQ_ALLOW_UNREGISTERED_VIEW && ( @@ -75,8 +78,11 @@ export const WikiFields = ({ values }) => { }; const NewWiki = ({ token, userRole }) => { + + const { getLocaleString } = useContext(LocaleContext); + if (userRole !== "admin") { - return You do not have permission to do that.; + return {getLocaleString("statYouNotPermission")}; } const { addNotification } = useContext(NotificationContext); @@ -113,12 +119,16 @@ const NewWiki = ({ token, userRole }) => { throw new Error(reason); } - addNotification("success", "Wiki page created successfully"); + addNotification("success", + `${getLocaleString("wikiPageCreateSuccess")}` + ); const slug = await createWikiRes.text(); router.push(`/wiki/${slug}`); } catch (e) { - addNotification("error", `Could not create wiki page: ${e.message}`); + addNotification("error", + `${getLocaleString("wikiCouldNotCreatePage")}: ${e.message}` + ); console.error(e); } @@ -127,14 +137,14 @@ const NewWiki = ({ token, userRole }) => { return ( <> - + - New wiki page + {getLocaleString("wikiNewPage")}
From 45572f281955a1d934cff202bd1394e9e0cb9b49 Mon Sep 17 00:00:00 2001 From: smlinux Date: Wed, 13 Sep 2023 07:51:34 +0200 Subject: [PATCH 059/125] locales: more strings rss (en, ru) --- client/locales.json | 14 ++++++++++++++ client/pages/rss.js | 23 ++++++++++++----------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/client/locales.json b/client/locales.json index c9ede58..76a5614 100644 --- a/client/locales.json +++ b/client/locales.json @@ -141,6 +141,13 @@ "reqInfohash": "Infohash", "reqSuggest": "Suggest", "resetPassword": "Reset password", + "rssThereRSSFeedAt": "There is an RSS feed at", + "rssToAuthenticateYourself": "To authenticate yourself, you must provide the cookies", + "rssAnd": "and", + "rssToRSSEndpoint": "to the RSS endpoint, containing your username and your password respectively.", + "rssNoQueryParametersAreProvided": "If no query parameters are provided, the RSS feed will contain the 100 latest torrents.", + "rssOnlyIncludeMatchingResults": "To only include matching results in the feed, you can add the", + "rssQueryParameter": "query parameter, e.g.", "searchSearchResults": "Search results for", "searchSearchError": "Search error", "statYouNotPermission": "You do not have permission to do that.", @@ -393,6 +400,13 @@ "reqInfohash": "Инфохеш", "reqSuggest": "Предложить", "resetPassword": "Сброс пароля", + "rssThereRSSFeedAt": "RSS-канал по адресу", + "rssToAuthenticateYourself": "Чтобы аутентифицировать себя, вы должны предоставить файлы cookie", + "rssAnd": "и", + "rssToRSSEndpoint": "к конечной точке RSS, содержащей ваше имя пользователя и пароль соответственно.", + "rssNoQueryParametersAreProvided": "Если параметры запроса не указаны, RSS-канал будет содержать 100 последних торрентов.", + "rssOnlyIncludeMatchingResults": "Чтобы включить в ленту только совпадающие результаты, вы можете добавить", + "rssQueryParameter": "параметр запроса, например", "searchSearchResults": "Результат поиска для", "searchSearchError": "Ошибка поиска", "statYouNotPermission": "У вас нет прав для просмотра.", diff --git a/client/pages/rss.js b/client/pages/rss.js index 172dff5..53bd99e 100644 --- a/client/pages/rss.js +++ b/client/pages/rss.js @@ -1,34 +1,35 @@ -import React from "react"; +import React, { useContext } from "react"; import getConfig from "next/config"; import SEO from "../components/SEO"; import Text from "../components/Text"; +import LocaleContext from "../utils/LocaleContext"; const Rss = () => { const { publicRuntimeConfig: { SQ_BASE_URL }, } = getConfig(); + const { getLocaleString } = useContext(LocaleContext); + return ( <> - + - RSS + {getLocaleString("navRSS")} - There is an RSS feed at {SQ_BASE_URL}/api/rss. + {getLocaleString("rssThereRSSFeedAt")} {SQ_BASE_URL}/api/rss. - To authenticate yourself, you must provide the cookies{" "} - username and password to the RSS - endpoint, containing your username and your password respectively. + {getLocaleString("rssToAuthenticateYourself")}{" "} + {getLocaleString("username")} {getLocaleString("rssAnd")} {getLocaleString("password")} {getLocaleString("rssToRSSEndpoint")} - If no query parameters are provided, the RSS feed will contain the 100 - latest torrents. + {getLocaleString("rssNoQueryParametersAreProvided")} - To only include matching results in the feed, you can add the{" "} - query query parameter, e.g.{" "} + {getLocaleString("rssOnlyIncludeMatchingResults")}{" "} + query {getLocaleString("rssQueryParameter")}{" "} /api/rss?query=loremipsum. From a8e21f02c58cb7bcf77d5cbf1cd230282e3b6172 Mon Sep 17 00:00:00 2001 From: technik Date: Wed, 13 Sep 2023 11:46:57 +0200 Subject: [PATCH 060/125] locales: more strings bookmarks, error, reports (en, ru) --- client/locales.json | 32 ++++++++++++++++++++++++++++++++ client/pages/_error.js | 18 ++++++++++-------- client/pages/bookmarks.js | 11 +++++++---- client/pages/reports/[id].js | 27 +++++++++++++++++---------- client/pages/reports/index.js | 22 +++++++++++++--------- 5 files changed, 79 insertions(+), 31 deletions(-) diff --git a/client/locales.json b/client/locales.json index 76a5614..5859e64 100644 --- a/client/locales.json +++ b/client/locales.json @@ -70,6 +70,8 @@ "annCouldNotUpdateAnnounce": "Could not update announcement", "annEditAnnounce": "Edit announcement", "annUpdateAnnounce": "Update announcement", + "bmYourBM": "Your bookmarks", + "bmYouNotHaveAnyBM": "You do not have any bookmarks.", "catCategories": "Categories", "catNoCategoryHaveBeenDefined": "No categories have been defined.", "catNoTagsHaveBeenDefined": "No tags have been defined.", @@ -78,6 +80,11 @@ "comDelUser": "deleted user", "comOn": "on", "email": "Email", + "errSomethingWentWrong": "Something went wrong", + "errTooManyRequests": "Too many requests! You have been rate limited. Please wait a while before trying again.", + "errIfErrorPersist": "If the error persists, please", + "errReportIt": "report it", + "errForNow": "For now,", "indexLatestAnnounce": "Latest announcement", "indexLatestTorrents": "Latest torrents", "indexSearch": "Search", @@ -140,6 +147,15 @@ "reqBy": "by", "reqInfohash": "Infohash", "reqSuggest": "Suggest", + "repUnresolvedRep": "Unresolved reports", + "repRepBy": "Reported by", + "repReason": "Reason", + "repRepMarkSolved": "Report marked as solved", + "repCouldNotResolveRep": "Could not resolve report", + "repRepOn": "Report on", + "repMarkSolved": "Mark as solved", + "repRep": "Reported", + "repTorrDetail": "Torrent details", "resetPassword": "Reset password", "rssThereRSSFeedAt": "There is an RSS feed at", "rssToAuthenticateYourself": "To authenticate yourself, you must provide the cookies", @@ -331,6 +347,8 @@ "annCouldNotUpdateAnnounce": "Не удалось обновить анонс", "annEditAnnounce": "Изменить анонс", "annUpdateAnnounce": "Обновить анонс", + "bmYourBM": "Ваши закладки", + "bmYouNotHaveAnyBM": "У вас нет закладок.", "catCategories": "Категории", "catNoCategoryHaveBeenDefined": "Категории не определены.", "catNoTagsHaveBeenDefined": "Метки не определены.", @@ -339,6 +357,11 @@ "comDelUser": "удаленный пользователь", "comOn": "на", "email": "Эл. почта", + "errSomethingWentWrong": "Что-то пошло не так", + "errTooManyRequests": "Слишком много запросов! Пожалуйста, подождите некоторое время, прежде чем повторить попытку.", + "errIfErrorPersist": "Если ошибка повторится, пожалуйста,", + "errReportIt": "напишите об этом", + "errForNow": "На данный момент,", "indexLatestAnnounce": "Последний анонс", "indexLatestTorrents": "Последние торренты", "indexSearch": "Поиск", @@ -399,6 +422,15 @@ "reqBy": "пользователем", "reqInfohash": "Инфохеш", "reqSuggest": "Предложить", + "repUnresolvedRep": "Неразрешенные жалобы", + "repRepBy": "Жалоба от", + "repReason": "Причина", + "repRepMarkSolved": "Жалоба помечена как решенная", + "repCouldNotResolveRep": "Не удалось решить жалобу", + "repRepOn": "Жалоба на", + "repMarkSolved": "Пометить как решенная", + "repRep": "Жалоба отправлена", + "repTorrDetail": "Детали торрента", "resetPassword": "Сброс пароля", "rssThereRSSFeedAt": "RSS-канал по адресу", "rssToAuthenticateYourself": "Чтобы аутентифицировать себя, вы должны предоставить файлы cookie", diff --git a/client/pages/_error.js b/client/pages/_error.js index 457d147..f865bbf 100644 --- a/client/pages/_error.js +++ b/client/pages/_error.js @@ -1,10 +1,11 @@ -import React, { useState, useEffect } from "react"; +import React, { useContext, useState, useEffect } from "react"; import Link from "next/link"; import getConfig from "next/config"; import NextErrorComponent from "next/error"; import * as Sentry from "@sentry/nextjs"; import SEO from "../components/SEO"; import Text from "../components/Text"; +import LocaleContext from "../utils/LocaleContext"; const ErrorPage = () => { const [rateLimited, setRateLimited] = useState(false); @@ -23,30 +24,31 @@ const ErrorPage = () => { checkRateLimit(); }, []); + const { getLocaleString } = useContext(LocaleContext); + return ( <> - + - Something went wrong :( + {getLocaleString("errSomethingWentWrong")} :( {rateLimited ? ( - Too many requests! You have been rate limited. Please wait a while - before trying again. + {getLocaleString("errTooManyRequests")} ) : ( - If the error persists, please{" "} + {getLocaleString("errIfErrorPersist")}{" "} - report it + {getLocaleString("errReportIt")} . For now,{" "} - return home + {getLocaleString("404ReturnHome")} . diff --git a/client/pages/bookmarks.js b/client/pages/bookmarks.js index 281219b..4b75326 100644 --- a/client/pages/bookmarks.js +++ b/client/pages/bookmarks.js @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useContext } from "react"; import getConfig from "next/config"; import { useRouter } from "next/router"; import qs from "qs"; @@ -6,17 +6,20 @@ import { withAuthServerSideProps } from "../utils/withAuth"; import SEO from "../components/SEO"; import Text from "../components/Text"; import TorrentList from "../components/TorrentList"; +import LocaleContext from "../utils/LocaleContext"; const Bookmarks = ({ results }) => { const { publicRuntimeConfig: { SQ_TORRENT_CATEGORIES }, } = getConfig(); + const { getLocaleString } = useContext(LocaleContext); + return ( <> - + - Your bookmarks + {getLocaleString("bmYourBM")} {results.torrents.length ? ( { total={results.total} /> ) : ( - You do not have any bookmarks. + {getLocaleString("bmYouNotHaveAnyBM")} )} ); diff --git a/client/pages/reports/[id].js b/client/pages/reports/[id].js index 5920a5a..5290084 100644 --- a/client/pages/reports/[id].js +++ b/client/pages/reports/[id].js @@ -15,6 +15,7 @@ import { Info } from "../torrent/[infoHash]"; import { withAuthServerSideProps } from "../../utils/withAuth"; import { NotificationContext } from "../../components/Notifications"; import LoadingContext from "../../utils/LoadingContext"; +import LocaleContext from "../../utils/LocaleContext"; const Report = ({ report, token, userRole }) => { const { addNotification } = useContext(NotificationContext); @@ -29,6 +30,8 @@ const Report = ({ report, token, userRole }) => { const handleResolve = async () => { setLoading(true); + const { getLocaleString } = useContext(LocaleContext); + try { const resolveRes = await fetch( `${SQ_API_URL}/reports/resolve/${report._id}`, @@ -45,11 +48,15 @@ const Report = ({ report, token, userRole }) => { throw new Error(reason); } - addNotification("success", "Report marked as solved"); + addNotification("success", + `${getLocaleString("repRepMarkSolved")}` + ); router.push("/reports"); } catch (e) { - addNotification("error", `Could not resolve report: ${e.message}`); + addNotification("error", + `${getLocaleString("repCouldNotResolveRep")}: ${e.message}` + ); console.error(e); } @@ -57,29 +64,29 @@ const Report = ({ report, token, userRole }) => { }; if (userRole !== "admin") { - return You do not have permission to do that.; + return {getLocaleString("statYouNotPermission")}; } return ( <> - + - Report on “{report.torrent.name}” - + {getLocaleString("repRepOn")} “{report.torrent.name}” + - Reported {moment(report.created).format("HH:mm Do MMM YYYY")} by{" "} + {getLocaleString("repRep")} {moment(report.created).format(`${getLocaleString("indexTime")}`)} by{" "} {report.reportedBy.username} @@ -96,7 +103,7 @@ const Report = ({ report, token, userRole }) => { {report.torrent.infoHash} ), - Created: moment(report.torrent.created).format("HH:mm Do MMM YYYY"), + {getLocaleString("accCreated")}: moment(report.torrent.created).format(`${getLocaleString("indexTime")}`), }} /> { _css={{ textTransform: "uppercase" }} mb={4} > - Reason for report + {getLocaleString("torrReasonForReport")} diff --git a/client/pages/reports/index.js b/client/pages/reports/index.js index ea582e3..d9e03ff 100644 --- a/client/pages/reports/index.js +++ b/client/pages/reports/index.js @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useContext } from "react"; import getConfig from "next/config"; import jwt from "jsonwebtoken"; import moment from "moment"; @@ -6,17 +6,21 @@ import SEO from "../../components/SEO"; import Text from "../../components/Text"; import { withAuthServerSideProps } from "../../utils/withAuth"; import List from "../../components/List"; +import LocaleContext from "../../utils/LocaleContext"; const Reports = ({ reports, userRole }) => { + + const { getLocaleString } = useContext(LocaleContext); + if (userRole !== "admin") { - return You do not have permission to do that.; + return {getLocaleString("statYouNotPermission")}; } return ( <> - + - Unresolved reports + {getLocaleString("repUnresolvedRep")} { }))} columns={[ { - header: "Torrent", + header: `${getLocaleString("torrTorrent")}`, accessor: "torrent.name", cell: ({ value }) => {value}, gridWidth: "1fr", }, { - header: "Reported by", + header: `${getLocaleString("repRepBy")}`, accessor: "reportedBy.username", cell: ({ value }) => {value}, gridWidth: "1fr", }, { - header: "Reason", + header: `${getLocaleString("repReason")}`, accessor: "reason", cell: ({ value }) => {value}, gridWidth: "1fr", }, { - header: "Created", + header: `${getLocaleString("accCreated")}`, accessor: "created", cell: ({ value }) => ( - {moment(value).format("HH:mm Do MMM YYYY")} + {moment(value).format(`${getLocaleString("indexTime")}`)} ), rightAlign: true, gridWidth: "175px", From d1d5a0546ff4126ef5641b985a7ec2b666c99e2c Mon Sep 17 00:00:00 2001 From: smlinux Date: Wed, 13 Sep 2023 15:34:02 +0200 Subject: [PATCH 061/125] locales: fix reports --- client/pages/reports/[id].js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/pages/reports/[id].js b/client/pages/reports/[id].js index 5290084..3483454 100644 --- a/client/pages/reports/[id].js +++ b/client/pages/reports/[id].js @@ -103,7 +103,7 @@ const Report = ({ report, token, userRole }) => { {report.torrent.infoHash} ), - {getLocaleString("accCreated")}: moment(report.torrent.created).format(`${getLocaleString("indexTime")}`), + [getLocaleString("accCreated")]: moment(report.torrent.created).format(`${getLocaleString("indexTime")}`), }} /> Date: Fri, 15 Sep 2023 08:23:17 +0200 Subject: [PATCH 062/125] locales: fix reports id --- client/pages/reports/[id].js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/pages/reports/[id].js b/client/pages/reports/[id].js index 3483454..2e7f4fe 100644 --- a/client/pages/reports/[id].js +++ b/client/pages/reports/[id].js @@ -27,11 +27,11 @@ const Report = ({ report, token, userRole }) => { const router = useRouter(); + const { getLocaleString } = useContext(LocaleContext); + const handleResolve = async () => { setLoading(true); - const { getLocaleString } = useContext(LocaleContext); - try { const resolveRes = await fetch( `${SQ_API_URL}/reports/resolve/${report._id}`, @@ -80,7 +80,7 @@ const Report = ({ report, token, userRole }) => {
- {getLocaleString("repRep")} {moment(report.created).format(`${getLocaleString("indexTime")}`)} by{" "} + {getLocaleString("repRep")} {moment(report.created).format(`${getLocaleString("indexTime")}`)} {getLocaleString("reqBy")}{" "} {report.reportedBy.username} @@ -88,13 +88,13 @@ const Report = ({ report, token, userRole }) => { {report.torrent.name} ), - Description: report.torrent.description, - "Info hash": ( + [getLocaleString("uploadDescription")]: report.torrent.description, + [getLocaleString("reqInfohash")]: ( Date: Sun, 17 Sep 2023 15:02:33 +0200 Subject: [PATCH 063/125] locales: more strings torrent, user (en, ru) --- client/locales.json | 8 ++++++++ client/pages/torrent/[infoHash].js | 2 +- client/pages/user/[username].js | 12 ++++++------ 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/client/locales.json b/client/locales.json index 5859e64..004fc2e 100644 --- a/client/locales.json +++ b/client/locales.json @@ -252,6 +252,10 @@ "userUploaded": "Uploaded", "userMyUploads": "My uploads", "usernameRules": "Can only consist of letters, numbers, and “.”", + "userUnban": "unban", + "userBan": "ban", + "userUnbanned": "unbanned", + "userBanned": "banned", "veCouldNotVerifyEmailAddress": "Could not verify email address:", "veEmailAddressVerifiedSuccess": "Email address verified successfully.", "veNoVerificationTokenProvided": "No verification token provided", @@ -526,6 +530,10 @@ "userUploaded": "Роздано", "userMyUploads": "Мои раздачи", "usernameRules": "Может состоять только из букв, цифр и “.”", + "userUnban": "разбанить", + "userBan": "забанить", + "userUnbanned": "разбанен", + "userBanned": "забанен", "veCouldNotVerifyEmailAddress": "Не удалось подтвердить адрес эл. почты:", "veEmailAddressVerifiedSuccess": "Адрес эл. почты успешно подтвержден.", "veNoVerificationTokenProvided": "Токен подтверждения не предоставлен", diff --git a/client/pages/torrent/[infoHash].js b/client/pages/torrent/[infoHash].js index a836a73..3e3be50 100644 --- a/client/pages/torrent/[infoHash].js +++ b/client/pages/torrent/[infoHash].js @@ -876,7 +876,7 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { > {getLocaleString("accCancel")} - +
diff --git a/client/pages/user/[username].js b/client/pages/user/[username].js index 8458c50..ae90c65 100644 --- a/client/pages/user/[username].js +++ b/client/pages/user/[username].js @@ -68,7 +68,7 @@ const User = ({ token, user, userRole }) => { addNotification( "success", - `${getLocaleString("User")} ${banned ? "unbanned" : "banned"} ${getLocaleString("userSuccessfully")}` + `${user.username} ${banned ? [getLocaleString("userUnbanned")] : [getLocaleString("userBanned")]} ${getLocaleString("userSuccessfully")}` ); setBanned((b) => !b); @@ -76,7 +76,7 @@ const User = ({ token, user, userRole }) => { } catch (e) { addNotification( "error", - `${getLocaleString("userCouldNot")} ${banned ? "unban" : "ban"} ${getLocaleString("User")}: ${e.message}` + `${getLocaleString("userCouldNot")} ${banned ? [getLocaleString("userUnban")] : [getLocaleString("userBan")]} ${user.username}: ${e.message}` ); console.error(e); } @@ -109,7 +109,7 @@ const User = ({ token, user, userRole }) => { )} {banned && ( - Banned + {getLocaleString("userBanned")} )}
@@ -122,7 +122,7 @@ const User = ({ token, user, userRole }) => { )} {userRole === "admin" && cookies.username !== user.username && ( )} @@ -295,7 +295,7 @@ const User = ({ token, user, userRole }) => { {showBanModal && ( setShowBanModal(false)}> - {getLocaleString("userYouSureWant")} {banned ? "unban" : "ban"} {getLocaleString("userThisUserQ")} + {getLocaleString("userYouSureWant")} {banned ? [getLocaleString("userUnban")] : [getLocaleString("userBan")]} {getLocaleString("userThisUserQ")} - + )} From 1443782bb29cd16453c780e8b7c41d8b91ff4d3b Mon Sep 17 00:00:00 2001 From: smlinux Date: Sun, 17 Sep 2023 15:54:57 +0200 Subject: [PATCH 064/125] locales: more strings account (en, ru) --- client/locales.json | 4 +++- client/pages/account.js | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/client/locales.json b/client/locales.json index 004fc2e..f2644f0 100644 --- a/client/locales.json +++ b/client/locales.json @@ -48,6 +48,7 @@ "accValidUntil": "Valid until", "accYouCurrentlyHave": "You currently have", "accYouWillEarn": "You will earn", + "accNotAvailableToBuy": "Not available to buy", "annPinnedAnnounce": "Pinned announcements", "annOtherAnnounce": "Other announcements", "annAnnounceCreatSuccess": "Announcement created successfully", @@ -292,7 +293,7 @@ "acc2FAuth": "Двухфакторная аутентификация", "accBonusPoints": "Бонусные баллы", "accBonusPointsHave": "бонус(а/ов)", - "accBuy": "Buy", + "accBuy": "Обменять", "accCancel": "Отмена", "accChangePass": "Изменить пароль", "accClaimed": "Заявлено", @@ -329,6 +330,7 @@ "accValidUntil": "Годен до", "accYouCurrentlyHave": "В настоящее время у вас есть", "accYouWillEarn": "Вы получите", + "accNotAvailableToBuy": "Недоступно для обмена", "annPinnedAnnounce": "Закрепленные анонсы", "annOtherAnnounce": "Другие анонсы", "annAnnounceCreatSuccess": "Анонс создан успешно", diff --git a/client/pages/account.js b/client/pages/account.js index e638f38..ef88a90 100644 --- a/client/pages/account.js +++ b/client/pages/account.js @@ -28,6 +28,7 @@ const BuyItem = ({ text, cost, wallet, handleBuy }) => { const [amount, setAmount] = useState(1); const unavailable = cost === 0; const cannotAfford = amount * cost > wallet; + const { getLocaleString } = useContext(LocaleContext); return ( { {unavailable - ? "Not available to buy" + ? [getLocaleString("accNotAvailableToBuy")] : `Cost: ${amount * cost} points`} { disabled={unavailable || cannotAfford} mr={3} /> - + From 67100a41bc9f57ed2cd548ad7381d749ad0ba2f3 Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Tue, 19 Sep 2023 17:29:45 +0100 Subject: [PATCH 065/125] separate locale files, adds partial de and zh translations --- client/components/Navigation.js | 2 +- client/locales.json | 563 -------------------------------- client/locales/de.json | 22 ++ client/locales/en.json | 282 ++++++++++++++++ client/locales/index.js | 11 + client/locales/ru.json | 279 ++++++++++++++++ client/locales/zh.json | 22 ++ client/pages/_app.js | 6 +- 8 files changed, 621 insertions(+), 566 deletions(-) delete mode 100644 client/locales.json create mode 100644 client/locales/de.json create mode 100644 client/locales/en.json create mode 100644 client/locales/index.js create mode 100644 client/locales/ru.json create mode 100644 client/locales/zh.json diff --git a/client/components/Navigation.js b/client/components/Navigation.js index bc028ea..05ff334 100644 --- a/client/components/Navigation.js +++ b/client/components/Navigation.js @@ -310,7 +310,7 @@ const Navigation = ({ isMobile, menuIsOpen, setMenuIsOpen }) => { value={locale} onChange={(e) => setLocale(e.target.value)} > - {locales.map((l) => ( + {locales.sort().map((l) => ( diff --git a/client/locales.json b/client/locales.json deleted file mode 100644 index f2644f0..0000000 --- a/client/locales.json +++ /dev/null @@ -1,563 +0,0 @@ -{ - "en": { - "404NotFound": "Not found", - "404PageDoesNotExist": "That page does not exist.", - "404ReturnHome": "Return home", - "acc2FADisabled": "2FA disabled", - "acc2FAEnabled": "2FA enabled", - "acc2FAScanQR": "Scan the QR code with your authenticator app and enter the one-time code", - "acc2FAText1": "2FA enabled successfully. These backup codes can be used to log in if you lose access to your authenticator app. Save them now, they will not be visible again.", - "acc2FAUseApp": "Use an authenticator app to add another layer of security to your account", - "acc2FAuth": "Two-factor authentication", - "accBonusPoints": "Bonus points", - "accBonusPointsHave": "bonus point(s)", - "accBuy": "Buy", - "accCancel": "Cancel", - "accChangePass": "Change password", - "accClaimed": "Claimed", - "accCopyLink": "Copy link", - "accCouldNotBuyItems": "Could not buy items", - "accCouldNotChangePass": "Could not change password", - "accCouldNotDelAcc": "Could not delete account", - "accCouldNotSendInvite": "Could not send invite", - "accCouldNotToggle2FA": "Could not toggle 2FA", - "accCreated": "Created", - "accCurrentPass": "Current password", - "accDangerZone": "Danger zone", - "accDelAccText1": "Are you sure you want to delete your account? This action cannot be undone, and you may not be able to register again. Your personal information will be deleted but your uploaded torrents will remain.", - "accDeleteMyAcc": "Delete my account", - "accDeleteMyAccYes": "Yes, delete my account", - "accDisable": "Disable", - "accEnable": "Enable", - "accEnable2FA": "Enable 2FA", - "accEveryRequestYouFulfill": "for every request you fulfill, or", - "accForEveryGBYouUpload": "for every GB you upload", - "accIfYouAreAlsUploaderAcceptTorrent": "if you are als the uploader of the accepted torrent", - "accInviteLinkCopiedClipboard": "Invite link copied to clipboard", - "accInvites": "Invites", - "accInviteSentSuccess": "Invite sent successfully", - "accInviteText1": "Enter an email address to send an invite. The invited user will need to sign up with the same email address. Once the invite is generated, you can also copy a direct invite link.", - "accItemsPurchasedSuccess": "Items purchased successfully", - "accMyAccount": "My account", - "accPassChangedSuccess": "Password changed successfully", - "accPurchaseInvites": "Purchase invites", - "accPurchaseUpload1GB": "Purchase upload (1 GB)", - "accRole": "Role", - "accSendInvite": "Send invite", - "accThisIsAdminAcc": "This is an admin account.", - "accValidUntil": "Valid until", - "accYouCurrentlyHave": "You currently have", - "accYouWillEarn": "You will earn", - "accNotAvailableToBuy": "Not available to buy", - "annPinnedAnnounce": "Pinned announcements", - "annOtherAnnounce": "Other announcements", - "annAnnounceCreatSuccess": "Announcement created successfully", - "annCouldNotCreateAnnounce": "Could not create announcement", - "annNewAnnounce": "New announcement", - "annBody": "Body", - "annPinThisAnnounceQ": "Pin this announcement?", - "annAllowCommentsQ": "Allow comments?", - "annCreateAnnounce": "Create announcement", - "annAnnounceDelSuccess": "Announcement deleted successfully", - "annCouldNotDelAnnounce": "Could not delete announcement", - "annUnpin": "unpin", - "annPin": "pin", - "annUnpinned": "unpinned", - "annPinned": "pinned", - "annLastUpdated": "Last updated", - "annCommentsDisabled": "Comments disabled.", - "annAreYouSureYouWantToDelThisannounceQ": "Are you sure you want to delete this announcement? This cannot be undone.", - "annAnnounceUpdatedSuccess": "Announcement updated successfully", - "annCouldNotUpdateAnnounce": "Could not update announcement", - "annEditAnnounce": "Edit announcement", - "annUpdateAnnounce": "Update announcement", - "bmYourBM": "Your bookmarks", - "bmYouNotHaveAnyBM": "You do not have any bookmarks.", - "catCategories": "Categories", - "catNoCategoryHaveBeenDefined": "No categories have been defined.", - "catNoTagsHaveBeenDefined": "No tags have been defined.", - "catNoResults": "No results.", - "comCommentBy": "Comment by", - "comDelUser": "deleted user", - "comOn": "on", - "email": "Email", - "errSomethingWentWrong": "Something went wrong", - "errTooManyRequests": "Too many requests! You have been rate limited. Please wait a while before trying again.", - "errIfErrorPersist": "If the error persists, please", - "errReportIt": "report it", - "errForNow": "For now,", - "indexLatestAnnounce": "Latest announcement", - "indexLatestTorrents": "Latest torrents", - "indexSearch": "Search", - "indexSearchTorrents": "Search torrents", - "indexText1": "Your email address is not yet verified. You will not be able to upload or download any data until this is done.", - "indexTime": "HH:mm Do MMM YYYY", - "listNoItemShow": "No items to show.", - "logIn": "Log in", - "logInFailed": "Could not log in", - "mdMarkdownPreview": "Markdown preview", - "navAnnouncements": "Announcements", - "navBookmarks": "Bookmarks", - "navBrowse": "Browse", - "navHome": "Home", - "navLogOut": "Log out", - "navReports": "Reports", - "navRequests": "Requests", - "navRSS": "RSS", - "navSearch": "Search", - "navStats": "Stats", - "navUpload": "Upload", - "navWiki": "Wiki", - "newPassword": "New password", - "password": "Password", - "passwordResetFailed": "Could not complete password reset", - "passwordResetRequestFailed": "Could not initiate password reset", - "passwordResetRequestSuccess": "If an account with that email address exists, you will receive an email shortly", - "passwordResetSuccess": "Password was reset successfully", - "poweredBy": "Powered by", - "register": "Register", - "registerFailed": "Could not register", - "registrationClosed": "Registration closed", - "reqCreateNew": "Create new", - "reqCreateReq": "Create request", - "reqTitle": "Title", - "reqPostedBy": "Posted by", - "reqPost": "Post", - "reqRequestCreatedSuccess": "Request created successfully", - "reqCouldNotCreateReq": "Could not create request", - "reqNewRequest": "New request", - "reqWhatYouLookForQ": "What are you looking for?", - "reqRequestDelSuccess": "Request deleted successfully", - "reqCouldNotDelReq": "Could not delete request", - "reqCommentPostSuccess": "Comment posted successfully", - "reqCommentNotPost": "Could not post comment", - "reqSuggestionAddSuccess": "Suggestion added successfully", - "reqSuggestionNotAdded": "Could not add suggestion", - "reqSuggestionAcceptSuccess": "Suggestion accepted successfully", - "reqCouldNotAcceptSuggestion": "Could not accept suggestion", - "reqDelete": "Delete", - "reqSuggestedTorrents": "Suggested torrents", - "reqSuggestATorrent": "Suggest a torrent", - "reqAccepted": "Accepted", - "reqAccept": "Accept", - "reqNoTorrentsHaveBeenSuggestedYet": "No torrents have been suggested yet.", - "reqPostAComment": "Post a comment", - "reqEnterInfohashTorrentBelow": "Enter the infohash of a torrent below.", - "reqFulfilled": "Fulfilled", - "reqPosted": "Posted", - "reqBy": "by", - "reqInfohash": "Infohash", - "reqSuggest": "Suggest", - "repUnresolvedRep": "Unresolved reports", - "repRepBy": "Reported by", - "repReason": "Reason", - "repRepMarkSolved": "Report marked as solved", - "repCouldNotResolveRep": "Could not resolve report", - "repRepOn": "Report on", - "repMarkSolved": "Mark as solved", - "repRep": "Reported", - "repTorrDetail": "Torrent details", - "resetPassword": "Reset password", - "rssThereRSSFeedAt": "There is an RSS feed at", - "rssToAuthenticateYourself": "To authenticate yourself, you must provide the cookies", - "rssAnd": "and", - "rssToRSSEndpoint": "to the RSS endpoint, containing your username and your password respectively.", - "rssNoQueryParametersAreProvided": "If no query parameters are provided, the RSS feed will contain the 100 latest torrents.", - "rssOnlyIncludeMatchingResults": "To only include matching results in the feed, you can add the", - "rssQueryParameter": "query parameter, e.g.", - "searchSearchResults": "Search results for", - "searchSearchError": "Search error", - "statYouNotPermission": "You do not have permission to do that.", - "statTrackerStat": "Tracker statistics", - "tagTaggedWith": "Tagged with", - "tokenError": "Token error", - "torrSeeders": "Seeders", - "torrLeechers": "Leechers", - "torrDownloads": "Downloads", - "torrUploaded": "Uploaded", - "torrTorrEditSuccess": "Torrent edited successfully", - "torrCouldEditTorr": "Could edit torrent", - "torrTorrDelSuccess": "Torrent deleted successfully", - "torrCouldNotDelTorr": "Could not delete torrent", - "torrVoteSubmitSuccess": "Vote submitted successfully", - "torrCouldNotSubmitVote": "Could not submit vote", - "torrReportSubmitSuccess": "Report submitted successfully", - "torrCouldNotSubmitReport": "Could not submit report", - "torrFLToggleSuccess": "Freeleech toggled successfully", - "torrCouldNotToggleFL": "Could not toggle freeleech", - "torrTorrRemFromGroupSuccess": "Torrent removed from group successfully", - "torrCouldNotRemTorrFromGroup": "Could not remove torrent from group", - "torrTorrent": "Torrent", - "torrCouldNotBookmarkTorr": "Could not bookmark torrent", - "torrFL": "FL!", - "torrEdit": "Edit", - "torrDownload": "Download", - "torrLogInDownload": "Log in to download", - "torrFiles": "Files", - "torrReport": "Report", - "torrRemTorr": "Remove this torrent", - "torrAddTorr": "Add a torrent", - "torrReasonForReport": "Reason for report", - "torrSureDeleteTorr": "Are you sure you want to delete this torrent? This cannot be undone.", - "torrFreeleech": "Freeleech", - "torrUnset": "Unset", - "torrSet": "Set", - "torrUploadedBy": "Uploaded by", - "torrDate": "Date", - "torrSize": "Size", - "torrYes": "Yes", - "torrNo": "No", - "torrGroupTorr": "Grouped torrents", - "torrThereAreNoOtherTorrGroup": "There are no other torrents in this group.", - "totp": "One-time code", - "uploadAddTag": "Add tag", - "uploadAnnounceURL": "Announce URL", - "uploadAnonymousUpload": "Anonymous upload", - "uploadCategory": "Category", - "uploadCouldNotGetGroupSuggestions": "Could not get group suggestions", - "uploadCouldNotUploadFile": "Could not upload file", - "uploadCouldNotUploadTorrent": "Could not upload .torrent", - "uploadDescription": "Description", - "uploadDragDropClickSelect": "Drag and drop .torrent file here, or click to select", - "uploadDropFileHere": "Drop the file here...", - "uploadGroupWith": "Group with", - "uploadGroupWithThisTorrent": "Group with this torrent", - "uploadInfoBox1": "The following file extensions are blacklisted. Any torrent containing files of these types will not be uploaded.", - "uploadInfoBox2": "It looks like these existing torrents have similar names. Would you like to group your upload with any of them? Groups should only contain very similar content, e.g. the same movie in different formats.", - "uploadInfoBox3": "Note: if you have started seeding a torrent before uploading, you may need to refresh trackers in your torrent client once the upload is complete.", - "uploadMarkdownSupport": "Markdown supported", - "uploadMediaInfo": "MediaInfo", - "uploadName": "Name", - "uploadSource": "Source", - "uploadTags": "Tags", - "uploadTorrentFile": "Torrent file", - "uploadTorrentUploadSuccess": "Torrent uploaded successfully", - "uploadUpload": "Upload", - "username": "Username", - "userUser": "User", - "userSuccessfully": "successfully", - "userCouldNot": "Could not", - "userProfile": "profile", - "userUserSince": "User since", - "userUserSinceTime": "Do MMM YYYY", - "userOnlyAdminsSee": "Only admins can see this", - "userInvitedBy": "Invited by", - "userEmailVerified": "Email verified", - "userRemainingInvites": "Remaining invites", - "userRatio": "Ratio", - "userDownloaded": "Downloaded", - "userComments": "Comments", - "userNoComments": "No comments.", - "userYouSureWant": "Are you sure that you want to", - "userThisUserQ": "this user?", - "userUploaded": "Uploaded", - "userMyUploads": "My uploads", - "usernameRules": "Can only consist of letters, numbers, and “.”", - "userUnban": "unban", - "userBan": "ban", - "userUnbanned": "unbanned", - "userBanned": "banned", - "veCouldNotVerifyEmailAddress": "Could not verify email address:", - "veEmailAddressVerifiedSuccess": "Email address verified successfully.", - "veNoVerificationTokenProvided": "No verification token provided", - "veVerifyEmail": "Verify email", - "welcome": "Welcome", - "welcomeBack": "Welcome back", - "wikiPath": "Path", - "wikiPageWillBeVisibleAt": "Page will be visible at", - "wikiAllowUnregisteredView": "Allow unregistered view", - "wikiPageCreateSuccess": "Wiki page created successfully", - "wikiCouldNotCreatePage": "Could not create wiki page", - "wikiNewPage": "New wiki page", - "wikiCreatePage": "Create wiki page", - "wikiPageDelSuccess": "Wiki page deleted successfully", - "wikiCouldNotDelPage": "Could not delete wiki page", - "wikiPageUpdateSuccess": "Wiki page updated successfully", - "wikiCouldNotUpdatePage": "Could not update wiki page", - "wikiAddPage": "Add page", - "wikiLastEdited": "Last edited", - "wikiPages": "Pages", - "wikiSaveChanges": "Save changes", - "wikiThereNothingHereYet": "There is nothing here yet.", - "wikiDelThisPageQ": "Are you sure you want to delete this wiki page? This cannot be undone." - }, - "ru": { - "404NotFound": "Не найдено", - "404PageDoesNotExist": "Эта страница не существует.", - "404ReturnHome": "Вернуться на главную страницу", - "acc2FADisabled": "2FA отключена", - "acc2FAEnabled": "2FA включена", - "acc2FAScanQR": "Отсканируйте QR-код с помощью приложения для аутентификации и введите одноразовый код", - "acc2FAText1": "2FA успешно включена. Эти резервные коды можно использовать для входа в систему, если вы потеряете доступ к приложению для проверки подлинности. Сохраните их сейчас, они больше не будут показаны.", - "acc2FAUseApp": "Используйте приложение для аутентификации, чтобы добавить еще один уровень безопасности к вашей учетной записи", - "acc2FAuth": "Двухфакторная аутентификация", - "accBonusPoints": "Бонусные баллы", - "accBonusPointsHave": "бонус(а/ов)", - "accBuy": "Обменять", - "accCancel": "Отмена", - "accChangePass": "Изменить пароль", - "accClaimed": "Заявлено", - "accCopyLink": "Копировать ссылку", - "accCouldNotBuyItems": "Не удалось обменять", - "accCouldNotChangePass": "Не удалось изменить пароль", - "accCouldNotDelAcc": "Не удалось удалить аккаунт", - "accCouldNotSendInvite": "Не удалось отправить приглашение", - "accCouldNotToggle2FA": "Не удалось изменить", - "accCreated": "Создан", - "accCurrentPass": "Текущий пароль", - "accDangerZone": "Зона опасности", - "accDelAccText1": "Вы уверены, что хотите удалить свою учетную запись? Это действие невозможно отменить, и, возможно, вы не сможете зарегистрироваться снова. Ваша личная информация будет удалена, но загруженные вами торренты останутся.", - "accDeleteMyAcc": "Удалите мой аккаунт", - "accDeleteMyAccYes": "Да, удалить мой аккаунт", - "accDisable": "Отключить", - "accEnable": "Включить", - "accEnable2FA": "Включить 2FA", - "accEveryRequestYouFulfill": "за каждый выполненный вами запрос или", - "accForEveryGBYouUpload": "за каждый ГБ, который раздали", - "accIfYouAreAlsUploaderAcceptTorrent": "если вы также являетесь пользователем торрента", - "accInviteLinkCopiedClipboard": "Ссылка для приглашения скопирована в буфер обмена", - "accInvites": "Приглашения", - "accInviteSentSuccess": "Приглашение успешно отправлено", - "accInviteText1": "Введите адрес эл. почты, чтобы отправить приглашение. Приглашенному пользователю необходимо будет зарегистрироваться, используя тот же адрес эл. почты. После создания приглашения вы также можете скопировать прямую ссылку для приглашения.", - "accItemsPurchasedSuccess": "Обмен успешно завершен", - "accMyAccount": "Мой аккаунт", - "accPassChangedSuccess": "Пароль успешно изменен", - "accPurchaseInvites": "Получить приглашения", - "accPurchaseUpload1GB": "Повысить ратио (получить 1 ГБ)", - "accRole": "Права", - "accSendInvite": "Отправить приглашение", - "accThisIsAdminAcc": "Это учетная запись администратора.", - "accValidUntil": "Годен до", - "accYouCurrentlyHave": "В настоящее время у вас есть", - "accYouWillEarn": "Вы получите", - "accNotAvailableToBuy": "Недоступно для обмена", - "annPinnedAnnounce": "Закрепленные анонсы", - "annOtherAnnounce": "Другие анонсы", - "annAnnounceCreatSuccess": "Анонс создан успешно", - "annCouldNotCreateAnnounce": "Не удалось создать анонс", - "annNewAnnounce": "Новый анонс", - "annBody": "Описание", - "annPinThisAnnounceQ": "Закрепить этот анонс?", - "annAllowCommentsQ": "Разрешить комментарии?", - "annCreateAnnounce": "Создать анонс", - "annAnnounceDelSuccess": "Анонс успешно удален", - "annCouldNotDelAnnounce": "Не удалось удалить анонс", - "annUnpin": "Открепить", - "annPin": "Прикрепить", - "annUnpinned": "откреплен", - "annPinned": "прикреплен", - "annLastUpdated": "Последнее обновление", - "annCommentsDisabled": "Комментарии отключены.", - "annAreYouSureYouWantToDelThisannounceQ": "Вы уверены, что хотите удалить этот анонс? Его потом не восстановить.", - "annAnnounceUpdatedSuccess": "Анонс успешно обновлен", - "annCouldNotUpdateAnnounce": "Не удалось обновить анонс", - "annEditAnnounce": "Изменить анонс", - "annUpdateAnnounce": "Обновить анонс", - "bmYourBM": "Ваши закладки", - "bmYouNotHaveAnyBM": "У вас нет закладок.", - "catCategories": "Категории", - "catNoCategoryHaveBeenDefined": "Категории не определены.", - "catNoTagsHaveBeenDefined": "Метки не определены.", - "catNoResults": "Нет результатов.", - "comCommentBy": "Комментарий от", - "comDelUser": "удаленный пользователь", - "comOn": "на", - "email": "Эл. почта", - "errSomethingWentWrong": "Что-то пошло не так", - "errTooManyRequests": "Слишком много запросов! Пожалуйста, подождите некоторое время, прежде чем повторить попытку.", - "errIfErrorPersist": "Если ошибка повторится, пожалуйста,", - "errReportIt": "напишите об этом", - "errForNow": "На данный момент,", - "indexLatestAnnounce": "Последний анонс", - "indexLatestTorrents": "Последние торренты", - "indexSearch": "Поиск", - "indexSearchTorrents": "Поиск торрентов", - "indexText1": "Ваш адрес эл. почты еще не подтвержден. Пока это не будет сделано, вы не сможете загружать или скачивать.", - "indexTime": "DD.MM.YYYY HH:mm", - "listNoItemShow": "Нет элементов для отображения.", - "logIn": "Вход", - "logInFailed": "Не удалось войти", - "mdMarkdownPreview": "Markdown предпросмотр", - "navAnnouncements": "Анонсы", - "navBookmarks": "Закладки", - "navBrowse": "Категории", - "navHome": "Главная", - "navLogOut": "Выход", - "navReports": "Жалобы", - "navRequests": "Запросы", - "navSearch": "Поиск", - "navStats": "Статистика", - "navUpload": "Загрузить", - "newPassword": "Новый пароль", - "password": "Пароль", - "passwordResetFailed": "Не удалось завершить сброс пароля", - "passwordResetRequestFailed": "Не удалось инициировать сброс пароля", - "passwordResetRequestSuccess": "Если учетная запись с таким адресом эл. почты существует, вы вскоре получите эл. письмо.", - "passwordResetSuccess": "Пароль успешно сброшен", - "register": "Регистрация", - "registerFailed": "Не удалось зарегистрироваться", - "registrationClosed": "Регистрация закрыта", - "reqCreateNew": "Создать новый", - "reqTitle": "Заголовок", - "reqPostedBy": "Сообщение от", - "reqPost": "Опубликовать", - "reqTitle": "Заголовок", - "reqRequestCreatedSuccess": "Запрос успешно создан", - "reqCouldNotCreateReq": "Не удалось создать запрос", - "reqNewRequest": "Новый запрос", - "reqCreateReq": "Создать запрос", - "reqWhatYouLookForQ": "Что Вы ищете?", - "reqRequestDelSuccess": "Запрос успешно удален", - "reqCouldNotDelReq": "Не удалось удалить запрос", - "reqCommentPostSuccess": "Комментарий успешно опубликован", - "reqCommentNotPost": "Не удалось опубликовать комментарий", - "reqSuggestionAddSuccess": "Предложение успешно добавлено", - "reqSuggestionNotAdded": "Не удалось добавить предложение", - "reqSuggestionAcceptSuccess": "Предложение успешно принято", - "reqCouldNotAcceptSuggestion": "Не удалось принять предложение", - "reqDelete": "Удалить", - "reqSuggestedTorrents": "Предлагаемые торренты", - "reqSuggestATorrent": "Предложить торрент", - "reqAccepted": "Принято", - "reqAccept": "Принять", - "reqNoTorrentsHaveBeenSuggestedYet": "Торренты пока не предложены.", - "reqPostAComment": "Оставить комментарий", - "reqEnterInfohashTorrentBelow": "Введите информационный хэш торрента ниже.", - "reqFulfilled": "Выполнено", - "reqPosted": "Опубликовано", - "reqBy": "пользователем", - "reqInfohash": "Инфохеш", - "reqSuggest": "Предложить", - "repUnresolvedRep": "Неразрешенные жалобы", - "repRepBy": "Жалоба от", - "repReason": "Причина", - "repRepMarkSolved": "Жалоба помечена как решенная", - "repCouldNotResolveRep": "Не удалось решить жалобу", - "repRepOn": "Жалоба на", - "repMarkSolved": "Пометить как решенная", - "repRep": "Жалоба отправлена", - "repTorrDetail": "Детали торрента", - "resetPassword": "Сброс пароля", - "rssThereRSSFeedAt": "RSS-канал по адресу", - "rssToAuthenticateYourself": "Чтобы аутентифицировать себя, вы должны предоставить файлы cookie", - "rssAnd": "и", - "rssToRSSEndpoint": "к конечной точке RSS, содержащей ваше имя пользователя и пароль соответственно.", - "rssNoQueryParametersAreProvided": "Если параметры запроса не указаны, RSS-канал будет содержать 100 последних торрентов.", - "rssOnlyIncludeMatchingResults": "Чтобы включить в ленту только совпадающие результаты, вы можете добавить", - "rssQueryParameter": "параметр запроса, например", - "searchSearchResults": "Результат поиска для", - "searchSearchError": "Ошибка поиска", - "statYouNotPermission": "У вас нет прав для просмотра.", - "statTrackerStat": "Статистика трекера", - "tagTaggedWith": "С метками", - "tokenError": "Ошибка токена", - "torrSeeders": "Сидеры", - "torrLeechers": "Личеры", - "torrDownloads": "Загрузки", - "torrUploaded": "Дата загрузки", - "torrTorrEditSuccess": "Торрент успешно отредактирован", - "torrCouldEditTorr": "Можно редактировать торрент", - "torrTorrDelSuccess": "Торрент успешно удален", - "torrCouldNotDelTorr": "Не удалось удалить торрент", - "torrVoteSubmitSuccess": "Голосование отправлено успешно", - "torrCouldNotSubmitVote": "Не удалось проголосовать", - "torrReportSubmitSuccess": "Жалоба успешно отправлен", - "torrCouldNotSubmitReport": "Не удалось отправить жалобу", - "torrFLToggleSuccess": "Фрилич успешно переключен", - "torrCouldNotToggleFL": "Не удалось переключить фрилич", - "torrTorrRemFromGroupSuccess": "Торрент успешно удален из группы", - "torrCouldNotRemTorrFromGroup": "Не удалось удалить торрент из группы", - "torrTorrent": "Торрент", - "torrCouldNotBookmarkTorr": "Не удалось добавить торрент в закладки", - "torrEdit": "Изменить", - "torrDownload": "Загрузить", - "torrLogInDownload": "Войдите, чтобы скачать", - "torrFiles": "Файлы", - "torrReport": "Жалоба", - "torrRemTorr": "Удалить этот торрент", - "torrAddTorr": "Добавить торрент", - "torrReasonForReport": "Причина жалобы", - "torrSureDeleteTorr": "Вы уверены, что хотите удалить этот торрент? Это действие нельзя отменить.", - "torrFreeleech": "Фрилич", - "torrUnset": "Отключить", - "torrSet": "Включить", - "torrUploadedBy": "Загружено пользователем", - "torrDate": "Дата", - "torrSize": "Размер", - "torrYes": "Да", - "torrNo": "Нет", - "torrGroupTorr": "Сгруппированные торренты", - "torrThereAreNoOtherTorrGroup": "Других торрентов в этой группе нет.", - "totp": "Одноразовый код", - "uploadAddTag": "Добавить метку", - "uploadAnnounceURL": "Адрес трекера", - "uploadAnonymousUpload": "Анонимная загрузка", - "uploadCategory": "Категория", - "uploadCouldNotGetGroupSuggestions": "Не удалось получить предложения группы", - "uploadCouldNotUploadFile": "Не удалось загрузить файл", - "uploadCouldNotUploadTorrent": "Не удалось загрузить .torrent", - "uploadDescription": "Описание", - "uploadDragDropClickSelect": "Перетащите файл .torrent сюда или щелкните, чтобы выбрать", - "uploadDropFileHere": "Перетащите файл сюда...", - "uploadGroupWith": "Группа с", - "uploadGroupWithThisTorrent": "Группа с этим торрентом", - "uploadInfoBox1": "Следующие расширения файлов занесены в черный список. Любой торрент, содержащий файлы этих типов, не будет загружен.", - "uploadInfoBox2": "Похоже, что эти существующие торренты имеют схожие названия. Хотите сгруппировать загрузку с каким-либо из них? Группы должны содержать только очень похожий контент, например один и тот же фильм в разных форматах.", - "uploadInfoBox3": "Примечание. Если вы начали раздачу торрента перед загрузкой, вам может потребоваться обновить трекеры в вашем торрент-клиенте после завершения загрузки.", - "uploadMarkdownSupport": "Поддержка Markdown", - "uploadMediaInfo": "Медиаинфо", - "uploadName": "Имя", - "uploadSource": "Источник", - "uploadTags": "Метки", - "uploadTorrentFile": "Торрент файл", - "uploadTorrentUploadSuccess": "Торрент успешно загружен", - "uploadUpload": "Загрузить", - "username": "Имя", - "userUser": "Пользователь", - "userSuccessfully": "успешно", - "userCouldNot": "Не удалось", - "userProfile": "профиль", - "userUserSince": "Пользователь с", - "userUserSinceTime": "DD.MM.YYYY", - "userOnlyAdminsSee": "Это могут видеть только администраторы", - "userInvitedBy": "По приглашению", - "userEmailVerified": "Эл. почта подтверждена", - "userRemainingInvites": "Оставшиеся приглашения", - "userRatio": "Ratio(соотношение)", - "userDownloaded": "Скачано", - "userComments": "Комментарии", - "userNoComments": "Комментариев нет.", - "userYouSureWant": "Вы уверены, что хотите", - "userThisUserQ": "этого пользователя?", - "userUploaded": "Роздано", - "userMyUploads": "Мои раздачи", - "usernameRules": "Может состоять только из букв, цифр и “.”", - "userUnban": "разбанить", - "userBan": "забанить", - "userUnbanned": "разбанен", - "userBanned": "забанен", - "veCouldNotVerifyEmailAddress": "Не удалось подтвердить адрес эл. почты:", - "veEmailAddressVerifiedSuccess": "Адрес эл. почты успешно подтвержден.", - "veNoVerificationTokenProvided": "Токен подтверждения не предоставлен", - "veVerifyEmail": "Подтвердить эл. почту", - "welcome": "Добро пожаловать", - "welcomeBack": "С возвращением", - "wikiPath": "Путь", - "wikiPageWillBeVisibleAt": "Страница будет видна по адресу", - "wikiAllowUnregisteredView": "Разрешить просмотр без авторизации", - "wikiPageCreateSuccess": "Wiki страница успешно создана", - "wikiCouldNotCreatePage": "Не удалось создать wiki страницу", - "wikiNewPage": "Новая wiki страница", - "wikiCreatePage": "Создать wiki страницу", - "wikiPageDelSuccess": "Wiki страница успешно удалена", - "wikiCouldNotDelPage": "Не удалось удалить wiki страницу", - "wikiPageUpdateSuccess": "Wiki страница успешно обновлена", - "wikiCouldNotUpdatePage": "Не удалось обновить wiki страницу", - "wikiAddPage": "Добавить страницу", - "wikiLastEdited": "Последнее редактирование", - "wikiPages": "Страницы", - "wikiSaveChanges": "Сохранить изменения", - "wikiThereNothingHereYet": "Здесь пока ничего нет.", - "wikiDelThisPageQ": "Вы уверены, что хотите удалить эту wiki страницу? Действие необратимо." - } -} diff --git a/client/locales/de.json b/client/locales/de.json new file mode 100644 index 0000000..eb23c7b --- /dev/null +++ b/client/locales/de.json @@ -0,0 +1,22 @@ +{ + "poweredBy": "Powered by", + "logIn": "Anmelden", + "register": "Registrieren", + "email": "E-Mail", + "username": "Benutzername", + "usernameRules": "Nur Buchstaben, Zahlen, und “.” erlaubt.", + "password": "Passwort", + "totp": "Einmalpasswort", + "resetPassword": "Passwort zurücksetzen", + "newPassword": "Neues Passwort", + "welcomeBack": "Willkommen zurück", + "logInFailed": "Anmeldung fehgeschlagen", + "welcome": "Willkommen", + "registerFailed": "Registrierung fehlgeschlagen", + "registrationClosed": "Registrierung geschlossen", + "passwordResetRequestSuccess": "Bestätigung versand, falls ein Konto mit dieser E-Mail Adresse existiert", + "passwordResetRequestFailed": "Passwort zurücksetzen konnte nicht initiallisiert werden", + "passwordResetSuccess": "Passwort wurde erfolgreich zurückgesetzt", + "passwordResetFailed": "Passwort wurde nicht erfolgreich zurückgesetzt", + "tokenError": "Token Fehler" +} diff --git a/client/locales/en.json b/client/locales/en.json new file mode 100644 index 0000000..da75482 --- /dev/null +++ b/client/locales/en.json @@ -0,0 +1,282 @@ +{ + "404NotFound": "Not found", + "404PageDoesNotExist": "That page does not exist.", + "404ReturnHome": "Return home", + "acc2FADisabled": "2FA disabled", + "acc2FAEnabled": "2FA enabled", + "acc2FAScanQR": "Scan the QR code with your authenticator app and enter the one-time code", + "acc2FAText1": "2FA enabled successfully. These backup codes can be used to log in if you lose access to your authenticator app. Save them now, they will not be visible again.", + "acc2FAUseApp": "Use an authenticator app to add another layer of security to your account", + "acc2FAuth": "Two-factor authentication", + "accBonusPoints": "Bonus points", + "accBonusPointsHave": "bonus point(s)", + "accBuy": "Buy", + "accCancel": "Cancel", + "accChangePass": "Change password", + "accClaimed": "Claimed", + "accCopyLink": "Copy link", + "accCouldNotBuyItems": "Could not buy items", + "accCouldNotChangePass": "Could not change password", + "accCouldNotDelAcc": "Could not delete account", + "accCouldNotSendInvite": "Could not send invite", + "accCouldNotToggle2FA": "Could not toggle 2FA", + "accCreated": "Created", + "accCurrentPass": "Current password", + "accDangerZone": "Danger zone", + "accDelAccText1": "Are you sure you want to delete your account? This action cannot be undone, and you may not be able to register again. Your personal information will be deleted but your uploaded torrents will remain.", + "accDeleteMyAcc": "Delete my account", + "accDeleteMyAccYes": "Yes, delete my account", + "accDisable": "Disable", + "accEnable": "Enable", + "accEnable2FA": "Enable 2FA", + "accEveryRequestYouFulfill": "for every request you fulfill, or", + "accForEveryGBYouUpload": "for every GB you upload", + "accIfYouAreAlsUploaderAcceptTorrent": "if you are als the uploader of the accepted torrent", + "accInviteLinkCopiedClipboard": "Invite link copied to clipboard", + "accInvites": "Invites", + "accInviteSentSuccess": "Invite sent successfully", + "accInviteText1": "Enter an email address to send an invite. The invited user will need to sign up with the same email address. Once the invite is generated, you can also copy a direct invite link.", + "accItemsPurchasedSuccess": "Items purchased successfully", + "accMyAccount": "My account", + "accPassChangedSuccess": "Password changed successfully", + "accPurchaseInvites": "Purchase invites", + "accPurchaseUpload1GB": "Purchase upload (1 GB)", + "accRole": "Role", + "accSendInvite": "Send invite", + "accThisIsAdminAcc": "This is an admin account.", + "accValidUntil": "Valid until", + "accYouCurrentlyHave": "You currently have", + "accYouWillEarn": "You will earn", + "accNotAvailableToBuy": "Not available to buy", + "annPinnedAnnounce": "Pinned announcements", + "annOtherAnnounce": "Other announcements", + "annAnnounceCreatSuccess": "Announcement created successfully", + "annCouldNotCreateAnnounce": "Could not create announcement", + "annNewAnnounce": "New announcement", + "annBody": "Body", + "annPinThisAnnounceQ": "Pin this announcement?", + "annAllowCommentsQ": "Allow comments?", + "annCreateAnnounce": "Create announcement", + "annAnnounceDelSuccess": "Announcement deleted successfully", + "annCouldNotDelAnnounce": "Could not delete announcement", + "annUnpin": "unpin", + "annPin": "pin", + "annUnpinned": "unpinned", + "annPinned": "pinned", + "annLastUpdated": "Last updated", + "annCommentsDisabled": "Comments disabled.", + "annAreYouSureYouWantToDelThisannounceQ": "Are you sure you want to delete this announcement? This cannot be undone.", + "annAnnounceUpdatedSuccess": "Announcement updated successfully", + "annCouldNotUpdateAnnounce": "Could not update announcement", + "annEditAnnounce": "Edit announcement", + "annUpdateAnnounce": "Update announcement", + "bmYourBM": "Your bookmarks", + "bmYouNotHaveAnyBM": "You do not have any bookmarks.", + "catCategories": "Categories", + "catNoCategoryHaveBeenDefined": "No categories have been defined.", + "catNoTagsHaveBeenDefined": "No tags have been defined.", + "catNoResults": "No results.", + "comCommentBy": "Comment by", + "comDelUser": "deleted user", + "comOn": "on", + "email": "Email", + "errSomethingWentWrong": "Something went wrong", + "errTooManyRequests": "Too many requests! You have been rate limited. Please wait a while before trying again.", + "errIfErrorPersist": "If the error persists, please", + "errReportIt": "report it", + "errForNow": "For now,", + "indexLatestAnnounce": "Latest announcement", + "indexLatestTorrents": "Latest torrents", + "indexSearch": "Search", + "indexSearchTorrents": "Search torrents", + "indexText1": "Your email address is not yet verified. You will not be able to upload or download any data until this is done.", + "indexTime": "HH:mm Do MMM YYYY", + "listNoItemShow": "No items to show.", + "logIn": "Log in", + "logInFailed": "Could not log in", + "mdMarkdownPreview": "Markdown preview", + "navAnnouncements": "Announcements", + "navBookmarks": "Bookmarks", + "navBrowse": "Browse", + "navHome": "Home", + "navLogOut": "Log out", + "navReports": "Reports", + "navRequests": "Requests", + "navRSS": "RSS", + "navSearch": "Search", + "navStats": "Stats", + "navUpload": "Upload", + "navWiki": "Wiki", + "newPassword": "New password", + "password": "Password", + "passwordResetFailed": "Could not complete password reset", + "passwordResetRequestFailed": "Could not initiate password reset", + "passwordResetRequestSuccess": "If an account with that email address exists, you will receive an email shortly", + "passwordResetSuccess": "Password was reset successfully", + "poweredBy": "Powered by", + "register": "Register", + "registerFailed": "Could not register", + "registrationClosed": "Registration closed", + "reqCreateNew": "Create new", + "reqCreateReq": "Create request", + "reqTitle": "Title", + "reqPostedBy": "Posted by", + "reqPost": "Post", + "reqRequestCreatedSuccess": "Request created successfully", + "reqCouldNotCreateReq": "Could not create request", + "reqNewRequest": "New request", + "reqWhatYouLookForQ": "What are you looking for?", + "reqRequestDelSuccess": "Request deleted successfully", + "reqCouldNotDelReq": "Could not delete request", + "reqCommentPostSuccess": "Comment posted successfully", + "reqCommentNotPost": "Could not post comment", + "reqSuggestionAddSuccess": "Suggestion added successfully", + "reqSuggestionNotAdded": "Could not add suggestion", + "reqSuggestionAcceptSuccess": "Suggestion accepted successfully", + "reqCouldNotAcceptSuggestion": "Could not accept suggestion", + "reqDelete": "Delete", + "reqSuggestedTorrents": "Suggested torrents", + "reqSuggestATorrent": "Suggest a torrent", + "reqAccepted": "Accepted", + "reqAccept": "Accept", + "reqNoTorrentsHaveBeenSuggestedYet": "No torrents have been suggested yet.", + "reqPostAComment": "Post a comment", + "reqEnterInfohashTorrentBelow": "Enter the infohash of a torrent below.", + "reqFulfilled": "Fulfilled", + "reqPosted": "Posted", + "reqBy": "by", + "reqInfohash": "Infohash", + "reqSuggest": "Suggest", + "repUnresolvedRep": "Unresolved reports", + "repRepBy": "Reported by", + "repReason": "Reason", + "repRepMarkSolved": "Report marked as solved", + "repCouldNotResolveRep": "Could not resolve report", + "repRepOn": "Report on", + "repMarkSolved": "Mark as solved", + "repRep": "Reported", + "repTorrDetail": "Torrent details", + "resetPassword": "Reset password", + "rssThereRSSFeedAt": "There is an RSS feed at", + "rssToAuthenticateYourself": "To authenticate yourself, you must provide the cookies", + "rssAnd": "and", + "rssToRSSEndpoint": "to the RSS endpoint, containing your username and your password respectively.", + "rssNoQueryParametersAreProvided": "If no query parameters are provided, the RSS feed will contain the 100 latest torrents.", + "rssOnlyIncludeMatchingResults": "To only include matching results in the feed, you can add the", + "rssQueryParameter": "query parameter, e.g.", + "searchSearchResults": "Search results for", + "searchSearchError": "Search error", + "statYouNotPermission": "You do not have permission to do that.", + "statTrackerStat": "Tracker statistics", + "tagTaggedWith": "Tagged with", + "tokenError": "Token error", + "torrSeeders": "Seeders", + "torrLeechers": "Leechers", + "torrDownloads": "Downloads", + "torrUploaded": "Uploaded", + "torrTorrEditSuccess": "Torrent edited successfully", + "torrCouldEditTorr": "Could edit torrent", + "torrTorrDelSuccess": "Torrent deleted successfully", + "torrCouldNotDelTorr": "Could not delete torrent", + "torrVoteSubmitSuccess": "Vote submitted successfully", + "torrCouldNotSubmitVote": "Could not submit vote", + "torrReportSubmitSuccess": "Report submitted successfully", + "torrCouldNotSubmitReport": "Could not submit report", + "torrFLToggleSuccess": "Freeleech toggled successfully", + "torrCouldNotToggleFL": "Could not toggle freeleech", + "torrTorrRemFromGroupSuccess": "Torrent removed from group successfully", + "torrCouldNotRemTorrFromGroup": "Could not remove torrent from group", + "torrTorrent": "Torrent", + "torrCouldNotBookmarkTorr": "Could not bookmark torrent", + "torrFL": "FL!", + "torrEdit": "Edit", + "torrDownload": "Download", + "torrLogInDownload": "Log in to download", + "torrFiles": "Files", + "torrReport": "Report", + "torrRemTorr": "Remove this torrent", + "torrAddTorr": "Add a torrent", + "torrReasonForReport": "Reason for report", + "torrSureDeleteTorr": "Are you sure you want to delete this torrent? This cannot be undone.", + "torrFreeleech": "Freeleech", + "torrUnset": "Unset", + "torrSet": "Set", + "torrUploadedBy": "Uploaded by", + "torrDate": "Date", + "torrSize": "Size", + "torrYes": "Yes", + "torrNo": "No", + "torrGroupTorr": "Grouped torrents", + "torrThereAreNoOtherTorrGroup": "There are no other torrents in this group.", + "totp": "One-time code", + "uploadAddTag": "Add tag", + "uploadAnnounceURL": "Announce URL", + "uploadAnonymousUpload": "Anonymous upload", + "uploadCategory": "Category", + "uploadCouldNotGetGroupSuggestions": "Could not get group suggestions", + "uploadCouldNotUploadFile": "Could not upload file", + "uploadCouldNotUploadTorrent": "Could not upload .torrent", + "uploadDescription": "Description", + "uploadDragDropClickSelect": "Drag and drop .torrent file here, or click to select", + "uploadDropFileHere": "Drop the file here...", + "uploadGroupWith": "Group with", + "uploadGroupWithThisTorrent": "Group with this torrent", + "uploadInfoBox1": "The following file extensions are blacklisted. Any torrent containing files of these types will not be uploaded.", + "uploadInfoBox2": "It looks like these existing torrents have similar names. Would you like to group your upload with any of them? Groups should only contain very similar content, e.g. the same movie in different formats.", + "uploadInfoBox3": "Note: if you have started seeding a torrent before uploading, you may need to refresh trackers in your torrent client once the upload is complete.", + "uploadMarkdownSupport": "Markdown supported", + "uploadMediaInfo": "MediaInfo", + "uploadName": "Name", + "uploadSource": "Source", + "uploadTags": "Tags", + "uploadTorrentFile": "Torrent file", + "uploadTorrentUploadSuccess": "Torrent uploaded successfully", + "uploadUpload": "Upload", + "username": "Username", + "userUser": "User", + "userSuccessfully": "successfully", + "userCouldNot": "Could not", + "userProfile": "profile", + "userUserSince": "User since", + "userUserSinceTime": "Do MMM YYYY", + "userOnlyAdminsSee": "Only admins can see this", + "userInvitedBy": "Invited by", + "userEmailVerified": "Email verified", + "userRemainingInvites": "Remaining invites", + "userRatio": "Ratio", + "userDownloaded": "Downloaded", + "userComments": "Comments", + "userNoComments": "No comments.", + "userYouSureWant": "Are you sure that you want to", + "userThisUserQ": "this user?", + "userUploaded": "Uploaded", + "userMyUploads": "My uploads", + "usernameRules": "Can only consist of letters, numbers, and “.”", + "userUnban": "unban", + "userBan": "ban", + "userUnbanned": "unbanned", + "userBanned": "banned", + "veCouldNotVerifyEmailAddress": "Could not verify email address:", + "veEmailAddressVerifiedSuccess": "Email address verified successfully.", + "veNoVerificationTokenProvided": "No verification token provided", + "veVerifyEmail": "Verify email", + "welcome": "Welcome", + "welcomeBack": "Welcome back", + "wikiPath": "Path", + "wikiPageWillBeVisibleAt": "Page will be visible at", + "wikiAllowUnregisteredView": "Allow unregistered view", + "wikiPageCreateSuccess": "Wiki page created successfully", + "wikiCouldNotCreatePage": "Could not create wiki page", + "wikiNewPage": "New wiki page", + "wikiCreatePage": "Create wiki page", + "wikiPageDelSuccess": "Wiki page deleted successfully", + "wikiCouldNotDelPage": "Could not delete wiki page", + "wikiPageUpdateSuccess": "Wiki page updated successfully", + "wikiCouldNotUpdatePage": "Could not update wiki page", + "wikiAddPage": "Add page", + "wikiLastEdited": "Last edited", + "wikiPages": "Pages", + "wikiSaveChanges": "Save changes", + "wikiThereNothingHereYet": "There is nothing here yet.", + "wikiDelThisPageQ": "Are you sure you want to delete this wiki page? This cannot be undone." +} diff --git a/client/locales/index.js b/client/locales/index.js new file mode 100644 index 0000000..0d88de5 --- /dev/null +++ b/client/locales/index.js @@ -0,0 +1,11 @@ +import en from "./en.json"; +import ru from "./ru.json"; +import de from "./de.json"; +import zh from "./zh.json"; + +export default { + en, + ru, + de, + zh, +}; diff --git a/client/locales/ru.json b/client/locales/ru.json new file mode 100644 index 0000000..9c4a6a3 --- /dev/null +++ b/client/locales/ru.json @@ -0,0 +1,279 @@ +{ + "404NotFound": "Не найдено", + "404PageDoesNotExist": "Эта страница не существует.", + "404ReturnHome": "Вернуться на главную страницу", + "acc2FADisabled": "2FA отключена", + "acc2FAEnabled": "2FA включена", + "acc2FAScanQR": "Отсканируйте QR-код с помощью приложения для аутентификации и введите одноразовый код", + "acc2FAText1": "2FA успешно включена. Эти резервные коды можно использовать для входа в систему, если вы потеряете доступ к приложению для проверки подлинности. Сохраните их сейчас, они больше не будут показаны.", + "acc2FAUseApp": "Используйте приложение для аутентификации, чтобы добавить еще один уровень безопасности к вашей учетной записи", + "acc2FAuth": "Двухфакторная аутентификация", + "accBonusPoints": "Бонусные баллы", + "accBonusPointsHave": "бонус(а/ов)", + "accBuy": "Обменять", + "accCancel": "Отмена", + "accChangePass": "Изменить пароль", + "accClaimed": "Заявлено", + "accCopyLink": "Копировать ссылку", + "accCouldNotBuyItems": "Не удалось обменять", + "accCouldNotChangePass": "Не удалось изменить пароль", + "accCouldNotDelAcc": "Не удалось удалить аккаунт", + "accCouldNotSendInvite": "Не удалось отправить приглашение", + "accCouldNotToggle2FA": "Не удалось изменить", + "accCreated": "Создан", + "accCurrentPass": "Текущий пароль", + "accDangerZone": "Зона опасности", + "accDelAccText1": "Вы уверены, что хотите удалить свою учетную запись? Это действие невозможно отменить, и, возможно, вы не сможете зарегистрироваться снова. Ваша личная информация будет удалена, но загруженные вами торренты останутся.", + "accDeleteMyAcc": "Удалите мой аккаунт", + "accDeleteMyAccYes": "Да, удалить мой аккаунт", + "accDisable": "Отключить", + "accEnable": "Включить", + "accEnable2FA": "Включить 2FA", + "accEveryRequestYouFulfill": "за каждый выполненный вами запрос или", + "accForEveryGBYouUpload": "за каждый ГБ, который раздали", + "accIfYouAreAlsUploaderAcceptTorrent": "если вы также являетесь пользователем торрента", + "accInviteLinkCopiedClipboard": "Ссылка для приглашения скопирована в буфер обмена", + "accInvites": "Приглашения", + "accInviteSentSuccess": "Приглашение успешно отправлено", + "accInviteText1": "Введите адрес эл. почты, чтобы отправить приглашение. Приглашенному пользователю необходимо будет зарегистрироваться, используя тот же адрес эл. почты. После создания приглашения вы также можете скопировать прямую ссылку для приглашения.", + "accItemsPurchasedSuccess": "Обмен успешно завершен", + "accMyAccount": "Мой аккаунт", + "accPassChangedSuccess": "Пароль успешно изменен", + "accPurchaseInvites": "Получить приглашения", + "accPurchaseUpload1GB": "Повысить ратио (получить 1 ГБ)", + "accRole": "Права", + "accSendInvite": "Отправить приглашение", + "accThisIsAdminAcc": "Это учетная запись администратора.", + "accValidUntil": "Годен до", + "accYouCurrentlyHave": "В настоящее время у вас есть", + "accYouWillEarn": "Вы получите", + "accNotAvailableToBuy": "Недоступно для обмена", + "annPinnedAnnounce": "Закрепленные анонсы", + "annOtherAnnounce": "Другие анонсы", + "annAnnounceCreatSuccess": "Анонс создан успешно", + "annCouldNotCreateAnnounce": "Не удалось создать анонс", + "annNewAnnounce": "Новый анонс", + "annBody": "Описание", + "annPinThisAnnounceQ": "Закрепить этот анонс?", + "annAllowCommentsQ": "Разрешить комментарии?", + "annCreateAnnounce": "Создать анонс", + "annAnnounceDelSuccess": "Анонс успешно удален", + "annCouldNotDelAnnounce": "Не удалось удалить анонс", + "annUnpin": "Открепить", + "annPin": "Прикрепить", + "annUnpinned": "откреплен", + "annPinned": "прикреплен", + "annLastUpdated": "Последнее обновление", + "annCommentsDisabled": "Комментарии отключены.", + "annAreYouSureYouWantToDelThisannounceQ": "Вы уверены, что хотите удалить этот анонс? Его потом не восстановить.", + "annAnnounceUpdatedSuccess": "Анонс успешно обновлен", + "annCouldNotUpdateAnnounce": "Не удалось обновить анонс", + "annEditAnnounce": "Изменить анонс", + "annUpdateAnnounce": "Обновить анонс", + "bmYourBM": "Ваши закладки", + "bmYouNotHaveAnyBM": "У вас нет закладок.", + "catCategories": "Категории", + "catNoCategoryHaveBeenDefined": "Категории не определены.", + "catNoTagsHaveBeenDefined": "Метки не определены.", + "catNoResults": "Нет результатов.", + "comCommentBy": "Комментарий от", + "comDelUser": "удаленный пользователь", + "comOn": "на", + "email": "Эл. почта", + "errSomethingWentWrong": "Что-то пошло не так", + "errTooManyRequests": "Слишком много запросов! Пожалуйста, подождите некоторое время, прежде чем повторить попытку.", + "errIfErrorPersist": "Если ошибка повторится, пожалуйста,", + "errReportIt": "напишите об этом", + "errForNow": "На данный момент,", + "indexLatestAnnounce": "Последний анонс", + "indexLatestTorrents": "Последние торренты", + "indexSearch": "Поиск", + "indexSearchTorrents": "Поиск торрентов", + "indexText1": "Ваш адрес эл. почты еще не подтвержден. Пока это не будет сделано, вы не сможете загружать или скачивать.", + "indexTime": "DD.MM.YYYY HH:mm", + "listNoItemShow": "Нет элементов для отображения.", + "logIn": "Вход", + "logInFailed": "Не удалось войти", + "mdMarkdownPreview": "Markdown предпросмотр", + "navAnnouncements": "Анонсы", + "navBookmarks": "Закладки", + "navBrowse": "Категории", + "navHome": "Главная", + "navLogOut": "Выход", + "navReports": "Жалобы", + "navRequests": "Запросы", + "navSearch": "Поиск", + "navStats": "Статистика", + "navUpload": "Загрузить", + "newPassword": "Новый пароль", + "password": "Пароль", + "passwordResetFailed": "Не удалось завершить сброс пароля", + "passwordResetRequestFailed": "Не удалось инициировать сброс пароля", + "passwordResetRequestSuccess": "Если учетная запись с таким адресом эл. почты существует, вы вскоре получите эл. письмо.", + "passwordResetSuccess": "Пароль успешно сброшен", + "register": "Регистрация", + "registerFailed": "Не удалось зарегистрироваться", + "registrationClosed": "Регистрация закрыта", + "reqCreateNew": "Создать новый", + "reqTitle": "Заголовок", + "reqPostedBy": "Сообщение от", + "reqPost": "Опубликовать", + "reqTitle": "Заголовок", + "reqRequestCreatedSuccess": "Запрос успешно создан", + "reqCouldNotCreateReq": "Не удалось создать запрос", + "reqNewRequest": "Новый запрос", + "reqCreateReq": "Создать запрос", + "reqWhatYouLookForQ": "Что Вы ищете?", + "reqRequestDelSuccess": "Запрос успешно удален", + "reqCouldNotDelReq": "Не удалось удалить запрос", + "reqCommentPostSuccess": "Комментарий успешно опубликован", + "reqCommentNotPost": "Не удалось опубликовать комментарий", + "reqSuggestionAddSuccess": "Предложение успешно добавлено", + "reqSuggestionNotAdded": "Не удалось добавить предложение", + "reqSuggestionAcceptSuccess": "Предложение успешно принято", + "reqCouldNotAcceptSuggestion": "Не удалось принять предложение", + "reqDelete": "Удалить", + "reqSuggestedTorrents": "Предлагаемые торренты", + "reqSuggestATorrent": "Предложить торрент", + "reqAccepted": "Принято", + "reqAccept": "Принять", + "reqNoTorrentsHaveBeenSuggestedYet": "Торренты пока не предложены.", + "reqPostAComment": "Оставить комментарий", + "reqEnterInfohashTorrentBelow": "Введите информационный хэш торрента ниже.", + "reqFulfilled": "Выполнено", + "reqPosted": "Опубликовано", + "reqBy": "пользователем", + "reqInfohash": "Инфохеш", + "reqSuggest": "Предложить", + "repUnresolvedRep": "Неразрешенные жалобы", + "repRepBy": "Жалоба от", + "repReason": "Причина", + "repRepMarkSolved": "Жалоба помечена как решенная", + "repCouldNotResolveRep": "Не удалось решить жалобу", + "repRepOn": "Жалоба на", + "repMarkSolved": "Пометить как решенная", + "repRep": "Жалоба отправлена", + "repTorrDetail": "Детали торрента", + "resetPassword": "Сброс пароля", + "rssThereRSSFeedAt": "RSS-канал по адресу", + "rssToAuthenticateYourself": "Чтобы аутентифицировать себя, вы должны предоставить файлы cookie", + "rssAnd": "и", + "rssToRSSEndpoint": "к конечной точке RSS, содержащей ваше имя пользователя и пароль соответственно.", + "rssNoQueryParametersAreProvided": "Если параметры запроса не указаны, RSS-канал будет содержать 100 последних торрентов.", + "rssOnlyIncludeMatchingResults": "Чтобы включить в ленту только совпадающие результаты, вы можете добавить", + "rssQueryParameter": "параметр запроса, например", + "searchSearchResults": "Результат поиска для", + "searchSearchError": "Ошибка поиска", + "statYouNotPermission": "У вас нет прав для просмотра.", + "statTrackerStat": "Статистика трекера", + "tagTaggedWith": "С метками", + "tokenError": "Ошибка токена", + "torrSeeders": "Сидеры", + "torrLeechers": "Личеры", + "torrDownloads": "Загрузки", + "torrUploaded": "Дата загрузки", + "torrTorrEditSuccess": "Торрент успешно отредактирован", + "torrCouldEditTorr": "Можно редактировать торрент", + "torrTorrDelSuccess": "Торрент успешно удален", + "torrCouldNotDelTorr": "Не удалось удалить торрент", + "torrVoteSubmitSuccess": "Голосование отправлено успешно", + "torrCouldNotSubmitVote": "Не удалось проголосовать", + "torrReportSubmitSuccess": "Жалоба успешно отправлен", + "torrCouldNotSubmitReport": "Не удалось отправить жалобу", + "torrFLToggleSuccess": "Фрилич успешно переключен", + "torrCouldNotToggleFL": "Не удалось переключить фрилич", + "torrTorrRemFromGroupSuccess": "Торрент успешно удален из группы", + "torrCouldNotRemTorrFromGroup": "Не удалось удалить торрент из группы", + "torrTorrent": "Торрент", + "torrCouldNotBookmarkTorr": "Не удалось добавить торрент в закладки", + "torrEdit": "Изменить", + "torrDownload": "Загрузить", + "torrLogInDownload": "Войдите, чтобы скачать", + "torrFiles": "Файлы", + "torrReport": "Жалоба", + "torrRemTorr": "Удалить этот торрент", + "torrAddTorr": "Добавить торрент", + "torrReasonForReport": "Причина жалобы", + "torrSureDeleteTorr": "Вы уверены, что хотите удалить этот торрент? Это действие нельзя отменить.", + "torrFreeleech": "Фрилич", + "torrUnset": "Отключить", + "torrSet": "Включить", + "torrUploadedBy": "Загружено пользователем", + "torrDate": "Дата", + "torrSize": "Размер", + "torrYes": "Да", + "torrNo": "Нет", + "torrGroupTorr": "Сгруппированные торренты", + "torrThereAreNoOtherTorrGroup": "Других торрентов в этой группе нет.", + "totp": "Одноразовый код", + "uploadAddTag": "Добавить метку", + "uploadAnnounceURL": "Адрес трекера", + "uploadAnonymousUpload": "Анонимная загрузка", + "uploadCategory": "Категория", + "uploadCouldNotGetGroupSuggestions": "Не удалось получить предложения группы", + "uploadCouldNotUploadFile": "Не удалось загрузить файл", + "uploadCouldNotUploadTorrent": "Не удалось загрузить .torrent", + "uploadDescription": "Описание", + "uploadDragDropClickSelect": "Перетащите файл .torrent сюда или щелкните, чтобы выбрать", + "uploadDropFileHere": "Перетащите файл сюда...", + "uploadGroupWith": "Группа с", + "uploadGroupWithThisTorrent": "Группа с этим торрентом", + "uploadInfoBox1": "Следующие расширения файлов занесены в черный список. Любой торрент, содержащий файлы этих типов, не будет загружен.", + "uploadInfoBox2": "Похоже, что эти существующие торренты имеют схожие названия. Хотите сгруппировать загрузку с каким-либо из них? Группы должны содержать только очень похожий контент, например один и тот же фильм в разных форматах.", + "uploadInfoBox3": "Примечание. Если вы начали раздачу торрента перед загрузкой, вам может потребоваться обновить трекеры в вашем торрент-клиенте после завершения загрузки.", + "uploadMarkdownSupport": "Поддержка Markdown", + "uploadMediaInfo": "Медиаинфо", + "uploadName": "Имя", + "uploadSource": "Источник", + "uploadTags": "Метки", + "uploadTorrentFile": "Торрент файл", + "uploadTorrentUploadSuccess": "Торрент успешно загружен", + "uploadUpload": "Загрузить", + "username": "Имя", + "userUser": "Пользователь", + "userSuccessfully": "успешно", + "userCouldNot": "Не удалось", + "userProfile": "профиль", + "userUserSince": "Пользователь с", + "userUserSinceTime": "DD.MM.YYYY", + "userOnlyAdminsSee": "Это могут видеть только администраторы", + "userInvitedBy": "По приглашению", + "userEmailVerified": "Эл. почта подтверждена", + "userRemainingInvites": "Оставшиеся приглашения", + "userRatio": "Ratio(соотношение)", + "userDownloaded": "Скачано", + "userComments": "Комментарии", + "userNoComments": "Комментариев нет.", + "userYouSureWant": "Вы уверены, что хотите", + "userThisUserQ": "этого пользователя?", + "userUploaded": "Роздано", + "userMyUploads": "Мои раздачи", + "usernameRules": "Может состоять только из букв, цифр и “.”", + "userUnban": "разбанить", + "userBan": "забанить", + "userUnbanned": "разбанен", + "userBanned": "забанен", + "veCouldNotVerifyEmailAddress": "Не удалось подтвердить адрес эл. почты:", + "veEmailAddressVerifiedSuccess": "Адрес эл. почты успешно подтвержден.", + "veNoVerificationTokenProvided": "Токен подтверждения не предоставлен", + "veVerifyEmail": "Подтвердить эл. почту", + "welcome": "Добро пожаловать", + "welcomeBack": "С возвращением", + "wikiPath": "Путь", + "wikiPageWillBeVisibleAt": "Страница будет видна по адресу", + "wikiAllowUnregisteredView": "Разрешить просмотр без авторизации", + "wikiPageCreateSuccess": "Wiki страница успешно создана", + "wikiCouldNotCreatePage": "Не удалось создать wiki страницу", + "wikiNewPage": "Новая wiki страница", + "wikiCreatePage": "Создать wiki страницу", + "wikiPageDelSuccess": "Wiki страница успешно удалена", + "wikiCouldNotDelPage": "Не удалось удалить wiki страницу", + "wikiPageUpdateSuccess": "Wiki страница успешно обновлена", + "wikiCouldNotUpdatePage": "Не удалось обновить wiki страницу", + "wikiAddPage": "Добавить страницу", + "wikiLastEdited": "Последнее редактирование", + "wikiPages": "Страницы", + "wikiSaveChanges": "Сохранить изменения", + "wikiThereNothingHereYet": "Здесь пока ничего нет.", + "wikiDelThisPageQ": "Вы уверены, что хотите удалить эту wiki страницу? Действие необратимо." +} diff --git a/client/locales/zh.json b/client/locales/zh.json new file mode 100644 index 0000000..fa2b623 --- /dev/null +++ b/client/locales/zh.json @@ -0,0 +1,22 @@ +{ + "poweredBy": "Powered by", + "logIn": "登录", + "register": "注册", + "email": "电子邮箱", + "username": "用户名", + "usernameRules": "只能由字母、数字和“.”组成", + "password": "密码", + "totp": "二步验证码", + "resetPassword": "忘记密码", + "newPassword": "新密码", + "welcomeBack": "欢迎回来", + "logInFailed": "登录失败", + "welcome": "欢迎", + "registerFailed": "注册失败", + "registrationClosed": "站点关闭注册", + "passwordResetRequestSuccess": "如果有使用该电子邮箱的帐户存在,您将收到一封邮件。", + "passwordResetRequestFailed": "无法进行密码重置操作,请确认您的信息填写正确后重试。", + "passwordResetSuccess": "密码重置成功", + "passwordResetFailed": "密码重置失败", + "tokenError": "token错误" +} diff --git a/client/pages/_app.js b/client/pages/_app.js index e562f84..412a855 100644 --- a/client/pages/_app.js +++ b/client/pages/_app.js @@ -28,7 +28,7 @@ import { NotificationsProvider } from "../components/Notifications"; import Text from "../components/Text"; import LoadingContext from "../utils/LoadingContext"; import LocaleContext from "../utils/LocaleContext"; -import locales from "../locales.json"; +import locales from "../locales"; const getThemeColours = (themeName, customTheme = {}) => { switch (themeName) { @@ -360,7 +360,9 @@ const SqTracker = ({ Component, pageProps, initialTheme }) => { ml={1} mr={2} > - {userStats.ratio === -1 ? "N/A" : userStats.ratio} + {userStats.ratio === -1 + ? "N/A" + : userStats.ratio} )} From 0870f3e0a127ecac4854ac4bb2e65e782abbd328 Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Tue, 19 Sep 2023 17:36:40 +0100 Subject: [PATCH 066/125] adds translation section to readme --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index c4f6925..73f87ba 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,21 @@ Alternatively, you can deploy each service individually on a PaaS cloud platform You will need to deploy each of the 4 components listed above. The Docker images for the client and API services are published in this repository. +## Adding a translation + +New translations are always appreciated! + +To add a new translation in your own language, create a new JSON file with your 2 character locale code in `client/locales`. For example, `client/locales/en.json`. In the `client/locales/index.js` file, you should then import your JSON file and add it to the exported object along with the existing locales. + +The best place to start is to copy the `en.json` file and work through it, translating each English string. + +### Existing translations + +* English (default) +* Russian: thanks to @smlinux +* German (incomplete): thanks to @MikeMatau +* Simplified Chinese (incomplete): thanks to @0EAC + ## Screenshots Splash screen From 8d6aee4f15f947a75fd5d9a3566cb23c8503b6cd Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Tue, 19 Sep 2023 17:59:28 +0100 Subject: [PATCH 067/125] localisation tweaks, add stats strings --- README.md | 10 +-- client/locales/en.json | 17 ++++- client/pages/announcements/[slug]/index.js | 46 ++++++++----- client/pages/stats.js | 8 +-- client/pages/torrent/[infoHash].js | 80 ++++++++++++---------- 5 files changed, 100 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 73f87ba..8a8b352 100644 --- a/README.md +++ b/README.md @@ -121,10 +121,12 @@ The best place to start is to copy the `en.json` file and work through it, trans ### Existing translations -* English (default) -* Russian: thanks to @smlinux -* German (incomplete): thanks to @MikeMatau -* Simplified Chinese (incomplete): thanks to @0EAC +| Language | Complete (estimate) | Contributed by | +|--------------------|---------------------|--------------------------------------------| +| English | 100% | | +| Russian | 95% | [@smlinux](https://github.com/smlinux) | +| German | 15% | [@MikeMatau](https://github.com/MikeMatau) | +| Simplified Chinese | 15% | [@0EAC](https://github.com/0EAC) | ## Screenshots diff --git a/client/locales/en.json b/client/locales/en.json index da75482..c874fd4 100644 --- a/client/locales/en.json +++ b/client/locales/en.json @@ -59,8 +59,8 @@ "annCreateAnnounce": "Create announcement", "annAnnounceDelSuccess": "Announcement deleted successfully", "annCouldNotDelAnnounce": "Could not delete announcement", - "annUnpin": "unpin", - "annPin": "pin", + "annUnpin": "Unpin", + "annPin": "Pin", "annUnpinned": "unpinned", "annPinned": "pinned", "annLastUpdated": "Last updated", @@ -168,6 +168,19 @@ "searchSearchError": "Search error", "statYouNotPermission": "You do not have permission to do that.", "statTrackerStat": "Tracker statistics", + "statActiveTorrents": "Active torrents", + "statBannedUsers": "Banned users", + "statCompletedDownloads": "Completed downloads", + "statFilledRequests": "Filled requests", + "statInvitesAccepted": "Invites accepted", + "statLeechers": "Leechers", + "statPeers": "Peers", + "statRegisteredUsers": "Registered users", + "statSeeders": "Seeders", + "statTotalComments": "Total comments", + "statTotalInvitesSent": "Total invites sent", + "statTotalRequests": "Total requests", + "statUploadedTorrents": "Uploaded torrents", "tagTaggedWith": "Tagged with", "tokenError": "Token error", "torrSeeders": "Seeders", diff --git a/client/pages/announcements/[slug]/index.js b/client/pages/announcements/[slug]/index.js index b5d09a7..5ef5c1a 100644 --- a/client/pages/announcements/[slug]/index.js +++ b/client/pages/announcements/[slug]/index.js @@ -60,13 +60,12 @@ const Announcement = ({ announcement, token, userRole }) => { throw new Error(reason); } - addNotification("success" - `${getLocaleString("annAnnounceDelSuccess")}` - ); + addNotification("success"`${getLocaleString("annAnnounceDelSuccess")}`); router.push("/announcements"); } catch (e) { - addNotification("error", + addNotification( + "error", `${getLocaleString("annCouldNotDelAnnounce")}: ${e.message}` ); console.error(e); @@ -81,7 +80,7 @@ const Announcement = ({ announcement, token, userRole }) => { try { const pinRes = await fetch( `${SQ_API_URL}/announcements/pin/${announcement._id}/${ - pinned ? [getLocaleString("annUnpin")] : [getLocaleString("annPin")] + pinned ? "unpin" : "pin" }`, { method: "POST", @@ -98,14 +97,18 @@ const Announcement = ({ announcement, token, userRole }) => { addNotification( "success", - `Announcement ${pinned ? [getLocaleString("annUnpinned")] : [getLocaleString("annPinned")]} successfully` + `Announcement ${ + pinned ? getLocaleString("annUnpinned") : getLocaleString("annPinned") + } successfully` ); setPinned((p) => !p); } catch (e) { addNotification( "error", - `${getLocaleString("userCouldNot")} ${pinned ? '${getLocaleString("annUnpin")}' : '${getLocaleString("annPin")}'} announcement: ${e.message}` + `${getLocaleString("userCouldNot")} ${ + pinned ? getLocaleString("annUnpin") : getLocaleString("annPin") + } announcement: ${e.message}` ); console.error(e); } @@ -138,9 +141,7 @@ const Announcement = ({ announcement, token, userRole }) => { throw new Error(reason); } - addNotification("success", - `${getLocaleString("reqCommentPostSuccess")}` - ); + addNotification("success", `${getLocaleString("reqCommentPostSuccess")}`); setComments((c) => { const newComment = { @@ -155,7 +156,8 @@ const Announcement = ({ announcement, token, userRole }) => { commentInputRef.current.value = ""; } catch (e) { - addNotification("error", + addNotification( + "error", `${getLocaleString("reqCommentNotPost")}: ${e.message}` ); console.error(e); @@ -166,7 +168,9 @@ const Announcement = ({ announcement, token, userRole }) => { return ( <> - + { {userRole === "admin" && ( @@ -204,7 +210,11 @@ const Announcement = ({ announcement, token, userRole }) => { - {getLocaleString("reqPosted")} {moment(announcement.created).format(`${getLocaleString("indexTime")}`)} {getLocaleString("reqBy")}{" "} + {getLocaleString("reqPosted")}{" "} + {moment(announcement.created).format( + `${getLocaleString("indexTime")}` + )}{" "} + {getLocaleString("reqBy")}{" "} {announcement.createdBy?.username ? ( {announcement.createdBy.username} @@ -216,7 +226,9 @@ const Announcement = ({ announcement, token, userRole }) => { {announcement.updated && ( {getLocaleString("annLastUpdated")}{" "} - {moment(announcement.updated).format(`${getLocaleString("indexTime")}`)} + {moment(announcement.updated).format( + `${getLocaleString("indexTime")}` + )} )} @@ -238,7 +250,9 @@ const Announcement = ({ announcement, token, userRole }) => { label={getLocaleString("reqPostAComment")} rows="5" placeholder={ - !announcement.allowComments ? `${getLocaleString("annCommentsDisabled")}` : undefined + !announcement.allowComments + ? `${getLocaleString("annCommentsDisabled")}` + : undefined } disabled={!announcement.allowComments} mb={4} diff --git a/client/pages/stats.js b/client/pages/stats.js index a67090b..2efda03 100644 --- a/client/pages/stats.js +++ b/client/pages/stats.js @@ -28,6 +28,8 @@ const Stats = ({ stats, userRole }) => { return {getLocaleString("statYouNotPermission")}; } + console.log(stats); + return ( <> @@ -36,12 +38,10 @@ const Stats = ({ stats, userRole }) => { {Object.entries(stats).map(([key, value]) => { - let readableKey = key.replace(/([A-Z])/g, " $1").toLowerCase(); - readableKey = - readableKey.charAt(0).toUpperCase() + readableKey.slice(1); + const localeKey = key.charAt(0).toUpperCase() + key.slice(1); return ( - {readableKey} + {getLocaleString(`stat${localeKey}`)} {value} ); diff --git a/client/pages/torrent/[infoHash].js b/client/pages/torrent/[infoHash].js index 3e3be50..7246211 100644 --- a/client/pages/torrent/[infoHash].js +++ b/client/pages/torrent/[infoHash].js @@ -202,13 +202,12 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { throw new Error(reason); } - addNotification("success", - `${getLocaleString("torrTorrEditSuccess")}` - ); + addNotification("success", `${getLocaleString("torrTorrEditSuccess")}`); window.location.reload(); } catch (e) { - addNotification("error", + addNotification( + "error", `${getLocaleString("torrCouldEditTorr")}: ${e.message}` ); console.error(e); @@ -236,13 +235,12 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { throw new Error(reason); } - addNotification("success", - `${getLocaleString("torrTorrDelSuccess")}` - ); + addNotification("success", `${getLocaleString("torrTorrDelSuccess")}`); router.push("/"); } catch (e) { - addNotification("error", + addNotification( + "error", `${getLocaleString("torrCouldNotDelTorr")}: ${e.message}` ); console.error(e); @@ -276,9 +274,7 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { throw new Error(reason); } - addNotification("success", - `${getLocaleString("reqCommentPostSuccess")}` - ); + addNotification("success", `${getLocaleString("reqCommentPostSuccess")}`); setComments((c) => { const newComment = { @@ -293,7 +289,8 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { commentInputRef.current.value = ""; } catch (e) { - addNotification("error", + addNotification( + "error", `${getLocaleString("reqCommentNotPost")}: ${e.message}` ); console.error(e); @@ -345,11 +342,10 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { throw new Error(reason); } - addNotification("success", - `${getLocaleString("torrVoteSubmitSuccess")}` - ); + addNotification("success", `${getLocaleString("torrVoteSubmitSuccess")}`); } catch (e) { - addNotification("error", + addNotification( + "error", `${getLocaleString("torrCouldNotSubmitVote")}: ${e.message}` ); console.error(e); @@ -383,13 +379,15 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { throw new Error(reason); } - addNotification("success", + addNotification( + "success", `${getLocaleString("torrReportSubmitSuccess")}` ); setShowReportModal(false); } catch (e) { - addNotification("error", + addNotification( + "error", `${getLocaleString("torrCouldNotSubmitReport")}: ${e.message}` ); console.error(e); @@ -417,13 +415,12 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { throw new Error(reason); } - addNotification("success", - `${getLocaleString("torrFLToggleSuccess")}` - ); + addNotification("success", `${getLocaleString("torrFLToggleSuccess")}`); setIsFreeleech((f) => !f); } catch (e) { - addNotification("error", + addNotification( + "error", `${getLocaleString("torrCouldNotToggleFL")}: ${e.message}` ); console.error(e); @@ -451,7 +448,8 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { throw new Error(reason); } - addNotification("success", + addNotification( + "success", `${getLocaleString("torrTorrRemFromGroupSuccess")}` ); @@ -488,12 +486,15 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { addNotification( "success", - `${getLocaleString("torrTorrent")} ${bookmarked ? "removed from" : "added to"} bookmarks` + `${getLocaleString("torrTorrent")} ${ + bookmarked ? "removed from" : "added to" + } bookmarks` ); setBookmarked((b) => !b); } catch (e) { - addNotification("error", + addNotification( + "error", `${getLocaleString("torrCouldNotBookmarkTorr")}: ${e.message}` ); console.error(e); @@ -574,7 +575,10 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { )} {userRole === "admin" && ( )} {userId ? ( @@ -630,7 +634,9 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { {source} ) : undefined, - [getLocaleString("torrDate")]: moment(torrent.created).format(`${getLocaleString("indexTime")}`), + [getLocaleString("torrDate")]: moment(torrent.created).format( + `${getLocaleString("indexTime")}` + ), [getLocaleString("reqInfohash")]: ( { ), [getLocaleString("torrSize")]: prettyBytes(torrent.size), [getLocaleString("torrDownloads")]: torrent.downloads, - [getLocaleString("torrSeeders")]: torrent.seeders !== undefined ? torrent.seeders : "?", - [getLocaleString("torrLeechers")]: torrent.leechers !== undefined ? torrent.leechers : "?", + [getLocaleString("torrSeeders")]: + torrent.seeders !== undefined ? torrent.seeders : "?", + [getLocaleString("torrLeechers")]: + torrent.leechers !== undefined ? torrent.leechers : "?", [getLocaleString("torrFreeleech")]: - torrent.freeleech || SQ_SITE_WIDE_FREELEECH === true ? [getLocaleString("torrYes")] : [getLocaleString("torrNo")], + torrent.freeleech || SQ_SITE_WIDE_FREELEECH === true + ? [getLocaleString("torrYes")] + : [getLocaleString("torrNo")], }} /> @@ -810,7 +820,9 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { categories={SQ_TORRENT_CATEGORIES} /> ) : ( - {getLocaleString("torrThereAreNoOtherTorrGroup")} + + {getLocaleString("torrThereAreNoOtherTorrGroup")} + )} @@ -850,7 +862,7 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { mb={4} required /> - + )} @@ -883,9 +895,7 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { )} {showDeleteModal && ( setShowDeleteModal(false)}> - - {getLocaleString("torrSureDeleteTorr")} - + {getLocaleString("torrSureDeleteTorr")} - {total.toLocaleString()} results — Page {page + 1} of{" "} + {total.toLocaleString()} {getLocaleString("torrResults")} — {getLocaleString("torrPage")} {page + 1} {getLocaleString("torrOf")}{" "} {(maxPage + 1).toLocaleString()} diff --git a/client/locales/en.json b/client/locales/en.json index c874fd4..613f092 100644 --- a/client/locales/en.json +++ b/client/locales/en.json @@ -221,6 +221,9 @@ "torrNo": "No", "torrGroupTorr": "Grouped torrents", "torrThereAreNoOtherTorrGroup": "There are no other torrents in this group.", + "torrResults": "results", + "torrPage": "Page", + "torrOf": "of", "totp": "One-time code", "uploadAddTag": "Add tag", "uploadAnnounceURL": "Announce URL", diff --git a/client/locales/ru.json b/client/locales/ru.json index 5e3be28..7e88c7d 100644 --- a/client/locales/ru.json +++ b/client/locales/ru.json @@ -218,6 +218,9 @@ "torrNo": "Нет", "torrGroupTorr": "Сгруппированные торренты", "torrThereAreNoOtherTorrGroup": "Других торрентов в этой группе нет.", + "torrResults": "результат(а/ов)", + "torrPage": "Страница", + "torrOf": "из", "totp": "Одноразовый код", "uploadAddTag": "Добавить метку", "uploadAnnounceURL": "Адрес трекера", From 4085cf94c0bf663925c7a0d744f091fe309d13d2 Mon Sep 17 00:00:00 2001 From: smlinux Date: Thu, 21 Sep 2023 08:24:51 +0200 Subject: [PATCH 072/125] locales: more strings --- client/pages/account.js | 6 +++--- client/pages/index.js | 4 ++-- client/pages/requests/[index].js | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/pages/account.js b/client/pages/account.js index ef88a90..392d290 100644 --- a/client/pages/account.js +++ b/client/pages/account.js @@ -536,7 +536,7 @@ const Account = ({ token, invites = [], user, userRole }) => { { { {getLocaleString("accInviteText1")}
- + {userRole === "admin" && ( + )} From b950a2ebb134339d12a8d36d9be5c5f745f39e07 Mon Sep 17 00:00:00 2001 From: smlinux Date: Sun, 1 Oct 2023 08:26:22 +0200 Subject: [PATCH 080/125] locales: more strings annonces, requests, torrent, user --- client/components/TorrentList.js | 2 +- client/locales/en.json | 2 ++ client/locales/eo.json | 2 ++ client/locales/ru.json | 2 ++ client/pages/announcements/[slug]/edit.js | 7 ++++--- client/pages/announcements/new.js | 7 ++++--- client/pages/requests/[index].js | 2 +- client/pages/torrent/[infoHash].js | 2 +- client/pages/user/[username].js | 2 +- 9 files changed, 18 insertions(+), 10 deletions(-) diff --git a/client/components/TorrentList.js b/client/components/TorrentList.js index 8818dff..0f7eec3 100644 --- a/client/components/TorrentList.js +++ b/client/components/TorrentList.js @@ -97,7 +97,7 @@ const TorrentList = ({ )} {(row.freeleech || SQ_SITE_WIDE_FREELEECH === true) && ( - FL! + {getLocaleString("torrFL")} )} diff --git a/client/locales/en.json b/client/locales/en.json index b6808ee..dfa8ab6 100644 --- a/client/locales/en.json +++ b/client/locales/en.json @@ -230,6 +230,7 @@ "torrOf": "of", "torrRemovedFrom": "removed from", "torrAddedTo": "added to", + "torrTorrNoTags": "This torrent has no tags.", "totp": "One-time code", "uploadAddTag": "Add tag", "uploadAnnounceURL": "Announce URL", @@ -280,6 +281,7 @@ "userBan": "ban", "userUnbanned": "unbanned", "userBanned": "banned", + "userAdmin": "Admin", "veCouldNotVerifyEmailAddress": "Could not verify email address:", "veEmailAddressVerifiedSuccess": "Email address verified successfully.", "veNoVerificationTokenProvided": "No verification token provided", diff --git a/client/locales/eo.json b/client/locales/eo.json index 6d8a254..6a12d04 100644 --- a/client/locales/eo.json +++ b/client/locales/eo.json @@ -228,6 +228,7 @@ "torrOf": "de", "torrRemovedFrom": "forigita de", "torrAddedTo": "aldonita al", + "torrTorrNoTags": "Ĉi tiu torento ne havas etikedojn.", "totp": "Unufoja kodo", "uploadAddTag": "Aldoni etikedon", "uploadAnnounceURL": "Anoncu URL", @@ -278,6 +279,7 @@ "userBan": "malpermeso", "userUnbanned": "nemalpermesita", "userBanned": "malpermesita", + "userAdmin": "Admin", "veCouldNotVerifyEmailAddress": "Ne eblis kontroli retadreson:", "veEmailAddressVerifiedSuccess": "Retadreso kontrolita sukcese.", "veNoVerificationTokenProvided": "Neniu konfirma ĵetono provizita", diff --git a/client/locales/ru.json b/client/locales/ru.json index c579b18..8160942 100644 --- a/client/locales/ru.json +++ b/client/locales/ru.json @@ -227,6 +227,7 @@ "torrOf": "из", "torrRemovedFrom": "удален из", "torrAddedTo": "добавлен в", + "torrTorrNoTags": "Этот торрент не имеет меток.", "totp": "Одноразовый код", "uploadAddTag": "Добавить метку", "uploadAnnounceURL": "Адрес трекера", @@ -277,6 +278,7 @@ "userBan": "забанить", "userUnbanned": "разбанен", "userBanned": "забанен", + "userAdmin": "Админ", "veCouldNotVerifyEmailAddress": "Не удалось подтвердить адрес эл. почты:", "veEmailAddressVerifiedSuccess": "Адрес эл. почты успешно подтвержден.", "veNoVerificationTokenProvided": "Токен подтверждения не предоставлен", diff --git a/client/pages/announcements/[slug]/edit.js b/client/pages/announcements/[slug]/edit.js index 143b45f..aeb4f35 100644 --- a/client/pages/announcements/[slug]/edit.js +++ b/client/pages/announcements/[slug]/edit.js @@ -14,8 +14,11 @@ import MarkdownInput from "../../../components/MarkdownInput"; import LocaleContext from "../../../utils/LocaleContext"; const EditAnnouncement = ({ announcement, token, userRole }) => { + + const { getLocaleString } = useContext(LocaleContext); + if (userRole !== "admin") { - return You do not have permission to do that.; + return {getLocaleString("statYouNotPermission")}; } const { addNotification } = useContext(NotificationContext); @@ -27,8 +30,6 @@ const EditAnnouncement = ({ announcement, token, userRole }) => { publicRuntimeConfig: { SQ_API_URL }, } = getConfig(); - const { getLocaleString } = useContext(LocaleContext); - const handleCreate = async (e) => { e.preventDefault(); setLoading(true); diff --git a/client/pages/announcements/new.js b/client/pages/announcements/new.js index 2fd23e9..2538fde 100644 --- a/client/pages/announcements/new.js +++ b/client/pages/announcements/new.js @@ -14,8 +14,11 @@ import MarkdownInput from "../../components/MarkdownInput"; import LocaleContext from "../../utils/LocaleContext"; const NewAnnouncement = ({ token, userRole }) => { + + const { getLocaleString } = useContext(LocaleContext); + if (userRole !== "admin") { - return You do not have permission to do that.; + return {getLocaleString("statYouNotPermission")}; } const { addNotification } = useContext(NotificationContext); @@ -27,8 +30,6 @@ const NewAnnouncement = ({ token, userRole }) => { publicRuntimeConfig: { SQ_API_URL }, } = getConfig(); - const { getLocaleString } = useContext(LocaleContext); - const handleCreate = async (e) => { e.preventDefault(); setLoading(true); diff --git a/client/pages/requests/[index].js b/client/pages/requests/[index].js index e5459b0..55776c6 100644 --- a/client/pages/requests/[index].js +++ b/client/pages/requests/[index].js @@ -280,7 +280,7 @@ const Request = ({ request, token, user }) => { {value} {(row.freeleech || SQ_SITE_WIDE_FREELEECH === true) && ( - FL! + {getLocaleString("torrFL")} )} diff --git a/client/pages/torrent/[infoHash].js b/client/pages/torrent/[infoHash].js index c3be5f0..4738336 100644 --- a/client/pages/torrent/[infoHash].js +++ b/client/pages/torrent/[infoHash].js @@ -724,7 +724,7 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { ))} ) : ( - This torrent has no tags. + {getLocaleString("torrTorrNoTags")} )} diff --git a/client/pages/user/[username].js b/client/pages/user/[username].js index fd6c61f..f97c71a 100644 --- a/client/pages/user/[username].js +++ b/client/pages/user/[username].js @@ -104,7 +104,7 @@ const User = ({ token, user, userRole }) => { {user.username} {getLocaleString("userProfile")} {user.role === "admin" && ( - Admin + {getLocaleString("userAdmin")} )} {banned && ( From cef7ff4159bd9aaa002bd1c5fd9462c2692502e2 Mon Sep 17 00:00:00 2001 From: smlinux Date: Mon, 2 Oct 2023 09:28:29 +0200 Subject: [PATCH 081/125] mobile: set variant secondary for open/close menu --- client/components/Navigation.js | 1 + client/pages/_app.js | 1 + 2 files changed, 2 insertions(+) diff --git a/client/components/Navigation.js b/client/components/Navigation.js index 8ce1e28..e185597 100644 --- a/client/components/Navigation.js +++ b/client/components/Navigation.js @@ -142,6 +142,7 @@ const Navigation = ({ isMobile, menuIsOpen, setMenuIsOpen }) => { > - {total.toLocaleString()} {getLocaleString("torrResults")} — {getLocaleString("torrPage")} {page + 1} {getLocaleString("torrOf")}{" "} + {total.toLocaleString()} {getLocaleString("torrResults")} —{" "} + {getLocaleString("torrPage")} {page + 1} {getLocaleString("torrOf")}{" "} {(maxPage + 1).toLocaleString()} From 409c89d3327b6f92db39af7b601fa542e0af67b6 Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Fri, 6 Oct 2023 16:46:51 +0100 Subject: [PATCH 087/125] issue #38: enable sending of invites in open mode for admin users --- api/src/controllers/user.js | 2 +- client/pages/account.js | 107 +++++++++++++++++++++--------------- 2 files changed, 64 insertions(+), 45 deletions(-) diff --git a/api/src/controllers/user.js b/api/src/controllers/user.js index fed1be5..9491498 100644 --- a/api/src/controllers/user.js +++ b/api/src/controllers/user.js @@ -257,7 +257,7 @@ export const login = async (req, res, next) => { }; export const generateInvite = (mail) => async (req, res, next) => { - if (process.env.SQ_ALLOW_REGISTER !== "invite") { + if (process.env.SQ_ALLOW_REGISTER !== "invite" && req.userRole !== "admin") { res .status(403) .send("Can only send invites when tracker is in invite only mode"); diff --git a/client/pages/account.js b/client/pages/account.js index deb770e..e725081 100644 --- a/client/pages/account.js +++ b/client/pages/account.js @@ -62,7 +62,9 @@ const BuyItem = ({ text, cost, wallet, handleBuy }) => { disabled={unavailable || cannotAfford} mr={3} /> - + @@ -131,15 +133,14 @@ const Account = ({ token, invites = [], user, userRole }) => { return currentInvitesList; }); - addNotification("success", - `${getLocaleString("accInviteSentSuccess")}` - ); + addNotification("success", `${getLocaleString("accInviteSentSuccess")}`); setRemainingInvites((r) => r - 1); setShowInviteModal(false); } catch (e) { - addNotification("error", + addNotification( + "error", `${getLocaleString("accCouldNotSendInvite")}: ${e.message}` ); console.error(e); @@ -174,9 +175,7 @@ const Account = ({ token, invites = [], user, userRole }) => { throw new Error(reason); } - addNotification("success", - `${getLocaleString("accPassChangedSuccess")}` - ); + addNotification("success", `${getLocaleString("accPassChangedSuccess")}`); const fields = e.target.querySelectorAll("input"); for (const field of fields) { @@ -184,7 +183,8 @@ const Account = ({ token, invites = [], user, userRole }) => { field.blur(); } } catch (e) { - addNotification("error", + addNotification( + "error", `${getLocaleString("accCouldNotChangePass")}: ${e.message}` ); console.error(e); @@ -218,7 +218,8 @@ const Account = ({ token, invites = [], user, userRole }) => { throw new Error(reason); } - addNotification("success", + addNotification( + "success", `${getLocaleString("accItemsPurchasedSuccess")}` ); @@ -233,7 +234,8 @@ const Account = ({ token, invites = [], user, userRole }) => { field.blur(); } } catch (e) { - addNotification("error", + addNotification( + "error", `${getLocaleString("accCouldNotBuyItems")}: ${e.message}` ); console.error(e); @@ -264,9 +266,7 @@ const Account = ({ token, invites = [], user, userRole }) => { setTotpBackupCodes(backupCodes); setTotpQrData(undefined); setTotpEnabled(true); - addNotification("success", - `${getLocaleString("acc2FAEnabled")}` - ); + addNotification("success", `${getLocaleString("acc2FAEnabled")}`); } else { const message = await enableRes.text(); addNotification("error", message); @@ -298,16 +298,15 @@ const Account = ({ token, invites = [], user, userRole }) => { if (disableRes.status === 200) { setTotpEnabled(false); - addNotification("success", - `${getLocaleString("acc2FADisabled")}` - ); + addNotification("success", `${getLocaleString("acc2FADisabled")}`); } else { const message = await disableRes.text(); addNotification("error", message); } } } catch (e) { - addNotification("error", + addNotification( + "error", `${getLocaleString("accCouldNotToggle2FA")}: ${e.message}` ); console.error(e); @@ -337,7 +336,8 @@ const Account = ({ token, invites = [], user, userRole }) => { await router.push("/logout"); } catch (e) { - addNotification("error", + addNotification( + "error", `${getLocaleString("accCouldNotDelAcc")}: ${e.message}` ); console.error(e); @@ -359,17 +359,29 @@ const Account = ({ token, invites = [], user, userRole }) => { {getLocaleString("accBonusPoints")} - {getLocaleString("accYouCurrentlyHave")} {bonusPoints} {getLocaleString("accBonusPointsHave")}. + {getLocaleString("accYouCurrentlyHave")} {bonusPoints}{" "} + {getLocaleString("accBonusPointsHave")}. - {getLocaleString("accYouWillEarn")} {SQ_BP_EARNED_PER_GB}{" "} - {pluralize(`${getLocaleString("accBonusPointsHave")}`, SQ_BP_EARNED_PER_GB)} {getLocaleString("accForEveryGBYouUpload")}. + {getLocaleString("accYouWillEarn")}{" "} + {SQ_BP_EARNED_PER_GB}{" "} + {pluralize( + `${getLocaleString("accBonusPointsHave")}`, + SQ_BP_EARNED_PER_GB + )}{" "} + {getLocaleString("accForEveryGBYouUpload")}. - {getLocaleString("accYouWillEarn")} {SQ_BP_EARNED_PER_FILLED_REQUEST}{" "} - {pluralize(`${getLocaleString("accBonusPointsHave")}`, SQ_BP_EARNED_PER_FILLED_REQUEST)} {getLocaleString("accEveryRequestYouFulfill")}{" "} - {SQ_BP_EARNED_PER_FILLED_REQUEST * 2} {getLocaleString("accIfYouAreAlsUploaderAcceptTorrent")}. + {getLocaleString("accYouWillEarn")}{" "} + {SQ_BP_EARNED_PER_FILLED_REQUEST}{" "} + {pluralize( + `${getLocaleString("accBonusPointsHave")}`, + SQ_BP_EARNED_PER_FILLED_REQUEST + )}{" "} + {getLocaleString("accEveryRequestYouFulfill")}{" "} + {SQ_BP_EARNED_PER_FILLED_REQUEST * 2}{" "} + {getLocaleString("accIfYouAreAlsUploaderAcceptTorrent")}. * + *": { mt: 3 } }} mb={5}> @@ -388,9 +400,8 @@ const Account = ({ token, invites = [], user, userRole }) => { handleBuy={(e) => handleBuy(e, "upload")} /> - {SQ_ALLOW_REGISTER === "invite" && ( + {(SQ_ALLOW_REGISTER === "invite" || userRole === "admin") && ( <> - {" "} { pl={4} > - {remainingInvites.toLocaleString()} {getLocaleString("accRemaining")} + {remainingInvites.toLocaleString()}{" "} + {getLocaleString("accRemaining")} + )} @@ -635,11 +650,15 @@ const Account = ({ token, invites = [], user, userRole }) => { )} {showInviteModal && ( setShowInviteModal(false)}> - - {getLocaleString("accInviteText1")} - + {getLocaleString("accInviteText1")} - + {userRole === "admin" && ( { > {getLocaleString("accCancel")} - + From 9538a6df540930be0398037024738e68001e2cee Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Fri, 6 Oct 2023 16:48:38 +0100 Subject: [PATCH 088/125] lint --- client/components/Comment.js | 4 +- client/pages/404.js | 1 - client/pages/_error.js | 4 +- client/pages/announcements/[slug]/edit.js | 7 +-- client/pages/announcements/index.js | 9 ++-- client/pages/announcements/new.js | 26 ++++++++--- client/pages/categories/index.js | 4 +- client/pages/index.js | 11 ++++- client/pages/reports/[id].js | 29 ++++++++---- client/pages/reports/index.js | 5 ++- client/pages/requests/[index].js | 44 +++++++++++------- client/pages/requests/index.js | 5 ++- client/pages/requests/new.js | 6 ++- client/pages/rss.js | 12 ++--- client/pages/search/[[...query]].js | 16 +++++-- client/pages/torrent/[infoHash].js | 4 +- client/pages/user/[username].js | 54 ++++++++++++++++++----- client/pages/verify-email.js | 4 +- client/pages/wiki/[[...slug]].js | 22 +++++---- client/pages/wiki/new.js | 11 +++-- 20 files changed, 187 insertions(+), 91 deletions(-) diff --git a/client/components/Comment.js b/client/components/Comment.js index d78fb81..690cf18 100644 --- a/client/components/Comment.js +++ b/client/components/Comment.js @@ -10,7 +10,6 @@ import Text from "./Text"; import LocaleContext from "../utils/LocaleContext"; const Comment = ({ comment }) => { - const { getLocaleString } = useContext(LocaleContext); return ( @@ -112,7 +111,8 @@ const Comment = ({ comment }) => {
)} - {getLocaleString("reqPosted")} {moment(comment.created).format(`${getLocaleString("indexTime")}`)} + {getLocaleString("reqPosted")}{" "} + {moment(comment.created).format(`${getLocaleString("indexTime")}`)} {comment.comment} diff --git a/client/pages/404.js b/client/pages/404.js index 09edabd..b0d9982 100644 --- a/client/pages/404.js +++ b/client/pages/404.js @@ -5,7 +5,6 @@ import Text from "../components/Text"; import LocaleContext from "../utils/LocaleContext"; const NotFound = () => { - const { getLocaleString } = useContext(LocaleContext); return ( diff --git a/client/pages/_error.js b/client/pages/_error.js index f865bbf..043e98f 100644 --- a/client/pages/_error.js +++ b/client/pages/_error.js @@ -33,9 +33,7 @@ const ErrorPage = () => { {getLocaleString("errSomethingWentWrong")} :( {rateLimited ? ( - - {getLocaleString("errTooManyRequests")} - + {getLocaleString("errTooManyRequests")} ) : ( {getLocaleString("errIfErrorPersist")}{" "} diff --git a/client/pages/announcements/[slug]/edit.js b/client/pages/announcements/[slug]/edit.js index aeb4f35..c6f5fde 100644 --- a/client/pages/announcements/[slug]/edit.js +++ b/client/pages/announcements/[slug]/edit.js @@ -14,7 +14,6 @@ import MarkdownInput from "../../../components/MarkdownInput"; import LocaleContext from "../../../utils/LocaleContext"; const EditAnnouncement = ({ announcement, token, userRole }) => { - const { getLocaleString } = useContext(LocaleContext); if (userRole !== "admin") { @@ -58,14 +57,16 @@ const EditAnnouncement = ({ announcement, token, userRole }) => { throw new Error(reason); } - addNotification("success", + addNotification( + "success", `${getLocaleString("annAnnounceUpdatedSuccess")}` ); const slug = await updateAnnouncementRes.text(); router.push(`/announcements/${slug}`); } catch (e) { - addNotification("error", + addNotification( + "error", `${getLocaleString("annCouldNotUpdateAnnounce")}: ${e.message}` ); console.error(e); diff --git a/client/pages/announcements/index.js b/client/pages/announcements/index.js index 4837d99..00ad65d 100644 --- a/client/pages/announcements/index.js +++ b/client/pages/announcements/index.js @@ -12,7 +12,6 @@ import List from "../../components/List"; import LocaleContext from "../../utils/LocaleContext"; const Announcements = ({ announcements, pinnedAnnouncements, userRole }) => { - const { getLocaleString } = useContext(LocaleContext); return ( @@ -61,7 +60,9 @@ const Announcements = ({ announcements, pinnedAnnouncements, userRole }) => { header: `${getLocaleString("accCreated")}`, accessor: "created", cell: ({ value }) => ( - {moment(value).format(`${getLocaleString("indexTime")}`)} + + {moment(value).format(`${getLocaleString("indexTime")}`)} + ), rightAlign: true, gridWidth: "175px", @@ -96,7 +97,9 @@ const Announcements = ({ announcements, pinnedAnnouncements, userRole }) => { header: `${getLocaleString("accCreated")}`, accessor: "created", cell: ({ value }) => ( - {moment(value).format(`${getLocaleString("indexTime")}`)} + + {moment(value).format(`${getLocaleString("indexTime")}`)} + ), rightAlign: true, gridWidth: "175px", diff --git a/client/pages/announcements/new.js b/client/pages/announcements/new.js index 2538fde..4a8c533 100644 --- a/client/pages/announcements/new.js +++ b/client/pages/announcements/new.js @@ -14,7 +14,6 @@ import MarkdownInput from "../../components/MarkdownInput"; import LocaleContext from "../../utils/LocaleContext"; const NewAnnouncement = ({ token, userRole }) => { - const { getLocaleString } = useContext(LocaleContext); if (userRole !== "admin") { @@ -58,14 +57,16 @@ const NewAnnouncement = ({ token, userRole }) => { throw new Error(reason); } - addNotification("success", + addNotification( + "success", `${getLocaleString("annAnnounceCreatSuccess")}` ); const slug = await createAnnouncementRes.text(); router.push(`/announcements/${slug}`); } catch (e) { - addNotification("error", + addNotification( + "error", `${getLocaleString("annCouldNotCreateAnnounce")}: ${e.message}` ); console.error(e); @@ -81,7 +82,12 @@ const NewAnnouncement = ({ token, userRole }) => { {getLocaleString("annNewAnnounce")}
- + { mb={4} required /> - - + + diff --git a/client/pages/categories/index.js b/client/pages/categories/index.js index aa18c80..8cc733b 100644 --- a/client/pages/categories/index.js +++ b/client/pages/categories/index.js @@ -59,7 +59,9 @@ const Categories = ({ tags }) => { ))} ) : ( - {getLocaleString("catNoCategoryHaveBeenDefined")} + + {getLocaleString("catNoCategoryHaveBeenDefined")} + )} diff --git a/client/pages/index.js b/client/pages/index.js index 226d755..1c6ca16 100644 --- a/client/pages/index.js +++ b/client/pages/index.js @@ -120,7 +120,9 @@ const Index = ({ {getLocaleString("reqPosted")}{" "} - {moment(latestAnnouncement.created).format(`${getLocaleString("indexTime")}`)}{" "} + {moment(latestAnnouncement.created).format( + `${getLocaleString("indexTime")}` + )}{" "} {getLocaleString("reqBy")}{" "} {latestAnnouncement.createdBy?.username ? ( )} - + diff --git a/client/pages/reports/[id].js b/client/pages/reports/[id].js index 2e7f4fe..2d2e528 100644 --- a/client/pages/reports/[id].js +++ b/client/pages/reports/[id].js @@ -48,13 +48,12 @@ const Report = ({ report, token, userRole }) => { throw new Error(reason); } - addNotification("success", - `${getLocaleString("repRepMarkSolved")}` - ); + addNotification("success", `${getLocaleString("repRepMarkSolved")}`); router.push("/reports"); } catch (e) { - addNotification("error", + addNotification( + "error", `${getLocaleString("repCouldNotResolveRep")}: ${e.message}` ); console.error(e); @@ -69,18 +68,28 @@ const Report = ({ report, token, userRole }) => { return ( <> - + - {getLocaleString("repRepOn")} “{report.torrent.name}” - + + {getLocaleString("repRepOn")} “{report.torrent.name}” + + - {getLocaleString("repRep")} {moment(report.created).format(`${getLocaleString("indexTime")}`)} {getLocaleString("reqBy")}{" "} + {getLocaleString("repRep")}{" "} + {moment(report.created).format(`${getLocaleString("indexTime")}`)}{" "} + {getLocaleString("reqBy")}{" "} {report.reportedBy.username} @@ -103,7 +112,9 @@ const Report = ({ report, token, userRole }) => { {report.torrent.infoHash} ), - [getLocaleString("accCreated")]: moment(report.torrent.created).format(`${getLocaleString("indexTime")}`), + [getLocaleString("accCreated")]: moment( + report.torrent.created + ).format(`${getLocaleString("indexTime")}`), }} /> { - const { getLocaleString } = useContext(LocaleContext); if (userRole !== "admin") { @@ -52,7 +51,9 @@ const Reports = ({ reports, userRole }) => { header: `${getLocaleString("accCreated")}`, accessor: "created", cell: ({ value }) => ( - {moment(value).format(`${getLocaleString("indexTime")}`)} + + {moment(value).format(`${getLocaleString("indexTime")}`)} + ), rightAlign: true, gridWidth: "175px", diff --git a/client/pages/requests/[index].js b/client/pages/requests/[index].js index 55776c6..dff6f47 100644 --- a/client/pages/requests/[index].js +++ b/client/pages/requests/[index].js @@ -65,13 +65,12 @@ const Request = ({ request, token, user }) => { throw new Error(reason); } - addNotification("success", - `${getLocaleString("reqRequestDelSuccess")}` - ); + addNotification("success", `${getLocaleString("reqRequestDelSuccess")}`); router.push("/requests"); } catch (e) { - addNotification("error", + addNotification( + "error", `${getLocaleString("reqCouldNotDelReq")}: ${e.message}` ); console.error(e); @@ -105,9 +104,7 @@ const Request = ({ request, token, user }) => { throw new Error(reason); } - addNotification("success", - `${getLocaleString("reqCommentPostSuccess")}` - ); + addNotification("success", `${getLocaleString("reqCommentPostSuccess")}`); setComments((c) => { const newComment = { @@ -122,7 +119,8 @@ const Request = ({ request, token, user }) => { commentInputRef.current.value = ""; } catch (e) { - addNotification("error", + addNotification( + "error", `${getLocaleString("reqCommentNotPost")}: ${e.message}` ); console.error(e); @@ -156,7 +154,8 @@ const Request = ({ request, token, user }) => { throw new Error(reason); } - addNotification("success", + addNotification( + "success", `${getLocaleString("reqSuggestionAddSuccess")}` ); @@ -165,7 +164,8 @@ const Request = ({ request, token, user }) => { setShowSuggestModal(false); } catch (e) { - addNotification("error", + addNotification( + "error", `${getLocaleString("reqSuggestionNotAdded")}: ${e.message}` ); console.error(e); @@ -197,14 +197,16 @@ const Request = ({ request, token, user }) => { throw new Error(reason); } - addNotification("success", + addNotification( + "success", `${getLocaleString("reqSuggestionAcceptSuccess")}` ); const { torrent } = await acceptRes.json(); setFulfilledBy(torrent); } catch (e) { - addNotification("error", + addNotification( + "error", `${getLocaleString("reqCouldNotAcceptSuggestion")}: ${e.message}` ); console.error(e); @@ -233,7 +235,9 @@ const Request = ({ request, token, user }) => { - {getLocaleString("reqPosted")} {moment(request.created).format(`${getLocaleString("indexTime")}`)} {getLocaleString("reqBy")}{" "} + {getLocaleString("reqPosted")}{" "} + {moment(request.created).format(`${getLocaleString("indexTime")}`)}{" "} + {getLocaleString("reqBy")}{" "} {request.createdBy?.username ? ( {request.createdBy.username} @@ -317,7 +321,9 @@ const Request = ({ request, token, user }) => { header: `${getLocaleString("userUploaded")}`, accessor: "created", cell: ({ value }) => ( - {moment(value).format(`${getLocaleString("indexTime")}`)} + + {moment(value).format(`${getLocaleString("indexTime")}`)} + ), gridWidth: "175px", rightAlign: true, @@ -345,7 +351,9 @@ const Request = ({ request, token, user }) => { ]} /> ) : ( - {getLocaleString("reqNoTorrentsHaveBeenSuggestedYet")} + + {getLocaleString("reqNoTorrentsHaveBeenSuggestedYet")} + )} @@ -379,7 +387,11 @@ const Request = ({ request, token, user }) => { setShowSuggestModal(false)}> {getLocaleString("reqEnterInfohashTorrentBelow")} - + {error ? ( - {getLocaleString("searchSearchError")}: {error} + + {getLocaleString("searchSearchError")}: {error} + ) : ( <> {query && ( diff --git a/client/pages/torrent/[infoHash].js b/client/pages/torrent/[infoHash].js index 4738336..d733e75 100644 --- a/client/pages/torrent/[infoHash].js +++ b/client/pages/torrent/[infoHash].js @@ -487,7 +487,9 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { addNotification( "success", `${getLocaleString("torrTorrent")} ${ - bookmarked ? getLocaleString("torrRemovedFrom") : getLocaleString("torrAddedTo") + bookmarked + ? getLocaleString("torrRemovedFrom") + : getLocaleString("torrAddedTo") } ${getLocaleString("navBookmarks")}` ); diff --git a/client/pages/user/[username].js b/client/pages/user/[username].js index f97c71a..25d0ef4 100644 --- a/client/pages/user/[username].js +++ b/client/pages/user/[username].js @@ -68,7 +68,11 @@ const User = ({ token, user, userRole }) => { addNotification( "success", - `${user.username} ${banned ? [getLocaleString("userUnbanned")] : [getLocaleString("userBanned")]} ${getLocaleString("userSuccessfully")}` + `${user.username} ${ + banned + ? [getLocaleString("userUnbanned")] + : [getLocaleString("userBanned")] + } ${getLocaleString("userSuccessfully")}` ); setBanned((b) => !b); @@ -76,7 +80,9 @@ const User = ({ token, user, userRole }) => { } catch (e) { addNotification( "error", - `${getLocaleString("userCouldNot")} ${banned ? [getLocaleString("userUnban")] : [getLocaleString("userBan")]} ${user.username}: ${e.message}` + `${getLocaleString("userCouldNot")} ${ + banned ? [getLocaleString("userUnban")] : [getLocaleString("userBan")] + } ${user.username}: ${e.message}` ); console.error(e); } @@ -101,7 +107,9 @@ const User = ({ token, user, userRole }) => { mb={3} > - {user.username} {getLocaleString("userProfile")} + + {user.username} {getLocaleString("userProfile")} + {user.role === "admin" && ( {getLocaleString("userAdmin")} @@ -122,12 +130,16 @@ const User = ({ token, user, userRole }) => { )} {userRole === "admin" && cookies.username !== user.username && ( )} - {getLocaleString("userUserSince")} {moment(user.created).format(`${getLocaleString("userUserSinceTime")}`)} + {getLocaleString("userUserSince")}{" "} + {moment(user.created).format(`${getLocaleString("userUserSinceTime")}`)} {userRole === "admin" && ( @@ -140,9 +152,16 @@ const User = ({ token, user, userRole }) => { {getLocaleString("userOnlyAdminsSee")}
    - {user.email &&
  • {getLocaleString("email")}: {user.email}
  • } + {user.email && ( +
  • + {getLocaleString("email")}: {user.email} +
  • + )} {typeof user.emailVerified === "boolean" && ( -
  • {getLocaleString("userEmailVerified")}: {user.emailVerified ? "yes" : "no"}
  • +
  • + {getLocaleString("userEmailVerified")}:{" "} + {user.emailVerified ? "yes" : "no"} +
  • )} {user.invitedBy && (
  • @@ -153,10 +172,15 @@ const User = ({ token, user, userRole }) => {
  • )} {typeof user.remainingInvites === "number" && ( -
  • {getLocaleString("userRemainingInvites")}: {user.remainingInvites}
  • +
  • + {getLocaleString("userRemainingInvites")}:{" "} + {user.remainingInvites} +
  • )} {typeof user.bonusPoints === "number" && ( -
  • {getLocaleString("accBonusPoints")}: {user.bonusPoints}
  • +
  • + {getLocaleString("accBonusPoints")}: {user.bonusPoints} +
  • )}
@@ -295,7 +319,11 @@ const User = ({ token, user, userRole }) => { {showBanModal && ( setShowBanModal(false)}> - {getLocaleString("userYouSureWant")} {banned ? [getLocaleString("userUnban")] : [getLocaleString("userBan")]} {getLocaleString("userThisUserQ")} + {getLocaleString("userYouSureWant")}{" "} + {banned + ? [getLocaleString("userUnban")] + : [getLocaleString("userBan")]}{" "} + {getLocaleString("userThisUserQ")} - + )} diff --git a/client/pages/verify-email.js b/client/pages/verify-email.js index fc5cb92..f87d23a 100644 --- a/client/pages/verify-email.js +++ b/client/pages/verify-email.js @@ -76,7 +76,9 @@ const VerifyEmail = () => { borderRadius={1} p={4} > - {getLocaleString("veCouldNotVerifyEmailAddress")} {tokenError} + + {getLocaleString("veCouldNotVerifyEmailAddress")} {tokenError} + )} diff --git a/client/pages/wiki/[[...slug]].js b/client/pages/wiki/[[...slug]].js index 885f72e..3fa65c6 100644 --- a/client/pages/wiki/[[...slug]].js +++ b/client/pages/wiki/[[...slug]].js @@ -59,15 +59,14 @@ const Wiki = ({ page, allPages, token, userRole, slug }) => { throw new Error(reason); } - addNotification("success", - `${getLocaleString("wikiPageDelSuccess")}` - ); + addNotification("success", `${getLocaleString("wikiPageDelSuccess")}`); setShowDeleteModal(false); await router.push("/wiki"); } catch (e) { - addNotification("error", + addNotification( + "error", `${getLocaleString("wikiCouldNotDelPage")}: ${e.message}` ); console.error(e); @@ -104,16 +103,15 @@ const Wiki = ({ page, allPages, token, userRole, slug }) => { throw new Error(reason); } - addNotification("success", - `${getLocaleString("wikiPageUpdateSuccess")}` - ); + addNotification("success", `${getLocaleString("wikiPageUpdateSuccess")}`); if (form.get("slug") === page.slug) window.location.reload(); else window.location.href = "/wiki" + (page.slug === "/" ? "" : form.get("slug")); } catch (e) { - addNotification("error", + addNotification( + "error", `${getLocaleString("wikiCouldNotUpdatePage")}: ${e.message}` ); console.error(e); @@ -167,7 +165,9 @@ const Wiki = ({ page, allPages, token, userRole, slug }) => { {getLocaleString("wikiLastEdited")}{" "} - {moment(page.updated ?? page.created).format(`${getLocaleString("indexTime")}`)}{" "} + {moment(page.updated ?? page.created).format( + `${getLocaleString("indexTime")}` + )}{" "} {getLocaleString("reqBy")}{" "} {page.createdBy?.username ? ( @@ -250,9 +250,7 @@ const Wiki = ({ page, allPages, token, userRole, slug }) => { )} {showDeleteModal && ( setShowDeleteModal(false)}> - - {getLocaleString("wikiDelThisPageQ")} - + {getLocaleString("wikiDelThisPageQ")} @@ -650,7 +660,11 @@ const Account = ({ token, invites = [], user, userRole }) => { )} {showInviteModal && ( setShowInviteModal(false)}> - {getLocaleString("accInviteText1")} + + {getLocaleString( + SQ_DISABLE_EMAIL ? "accInviteText1NoEmail" : "accInviteText1" + )} + { > {getLocaleString("accCancel")} - + diff --git a/config.example.js b/config.example.js index ce2f57a..8fbd1e1 100644 --- a/config.example.js +++ b/config.example.js @@ -85,18 +85,26 @@ module.exports = { // The URL of your MongoDB server. Under the recommended setup, it should be `mongodb://sq_mongodb/sqtracker`. SQ_MONGO_URL: "mongodb://sq_mongodb/sqtracker", + // Disables sending of any emails and removes the need for an SMTP server. + // Fine for testing, not recommended in production as users will not be able to reset their passwords. + SQ_DISABLE_EMAIL: false, + // The email address that mail will be sent from. + // Not required if SQ_DISABLE_EMAIL=true. SQ_MAIL_FROM_ADDRESS: "mail@sqtracker.dev", // The hostname of your SMTP server. + // Not required if SQ_DISABLE_EMAIL=true. SQ_SMTP_HOST: "smtp.example.com", // The port of your SMTP server. + // Not required if SQ_DISABLE_EMAIL=true. SQ_SMTP_PORT: 587, // Whether to force SMTP TLS: if true the connection will use TLS when connecting to server. // If false (the default) then TLS is used if server supports the STARTTLS extension. // In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. + // Not required if SQ_DISABLE_EMAIL=true. SQ_SMTP_SECURE: false, }, secrets: { @@ -111,9 +119,11 @@ module.exports = { SQ_ADMIN_EMAIL: "admin@example.com", // The username to authenticate with your SMTP server with. + // Not required if SQ_DISABLE_EMAIL=true. SQ_SMTP_USER: "smtp_username", // The password to authenticate with your SMTP server with. + // Not required if SQ_DISABLE_EMAIL=true. SQ_SMTP_PASS: "smtp_password", }, }; From 5e1e955508d2c4103050e403b2eb3a9942af57b9 Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Wed, 21 Feb 2024 13:47:48 +0000 Subject: [PATCH 124/125] update dockerfiles, add nginx config --- README.md | 2 ++ api/Dockerfile | 2 +- api/src/utils/validateConfig.js | 8 -------- client/Dockerfile | 2 +- docker-compose.dev.yml | 10 +++++++-- docker-compose.yml | 8 ++++++++ nginx.conf | 36 +++++++++++++++++++++++++++++++++ 7 files changed, 56 insertions(+), 12 deletions(-) create mode 100644 nginx.conf diff --git a/README.md b/README.md index 7d33c8f..a49d947 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,8 @@ The sqtracker client service provides the modern, responsive web interface that The HTTP proxy allows the client, API, and BitTorrent tracker to all be accessible via a single endpoint. +Traefik is recommended and is configured by default. An Nginx config file is also provided for those that prefer it and the `docker-compose.yml` file contains an Nginx block that can be enabled. + ### Deploying with Docker compose The sqtracker platform is designed to be deployed via Docker. Once a configuration file is created, deploying is as simple as running `docker compose up -d` at the root of the project. diff --git a/api/Dockerfile b/api/Dockerfile index 9cd92df..1cfed1d 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -1,4 +1,4 @@ -FROM node:16-alpine +FROM node:16 ENV NODE_ENV=production ENV SENTRY_DSN="https://9b9761216607428180ea3b32bd1c8e58@o140996.ingest.sentry.io/4504645996576768" LABEL org.opencontainers.image.source=https://github.com/tdjsnelling/sqtracker diff --git a/api/src/utils/validateConfig.js b/api/src/utils/validateConfig.js index 38cc5c1..554b4c3 100644 --- a/api/src/utils/validateConfig.js +++ b/api/src/utils/validateConfig.js @@ -1,12 +1,4 @@ import * as yup from "yup"; -import en from "@sqtracker/client/locales/en.json"; -import es from "@sqtracker/client/locales/es.json"; -import it from "@sqtracker/client/locales/it.json"; -import ru from "@sqtracker/client/locales/ru.json"; -import de from "@sqtracker/client/locales/de.json"; -import zh from "@sqtracker/client/locales/zh.json"; -import eo from "@sqtracker/client/locales/eo.json"; -import fr from "@sqtracker/client/locales/fr.json"; const httpRegex = /http(s)?:\/\/.*/; const mongoRegex = /mongodb:\/\/.*/; diff --git a/client/Dockerfile b/client/Dockerfile index 0b19958..d76cb29 100644 --- a/client/Dockerfile +++ b/client/Dockerfile @@ -1,4 +1,4 @@ -FROM node:16-alpine AS builder +FROM node:16 AS builder ARG SENTRY_AUTH_TOKEN ENV NODE_ENV=production ENV SENTRY_ORG=tdjsnelling diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 8874d31..c676757 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -17,11 +17,17 @@ services: volumes: - "/var/run/docker.sock:/var/run/docker.sock:ro" - ./traefik.yml:/config/traefik.yml +# nginx: +# image: "nginx:latest" +# container_name: "sq_nginx" +# restart: always +# ports: +# - "80:80" +# volumes: +# - ./nginx.conf:/etc/nginx/nginx.conf database: container_name: sq_mongodb image: mongo:6.0 - ports: - - "127.0.0.1:27017:27017" volumes: - ./data:/data/db api: diff --git a/docker-compose.yml b/docker-compose.yml index 245287d..ca4737a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,6 +23,14 @@ services: - "/var/run/docker.sock:/var/run/docker.sock:ro" - ./letsencrypt:/letsencrypt - ./traefik.yml:/config/traefik.yml +# nginx: +# image: "nginx:latest" +# container_name: "sq_nginx" +# restart: always +# ports: +# - "80:80" +# volumes: +# - ./nginx.conf:/etc/nginx/nginx.conf database: container_name: sq_mongodb image: mongo:6.0 diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..b93617a --- /dev/null +++ b/nginx.conf @@ -0,0 +1,36 @@ +events {} + +http { + server { + listen 80; + resolver 127.0.0.11; + + location / { + proxy_pass http://sq_client:3000; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + } + + location /api/ { + rewrite /api/(.*) /$1 break; + proxy_pass http://sq_api:3001; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + } + + location /sq/ { + proxy_pass http://sq_api:3001; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + } + } +} From f8bced481d52e1c8c3dd4fccf458382f2d9651d1 Mon Sep 17 00:00:00 2001 From: Tom Snelling Date: Tue, 18 Jun 2024 13:53:02 +0100 Subject: [PATCH 125/125] include development URLs in config.example.js --- config.example.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config.example.js b/config.example.js index 8fbd1e1..cd1e816 100644 --- a/config.example.js +++ b/config.example.js @@ -77,12 +77,15 @@ module.exports = { SQ_SITE_DEFAULT_LOCALE: "en", // The URL of your tracker site. + // For local development, this should be `http://127.0.0.1:3000`. SQ_BASE_URL: "https://sqtracker.dev", // The URL of your API. Under the recommended setup, it should be `${SQ_BASE_URL}/api`. + // For local development, this should be `http://127.0.0.1:3001`. SQ_API_URL: "https://sqtracker.dev/api", // The URL of your MongoDB server. Under the recommended setup, it should be `mongodb://sq_mongodb/sqtracker`. + // For local development, this should be `mongodb://127.0.0.1/sqtracker`. SQ_MONGO_URL: "mongodb://sq_mongodb/sqtracker", // Disables sending of any emails and removes the need for an SMTP server.