diff --git a/README.md b/README.md index df8f8ff..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. @@ -116,15 +118,20 @@ To add a new translation in your own language, create a new JSON file with your The best place to start is to copy the `en.json` file and work through it, translating each English string. +There is also an [inlang project](https://fink.inlang.com/github.com/tdjsnelling/sqtracker) to aid with translation. + ### Existing translations -| Language | Complete (estimate) | Contributed by | -|--------------------|---------------------|--------------------------------------------| -| English | 100% | | -| Russian | 100% | [@smlinux](https://github.com/smlinux) | -| Esperanto | 100% | [@smlinux](https://github.com/smlinux) | -| German | 15% | [@MikeMatau](https://github.com/MikeMatau) | -| Simplified Chinese | 95% | [@0EAC](https://github.com/0EAC) | +| Language | Contributed by | +|--------------------|------------------------------------------------------| +| English | | +| Russian | [@smlinux](https://github.com/smlinux) | +| Esperanto | [@smlinux](https://github.com/smlinux) | +| German | [@EchterAlsFake](https://github.com/EchterAlsFake) | +| Simplified Chinese | [@0EAC](https://github.com/0EAC) | +| French | [@Klaiment](https://github.com/Klaiment) | +| Spanish | [@CerealKillerjs](https://github.com/CerealKillerjs) | +| Italian | [@NotLugozzi](https://github.com/NotLugozzi) | ## Screenshots diff --git a/api/Dockerfile b/api/Dockerfile index 3d12460..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 @@ -6,4 +6,4 @@ WORKDIR /sqtracker/app COPY . . RUN yarn install EXPOSE 3001 -CMD yarn start \ No newline at end of file +CMD yarn start diff --git a/api/package.json b/api/package.json index eb9dea5..dc02c25 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "@sqtracker/api", - "version": "1.3.3", + "version": "1.5.0", "private": true, "license": "GPL-3.0-only", "scripts": { diff --git a/api/src/controllers/rss.js b/api/src/controllers/rss.js index 236062e..ad77fce 100644 --- a/api/src/controllers/rss.js +++ b/api/src/controllers/rss.js @@ -5,7 +5,6 @@ import { embellishTorrentsWithTrackerScrape } from "./torrent"; // prettier-ignore const getTorrentXml = (torrent, userId) => { - const announceUrl = `${process.env.SQ_BASE_URL}/sq/${userId}/announce` return ` ${torrent.name} ${torrent.description} @@ -14,11 +13,10 @@ const getTorrentXml = (torrent, userId) => { ${torrent.name} ${torrent.size} - magnet:?xt=urn:btih:${torrent.infoHash}&dn=${encodeURIComponent(torrent.name)}&tr=${encodeURIComponent(announceUrl)} - ${announceUrl} + ${process.env.SQ_BASE_URL}/sq/${userId}/announce diff --git a/api/src/controllers/torrent.js b/api/src/controllers/torrent.js index baa5e25..e82bdc2 100644 --- a/api/src/controllers/torrent.js +++ b/api/src/controllers/torrent.js @@ -137,6 +137,7 @@ export const uploadTorrent = async (req, res, next) => { source: req.body.source, infoHash, binary: req.body.torrent, + poster: req.body.poster, uploadedBy: req.userId, downloads: 0, anonymous: false, @@ -154,7 +155,6 @@ export const uploadTorrent = async (req, res, next) => { group: groupId, mediaInfo: req.body.mediaInfo, }); - await newTorrent.save(); if (groupId) await addToGroup(groupId, infoHash); @@ -287,6 +287,7 @@ export const fetchTorrent = (tracker) => async (req, res, next) => { uploadedBy: 1, downloads: 1, anonymous: 1, + poster: 1, size: 1, files: 1, created: 1, diff --git a/api/src/controllers/user.js b/api/src/controllers/user.js index fed1be5..7fba1b5 100644 --- a/api/src/controllers/user.js +++ b/api/src/controllers/user.js @@ -110,7 +110,7 @@ export const register = (mail) => async (req, res, next) => { role, invitedBy: invite?.invitingUser, remainingInvites: 0, - emailVerified: false, + emailVerified: process.env.SQ_DISABLE_EMAIL, bonusPoints: 0, totp: { enabled: false, @@ -125,19 +125,21 @@ export const register = (mail) => async (req, res, next) => { const createdUser = await newUser.save(); - const emailVerificationValidUntil = created + 48 * 60 * 60 * 1000; - const emailVerificationToken = jwt.sign( - { - user: req.body.email, - validUntil: emailVerificationValidUntil, - }, - process.env.SQ_JWT_SECRET - ); - await sendVerificationEmail( - mail, - req.body.email, - emailVerificationToken - ); + if (!process.env.SQ_DISABLE_EMAIL) { + const emailVerificationValidUntil = created + 48 * 60 * 60 * 1000; + const emailVerificationToken = jwt.sign( + { + user: req.body.email, + validUntil: emailVerificationValidUntil, + }, + process.env.SQ_JWT_SECRET + ); + await sendVerificationEmail( + mail, + req.body.email, + emailVerificationToken + ); + } if (createdUser) { if (req.body.invite) { @@ -257,7 +259,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"); @@ -293,14 +295,16 @@ export const generateInvite = (mail) => async (req, res, next) => { const createdInvite = await invite.save(); if (createdInvite) { - await mail.sendMail({ - from: `"${process.env.SQ_SITE_NAME}" <${process.env.SQ_MAIL_FROM_ADDRESS}>`, - to: email, - subject: "Invite", - text: `You have been invited to join ${process.env.SQ_SITE_NAME}. Please follow the link below to register. + if (!process.env.SQ_DISABLE_EMAIL) { + await mail.sendMail({ + from: `"${process.env.SQ_SITE_NAME}" <${process.env.SQ_MAIL_FROM_ADDRESS}>`, + to: email, + subject: "Invite", + text: `You have been invited to join ${process.env.SQ_SITE_NAME}. Please follow the link below to register. ${process.env.SQ_BASE_URL}/register?token=${createdInvite.token}`, - }); + }); + } res.send(createdInvite); } } else { @@ -343,18 +347,20 @@ export const changePassword = (mail) => async (req, res, next) => { { $set: { password: hash } } ); - await mail.sendMail({ - from: `"${process.env.SQ_SITE_NAME}" <${process.env.SQ_MAIL_FROM_ADDRESS}>`, - to: user.email, - subject: "Your password was changed", - text: `Your password was updated successfully at ${new Date().toISOString()} from ${ - req.ip - }. + if (!process.env.SQ_DISABLE_EMAIL) { + await mail.sendMail({ + from: `"${process.env.SQ_SITE_NAME}" <${process.env.SQ_MAIL_FROM_ADDRESS}>`, + to: user.email, + subject: "Your password was changed", + text: `Your password was updated successfully at ${new Date().toISOString()} from ${ + req.ip + }. If you did not perform this action, follow the link below immediately to reset your password. If this was you, no action is required. ${process.env.SQ_BASE_URL}/reset-password/initiate`, - }); + }); + } res.sendStatus(200); } catch (e) { @@ -388,14 +394,16 @@ export const initiatePasswordReset = (mail) => async (req, res, next) => { process.env.SQ_JWT_SECRET ); - await mail.sendMail({ - from: `"${process.env.SQ_SITE_NAME}" <${process.env.SQ_MAIL_FROM_ADDRESS}>`, - to: user.email, - subject: "Password reset", - text: `Please follow the link below to reset your password. + if (!process.env.SQ_DISABLE_EMAIL) { + await mail.sendMail({ + from: `"${process.env.SQ_SITE_NAME}" <${process.env.SQ_MAIL_FROM_ADDRESS}>`, + to: user.email, + subject: "Password reset", + text: `Please follow the link below to reset your password. ${process.env.SQ_BASE_URL}/reset-password/finalise?token=${token}`, - }); + }); + } res.sendStatus(200); } catch (e) { diff --git a/api/src/index.js b/api/src/index.js index ad9fc33..09968e5 100644 --- a/api/src/index.js +++ b/api/src/index.js @@ -52,15 +52,19 @@ validateConfig(config).then(() => { }); } - const mail = nodemailer.createTransport({ - host: process.env.SQ_SMTP_HOST, - port: process.env.SQ_SMTP_PORT, - secure: process.env.SQ_SMTP_SECURE, - auth: { - user: process.env.SQ_SMTP_USER, - pass: process.env.SQ_SMTP_PASS, - }, - }); + let mail; + + if (!process.env.SQ_DISABLE_EMAIL) { + mail = nodemailer.createTransport({ + host: process.env.SQ_SMTP_HOST, + port: process.env.SQ_SMTP_PORT, + secure: process.env.SQ_SMTP_SECURE, + auth: { + user: process.env.SQ_SMTP_USER, + pass: process.env.SQ_SMTP_PASS, + }, + }); + } const connectToDb = () => { console.log("[sq] initiating db connection..."); diff --git a/api/src/schema/torrent.js b/api/src/schema/torrent.js index afc5acd..8656ebb 100644 --- a/api/src/schema/torrent.js +++ b/api/src/schema/torrent.js @@ -4,6 +4,7 @@ import fuzzySearch from "mongoose-fuzzy-searching"; const Torrent = new mongoose.Schema({ infoHash: String, binary: String, + poster: String, uploadedBy: mongoose.Schema.ObjectId, name: String, description: String, diff --git a/api/src/setup/createAdminUser.js b/api/src/setup/createAdminUser.js index 63b9dc0..5b0dfb6 100644 --- a/api/src/setup/createAdminUser.js +++ b/api/src/setup/createAdminUser.js @@ -18,6 +18,7 @@ const createAdminUser = async (mail) => { password: hash, created, remainingInvites: Number.MAX_SAFE_INTEGER, + emailVerified: process.env.SQ_DISABLE_EMAIL, }); adminUser.uid = crypto .createHash("sha256") @@ -35,19 +36,21 @@ const createAdminUser = async (mail) => { await adminUser.save(); - const emailVerificationValidUntil = created + 48 * 60 * 60 * 1000; - const emailVerificationToken = jwt.sign( - { - user: process.env.SQ_ADMIN_EMAIL, - validUntil: emailVerificationValidUntil, - }, - process.env.SQ_JWT_SECRET - ); - await sendVerificationEmail( - mail, - process.env.SQ_ADMIN_EMAIL, - emailVerificationToken - ); + if (!process.env.SQ_DISABLE_EMAIL) { + const emailVerificationValidUntil = created + 48 * 60 * 60 * 1000; + const emailVerificationToken = jwt.sign( + { + user: process.env.SQ_ADMIN_EMAIL, + validUntil: emailVerificationValidUntil, + }, + process.env.SQ_JWT_SECRET + ); + await sendVerificationEmail( + mail, + process.env.SQ_ADMIN_EMAIL, + emailVerificationToken + ); + } console.log("[sq] created initial admin user"); } diff --git a/api/src/utils/validateConfig.js b/api/src/utils/validateConfig.js index 096a15a..554b4c3 100644 --- a/api/src/utils/validateConfig.js +++ b/api/src/utils/validateConfig.js @@ -50,13 +50,37 @@ const configSchema = yup grey: yup.string().matches(hexRegex), }), SQ_EXTENSION_BLACKLIST: yup.array().of(yup.string()).min(0), + SQ_SITE_DEFAULT_LOCALE: yup + .string() + .oneOf(["en", "es", "it", "ru", "de", "zh", "eo", "fr"]), SQ_BASE_URL: yup.string().matches(httpRegex).required(), SQ_API_URL: yup.string().matches(httpRegex).required(), SQ_MONGO_URL: yup.string().matches(mongoRegex).required(), - SQ_MAIL_FROM_ADDRESS: yup.string().email().required(), - SQ_SMTP_HOST: yup.string().required(), - SQ_SMTP_PORT: yup.number().integer().min(1).max(65535).required(), - SQ_SMTP_SECURE: yup.boolean().required(), + SQ_DISABLE_EMAIL: yup.boolean(), + SQ_MAIL_FROM_ADDRESS: yup + .string() + .email() + .when("SQ_DISABLE_EMAIL", { + is: (val) => val !== true, + then: (schema) => schema.required(), + }), + SQ_SMTP_HOST: yup.string().when("SQ_DISABLE_EMAIL", { + is: (val) => val !== true, + then: (schema) => schema.required(), + }), + SQ_SMTP_PORT: yup + .number() + .integer() + .min(1) + .max(65535) + .when("SQ_DISABLE_EMAIL", { + is: (val) => val !== true, + then: (schema) => schema.required(), + }), + SQ_SMTP_SECURE: yup.boolean().when("SQ_DISABLE_EMAIL", { + is: (val) => val !== true, + then: (schema) => schema.required(), + }), }) .strict() .noUnknown() @@ -66,8 +90,16 @@ const configSchema = yup SQ_JWT_SECRET: yup.string().required(), SQ_SERVER_SECRET: yup.string().required(), SQ_ADMIN_EMAIL: yup.string().email().required(), - SQ_SMTP_USER: yup.string().required(), - SQ_SMTP_PASS: yup.string().required(), + SQ_SMTP_USER: yup.string(), + SQ_SMTP_PASS: yup.string(), + }) + .when("envs.SQ_DISABLE_EMAIL", { + is: (val) => val !== true, + then: (schema) => { + schema.fields.SQ_SMTP_USER = yup.string().required(); + schema.fields.SQ_SMTP_PASS = yup.string().required(); + return schema; + }, }) .strict() .noUnknown() 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/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/components/Input.js b/client/components/Input.js index c01b49d..9415317 100644 --- a/client/components/Input.js +++ b/client/components/Input.js @@ -56,18 +56,40 @@ export const WrapLabel = ({ label, children, as = "label", ...rest }) => children ); -const Input = ({ label, rows, my, mt, mb, width, forwardedRef, ...rest }) => { +const Input = ({ + label, + rows, + my, + mt, + mb, + width, + forwardedRef, + required, + ...rest +}) => { return ( - + {rows ? ( ) : ( - + )} ); diff --git a/client/components/Select.js b/client/components/Select.js index c76f35c..20f2a3f 100644 --- a/client/components/Select.js +++ b/client/components/Select.js @@ -31,11 +31,11 @@ const StyledSelect = styled.select( typography ); -const Select = ({ label, fRef, ...rest }) => { +const Select = ({ label, required, fRef, ...rest }) => { return ( - + - + ), - gridWidth: "2fr", + gridWidth: "minmax(150px, 2fr)", }, { header: `${getLocaleString("uploadCategory")}`, @@ -118,7 +118,7 @@ const TorrentList = ({ ); }, - gridWidth: "1fr", + gridWidth: "minmax(75px, 1fr)", }, { header: `${getLocaleString("torrSeeders")}`, @@ -184,7 +184,11 @@ const TorrentList = ({ header: `${getLocaleString("torrUploaded")}`, accessor: "created", cell: ({ value }) => ( - {moment(value).format(`${getLocaleString("userUserSinceTime")}`)} + + {moment(value).format( + `${getLocaleString("userUserSinceTime")}` + )} + ), gridWidth: "140px", rightAlign: true, @@ -235,7 +239,8 @@ const TorrentList = ({ - {total.toLocaleString()} {getLocaleString("torrResults")} — {getLocaleString("torrPage")} {page + 1} {getLocaleString("torrOf")}{" "} + {total.toLocaleString()} {getLocaleString("torrResults")} —{" "} + {getLocaleString("torrPage")} {page + 1} {getLocaleString("torrOf")}{" "} {(maxPage + 1).toLocaleString()} diff --git a/client/locales/de.json b/client/locales/de.json index eb23c7b..3ab8c41 100644 --- a/client/locales/de.json +++ b/client/locales/de.json @@ -1,22 +1,308 @@ { - "poweredBy": "Powered by", + "404NotFound": "Nicht gefunden", + "404PageDoesNotExist": "Die angeforderte Seite existiert nicht", + "404ReturnHome": "Zurück zur Startseite", + "acc2FADisabled": "2FA deaktiviert", + "acc2FAEnabled": "2FA aktiviert", + "acc2FAScanQR": "Scannen sie den QR Code mit ihrer Authenticator App und geben Sie den Einmal-Code ein.", + "acc2FAText1": "2FA wurde erfolgreich aktiviert. Diese Backup codes können für den Login benutzt werden, falls Sie Zugriff auf ihre Authenticator App verlieren. Speichern Sie sie jetzt! Sie werden nicht noch einmal angezeigt werden.", + "acc2FAUseApp": "Nutzen Sie eine Authenticator App für eine zusätzliche Sicherung", + "acc2FAuth": "Zwei Faktor Authentifizierung", + "accBonusPoints": "Bonus Punkte", + "accBonusPointsHave": "Bonus Punkt(e)", + "accBuy": "Kaufen", + "accCancel": "Abbrechen", + "accChangePass": "Passwort ändern", + "accClaimed": "Beansprucht", + "accCopyLink": "Link kopieren", + "accCouldNotBuyItems": "Konnte keine Items kaufen", + "accCouldNotChangePass": "Konnte das Passwort nicht ändern", + "accCouldNotDelAcc": "Account konnte nicht gelöscht werden", + "accCouldNotSendInvite": "Einladung konnte nicht verschickt werden", + "accCouldNotToggle2FA": "Konnte 2FA nicht umschalten", + "accCreated": "Erstellt", + "accCurrentPass": "Aktuelles Passwort", + "accDangerZone": "Gefahrenzone", + "accDelAccText1": "Sind sie sicher, dass sie ihren Account löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden und Sie werden nicht in der Lage sein sich erneut zu registrieren. Ihre persönlichen Informationen werden gelöscht, aber ihre hochgeladenen Torrents verbleiben.", + "accDeleteMyAcc": "Lösche meinen Account", + "accDeleteMyAccYes": "Ja, lösche meinen Account", + "accDisable": "Deaktivieren", + "accEnable": "Aktivieren", + "accEnable2FA": "Aktiviere 2FA", + "accEveryRequestYouFulfill": "für jede Anfrage, die Sie erfüllen, oder", + "accForEveryGBYouUpload": "für jedes GB, das Sie hochladen", + "accIfYouAreAlsUploaderAcceptTorrent": "wenn Sie auch der Uploader des akzeptierten Torrents sind", + "accInviteLinkCopiedClipboard": "Einladungslink wurde in die Zwischenablage kopiert", + "accInvites": "Einladungen", + "accRemaining": "verbleibend", + "accRoleUser": "Rolle: Nutzer", + "accRoleAdmin": "Rolle: Admin", + "accInviteSentSuccess": "Einladung erfolgreich gesendet", + "accInviteText1": "Geben Sie eine E-Mail Adresse ein, um die Einladung zu senden. Der eingeladene Nutzer muss sich mit der selben E-Mail Addresse registrieren. Sobald die Einladung generiert ist, können sie auch einen direkten Link kopieren.", + "accItemsPurchasedSuccess": "Die Items wurden erfolgreich gekauft", + "accMyAccount": "Mein Account", + "accPassChangedSuccess": "Das Passwort wurde erfolgreich geändert", + "accPurchaseInvites": "Kaufe Einladungen", + "accPurchaseUpload1GB": "Kaufe Upload (1 GB)", + "accRole": "Rolle", + "accSendInvite": "Eine Einladung senden", + "accThisIsAdminAcc": "Dies ist ein Admin Account.", + "accValidUntil": "Gültig bis", + "accYouCurrentlyHave": "Sie haben aktuell", + "accYouWillEarn": "Sie werden verdienen", + "accNotAvailableToBuy": "Nicht zum Kauf verfügbar", + "annPinnedAnnounce": "Angepinnte Bekanntmachungen", + "annOtherAnnounce": "Andere Bekanntmachungen", + "annAnnounceCreatSuccess": "Bekanntmachung erfolgreich erstellt", + "annCouldNotCreateAnnounce": "Konnte Bekanntmachung nicht erstellen", + "annNewAnnounce": "Neue Bekanntmachung", + "annBody": "Inhalt", + "annPinThisAnnounceQ": "Diese Bekanntmachung pinnen?", + "annAllowCommentsQ": "Kommentare erlauben?", + "annCreateAnnounce": "Eine Bekanntmachung erstellen", + "annAnnounceDelSuccess": "Bekanntmachung wurde erfolgreich gelöscht", + "annCouldNotDelAnnounce": "Konnte Bekanntmachung nicht löschen", + "annUnpin": "Loslösen", + "annPin": "Pinnen", + "annUnpinned": "Losgelöst", + "annPinned": "Angepinnt", + "annLastUpdated": "Zuletzt aktualisiert", + "annCommentsDisabled": "Kommentare deaktiviert.", + "annAreYouSureYouWantToDelThisannounceQ": "Sind Sie sicher, dass sie diese Ankündigung löschen möchten? Dies kann nicht rückgängig gemacht werden.", + "annAnnounceUpdatedSuccess": "Die Ankündigung wurde erfolgreich aktualisiert", + "annCouldNotUpdateAnnounce": "Die Ankündigung konnte nicht aktualisiert werden", + "annEditAnnounce": "Ankündigung bearbeiten", + "annUpdateAnnounce": "Ankündigung aktualisieren", + "annAnnounce": "Ankündigung", + "bmYourBM": "Deine Lesezeichen", + "bmYouNotHaveAnyBM": "Du hast keine Lesezeichen.", + "catCategories": "Kategorien", + "catNoCategoryHaveBeenDefined": "Es wurden keine Kategorien festgelegt.", + "catNoTagsHaveBeenDefined": "Es wurden keine Tags definiert.", + "catNoResults": "Keine Ergebnisse.", + "comCommentBy": "Kommentar von", + "comDelUser": "Gelöschter Benutzer", + "comOn": "an", + "email": "Email", + "errSomethingWentWrong": "Etwas ist schief gelaufen", + "errTooManyRequests": "Zu viele Anfragen! Sie wurden limitiert. Bitte warten Sie eine kurze Zeit bevor Sie es erneut versuchen.", + "errIfErrorPersist": "Wenn der Fehler weiterhin besteht, bitte", + "errReportIt": "Melde ihn", + "errForNow": "Für jetzt,", + "indexLatestAnnounce": "Letzte Bekanntmachungen", + "indexLatestTorrents": "Letzte Torrents", + "indexSearch": "Suche", + "indexSearchTorrents": "Suche Torrents", + "indexText1": "Ihre E-Mail Adresse ist noch nicht verifiziert. Sie können keine Daten hoch / runterladen, bevor ihre E-Mail nicht verifiziert wurde.", + "indexTime": "HH:mm Do MMM YYYY", + "listNoItemShow": "Keine Items zum Anzeigen.", "logIn": "Anmelden", - "register": "Registrieren", - "email": "E-Mail", - "username": "Benutzername", - "usernameRules": "Nur Buchstaben, Zahlen, und “.” erlaubt.", - "password": "Passwort", - "totp": "Einmalpasswort", - "resetPassword": "Passwort zurücksetzen", + "logInFailed": "Anmeldung fehlgeschlagen", + "mdMarkdownPreview": "Markdown Vorschau", + "navAnnouncements": "Ankündigungen", + "navBookmarks": "Lesezeichen", + "navBrowse": "Durchsuchen", + "navHome": "Start", + "navLogOut": "Ausloggen", + "navReports": "Meldungen", + "navRequests": "Anfragen", + "navRSS": "RSS", + "navSearch": "Suche", + "navStats": "Statistiken", + "navUpload": "Upload", + "navWiki": "Wiki", "newPassword": "Neues Passwort", - "welcomeBack": "Willkommen zurück", - "logInFailed": "Anmeldung fehgeschlagen", - "welcome": "Willkommen", + "password": "Passwort", + "passwordResetFailed": "Das Zurücksetzen des Passworts konnte nicht abgeschlossen werden", + "passwordResetRequestFailed": "Das Zurücksetzen des Passworts konnte nicht eingeleitet werden", + "passwordResetRequestSuccess": "Wenn ein Konto mit dieser E-Mail-Adresse existiert, erhalten Sie in Kürze eine E-Mail", + "passwordResetSuccess": "Das Passwort wurde erfolgreich zurückgesetzt", + "poweredBy": "Powered by", + "register": "Registrieren", "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" + "reqCreateNew": "Erstelle Neu", + "reqCreateReq": "Erstelle Anfrage", + "reqTitle": "Titel", + "reqPostedBy": "Geschrieben von", + "reqPost": "Post", + "reqRequestCreatedSuccess": "Anfrage erfolgreich erstellt", + "reqCouldNotCreateReq": "Konnte Anfrage nicht erstellen", + "reqNewRequest": "Neue Anfrage", + "reqWhatYouLookForQ": "Nach was suchen Sie?", + "reqRequestDelSuccess": "Anfrage wurde erfolgreich gelöscht", + "reqCouldNotDelReq": "Anfrage konnte nicht gelöscht werden", + "reqCommentPostSuccess": "Kommentar wurde erfolgreich gepostet", + "reqCommentNotPost": "Kommentar konnte nicht gepostet werden", + "reqSuggestionAddSuccess": "Vorschlag erfolgreich hinzugefügt", + "reqSuggestionNotAdded": "Vorschlag konnte nicht hinzugefügt werden", + "reqSuggestionAcceptSuccess": "Vorschlag wurde erfolgreich akzeptiert", + "reqCouldNotAcceptSuggestion": "Vorschlag konnte nicht akzeptiert werden", + "reqDelete": "Löschen", + "reqSuggestedTorrents": "Vorgeschlagene Torrents", + "reqSuggestATorrent": "Schlag einen Torrent vor", + "reqAccepted": "Akzeptiert", + "reqAccept": "Akzeptieren", + "reqNoTorrentsHaveBeenSuggestedYet": "Es wurden noch keine Torrents vorgeschlagen.", + "reqPostAComment": "Poste einen Kommentar", + "reqEnterInfohashTorrentBelow": "Gib den Info Hash eines Torrens unten ein.", + "reqFulfilled": "Erfüllt", + "reqPosted": "Gesendet", + "reqBy": "von", + "reqInfohash": "Info Hash", + "reqSuggest": "Vorschlagen", + "repUnresolvedRep": "Ungelöste Berichte", + "repRepBy": "Gemeldet von", + "repReason": "Grund", + "repRepMarkSolved": "Meldung als gelöst markiert", + "repCouldNotResolveRep": "Die Meldung konnte nicht gelöst werden", + "repRepOn": "Berichten über", + "repMarkSolved": "Als gelöst markieren", + "repRep": "Gemeldet", + "repTorrDetail": "Torrent Einzelheiten", + "resetPassword": "Passwort zurücksetzen", + "rssThereRSSFeedAt": "Es gibt einen RSS Feed", + "rssToAuthenticateYourself": "Um sich zu authentifizieren, müssen Sie Ihre Cookies übermitteln", + "rssAnd": "und", + "rssToRSSEndpoint": "an den RSS-Endpunkt, der Ihren Benutzernamen bzw. Ihr Passwort enthält.", + "rssNoQueryParametersAreProvided": "Wenn keine Abfrageparameter angegeben werden, enthält der RSS-Feed die 100 neuesten Torrents.", + "rssOnlyIncludeMatchingResults": "Um nur passende Ergebnisse in den Feed aufzunehmen, können Sie Folgendes hinzufügen", + "rssQueryParameter": "Abfrageparameter, z.B.", + "searchSearchResults": "Suchergebnisse für", + "searchSearchError": "Suchfehler", + "statYouNotPermission": "Sie haben keine Erlaubnis dies zu tun.", + "statTrackerStat": "Tracker Statistiken", + "statActiveTorrents": "Aktive Torrents", + "statBannedUsers": "Gebannte Nutzer", + "statCompletedDownloads": "Abgeschlossene Downloads", + "statFilledRequests": "Erfüllte Anfragen", + "statInvitesAccepted": "Akzeptierte Einladungen", + "statLeechers": "Leechers", + "statPeers": "Peers", + "statRegisteredUsers": "Registrierte Nutzer", + "statSeeders": "Seeders", + "statTotalComments": "Alle Kommentare", + "statTotalInvitesSent": "Alle gesendeten Einladungen", + "statTotalRequests": "Alle Anfragen", + "statUploadedTorrents": "Hochgeladene Torrents", + "tagTaggedWith": "Markiert mit", + "tokenError": "Token Fehler", + "torrSeeders": "Seeders", + "torrLeechers": "Leechers", + "torrDownloads": "Downloads", + "torrUploaded": "Hochgeladen", + "torrTorrEditSuccess": "Torrent erfolgreich bearbeitet", + "torrCouldEditTorr": "Konnte den Torrent nicht bearbeiten", + "torrTorrDelSuccess": "Torrent wurde erfolgreich gelöscht", + "torrCouldNotDelTorr": "Torrent konnte nicht gelöscht werden", + "torrVoteSubmitSuccess": "Abstimmung erfolgreich eingereicht", + "torrCouldNotSubmitVote": "Stimme konnte nicht abgegeben werden.", + "torrReportSubmitSuccess": "Meldung erfolgreich eingereicht", + "torrCouldNotSubmitReport": "Meldung konnte nicht eingereicht werden", + "torrFLToggleSuccess": "Freeleech erfolgreich umgeschaltet", + "torrCouldNotToggleFL": "Konnte freeleech nicht umschalten", + "torrTorrRemFromGroupSuccess": "Der Torrent wurde von der Gruppe entfernt", + "torrCouldNotRemTorrFromGroup": "Konnte den Torrent nicht von der Gruppe entfernen", + "torrTorrent": "Torrent", + "torrCouldNotBookmarkTorr": "Torrent konnte nicht als Lesezeichen gespeichert werden", + "torrFL": "FL!", + "torrEdit": "Bearbeiten", + "torrDownload": "Herunterladen", + "torrLogInDownload": "Einloggen zum Herunterladen", + "torrFiles": "Dateien", + "torrReport": "Melden", + "torrRemTorr": "Entferne diesen Torrent", + "torrAddTorr": "Einen Torrent hinzufügen", + "torrReasonForReport": "Grund für Meldung", + "torrSureDeleteTorr": "Sind Sie sich sicher, dass Sie den Torrent löschen möchten? Dies kann nicht rückgängig gemacht werden.", + "torrFreeleech": "Freeleech", + "torrUnset": "Unset", + "torrSet": "Set", + "torrUploadedBy": "Hochgeladen von", + "torrDate": "Datum", + "torrSize": "Größe", + "torrYes": "Ja", + "torrNo": "Nein", + "torrGroupTorr": "Gruppierte Torrents", + "torrThereAreNoOtherTorrGroup": "Es sind keine anderen Torrents in dieser Gruppe.", + "torrResults": "Ergebnisse", + "torrPage": "Seite", + "torrOf": "von", + "torrRemovedFrom": "entfernt von", + "torrAddedTo": "hinzugefügt zu", + "torrTorrNoTags": "Dieser Torrent hat keine Markierung.", + "totp": "Einmal code", + "uploadAddTag": "Füge eine Markierung hinzu", + "uploadAnnounceURL": "Announce URL", + "uploadAnonymousUpload": "Anonymer Upload", + "uploadCategory": "Kategorie", + "uploadCouldNotGetGroupSuggestions": "Es konnten keine Gruppenvorschläge abgerufen werden", + "uploadCouldNotUploadFile": "Die Datei konnte nicht hochgeladen werden", + "uploadCouldNotUploadTorrent": "Konnte .torrent nicht hochladen", + "uploadDescription": "Beschreibung", + "uploadDragDropClickSelect": "Ziehen Sie die Torrent-Datei per Drag-and-Drop hierher oder klicken Sie, um sie auszuwählen", + "uploadDropFileHere": "Legen Sie die Datei hier ab...", + "uploadGroupWith": "Gruppe mit", + "uploadGroupWithThisTorrent": "Gruppe mit diesem Torrent", + "uploadInfoBox1": "Die folgenden Dateierweiterungen stehen auf der schwarzen Liste. Torrents, die Dateien dieser Art enthalten, werden nicht hochgeladen.", + "uploadInfoBox2": "Es sieht so aus, als hätten diese vorhandenen Torrents ähnliche Namen. Möchten Sie Ihren Upload mit einem davon gruppieren? Gruppen sollten nur sehr ähnliche Inhalte enthalten, z.B. den gleichen Film in verschiedenen Formaten.", + "uploadInfoBox3": "Hinweis: Wenn Sie vor dem Hochladen mit dem Seeding eines Torrents begonnen haben, müssen Sie möglicherweise die Tracker in Ihrem Torrent-Client aktualisieren, sobald der Upload abgeschlossen ist.", + "uploadMarkdownSupport": "Markdown unterstützt", + "uploadMediaInfo": "Medien Information", + "uploadName": "Name", + "uploadSource": "Quelle", + "uploadTags": "Markierungen", + "uploadTorrentFile": "Torrent Datei", + "uploadTorrentUploadSuccess": "Torrent erfolgreich hochgeladen", + "uploadUpload": "Hochladen", + "uploadRemove": "Entfernen", + "username": "Nutzername", + "userUser": "Nutzer", + "userSuccessfully": "erfolgreich", + "userCouldNot": "Konnte nicht", + "userProfile": "Profil", + "userUserSince": "Nutzer seit", + "userUserSinceTime": "Do MMM YYYY", + "userOnlyAdminsSee": "Nur Admins können dies sehen", + "userInvitedBy": "Eingeladen von", + "userEmailVerified": "Email verifiziert", + "userRemainingInvites": "Verbleibende Einladungen", + "userRatio": "Ratio", + "userHitNRuns": "Hit'n'runs", + "userDownloaded": "Heruntergeladen", + "userComments": "Kommentare", + "userNoComments": "Keine Kommentare.", + "userYouSureWant": "Sind Sie sicher, dass Sie das möchten?", + "userThisUserQ": "dieser Nutzer??", + "userUploaded": "Hochgeladen", + "userMyUploads": "Meine Uploads", + "usernameRules": "Darf nur aus Buchstaben, Zahlen und „.“ bestehen.“", + "userUnban": "entsperren", + "userBan": "sperren", + "userUnbanned": "entsperrt", + "userBanned": "gesperrt", + "userAdmin": "Admin", + "veCouldNotVerifyEmailAddress": "Konnte diese E-Mail Adresse nicht verifizieren:", + "veEmailAddressVerifiedSuccess": "E-Mail wurde erfolgreich verifiziert.", + "veNoVerificationTokenProvided": "Kein Verifizierungs-Token angegeben", + "veVerifyEmail": "Verifiziere E-Mail", + "welcome": "Willkommen", + "welcomeBack": "Willkommen zurück", + "wikiPath": "Pfad", + "wikiPageWillBeVisibleAt": "Die Seite wird sichtbar sein am", + "wikiAllowUnregisteredView": "Nicht registrierte Ansicht zulassen", + "wikiPageCreateSuccess": "Wiki-Seite erfolgreich erstellt", + "wikiCouldNotCreatePage": "Konnte die Wiki-Seite nicht erstellen", + "wikiNewPage": "Neue Wiki-Seite", + "wikiCreatePage": "Erstelle Wiki-Seite", + "wikiPageDelSuccess": "Wiki-Seite wurde erfolgreich gelöscht", + "wikiCouldNotDelPage": "Konnte die Wiki-Seite nicht löschen", + "wikiPageUpdateSuccess": "Die Wiki-Seite wurde erfolgreich aktualisiert", + "wikiCouldNotUpdatePage": "Die Wiki-Seite konnte nicht aktualisiert werden", + "wikiAddPage": "Füge eine Seite hinzu", + "wikiLastEdited": "Zuletzt geändert", + "wikiPages": "Seiten", + "wikiSaveChanges": "Änderungen speichern", + "wikiThereNothingHereYet": "Hier ist noch nichts", + "wikiDelThisPageQ": "Sind Sie sicher, dass Sie diese Wiki-Seite löschen möchten? Das kann nicht rückgängig gemacht werden." } diff --git a/client/locales/en.json b/client/locales/en.json index dfa8ab6..646f16b 100644 --- a/client/locales/en.json +++ b/client/locales/en.json @@ -31,14 +31,16 @@ "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", + "accIfYouAreAlsUploaderAcceptTorrent": "if you are also the uploader of the accepted torrent", "accInviteLinkCopiedClipboard": "Invite link copied to clipboard", "accInvites": "Invites", "accRemaining": "remaining", "accRoleUser": "Role: user", "accRoleAdmin": "Role: admin", "accInviteSentSuccess": "Invite sent successfully", + "accInviteSentSuccessNoEmail": "Invite created 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.", + "accInviteText1NoEmail": "Enter an email address to generate an invite. The invited user will need to sign up with the same email address. This tracker has email sending disabled, so you will need to copy and send the invite link yourself.", "accItemsPurchasedSuccess": "Items purchased successfully", "accMyAccount": "My account", "accPassChangedSuccess": "Password changed successfully", @@ -46,6 +48,7 @@ "accPurchaseUpload1GB": "Purchase upload (1 GB)", "accRole": "Role", "accSendInvite": "Send invite", + "accSendInviteNoEmail": "Create invite", "accThisIsAdminAcc": "This is an admin account.", "accValidUntil": "Valid until", "accYouCurrentlyHave": "You currently have", @@ -117,6 +120,7 @@ "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", + "posterImage": "Cover image", "poweredBy": "Powered by", "register": "Register", "registerFailed": "Could not register", @@ -241,6 +245,7 @@ "uploadCouldNotUploadTorrent": "Could not upload .torrent", "uploadDescription": "Description", "uploadDragDropClickSelect": "Drag and drop .torrent file here, or click to select", + "uploadDragDropClickSelectPoster": "Drag and drop your cover image here, or click to select", "uploadDropFileHere": "Drop the file here...", "uploadGroupWith": "Group with", "uploadGroupWithThisTorrent": "Group with this torrent", @@ -277,8 +282,8 @@ "userUploaded": "Uploaded", "userMyUploads": "My uploads", "usernameRules": "Can only consist of letters, numbers, and “.”", - "userUnban": "unban", - "userBan": "ban", + "userUnban": "Unban", + "userBan": "Ban", "userUnbanned": "unbanned", "userBanned": "banned", "userAdmin": "Admin", diff --git a/client/locales/eo.json b/client/locales/eo.json index 6a12d04..6f9e8e5 100644 --- a/client/locales/eo.json +++ b/client/locales/eo.json @@ -116,6 +116,7 @@ "passwordResetRequestFailed": "Ne eblis komenci pasvortigon", "passwordResetRequestSuccess": "Se konto kun tiu retadreso ekzistas, vi ricevos retmesaĝon baldaŭ", "passwordResetSuccess": "Pasvorto estis restarigita sukcese", + "posterImage": "Afiŝo", "poweredBy": "Funkciigita de", "register": "Registriĝi", "registerFailed": "Ne eblis registri", @@ -239,6 +240,7 @@ "uploadCouldNotUploadTorrent": "Ne eblis alŝuti .torrent", "uploadDescription": "Priskribo", "uploadDragDropClickSelect": "Trenu kaj faligi .torrent-dosieron ĉi tie, aŭ alklaku por elekti", + "uploadDragDropClickSelectPoster": "Tiri kaj demeti vian afiŝo-dosieron ĉi tien, aŭ klaku por elekti", "uploadDropFileHere": "Faligi la dosieron ĉi tie...", "uploadGroupWith": "Grupu kun", "uploadGroupWithThisTorrent": "Grupu kun ĉi tiu torento", diff --git a/client/locales/es.json b/client/locales/es.json new file mode 100644 index 0000000..2912eb1 --- /dev/null +++ b/client/locales/es.json @@ -0,0 +1,310 @@ +{ + "404NotFound": "No encontrado", + "404PageDoesNotExist": "Esa página no existe.", + "404ReturnHome": "Volver a inicio", + "acc2FADisabled": "2FA desactivado", + "acc2FAEnabled": "2FA activado", + "acc2FAScanQR": "Escanea el código QR con tu aplicación de autenticación y introduce el código de un solo uso", + "acc2FAText1": "2FA habilitado con éxito. Estos códigos de respaldo pueden ser utilizados para iniciar sesión si pierdes el acceso a tu aplicación de autenticación. Guárdalos ahora, no serán visibles nuevamente.", + "acc2FAUseApp": "Usa una aplicación de autenticación para agregar otra capa de seguridad a tu cuenta", + "acc2FAuth": "Autenticación de dos factores", + "accBonusPoints": "Puntos de bonificación", + "accBonusPointsHave": "punto(s) de bonificación", + "accBuy": "Comprar", + "accCancel": "Cancelar", + "accChangePass": "Cambiar contraseña", + "accClaimed": "Reclamado", + "accCopyLink": "Copiar enlace", + "accCouldNotBuyItems": "No se pudieron comprar los artículos", + "accCouldNotChangePass": "No se pudo cambiar la contraseña", + "accCouldNotDelAcc": "No se pudo eliminar la cuenta", + "accCouldNotSendInvite": "No se pudo enviar la invitación", + "accCouldNotToggle2FA": "No se pudo activar/desactivar la autenticación de dos factores (2FA)", + "accCreated": "Creado", + "accCurrentPass": "Contraseña actual", + "accDangerZone": "Zona peligrosa", + "accDelAccText1": "¿Estás seguro de que deseas eliminar tu cuenta? Esta acción no se puede deshacer y es posible que no puedas registrarte nuevamente. Tu información personal se eliminará, pero tus torrents cargados permanecerán.", + "accDeleteMyAcc": "Eliminar mi cuenta", + "accDeleteMyAccYes": "Sí, eliminar mi cuenta", + "accDisable": "Desactivar", + "accEnable": "Activar", + "accEnable2FA": "Activar 2FA", + "accEveryRequestYouFulfill": "por cada solicitud que cumplas, o", + "accForEveryGBYouUpload": "por cada GB que cargues", + "accIfYouAreAlsUploaderAcceptTorrent": "si también eres el cargador del torrent aceptado", + "accInviteLinkCopiedClipboard": "Enlace de invitación copiado al portapapeles", + "accInvites": "Invitaciones", + "accRemaining": "restantes", + "accRoleUser": "Rol: usuario", + "accRoleAdmin": "Rol: administrador", + "accInviteSentSuccess": "Invitación enviada exitosamente", + "accInviteText1": "Ingresa una dirección de correo electrónico para enviar una invitación. El usuario invitado deberá registrarse con la misma dirección de correo electrónico. Una vez que se genere la invitación, también puedes copiar un enlace directo de invitación.", + "accItemsPurchasedSuccess": "Artículos comprados exitosamente", + "accMyAccount": "Mi cuenta", + "accPassChangedSuccess": "Contraseña cambiada exitosamente", + "accPurchaseInvites": "Comprar invitaciones", + "accPurchaseUpload1GB": "Comprar carga (1 GB)", + "accRole": "Rol", + "accSendInvite": "Enviar invitación", + "accThisIsAdminAcc": "Esta es una cuenta de administrador.", + "accValidUntil": "Válido hasta", + "accYouCurrentlyHave": "Actualmente tienes", + "accYouWillEarn": "Ganarás", + "accNotAvailableToBuy": "No disponible para comprar", + "annPinnedAnnounce": "Anuncios fijados", + "annOtherAnnounce": "Otros anuncios", + "annAnnounceCreatSuccess": "Anuncio creado exitosamente", + "annCouldNotCreateAnnounce": "No se pudo crear el anuncio", + "annNewAnnounce": "Nuevo anuncio", + "annBody": "Cuerpo", + "annPinThisAnnounceQ": "¿Fijar este anuncio?", + "annAllowCommentsQ": "¿Permitir comentarios?", + "annCreateAnnounce": "Crear anuncio", + "annAnnounceDelSuccess": "Anuncio eliminado exitosamente", + "annCouldNotDelAnnounce": "No se pudo eliminar el anuncio", + "annUnpin": "Desfijar", + "annPin": "Fijar", + "annUnpinned": "desfijado", + "annPinned": "fijado", + "annLastUpdated": "Última actualización", + "annCommentsDisabled": "Comentarios desactivados.", + "annAreYouSureYouWantToDelThisannounceQ": "¿Estás seguro de que deseas eliminar este anuncio? Esto no se puede deshacer.", + "annAnnounceUpdatedSuccess": "Anuncio actualizado exitosamente", + "annCouldNotUpdateAnnounce": "No se pudo actualizar el anuncio", + "annEditAnnounce": "Editar anuncio", + "annUpdateAnnounce": "Actualizar anuncio", + "annAnnounce": "Anuncio", + "bmYourBM": "Tus marcadores", + "bmYouNotHaveAnyBM": "No tienes ningún marcador.", + "catCategories": "Categorías", + "catNoCategoryHaveBeenDefined": "No se han definido categorías.", + "catNoTagsHaveBeenDefined": "No se han definido etiquetas.", + "catNoResults": "Sin resultados.", + "comCommentBy": "Comentario de", + "comDelUser": "usuario eliminado", + "comOn": "en", + "email": "Correo electrónico", + "errSomethingWentWrong": "Algo salió mal", + "errTooManyRequests": "¡Demasiadas solicitudes! Se ha aplicado un límite de velocidad. Por favor, espera un tiempo antes de intentarlo nuevamente.", + "errIfErrorPersist": "Si el error persiste, por favor", + "errReportIt": "informa", + "errForNow": "Por ahora,", + "indexLatestAnnounce": "Último anuncio", + "indexLatestTorrents": "Últimos torrents", + "indexSearch": "Buscar", + "indexSearchTorrents": "Buscar torrents", + "indexText1": "Tu dirección de correo electrónico aún no está verificada. No podrás cargar ni descargar datos hasta que esto se haga.", + "indexTime": "HH:mm Do MMM YYYY", + "listNoItemShow": "No hay elementos para mostrar.", + "logIn": "Iniciar sesión", + "logInFailed": "No se pudo iniciar sesión", + "mdMarkdownPreview": "Vista previa de Markdown", + "navAnnouncements": "Anuncios", + "navBookmarks": "Marcadores", + "navBrowse": "Navegar", + "navHome": "Inicio", + "navLogOut": "Cerrar sesión", + "navReports": "Informes", + "navRequests": "Solicitudes", + "navRSS": "RSS", + "navSearch": "Buscar", + "navStats": "Estadísticas", + "navUpload": "Subir", + "navWiki": "Wiki", + "newPassword": "Nueva contraseña", + "password": "Contraseña", + "passwordResetFailed": "No se pudo completar el restablecimiento de la contraseña", + "passwordResetRequestFailed": "No se pudo iniciar la solicitud de restablecimiento de contraseña", + "passwordResetRequestSuccess": "Si existe una cuenta con esa dirección de correo electrónico, recibirás un correo electrónico en breve", + "passwordResetSuccess": "La contraseña se restableció con éxito", + "posterImage": "Imagen de portada", + "poweredBy": "Proporcionado por", + "register": "Registrarse", + "registerFailed": "No se pudo registrar", + "registrationClosed": "Registro cerrado", + "reqCreateNew": "Crear nuevo", + "reqCreateReq": "Crear solicitud", + "reqTitle": "Título", + "reqPostedBy": "Publicado por", + "reqPost": "Publicar", + "reqRequestCreatedSuccess": "Solicitud creada exitosamente", + "reqCouldNotCreateReq": "No se pudo crear la solicitud", + "reqNewRequest": "Nueva solicitud", + "reqWhatYouLookForQ": "¿Qué estás buscando?", + "reqRequestDelSuccess": "Solicitud eliminada exitosamente", + "reqCouldNotDelReq": "No se pudo eliminar la solicitud", + "reqCommentPostSuccess": "Comentario publicado exitosamente", + "reqCommentNotPost": "No se pudo publicar el comentario", + "reqSuggestionAddSuccess": "Sugerencia añadida exitosamente", + "reqSuggestionNotAdded": "No se pudo agregar la sugerencia", + "reqSuggestionAcceptSuccess": "Sugerencia aceptada exitosamente", + "reqCouldNotAcceptSuggestion": "No se pudo aceptar la sugerencia", + "reqDelete": "Eliminar", + "reqSuggestedTorrents": "Torrents sugeridos", + "reqSuggestATorrent": "Sugerir un torrent", + "reqAccepted": "Aceptado", + "reqAccept": "Aceptar", + "reqNoTorrentsHaveBeenSuggestedYet": "Aún no se han sugerido torrents.", + "reqPostAComment": "Publicar un comentario", + "reqEnterInfohashTorrentBelow": "Ingresa el infohash de un torrent a continuación.", + "reqFulfilled": "Cumplido", + "reqPosted": "Publicado", + "reqBy": "por", + "reqInfohash": "Infohash", + "reqSuggest": "Sugerir", + "repUnresolvedRep": "Informes no resueltos", + "repRepBy": "Reportado por", + "repReason": "Motivo", + "repRepMarkSolved": "Informe marcado como resuelto", + "repCouldNotResolveRep": "No se pudo resolver el informe", + "repRepOn": "Informe sobre", + "repMarkSolved": "Marcar como resuelto", + "repRep": "Reportado", + "repTorrDetail": "Detalles del torrent", + "resetPassword": "Restablecer contraseña", + "rssThereRSSFeedAt": "Hay un feed RSS en", + "rssToAuthenticateYourself": "Para autenticarte, debes proporcionar las cookies", + "rssAnd": "y", + "rssToRSSEndpoint": "al punto final de RSS, conteniendo tu nombre de usuario y tu contraseña respectivamente.", + "rssNoQueryParametersAreProvided": "Si no se proporcionan parámetros de consulta, el feed RSS contendrá los 100 torrents más recientes.", + "rssOnlyIncludeMatchingResults": "Para incluir solo resultados coincidentes en el feed, puedes agregar el", + "rssQueryParameter": "parámetro de consulta, por ejemplo", + "searchSearchResults": "Resultados de búsqueda para", + "searchSearchError": "Error de búsqueda", + "statYouNotPermission": "No tienes permiso para hacer eso.", + "statTrackerStat": "Estadísticas del tracker", + "statActiveTorrents": "Torrents activos", + "statBannedUsers": "Usuarios prohibidos", + "statCompletedDownloads": "Descargas completadas", + "statFilledRequests": "Solicitudes completadas", + "statInvitesAccepted": "Invitaciones aceptadas", + "statLeechers": "Leechers", + "statPeers": "Pares", + "statRegisteredUsers": "Usuarios registrados", + "statSeeders": "Seeders", + "statTotalComments": "Comentarios totales", + "statTotalInvitesSent": "Invitaciones totales enviadas", + "statTotalRequests": "Solicitudes totales", + "statUploadedTorrents": "Torrents subidos", + "tagTaggedWith": "Etiquetado con", + "tokenError": "Error de token", + "torrSeeders": "Seeders", + "torrLeechers": "Leechers", + "torrDownloads": "Descargas", + "torrUploaded": "Subido", + "torrTorrEditSuccess": "Torrent editado exitosamente", + "torrCouldEditTorr": "No se pudo editar el torrent", + "torrTorrDelSuccess": "Torrent eliminado exitosamente", + "torrCouldNotDelTorr": "No se pudo eliminar el torrent", + "torrVoteSubmitSuccess": "Voto enviado exitosamente", + "torrCouldNotSubmitVote": "No se pudo enviar el voto", + "torrReportSubmitSuccess": "Informe enviado exitosamente", + "torrCouldNotSubmitReport": "No se pudo enviar el informe", + "torrFLToggleSuccess": "Freeleech cambiado exitosamente", + "torrCouldNotToggleFL": "No se pudo cambiar a freeleech", + "torrTorrRemFromGroupSuccess": "Torrent eliminado del grupo exitosamente", + "torrCouldNotRemTorrFromGroup": "No se pudo eliminar el torrent del grupo", + "torrTorrent": "Torrent", + "torrCouldNotBookmarkTorr": "No se pudo marcar el torrent", + "torrFL": "FL!", + "torrEdit": "Editar", + "torrDownload": "Descargar", + "torrLogInDownload": "Inicia sesión para descargar", + "torrFiles": "Archivos", + "torrReport": "Informe", + "torrRemTorr": "Eliminar este torrent", + "torrAddTorr": "Añadir un torrent", + "torrReasonForReport": "Motivo del informe", + "torrSureDeleteTorr": "¿Estás seguro de que quieres eliminar este torrent? Esto no se puede deshacer.", + "torrFreeleech": "Freeleech", + "torrUnset": "Desactivar", + "torrSet": "Activar", + "torrUploadedBy": "Subido por", + "torrDate": "Fecha", + "torrSize": "Tamaño", + "torrYes": "Sí", + "torrNo": "No", + "torrGroupTorr": "Torrents agrupados", + "torrThereAreNoOtherTorrGroup": "No hay otros torrents en este grupo.", + "torrResults": "resultados", + "torrPage": "Página", + "torrOf": "de", + "torrRemovedFrom": "eliminado de", + "torrAddedTo": "añadido a", + "torrTorrNoTags": "Este torrent no tiene etiquetas.", + "totp": "Código de un solo uso", + "uploadAddTag": "Añadir etiqueta", + "uploadAnnounceURL": "URL de anuncio", + "uploadAnonymousUpload": "Subida anónima", + "uploadCategory": "Categoría", + "uploadCouldNotGetGroupSuggestions": "No se pudieron obtener sugerencias de grupos", + "uploadCouldNotUploadFile": "No se pudo cargar el archivo", + "uploadCouldNotUploadTorrent": "No se pudo cargar el archivo .torrent", + "uploadDescription": "Descripción", + "uploadDragDropClickSelect": "Arrastra y suelta el archivo .torrent aquí, o haz clic para seleccionar", + "uploadDragDropClickSelectPoster": "Arrastra y suelta tu imagen de portada aquí, o haz clic para seleccionar", + "uploadDropFileHere": "Suelta el archivo aquí...", + "uploadGroupWith": "Agrupar con", + "uploadGroupWithThisTorrent": "Agrupar con este torrent", + "uploadInfoBox1": "Las siguientes extensiones de archivo están en la lista negra. Cualquier torrent que contenga archivos de estos tipos no se cargará.", + "uploadInfoBox2": "Parece que estos torrents existentes tienen nombres similares. ¿Te gustaría agrupar tu carga con alguno de ellos? Los grupos deben contener solo contenido muy similar, por ejemplo, la misma película en diferentes formatos.", + "uploadInfoBox3": "Nota: si has comenzado a compartir un torrent antes de cargarlo, es posible que necesites actualizar los rastreadores en tu cliente de torrents una vez que la carga esté completa.", + "uploadMarkdownSupport": "Compatibilidad con Markdown", + "uploadMediaInfo": "Información multimedia", + "uploadName": "Nombre", + "uploadSource": "Fuente", + "uploadTags": "Etiquetas", + "uploadTorrentFile": "Archivo Torrent", + "uploadTorrentUploadSuccess": "Torrent cargado exitosamente", + "uploadUpload": "Cargar", + "uploadRemove": "Eliminar", + "username": "Nombre de usuario", + "userUser": "Usuario", + "userSuccessfully": "exitosamente", + "userCouldNot": "No se pudo", + "userProfile": "perfil", + "userUserSince": "Usuario desde", + "userUserSinceTime": "DD MMM AAAA", + "userOnlyAdminsSee": "Solo los administradores pueden ver esto", + "userInvitedBy": "Invitado por", + "userEmailVerified": "Correo electrónico verificado", + "userRemainingInvites": "Invitaciones restantes", + "userRatio": "Ratio", + "userHitNRuns": "Hit'n'runs", + "userDownloaded": "Descargado", + "userComments": "Comentarios", + "userNoComments": "Sin comentarios.", + "userYouSureWant": "¿Estás seguro de que quieres", + "userThisUserQ": "a este usuario?", + "userUploaded": "Subido", + "userMyUploads": "Mis subidas", + "usernameRules": "Solo puede consistir en letras, números y “.”", + "userUnban": "desbanear", + "userBan": "banear", + "userUnbanned": "desbaneado", + "userBanned": "baneado", + "userAdmin": "Administrador", + "veCouldNotVerifyEmailAddress": "No se pudo verificar la dirección de correo electrónico:", + "veEmailAddressVerifiedSuccess": "Dirección de correo electrónico verificada exitosamente.", + "veNoVerificationTokenProvided": "No se proporcionó un token de verificación", + "veVerifyEmail": "Verificar correo electrónico", + "welcome": "Bienvenido", + "welcomeBack": "Bienvenido de nuevo", + "wikiPath": "Ruta", + "wikiPageWillBeVisibleAt": "La página será visible en", + "wikiAllowUnregisteredView": "Permitir vista no registrada", + "wikiPageCreateSuccess": "Página wiki creada exitosamente", + "wikiCouldNotCreatePage": "No se pudo crear la página wiki", + "wikiNewPage": "Nueva página wiki", + "wikiCreatePage": "Crear página wiki", + "wikiPageDelSuccess": "Página wiki eliminada exitosamente", + "wikiCouldNotDelPage": "No se pudo eliminar la página wiki", + "wikiPageUpdateSuccess": "Página wiki actualizada exitosamente", + "wikiCouldNotUpdatePage": "No se pudo actualizar la página wiki", + "wikiAddPage": "Agregar página", + "wikiLastEdited": "Última edición", + "wikiPages": "Páginas", + "wikiSaveChanges": "Guardar cambios", + "wikiThereNothingHereYet": "Todavía no hay nada aquí.", + "wikiDelThisPageQ": "¿Estás seguro de que quieres eliminar esta página wiki? Esto no se puede deshacer." +} diff --git a/client/locales/fr.json b/client/locales/fr.json new file mode 100644 index 0000000..0623e6e --- /dev/null +++ b/client/locales/fr.json @@ -0,0 +1,355 @@ +{ + "404NotFound": "Page non trouvée", + "404PageDoesNotExist": "Cette page n'existe pas.", + "404ReturnHome": "Retour à la page d'accueil", + "acc2FADisabled": "Authentification à deux facteurs désactivée", + "acc2FAEnabled": "Authentification à deux facteurs activée", + "acc2FAScanQR": "Scannez le code QR avec votre application d'authentification et saisissez le code à usage unique", + "acc2FAText1": "L'authentification à deux facteurs a été activée avec succès. Ces codes de secours peuvent être utilisés pour vous connecter en cas de perte d'accès à votre application d'authentification. Enregistrez-les maintenant, ils ne seront plus visibles.", + "acc2FAUseApp": "Utilisez une application d'authentification pour ajouter une couche supplémentaire de sécurité à votre compte", + "acc2FAuth": "Authentification à deux facteurs", + "accBonusPoints": "Points bonus", + "accBonusPointsHave": "point(s) bonus", + "accBuy": "Acheter", + "accCancel": "Annuler", + "accChangePass": "Changer le mot de passe", + "accClaimed": "Réclamé", + "accCopyLink": "Copier le lien", + "accCouldNotBuyItems": "Impossible d'acheter des articles", + "accCouldNotChangePass": "Impossible de changer le mot de passe", + "accCouldNotDelAcc": "Impossible de supprimer le compte", + "accCouldNotSendInvite": "Impossible d'envoyer une invitation", + "accCouldNotToggle2FA": "Impossible d'activer/désactiver l'authentification à deux facteurs", + "accCreated": "Créé", + "accCurrentPass": "Mot de passe actuel", + "accDangerZone": "Zone dangereuse", + "accDelAccText1": "Êtes-vous sûr de vouloir supprimer votre compte ? Cette action ne peut pas être annulée, et vous pourriez ne pas être en mesure de vous réinscrire. Vos informations personnelles seront supprimées, mais vos torrents téléchargés resteront.", + "accDeleteMyAcc": "Supprimer mon compte", + "accDeleteMyAccYes": "Oui, supprimer mon compte", + "accDisable": "Désactiver", + "accEnable": "Activer", + "accEnable2FA": "Activer l'authentification à deux facteurs", + "accEveryRequestYouFulfill": "pour chaque demande que vous accomplissez, ou", + "accForEveryGBYouUpload": "pour chaque Go que vous téléchargez", + "accIfYouAreAlsUploaderAcceptTorrent": "si vous êtes également l'uploader du torrent accepté", + "accInviteLinkCopiedClipboard": "Lien d'invitation copié dans le presse-papiers", + "accInvites": "Invitations", + "accRemaining": "restantes", + "accRoleUser": "Rôle : utilisateur", + "accRoleAdmin": "Rôle : administrateur", + "accInviteSentSuccess": "Invitation envoyée avec succès", + "accInviteText1": "Saisissez une adresse e-mail pour envoyer une invitation. L'utilisateur invité devra s'inscrire avec la même adresse e-mail. Une fois l'invitation générée, vous pouvez également copier un lien d'invitation direct.", + "accItemsPurchasedSuccess": "Articles achetés avec succès", + "accMyAccount": "Mon compte", + "accPassChangedSuccess": "Mot de passe changé avec succès", + "accPurchaseInvites": "Acheter des invitations", + "accPurchaseUpload1GB": "Acheter l'envoi (1 Go)", + "accRole": "Rôle", + "accSendInvite": "Envoyer une invitation", + "accThisIsAdminAcc": "Il s'agit d'un compte administrateur.", + "accValidUntil": "Valide jusqu'au", + "accYouCurrentlyHave": "Vous avez actuellement", + "accYouWillEarn": "Vous gagnerez", + "accNotAvailableToBuy": "Non disponible à l'achat", + "annPinnedAnnounce": "Annonces épinglées", + "annOtherAnnounce": "Autres annonces", + "annAnnounceCreatSuccess": "Annonce créée avec succès", + "annCouldNotCreateAnnounce": "Impossible de créer l'annonce", + "annNewAnnounce": "Nouvelle annonce", + "annBody": "Corps", + "annPinThisAnnounceQ": "Épingler cette annonce ?", + "annAllowCommentsQ": "Autoriser les commentaires ?", + "annCreateAnnounce": "Créer une annonce", + "annAnnounceDelSuccess": "Annonce supprimée avec succès", + "annCouldNotDelAnnounce": "Impossible de supprimer l'annonce", + "annUnpin": "Détacher", + "annPin": "Épingler", + "annUnpinned": "détachée", + "annPinned": "épinglée", + "annLastUpdated": "Dernière mise à jour", + "annCommentsDisabled": "Commentaires désactivés.", + "annAreYouSureYouWantToDelThisannounceQ": "Êtes-vous sûr de vouloir supprimer cette annonce ? Cela ne peut pas être annulé.", + "annAnnounceUpdatedSuccess": "Annonce mise à jour avec succès", + "annCouldNotUpdateAnnounce": "Impossible de mettre à jour l'annonce", + "annEditAnnounce": "Modifier l'annonce", + "annUpdateAnnounce": "Mettre à jour l'annonce", + "annAnnounce": "Annonce", + "bmYourBM": "Vos favoris", + "bmYouNotHaveAnyBM": "Vous n'avez aucun favori.", + "catCategories": "Catégories", + "catNoCategoryHaveBeenDefined": "Aucune catégorie n'a été définie.", + "catNoTagsHaveBeenDefined": "Aucun tag n'a été défini.", + "catNoResults": "Aucun résultat.", + "comCommentBy": "Commentaire de", + "comDelUser": "utilisateur supprimé", + "comOn": "le", + "email": "E-mail", + "errSomethingWentWrong": "Quelque chose s'est mal passé", + "errTooManyRequests": "Trop de demandes ! Vous avez été soumis à une limitation de taux. Veuillez attendre un moment avant de réessayer.", + "errIfErrorPersist": "Si l'erreur persiste, veuillez", + "errReportIt": "la signaler", + "errForNow": "Pour l'instant,", + "indexLatestAnnounce": "Dernière annonce", + "indexLatestTorrents": "Derniers torrents", + "indexSearch": "Rechercher", + "indexSearchTorrents": "Rechercher des torrents", + "indexText1": "Votre adresse e-mail n'est pas encore vérifiée. Vous ne pourrez pas télécharger ou téléverser de données tant que cela ne sera pas fait.", + "indexTime": "HH:mm Do MMM YYYY", + "listNoItemShow": "Aucun élément à afficher.", + "logIn": "Se connecter", + "logInFailed": "Impossible de se connecter", + "mdMarkdownPreview": "Aperçu Markdown", + "navAnnouncements": "Annonces", + "navBookmarks": "Favoris", + "navBrowse": "Parcourir", + "navHome": "Accueil", + "navLogOut": "Déconnexion", + "navReports": "Signalements", + "navRequests": "Demandes", + "navRSS": "RSS", + "navSearch": "Recherche", + "navStats": "Statistiques", + "navUpload": "Téléverser", + "navWiki": "Wiki", + "newPassword": "Nouveau mot de passe", + "password": "Mot de passe", + "passwordResetFailed": "Réinitialisation du mot de passe impossible", + "passwordResetRequestFailed": "Impossible d'initier la réinitialisation du mot de passe", + "passwordResetRequestSuccess": "Si un compte avec cette adresse e-mail existe, vous recevrez bientôt un e-mail", + "passwordResetSuccess": "Le mot de passe a été réinitialisé avec succès", + "posterImage": "Affiche", + "poweredBy": "Propulsé par", + "register": "S'inscrire", + "registerFailed": "Impossible de s'inscrire", + "registrationClosed": "Inscription fermée", + "reqCreateNew": "Créer un nouveau", + "reqCreateReq": "Créer une demande", + "reqTitle": "Titre", + "reqPostedBy": "Posté par", + "reqPost": "Poster", + "reqRequestCreatedSuccess": "Demande créée avec succès", + "reqCouldNotCreateReq": "Impossible de créer la demande", + "reqNewRequest": "Nouvelle demande", + "reqWhatYouLookForQ": "Que cherchez-vous ?", + "reqRequestDelSuccess": "Demande supprimée avec succès", + "reqCouldNotDelReq": "Impossible de supprimer la demande", + "reqCommentPostSuccess": "Commentaire posté avec succès", + "reqCommentNotPost": "Impossible de poster un commentaire", + "reqSuggestionAddSuccess": "Suggestion ajoutée avec succès", + "reqSuggestionNotAdded": "Impossible d'ajouter une suggestion", + "reqSuggestionAcceptSuccess": "Suggestion acceptée avec succès", + "reqCouldNotAcceptSuggestion": "Impossible d'accepter la suggestion", + "reqDelete": "Supprimer", + "reqSuggestedTorrents": "Torrents suggérés", + "reqSuggestATorrent": "Proposer un torrent", + "reqAccepted": "Acceptée", + "reqAccept": "Accepter", + "reqNoTorrentsHaveBeenSuggestedYet": "Aucun torrent n'a encore été suggéré.", + "reqPostAComment": "Poster un commentaire", + "reqEnterInfohashTorrentBelow": "Saisissez l'infohash d'un torrent ci-dessous.", + "reqFulfilled": "Réalisée", + "reqPosted": "Postée", + "reqBy": "par", + "reqInfohash": "Infohash", + "reqSuggest": "Suggérer", + "repUnresolvedRep": "Signalements non résolus", + "repRepBy": "Signalé par", + "repReason": "Raison", + "repRepMarkSolved": "Signalement marqué comme résolu", + "repCouldNotResolveRep": "Impossible de résoudre le signalement", + "repRepOn": "Signalement le", + "repMarkSolved": "Marquer comme résolu", + "repRep": "Signalement", + "repTorrDetail": "Détails du torrent", + "resetPassword": "Réinitialiser le mot de passe", + "rssThereRSSFeedAt": "Il y a un flux RSS à", + "rssToAuthenticateYourself": "Pour vous authentifier, vous devez fournir les cookies", + "rssAnd": "et", + "rssToRSSEndpoint": "à l'URL du flux RSS, contenant votre nom d'utilisateur et votre mot de passe respectivement.", + "rssNoQueryParametersAreProvided": "Si aucun paramètre de requête n'est fourni, le flux RSS contiendra les 100 derniers torrents.", + "rssOnlyIncludeMatchingResults": "Pour inclure uniquement les résultats correspondants dans le flux, vous pouvez ajouter le", + "rssQueryParameter": "paramètre de requête, par exemple", + "searchSearchResults": "Résultats de la recherche pour", + "searchSearchError": "Erreur de recherche", + "statYouNotPermission": "Vous n'avez pas l'autorisation de faire cela.", + "statTrackerStat": "Statistiques du tracker", + "statActiveTorrents": "Torrents actifs", + "statBannedUsers": "Utilisateurs bannis", + "statCompletedDownloads": "Téléchargements terminés", + "statFilledRequests": "Demandes satisfaites", + "statInvitesAccepted": "Invitations acceptées", + "statLeechers": "Lecheurs", + "statPeers": "Pairs", + "statRegisteredUsers": "Utilisateurs enregistrés", + "statSeeders": "Seeders", + "statTotalComments": "Commentaires totaux", + "statTotalInvitesSent": "Invitations totales envoyées", + "statTotalRequests": "Demandes totales", + "statUploadedTorrents": "Torrents téléchargés", + "tagTaggedWith": "Étiqueté avec", + "tokenError": "Erreur de jeton", + "torrSeeders": "Seeders", + "torrLeechers": "Lecheurs", + "torrDownloads": "Téléchargements", + "torrUploaded": "Téléchargé", + "torrTorrEditSuccess": "Torrent modifié avec succès", + "torrCouldEditTorr": "Impossible de modifier le torrent", + "torrTorrDelSuccess": "Torrent supprimé avec succès", + "torrCouldNotDelTorr": "Impossible de supprimer le torrent", + "torrVoteSubmitSuccess": "Vote soumis avec succès", + "torrCouldNotSubmitVote": "Impossible de soumettre un vote", + "torrReportSubmitSuccess": "Signalement soumis avec succès", + "torrCouldNotSubmitReport": "Impossible de soumettre un signalement", + "torrFLToggleSuccess": "Freeleech activé/désactivé avec succès", + "torrCouldNotToggleFL": "Impossible d'activer/désactiver le freeleech", + "torrTorrRemFromGroupSuccess": "Torrent retiré du groupe avec succès", + "torrCouldNotRemTorrFromGroup": "Impossible de retirer le torrent du groupe", + "torrTorrent": "Torrent", + "torrCouldNotBookmarkTorr": "Impossible de mettre le torrent en favori", + "torrFL": "Freeleech", + "torrEdit": "Modifier", + "torrDownload": "Télécharger", + "torrLogInDownload": "Connectez-vous pour télécharger", + "torrFiles": "Fichiers", + "torrReport": "Signaler", + "torrRemTorr": "Supprimer ce torrent", + "torrAddTorr": "Ajouter un torrent", + "torrReasonForReport": "Raison du signalement", + "torrSureDeleteTorr": "Êtes-vous sûr de vouloir supprimer ce torrent ? Cela ne peut pas être annulé.", + "torrFreeleech": "Freeleech", + "torrUnset": "Non défini", + "torrSet": "Défini", + "torrUploadedBy": "Téléchargé par", + "torrDate": "Date", + "torrSize": "Taille", + "torrYes": "Oui", + "torrNo": "Non", + "torrGroupTorr": "Torrents groupés", + "torrThereAreNoOtherTorrGroup": "Il n'y a pas d'autres torrents dans ce groupe.", + "torrResults": "résultats", + "torrPage": "Page", + "torrOf": "de", + "torrRemovedFrom": "retiré de", + "torrAddedTo": "ajouté à", + "torrTorrNoTags": "Ce torrent n'a pas de tags.", + "totp": "Code à usage unique", + "uploadAddTag": "Ajouter un tag", + "uploadAnnounceURL": "URL de l'annonce", + "uploadAnonymousUpload": "Téléchargement anonyme", + "uploadCategory": "Catégorie", + "uploadCouldNotGetGroupSuggestions": "Impossible d'obtenir des suggestions de groupe", + "uploadCouldNotUploadFile": "Impossible de télécharger le fichier", + "uploadCouldNotUploadTorrent": "Impossible de télécharger le fichier .torrent", + "uploadDescription": "Description", + "uploadDragDropClickSelect": "Faites glisser et déposez un fichier .torrent ici, ou cliquez pour sélectionner", + "uploadDragDropClickSelectPoster": "Glissez et déposez votre fichier d'affiche ici, ou cliquez pour sélectionner", + "uploadDropFileHere": "Déposez le fichier ici...", + "uploadGroupWith": "Regrouper avec", + "uploadGroupWithThisTorrent": "Regrouper avec ce torrent", + "uploadInfoBox1": "Les extensions de fichier suivantes sont interdites. Tout torrent contenant des fichiers de ces types ne sera pas téléchargé.", + "uploadInfoBox2": "Il semble que ces torrents existants aient des noms similaires. Souhaitez-vous regrouper votre téléchargement avec l'un d'entre eux ? Les groupes ne doivent contenir que du contenu très similaire, par exemple le même film dans différents formats.", + "uploadInfoBox3": "Remarque : si vous avez commencé à semer un torrent avant de le télécharger, vous devrez peut-être actualiser les trackers dans votre client torrent une fois le téléchargement terminé.", + "uploadMarkdownSupport": "Prise en charge de Markdown", + "uploadMediaInfo": "Informations multimédias", + "uploadName": "Nom", + "uploadSource": "Source", + "uploadTags": "Tags", + "uploadTorrentFile": "Fichier .torrent", + "uploadTorrentUploadSuccess": "Torrent téléchargé avec succès", + "uploadUpload": "Téléverser", + "uploadRemove": "Supprimer", + "username": "Nom d'utilisateur", + "userUser": "Utilisateur", + "userSuccessfully": "avec succès", + "userCouldNot": "Impossible de", + "userProfile": "profil", + "userUserSince": "Utilisateur depuis", + "userUserSinceTime": "Do MMM YYYY", + "userOnlyAdminsSee": "Seuls les administrateurs peuvent voir ceci", + "userInvitedBy": "Invité par", + "userEmailVerified": "E-mail vérifié", + "userRemainingInvites": "Invitations restantes", + "userRatio": "Ratio", + "userHitNRuns": "Hit'n'runs", + "userDownloaded": "Téléchargé", + "userComments": "Commentaires", + "userNoComments": "Aucun commentaire.", + "userYouSureWant": "Êtes-vous sûr de vouloir", + "userThisUserQ": "cet utilisateur ?", + "userUploaded": "Téléversé", + "userMyUploads": "Mes téléversements", + "userMyDownloads": "Mes téléchargements", + "userMyComments": "Mes commentaires", + "userWarned": "Avertissement", + "userBanned": "Banni", + "userUnbanned": "Débanni", + "userChangePassword": "Changer le mot de passe", + "userSaveChanges": "Enregistrer les modifications", + "wiki": "Wiki", + "wiki404": "Page du wiki introuvable", + "wiki404Text": "La page que vous recherchez n'a pas pu être trouvée. Elle peut avoir été supprimée ou n'a peut-être jamais existé. Vous pouvez revenir à la page d'accueil ou utiliser la recherche pour trouver ce que vous cherchez.", + "wikiEdit": "Modifier", + "wikiLastEdited": "Dernière modification", + "wikiSave": "Enregistrer", + "wikiCancel": "Annuler", + "wikiRevisions": "Révisions", + "wikiRestoreThisRevision": "Restaurer cette révision", + "wikiSureToRestoreThisRevision": "Êtes-vous sûr de vouloir restaurer cette révision ?", + "wikiThisRevision": "Cette révision", + "wikiWasRestored": "a été restaurée", + "wikiBy": "par", + "wikiOn": "le", + "wikiViewSource": "Voir la source", + "wikiPageEdit": "Modifier la page du wiki", + "wikiEnterContent": "Saisissez le contenu de la page ici...", + "wikiEditSummary": "Résumé de la modification", + "wikiEditSummaryOptional": "Résumé de la modification (facultatif)", + "wikiEditConflict": "Conflit de modification", + "wikiEditText1": "Une autre personne a modifié cette page pendant que vous la modifiiez. Vous pouvez soit écraser les modifications de l'autre personne, soit les fusionner avec les vôtres. Assurez-vous de ne pas écraser des modifications importantes.", + "wikiEditOverwrite": "Écraser les modifications", + "wikiEditMerge": "Fusionner les modifications", + "wikiEditConflictText1": "Vous avez choisi d'écraser les modifications de l'autre personne. Toutes les modifications qu'ils ont apportées à la page seront perdues.", + "wikiEditConflictText2": "Vous avez choisi de fusionner les modifications de l'autre personne. Veuillez vérifier les conflits et décider quelles modifications doivent être conservées.", + "wikiEditConflictText3": "Les conflits suivants ont été détectés :", + "wikiThisPage": "Cette page", + "wikiHasBeenUpdated": "a été mise à jour", + "wikiReturnTo": "Retour à la page du wiki", + "wikiEditPage": "Modifier la page", + "wikiPageContent": "Contenu de la page", + "wikiDiscussion": "Discussion", + "wikiDiscussThisPage": "Discuter de cette page", + "wikiNoDiscussion": "Aucune discussion.", + "wikiDiscussionLocked": "Discussion verrouillée", + "wikiDiscussionUnlock": "Déverrouiller la discussion", + "wikiDiscussionLock": "Verrouiller la discussion", + "wikiDiscussionRemove": "Supprimer la discussion", + "wikiDiscussionPost": "Publier un message", + "wikiDiscussionPostSuccess": "Message posté avec succès", + "wikiDiscussionNotPost": "Impossible de poster un message", + "wikiDiscussionUnlockSuccess": "Discussion déverrouillée avec succès", + "wikiDiscussionLockSuccess": "Discussion verrouillée avec succès", + "wikiDiscussionRemoveSuccess": "Discussion supprimée avec succès", + "wikiDiscussionCreate": "Créer une discussion", + "wikiDiscussionTitle": "Titre de la discussion", + "wikiDiscussionTitleOptional": "Titre de la discussion (facultatif)", + "wikiDiscussionCreateSuccess": "Discussion créée avec succès", + "wikiDiscussionCouldNotCreate": "Impossible de créer la discussion", + "wikiDiscussionEdit": "Modifier la discussion", + "wikiDiscussionEditSuccess": "Discussion modifiée avec succès", + "wikiDiscussionCouldNotEdit": "Impossible de modifier la discussion", + "wikiDiscussionViewSource": "Voir la source de la discussion", + "wikiDeletePage": "Supprimer la page", + "wikiDeletePageSuccess": "Page supprimée avec succès", + "wikiDeletePageConfirm": "Êtes-vous sûr de vouloir supprimer cette page ? Cela ne peut pas être annulé.", + "wikiDeletePageText1": "Supprimer cette page supprimera également toutes les révisions associées et la discussion.", + "wikiEditDiscussion": "Modifier la discussion", + "wikiEditDiscussionPost": "Publier un message de discussion", + "wikiEditDiscussionPostSuccess": "Message de discussion posté avec succès", + "wikiEditDiscussionNotPost": "Impossible de poster un message de discussion", + "wikiEditDiscussionCreate": "Créer une discussion", + "wikiEditDiscussionCreateSuccess": "Discussion créée avec succès", + "wikiEditDiscussionCouldNotCreate": "Impossible de créer la discussion", + "wikiEditDiscussionEdit": "Modifier la discussion", + "wikiEditDiscussionEditSuccess": "Discussion modifiée avec succès", + "wikiEditDiscussionCouldNotEdit": "Impossible de modifier la discussion" +} diff --git a/client/locales/index.js b/client/locales/index.js index 362c5c6..a9bb622 100644 --- a/client/locales/index.js +++ b/client/locales/index.js @@ -1,13 +1,19 @@ import en from "./en.json"; +import es from "./es.json"; import ru from "./ru.json"; import de from "./de.json"; import zh from "./zh.json"; import eo from "./eo.json"; +import fr from "./fr.json"; +import it from "./it.json"; export default { en, + es, + it, ru, de, zh, eo, + fr, }; diff --git a/client/locales/it.json b/client/locales/it.json new file mode 100644 index 0000000..ee0e9d3 --- /dev/null +++ b/client/locales/it.json @@ -0,0 +1,310 @@ +{ + "404NotFound": "Non Trovato", + "404PageDoesNotExist": "Quella pagina non esiste", + "404ReturnHome": "Torna alla home", + "acc2FADisabled": "2FA disabilitato", + "acc2FAEnabled": "2FA abilitato", + "acc2FAScanQR": "Scansiona il codice QR con la tua app authenticator ed inserisci il codice", + "acc2FAText1": "2FA abilitato con successo. Questi codici di backup possono essere utilizzati per accedere se si perde l'accesso all'app Authenticator. Salvateli ora, non saranno più visibili.", + "acc2FAUseApp": "Utilizzate un'app di autenticazione per aggiungere un ulteriore livello di sicurezza al vostro account.", + "acc2FAuth": "Autenticazione a due fattori", + "accBonusPoints": "Punti Bonus", + "accBonusPointsHave": "punti bonus", + "accBuy": "Compra", + "accCancel": "Canceclla", + "accChangePass": "Cambia Password", + "accClaimed": "Rivendicato", + "accCopyLink": "Copia link", + "accCouldNotBuyItems": "Non è stato possibile acquistare articoli", + "accCouldNotChangePass": "Non è stato possibile cambiare la password", + "accCouldNotDelAcc": "Non è stato possibile eliminare il profilo", + "accCouldNotSendInvite": "Non è stato possibile inviare l'invito", + "accCouldNotToggle2FA": "Impossibile attivare la 2FA", + "accCreated": "Creato", + "accCurrentPass": "Password attuale", + "accDangerZone": "Danger zone", + "accDelAccText1": "Sei sicuro di voler cancellare il tuo account? Questa azione non può essere annullata e potresti non essere in grado di registrarti di nuovo. I tuoi dati personali saranno cancellati, ma i torrent caricati rimarranno.", + "accDeleteMyAcc": "Cancella il mio account", + "accDeleteMyAccYes": "Si, cancella il mio account", + "accDisable": "Disabilita", + "accEnable": "Abilita", + "accEnable2FA": "Abilita 2FA", + "accEveryRequestYouFulfill": "per ogni richiesta soddisfatta, oppure", + "accForEveryGBYouUpload": "per ogni GB caricato", + "accIfYouAreAlsUploaderAcceptTorrent": "se si è anche l'uploader del torrent accettato", + "accInviteLinkCopiedClipboard": "Collegamento all'invito copiato negli appunti", + "accInvites": "Inviti", + "accRemaining": "rimasti", + "accRoleUser": "Ruolo: utente", + "accRoleAdmin": "Ruolo: admin", + "accInviteSentSuccess": "Invito inviato con successo", + "accInviteText1": "Immettere un indirizzo e-mail per inviare un invito. L'utente invitato dovrà registrarsi con lo stesso indirizzo e-mail. Una volta generato l'invito, è possibile anche copiare un link diretto", + "accItemsPurchasedSuccess": "Articoli acquistati con successo", + "accMyAccount": "Il mio account", + "accPassChangedSuccess": "La password è stata modificata con successo", + "accPurchaseInvites": "Compra inviti", + "accPurchaseUpload1GB": "Compra Upload (1 GB)", + "accRole": "Ruolo", + "accSendInvite": "Invia Invito", + "accThisIsAdminAcc": "Questo è un account amministratore.", + "accValidUntil": "Valido fino a", + "accYouCurrentlyHave": "Attualmente hai", + "accYouWillEarn": "Guadagnerai", + "accNotAvailableToBuy": "Non disponibile per l'acquisto", + "annPinnedAnnounce": "Annunci in evidenza", + "annOtherAnnounce": "Altri annunci", + "annAnnounceCreatSuccess": "Annuncio creato con successo", + "annCouldNotCreateAnnounce": "Impossibile creare l'annuncio", + "annNewAnnounce": "Nuovo annuncio", + "annBody": "Corpo", + "annPinThisAnnounceQ": "Fissare questo annuncio?", + "annAllowCommentsQ": "Consentire commenti?", + "annCreateAnnounce": "Crea annuncio", + "annAnnounceDelSuccess": "Annuncio eliminato con successo", + "annCouldNotDelAnnounce": "Impossibile eliminare l'annuncio", + "annUnpin": "Scollega", + "annPin": "Fissa", + "annUnpinned": "scollegato", + "annPinned": "fissato", + "annLastUpdated": "Ultimo aggiornamento", + "annCommentsDisabled": "Commenti disabilitati.", + "annAreYouSureYouWantToDelThisannounceQ": "Sei sicuro di voler eliminare questo annuncio? Questo non può essere annullato.", + "annAnnounceUpdatedSuccess": "Annuncio aggiornato con successo", + "annCouldNotUpdateAnnounce": "Impossibile aggiornare l'annuncio", + "annEditAnnounce": "Modifica annuncio", + "annUpdateAnnounce": "Aggiorna annuncio", + "annAnnounce": "Annuncio", + "bmYourBM": "I tuoi segnalibri", + "bmYouNotHaveAnyBM": "Non hai alcun segnalibro.", + "catCategories": "Categorie", + "catNoCategoryHaveBeenDefined": "Non sono state definite categorie.", + "catNoTagsHaveBeenDefined": "Non sono stati definiti tag.", + "catNoResults": "Nessun risultato.", + "comCommentBy": "Commento di", + "comDelUser": "utente eliminato", + "comOn": "su", + "email": "Email", + "errSomethingWentWrong": "Qualcosa è andato storto", + "errTooManyRequests": "Troppe richieste! Sei stato limitato. Attendi qualche minuto prima di riprovare.", + "errIfErrorPersist": "Se l'errore persiste, per favore", + "errReportIt": "segnalalo", + "errForNow": "Per ora,", + "indexLatestAnnounce": "Ultimo annuncio", + "indexLatestTorrents": "Ultimi torrent", + "indexSearch": "Cerca", + "indexSearchTorrents": "Cerca torrent", + "indexText1": "Il tuo indirizzo email non è ancora verificato. Non sarai in grado di caricare o scaricare dati finché non sarà completata la verifica.", + "indexTime": "HH:mm Do MMM YYYY", + "listNoItemShow": "Nessun elemento da mostrare.", + "logIn": "Accedi", + "logInFailed": "Accesso non riuscito", + "mdMarkdownPreview": "Anteprima Markdown", + "navAnnouncements": "Annunci", + "navBookmarks": "Segnalibri", + "navBrowse": "Esplora", + "navHome": "Home", + "navLogOut": "Esci", + "navReports": "Segnalazioni", + "navRequests": "Richieste", + "navRSS": "RSS", + "navSearch": "Cerca", + "navStats": "Statistiche", + "navUpload": "Carica", + "navWiki": "Wiki", + "newPassword": "Nuova password", + "password": "Password", + "passwordResetFailed": "Impossibile reimpostare la password", + "passwordResetRequestFailed": "Impossibile iniziare il reset della password", + "passwordResetRequestSuccess": "Se un account con questo indirizzo email esiste, riceverai presto una email", + "passwordResetSuccess": "Password reimpostata con successo", + "posterImage": "Immagine di copertina", + "poweredBy": "Powered by", + "register": "Registrati", + "registerFailed": "Registrazione non riuscita", + "registrationClosed": "Registrazione chiusa", + "reqCreateNew": "Crea nuovo", + "reqCreateReq": "Crea richiesta", + "reqTitle": "Titolo", + "reqPostedBy": "Pubblicato da", + "reqPost": "Pubblica", + "reqRequestCreatedSuccess": "Richiesta creata con successo", + "reqCouldNotCreateReq": "Impossibile creare la richiesta", + "reqNewRequest": "Nuova richiesta", + "reqWhatYouLookForQ": "Cosa stai cercando?", + "reqRequestDelSuccess": "Richiesta eliminata con successo", + "reqCouldNotDelReq": "Impossibile eliminare la richiesta", + "reqCommentPostSuccess": "Commento pubblicato con successo", + "reqCommentNotPost": "Impossibile pubblicare il commento", + "reqSuggestionAddSuccess": "Suggerimento aggiunto con successo", + "reqSuggestionNotAdded": "Impossibile aggiungere il suggerimento", + "reqSuggestionAcceptSuccess": "Suggerimento accettato con successo", + "reqCouldNotAcceptSuggestion": "Impossibile accettare il suggerimento", + "reqDelete": "Elimina", + "reqSuggestedTorrents": "Torrent suggeriti", + "reqSuggestATorrent": "Suggerisci un torrent", + "reqAccepted": "Accettato", + "reqAccept": "Accetta", + "reqNoTorrentsHaveBeenSuggestedYet": "Non sono stati ancora suggeriti torrent.", + "reqPostAComment": "Pubblica un commento", + "reqEnterInfohashTorrentBelow": "Inserisci l'infohash di un torrent qui sotto.", + "reqFulfilled": "Soddisfatto", + "reqPosted": "Pubblicato", + "reqBy": "da", + "reqInfohash": "Infohash", + "reqSuggest": "Suggerisci", + "repUnresolvedRep": "Segnalazioni non risolte", + "repRepBy": "Segnalato da", + "repReason": "Motivo", + "repRepMarkSolved": "Segnalazione contrassegnata come risolta", + "repCouldNotResolveRep": "Impossibile risolvere la segnalazione", + "repRepOn": "Segnala", + "repMarkSolved": "Segna come risolto", + "repRep": "Segnalato", + "repTorrDetail": "Dettagli torrent", + "resetPassword": "Ripristina password", + "rssThereRSSFeedAt": "C'è un feed RSS su", + "rssToAuthenticateYourself": "Per autenticarti, devi fornire i cookie", + "rssAnd": "e", + "rssToRSSEndpoint": "all'endpoint RSS, contenenti rispettivamente il tuo nome utente e la tua password.", + "rssNoQueryParametersAreProvided": "Se non vengono forniti parametri di query, il feed RSS conterrà i 100 torrent più recenti.", + "rssOnlyIncludeMatchingResults": "Per includere solo risultati corrispondenti nel feed, puoi aggiungere il", + "rssQueryParameter": "parametro di query, ad esempio", + "searchSearchResults": "Risultati della ricerca per", + "searchSearchError": "Errore di ricerca", + "statYouNotPermission": "Non hai il permesso per farlo.", + "statTrackerStat": "Statistiche del tracker", + "statActiveTorrents": "Torrent attivi", + "statBannedUsers": "Utenti banditi", + "statCompletedDownloads": "Download completati", + "statFilledRequests": "Richieste soddisfatte", + "statInvitesAccepted": "Inviti accettati", + "statLeechers": "Leecher", + "statPeers": "Peers", + "statRegisteredUsers": "Utenti registrati", + "statSeeders": "Seeders", + "statTotalComments": "Commenti totali", + "statTotalInvitesSent": "Inviti totali inviati", + "statTotalRequests": "Richieste totali", + "statUploadedTorrents": "Torrent caricati", + "tagTaggedWith": "Taggato con", + "tokenError": "Errore del token", + "torrSeeders": "Seeders", + "torrLeechers": "Leecher", + "torrDownloads": "Download", + "torrUploaded": "Caricato", + "torrTorrEditSuccess": "Torrent modificato con successo", + "torrCouldEditTorr": "Non è stato possibile modificare il torrent", + "torrTorrDelSuccess": "Torrent eliminato con successo", + "torrCouldNotDelTorr": "Impossibile eliminare il torrent", + "torrVoteSubmitSuccess": "Voto inviato con successo", + "torrCouldNotSubmitVote": "Impossibile inviare il voto", + "torrReportSubmitSuccess": "Segnalazione inviata con successo", + "torrCouldNotSubmitReport": "Impossibile inviare la segnalazione", + "torrFLToggleSuccess": "Freeleech attivato/disattivato con successo", + "torrCouldNotToggleFL": "Impossibile attivare/disattivare freeleech", + "torrTorrRemFromGroupSuccess": "Torrent rimosso dal gruppo con successo", + "torrCouldNotRemTorrFromGroup": "Impossibile rimuovere il torrent dal gruppo", + "torrTorrent": "Torrent", + "torrCouldNotBookmarkTorr": "Impossibile aggiungere il torrent ai segnalibri", + "torrFL": "FL!", + "torrEdit": "Modifica", + "torrDownload": "Scarica", + "torrLogInDownload": "Accedi per scaricare", + "torrFiles": "File", + "torrReport": "Segnala", + "torrRemTorr": "Rimuovi questo torrent", + "torrAddTorr": "Aggiungi un torrent", + "torrReasonForReport": "Motivo della segnalazione", + "torrSureDeleteTorr": "Sei sicuro di voler eliminare questo torrent? Questa azione non può essere annullata.", + "torrFreeleech": "Freeleech", + "torrUnset": "Non impostato", + "torrSet": "Impostato", + "torrUploadedBy": "Caricato da", + "torrDate": "Data", + "torrSize": "Dimensione", + "torrYes": "Sì", + "torrNo": "No", + "torrGroupTorr": "Torrent raggruppati", + "torrThereAreNoOtherTorrGroup": "Non ci sono altri torrent in questo gruppo.", + "torrResults": "risultati", + "torrPage": "Pagina", + "torrOf": "di", + "torrRemovedFrom": "rimosso da", + "torrAddedTo": "aggiunto a", + "torrTorrNoTags": "Questo torrent non ha tag.", + "totp": "Codice monouso", + "uploadAddTag": "Aggiungi tag", + "uploadAnnounceURL": "URL di annuncio", + "uploadAnonymousUpload": "Caricamento anonimo", + "uploadCategory": "Categoria", + "uploadCouldNotGetGroupSuggestions": "Impossibile ottenere suggerimenti per il gruppo", + "uploadCouldNotUploadFile": "Impossibile caricare il file", + "uploadCouldNotUploadTorrent": "Impossibile caricare il file .torrent", + "uploadDescription": "Descrizione", + "uploadDragDropClickSelect": "Trascina e rilascia il file .torrent qui, o clicca per selezionare", + "uploadDragDropClickSelectPoster": "Trascina e rilascia la tua immagine di copertina qui, o clicca per selezionare", + "uploadDropFileHere": "Rilascia il file qui...", + "uploadGroupWith": "Raggruppa con", + "uploadGroupWithThisTorrent": "Raggruppa con questo torrent", + "uploadInfoBox1": "Le seguenti estensioni di file sono inserite nella lista nera. Qualsiasi torrent contenente file di questi tipi non verrà caricato.", + "uploadInfoBox2": "Sembra che questi torrent esistenti abbiano nomi simili. Vorresti raggruppare il tuo caricamento con uno di essi? I gruppi dovrebbero contenere solo contenuti molto simili, ad esempio lo stesso film in formati diversi.", + "uploadInfoBox3": "Nota: se hai iniziato a seedare un torrent prima di caricarlo, potresti dover aggiornare i tracker nel tuo client torrent una volta completato il caricamento.", + "uploadMarkdownSupport": "Supporto Markdown", + "uploadMediaInfo": "Informazioni sui media", + "uploadName": "Nome", + "uploadSource": "Fonte", + "uploadTags": "Tag", + "uploadTorrentFile": "File torrent", + "uploadTorrentUploadSuccess": "Torrent caricato con successo", + "uploadUpload": "Carica", + "uploadRemove": "Rimuovi", + "username": "Nome utente", + "userUser": "Utente", + "userSuccessfully": "con successo", + "userCouldNot": "Impossibile", + "userProfile": "profilo", + "userUserSince": "Utente dal", + "userUserSinceTime": "Do MMM YYYY", + "userOnlyAdminsSee": "Solo gli amministratori possono vedere questo", + "userInvitedBy": "Invitato da", + "userEmailVerified": "Email verificata", + "userRemainingInvites": "Inviti rimanenti", + "userRatio": "Rapporto", + "userHitNRuns": "Hit'n'runs", + "userDownloaded": "Scaricato", + "userComments": "Commenti", + "userNoComments": "Nessun commento.", + "userYouSureWant": "Sei sicuro di voler", + "userThisUserQ": "questo utente?", + "userUploaded": "Caricato", + "userMyUploads": "I miei caricamenti", + "usernameRules": "Può contenere solo lettere, numeri e “.”", + "userUnban": "rimuovere il ban a", + "userBan": "bannare", + "userUnbanned": "rimosso il ban", + "userBanned": "bannato", + "userAdmin": "Admin", + "veCouldNotVerifyEmailAddress": "Impossibile verificare l'indirizzo email:", + "veEmailAddressVerifiedSuccess": "Indirizzo email verificato con successo.", + "veNoVerificationTokenProvided": "Nessun token di verifica fornito", + "veVerifyEmail": "Verifica email", + "welcome": "Benvenuto", + "welcomeBack": "Bentornato", + "wikiPath": "Percorso", + "wikiPageWillBeVisibleAt": "La pagina sarà visibile su", + "wikiAllowUnregisteredView": "Consenti visualizzazione non registrata", + "wikiPageCreateSuccess": "Pagina wiki creata con successo", + "wikiCouldNotCreatePage": "Impossibile creare la pagina wiki", + "wikiNewPage": "Nuova pagina wiki", + "wikiCreatePage": "Crea pagina wiki", + "wikiPageDelSuccess": "Pagina wiki eliminata con successo", + "wikiCouldNotDelPage": "Impossibile eliminare la pagina wiki", + "wikiPageUpdateSuccess": "Pagina wiki aggiornata con successo", + "wikiCouldNotUpdatePage": "Impossibile aggiornare la pagina wiki", + "wikiAddPage": "Aggiungi pagina", + "wikiLastEdited": "Ultima modifica", + "wikiPages": "Pagine", + "wikiSaveChanges": "Salva modifiche", + "wikiThereNothingHereYet": "Non c'è ancora nulla qui.", + "wikiDelThisPageQ": "Sei sicuro di voler eliminare questa pagina della wiki? Questa azione non può essere annullata." + } \ No newline at end of file diff --git a/client/locales/ru.json b/client/locales/ru.json index 8160942..cf3a0f6 100644 --- a/client/locales/ru.json +++ b/client/locales/ru.json @@ -115,6 +115,7 @@ "passwordResetRequestFailed": "Не удалось инициировать сброс пароля", "passwordResetRequestSuccess": "Если учетная запись с таким адресом эл. почты существует, вы вскоре получите эл. письмо.", "passwordResetSuccess": "Пароль успешно сброшен", + "posterImage": "Плакат", "register": "Регистрация", "registerFailed": "Не удалось зарегистрироваться", "registrationClosed": "Регистрация закрыта", @@ -238,6 +239,7 @@ "uploadCouldNotUploadTorrent": "Не удалось загрузить .torrent", "uploadDescription": "Описание", "uploadDragDropClickSelect": "Перетащите файл .torrent сюда или щелкните, чтобы выбрать", + "uploadDragDropClickSelectPoster": "Перетащите ваш файл плаката сюда или щелкните, чтобы выбрать", "uploadDropFileHere": "Перетащите файл сюда...", "uploadGroupWith": "Группа с", "uploadGroupWithThisTorrent": "Группа с этим торрентом", diff --git a/client/locales/zh.json b/client/locales/zh.json index 28e6a48..f9d0fa1 100644 --- a/client/locales/zh.json +++ b/client/locales/zh.json @@ -34,6 +34,9 @@ "accIfYouAreAlsUploaderAcceptTorrent": "分,需要您同时也是被采纳种子的发种人", "accInviteLinkCopiedClipboard": "邀请链接已复制到剪贴板", "accInvites": "邀请", + "accRemaining": "剩余", + "accRoleUser": "角色: 用户", + "accRoleAdmin": "角色: 管理员", "accInviteSentSuccess": "邀请已成功发送", "accInviteText1": "请输入受邀用户的邮箱地址。受邀用户需使用该邮箱地址注册。您也可以在此直接复制邀请链接。", "accItemsPurchasedSuccess": "购买成功", @@ -70,6 +73,7 @@ "annCouldNotUpdateAnnounce": "无法更新公告", "annEditAnnounce": "编辑公告", "annUpdateAnnounce": "更新公告", + "annAnnounce": "公告", "bmYourBM": "您的书签", "bmYouNotHaveAnyBM": "您没有任何书签。", "catCategories": "分类", @@ -113,6 +117,7 @@ "passwordResetRequestFailed": "无法发起密码重置", "passwordResetRequestSuccess": "如果存在该邮箱地址的帐户,将很快收到邮件", "passwordResetSuccess": "密码重置成功", + "posterImage": "海报", "poweredBy": "Powered by", "register": "注册", "registerFailed": "无法注册", @@ -195,8 +200,8 @@ "torrCouldNotSubmitVote": "无法提交投票", "torrReportSubmitSuccess": "报告提交成功", "torrCouldNotSubmitReport": "无法提交报告", - "torrFLToggleSuccess": "FREE状态切换成功", - "torrCouldNotToggleFL": "无法切换至FREE状态", + "torrFLToggleSuccess": "FREE 状态切换成功", + "torrCouldNotToggleFL": "无法切换至 FREE 状态", "torrTorrRemFromGroupSuccess": "成功从组中删除种子", "torrCouldNotRemTorrFromGroup": "无法从组中删除种子", "torrTorrent": "种子", @@ -221,9 +226,15 @@ "torrNo": "否", "torrGroupTorr": "已分组的种子", "torrThereAreNoOtherTorrGroup": "此组中没有其他种子。", + "torrResults": "结果", + "torrPage": "页面", + "torrOf": "的", + "torrRemovedFrom": "移除于", + "torrAddedTo": "增加至", + "torrTorrNoTags": "这个种子还没有标签", "totp": "一次性代码", "uploadAddTag": "添加标签", - "uploadAnnounceURL": "Tracker地址", + "uploadAnnounceURL": "Tracker 地址", "uploadAnonymousUpload": "匿名上传", "uploadCategory": "分类", "uploadCouldNotGetGroupSuggestions": "无法获取组建议", @@ -231,12 +242,13 @@ "uploadCouldNotUploadTorrent": "无法上传 .torrent", "uploadDescription": "描述", "uploadDragDropClickSelect": "将 .torrent 文件拖放到此处,或单击选择", + "uploadDragDropClickSelectPoster": "拖拽您的海报文件至此,或点击以选择", "uploadDropFileHere": "将文件拖放到此处...", "uploadGroupWith": "与以下种子同组", "uploadGroupWithThisTorrent": "与此种子同组", "uploadInfoBox1": "以下文件扩展名被列入黑名单。包含这些类型文件的种子将无法发布。", "uploadInfoBox2": "看起来与现有的种子有着相似的名称。您是否想要将您的种子分到其中的任意分组?分组应只包含非常相似的内容,例如相同电影的不同格式。", - "uploadInfoBox3": "注意:如果您在上传前已开始做种,上传完成后可能需要在您的客户端中强制重新汇报Tracker。", + "uploadInfoBox3": "注意:如果您在上传前已开始做种,上传完成后可能需要在您的客户端中强制重新汇报 Tracker。", "uploadMarkdownSupport": "支持 Markdown", "uploadMediaInfo": "MediaInfo", "uploadName": "名称", @@ -245,6 +257,7 @@ "uploadTorrentFile": "Torrent 文件", "uploadTorrentUploadSuccess": "Torrent 上传成功", "uploadUpload": "上传", + "uploadRemove": "移除", "username": "用户名", "userUser": "用户", "userSuccessfully": "成功", @@ -257,6 +270,7 @@ "userEmailVerified": "邮箱已验证", "userRemainingInvites": "剩余邀请", "userRatio": "分享率", + "userHitNRuns": "H&R", "userDownloaded": "已下载", "userComments": "评论", "userNoComments": "无评论。", @@ -269,6 +283,7 @@ "userBan": "封禁", "userUnbanned": "已解封", "userBanned": "已封禁", + "userAdmin": "设为管理", "veCouldNotVerifyEmailAddress": "无法验证邮件地址:", "veEmailAddressVerifiedSuccess": "邮箱地址验证成功。", "veNoVerificationTokenProvided": "未提供验证令牌", @@ -291,5 +306,5 @@ "wikiPages": "页面", "wikiSaveChanges": "保存更改", "wikiThereNothingHereYet": "目前还没有内容。", - "wikiDelThisPageQ": "您确定要删除这个Wiki页面吗?此操作无法撤销。" + "wikiDelThisPageQ": "您确定要删除这个维基页面吗?此操作无法撤销。" } diff --git a/client/package.json b/client/package.json index 71a9ff1..342cee3 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@sqtracker/client", - "version": "1.3.3", + "version": "1.5.0", "private": true, "license": "GPL-3.0-only", "scripts": { 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/_app.js b/client/pages/_app.js index 4dc7070..8950ac3 100644 --- a/client/pages/_app.js +++ b/client/pages/_app.js @@ -164,7 +164,6 @@ 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(); @@ -181,9 +180,12 @@ const SqTracker = ({ Component, pageProps, initialTheme }) => { SQ_API_URL, SQ_MINIMUM_RATIO, SQ_MAXIMUM_HIT_N_RUNS, + SQ_SITE_DEFAULT_LOCALE, }, } = getConfig(); + const [locale, setLocale] = useState(SQ_SITE_DEFAULT_LOCALE ?? "en"); + const allowThemeToggle = !Object.keys(SQ_CUSTOM_THEME ?? {}).some( (key) => key !== "primary" ); 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/account.js b/client/pages/account.js index deb770e..8335c0e 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} /> - + @@ -96,6 +98,7 @@ const Account = ({ token, invites = [], user, userRole }) => { SQ_BP_COST_PER_INVITE, SQ_BP_COST_PER_GB, SQ_ALLOW_REGISTER, + SQ_DISABLE_EMAIL, }, } = getConfig(); @@ -131,15 +134,21 @@ const Account = ({ token, invites = [], user, userRole }) => { return currentInvitesList; }); - addNotification("success", - `${getLocaleString("accInviteSentSuccess")}` + addNotification( + "success", + `${getLocaleString( + SQ_DISABLE_EMAIL + ? "accInviteSentSuccessNoEmail" + : "accInviteSentSuccess" + )}` ); setRemainingInvites((r) => r - 1); setShowInviteModal(false); } catch (e) { - addNotification("error", + addNotification( + "error", `${getLocaleString("accCouldNotSendInvite")}: ${e.message}` ); console.error(e); @@ -174,9 +183,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 +191,8 @@ const Account = ({ token, invites = [], user, userRole }) => { field.blur(); } } catch (e) { - addNotification("error", + addNotification( + "error", `${getLocaleString("accCouldNotChangePass")}: ${e.message}` ); console.error(e); @@ -218,7 +226,8 @@ const Account = ({ token, invites = [], user, userRole }) => { throw new Error(reason); } - addNotification("success", + addNotification( + "success", `${getLocaleString("accItemsPurchasedSuccess")}` ); @@ -233,7 +242,8 @@ const Account = ({ token, invites = [], user, userRole }) => { field.blur(); } } catch (e) { - addNotification("error", + addNotification( + "error", `${getLocaleString("accCouldNotBuyItems")}: ${e.message}` ); console.error(e); @@ -264,9 +274,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 +306,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 +344,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 +367,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 +408,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")} @@ -450,7 +472,9 @@ const Account = ({ token, invites = [], user, userRole }) => { header: `${getLocaleString("accCreated")}`, accessor: "created", cell: ({ value }) => ( - {moment(value).format(`${getLocaleString("indexTime")}`)} + + {moment(value).format(`${getLocaleString("indexTime")}`)} + ), gridWidth: "175px", }, @@ -503,9 +527,7 @@ const Account = ({ token, invites = [], user, userRole }) => { {getLocaleString("acc2FAuth")} - - {getLocaleString("acc2FAUseApp")}. - + {getLocaleString("acc2FAUseApp")}.
{totpQrData ? ( @@ -558,9 +580,7 @@ const Account = ({ token, invites = [], user, userRole }) => { <> {totpBackupCodes ? ( <> - - {getLocaleString("acc2FAText1")} - + {getLocaleString("acc2FAText1")} {totpBackupCodes.split(",").map((code) => ( { mb={4} /> )} - + )} @@ -636,10 +661,18 @@ const Account = ({ token, invites = [], user, userRole }) => { {showInviteModal && ( setShowInviteModal(false)}> - {getLocaleString("accInviteText1")} + {getLocaleString( + SQ_DISABLE_EMAIL ? "accInviteText1NoEmail" : "accInviteText1" + )} - + {userRole === "admin" && ( { > {getLocaleString("accCancel")} - + 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..d9661d7 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")}` ); @@ -522,6 +524,11 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { (Number(SQ_MAXIMUM_HIT_N_RUNS) !== -1 && userStats.hitnruns > Number(SQ_MAXIMUM_HIT_N_RUNS)); + function isPngImage(data) { + const pngHeader = "data:image/png;base64,"; + return data.startsWith(pngHeader); + } + return ( <> @@ -658,6 +665,29 @@ const Torrent = ({ token, torrent = {}, userId, userRole, uid, userStats }) => { : [getLocaleString("torrNo")], }} /> + {torrent.poster && ( + + + {getLocaleString("posterImage")} + + + + )} { const [category, setCategory] = useState( - values?.type ?? slugify(Object.keys(categories)[0], { lower: true }) + values?.category ?? slugify(Object.keys(categories)[0], { lower: true }) ); const [sources, setSources] = useState([]); const [tags, setTags] = useState(values?.tags?.split(",") ?? []); @@ -185,6 +185,7 @@ export const TorrentFields = ({ const Upload = ({ token, userId }) => { const [torrentFile, setTorrentFile] = useState(); + const [posterFile, setPosterFile] = useState(); const [dropError, setDropError] = useState(""); const [groupSuggestions, setGroupSuggestions] = useState([]); @@ -235,11 +236,50 @@ const Upload = ({ token, userId }) => { } }, []); + const onPosterDrop = useCallback((acceptedFiles) => { + try { + const [file] = acceptedFiles; + if (file) { + const reader = new FileReader(); + reader.onload = async () => { + console.log( + `[DEBUG] Poster upload complete: ${reader.result.slice(0, 64)}...` + ); + const [, posterB64] = reader.result.split("base64,"); + setPosterFile({ name: file.name, b64: posterB64 }); + }; + reader.onerror = () => { + console.log(`[DEBUG] Poster upload error: ${reader.error}`); + }; + reader.readAsDataURL(file); + } + } catch (e) { + console.error(e); + } + }, []); + const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, accept: { "application/x-bittorrent": [".torrent"] }, maxFiles: 1, }); + const { + getRootProps: getPosterRootProps, + getInputProps: getPosterInputProps, + isDragActive: isPosterDragActive, + } = useDropzone({ + onDrop: onPosterDrop, + accept: { + "image/jpeg": [".jpg", ".jpeg"], + "image/png": [".png"], + }, + maxFiles: 1, + maxSize: 5242880, //5Mo + }); + function isPngImage(data) { + const pngHeader = "data:image/png;base64,"; + return data.startsWith(pngHeader); + } const handleUpload = async (e) => { e.preventDefault(); @@ -248,7 +288,6 @@ const Upload = ({ token, userId }) => { try { if (!torrentFile) throw new Error("No .torrent file added"); - const uploadRes = await fetch(`${SQ_API_URL}/torrent/upload`, { method: "POST", headers: { @@ -265,6 +304,7 @@ const Upload = ({ token, userId }) => { tags: form.get("tags"), groupWith, mediaInfo: form.get("mediaInfo"), + poster: posterFile ? posterFile.b64 : null, }), }); @@ -340,9 +380,7 @@ const Upload = ({ token, userId }) => { {!!SQ_EXTENSION_BLACKLIST.length && ( - - {getLocaleString("uploadInfoBox1")} - + {getLocaleString("uploadInfoBox1")} { )} - - - - - {torrentFile ? ( - - {torrentFile.name} - - ) : isDragActive ? ( - {getLocaleString("uploadDropFileHere")} - ) : ( - - {getLocaleString("uploadDragDropClickSelect")} - - )} - - - {dropError && ( - - {getLocaleString("uploadCouldNotUploadTorrent")}: {dropError} - - )} + + + + + + {torrentFile ? ( + + {torrentFile.name} + + ) : isDragActive ? ( + + {getLocaleString("uploadDropFileHere")} + + ) : ( + + {getLocaleString("uploadDragDropClickSelect")} + + )} + + + {dropError && ( + + {getLocaleString("uploadCouldNotUploadTorrent")}: {dropError} + + )} + + + + + + {posterFile ? ( + Poster + ) : isPosterDragActive ? ( + + {getLocaleString("uploadDropImageHere")} + + ) : ( + + {getLocaleString("uploadDragDropClickSelectPoster")} + + )} + + + { )} {SQ_ALLOW_ANONYMOUS_UPLOAD && ( - + )} )} - {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")}