mirror of
https://github.com/stonith404/pingvin-share.git
synced 2024-06-30 06:30:11 +02:00
feat: localization (#196)
* Started adding locale translations :) * Added some more translations * Working on translating even more pages * More translations * Added test default locale retrieval * replace `intl.formatMessage` with custom `t` hook * add more translations * improve title syntax * add more translations * translate admin config page * translated error messages * add language selecter * minor fixes * improve language handling * add upcoming languages * add `crowdin.yml` * run formatter --------- Co-authored-by: Steve Tautonico <stautonico@gmail.com>
This commit is contained in:
parent
7c5ec8d0ea
commit
b9f6e3bd08
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `description` on the `Config` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- RedefineTables
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_Config" (
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"category" TEXT NOT NULL,
|
||||||
|
"type" TEXT NOT NULL,
|
||||||
|
"defaultValue" TEXT NOT NULL DEFAULT '',
|
||||||
|
"value" TEXT,
|
||||||
|
"obscured" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"secret" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"locked" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"order" INTEGER NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY ("name", "category")
|
||||||
|
);
|
||||||
|
INSERT INTO "new_Config" ("category", "defaultValue", "locked", "name", "obscured", "order", "secret", "type", "updatedAt", "value") SELECT "category", "defaultValue", "locked", "name", "obscured", "order", "secret", "type", "updatedAt", "value" FROM "Config";
|
||||||
|
DROP TABLE "Config";
|
||||||
|
ALTER TABLE "new_Config" RENAME TO "Config";
|
||||||
|
PRAGMA foreign_key_check;
|
||||||
|
PRAGMA foreign_keys=ON;
|
|
@ -136,7 +136,6 @@ model Config {
|
||||||
type String
|
type String
|
||||||
defaultValue String @default("")
|
defaultValue String @default("")
|
||||||
value String?
|
value String?
|
||||||
description String
|
|
||||||
obscured Boolean @default(false)
|
obscured Boolean @default(false)
|
||||||
secret Boolean @default(true)
|
secret Boolean @default(true)
|
||||||
locked Boolean @default(false)
|
locked Boolean @default(false)
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { Prisma, PrismaClient } from "@prisma/client";
|
import { Prisma, PrismaClient } from "@prisma/client";
|
||||||
import * as crypto from "crypto";
|
import * as crypto from "crypto";
|
||||||
|
|
||||||
const configVariables: ConfigVariables = {
|
const configVariables: ConfigVariables = {
|
||||||
internal: {
|
internal: {
|
||||||
jwtSecret: {
|
jwtSecret: {
|
||||||
description: "Long random string used to sign JWT tokens",
|
|
||||||
type: "string",
|
type: "string",
|
||||||
defaultValue: crypto.randomBytes(256).toString("base64"),
|
defaultValue: crypto.randomBytes(256).toString("base64"),
|
||||||
locked: true,
|
locked: true,
|
||||||
|
@ -11,20 +11,17 @@ const configVariables: ConfigVariables = {
|
||||||
},
|
},
|
||||||
general: {
|
general: {
|
||||||
appName: {
|
appName: {
|
||||||
description: "Name of the application",
|
|
||||||
type: "string",
|
type: "string",
|
||||||
defaultValue: "Pingvin Share",
|
defaultValue: "Pingvin Share",
|
||||||
secret: false,
|
secret: false,
|
||||||
},
|
},
|
||||||
appUrl: {
|
appUrl: {
|
||||||
description: "On which URL Pingvin Share is available",
|
|
||||||
type: "string",
|
type: "string",
|
||||||
defaultValue: "http://localhost:3000",
|
defaultValue: "http://localhost:3000",
|
||||||
|
|
||||||
secret: false,
|
secret: false,
|
||||||
},
|
},
|
||||||
showHomePage: {
|
showHomePage: {
|
||||||
description: "Whether to show the home page",
|
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
defaultValue: "true",
|
defaultValue: "true",
|
||||||
secret: false,
|
secret: false,
|
||||||
|
@ -32,21 +29,17 @@ const configVariables: ConfigVariables = {
|
||||||
},
|
},
|
||||||
share: {
|
share: {
|
||||||
allowRegistration: {
|
allowRegistration: {
|
||||||
description: "Whether registration is allowed",
|
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
defaultValue: "true",
|
defaultValue: "true",
|
||||||
|
|
||||||
secret: false,
|
secret: false,
|
||||||
},
|
},
|
||||||
allowUnauthenticatedShares: {
|
allowUnauthenticatedShares: {
|
||||||
description: "Whether unauthorized users can create shares",
|
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
defaultValue: "false",
|
defaultValue: "false",
|
||||||
|
|
||||||
secret: false,
|
secret: false,
|
||||||
},
|
},
|
||||||
maxSize: {
|
maxSize: {
|
||||||
description: "Maximum share size in bytes",
|
|
||||||
type: "number",
|
type: "number",
|
||||||
defaultValue: "1073741824",
|
defaultValue: "1073741824",
|
||||||
|
|
||||||
|
@ -55,61 +48,43 @@ const configVariables: ConfigVariables = {
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
enableShareEmailRecipients: {
|
enableShareEmailRecipients: {
|
||||||
description:
|
|
||||||
"Whether to allow emails to share recipients. Only enable this if you have enabled SMTP.",
|
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
defaultValue: "false",
|
defaultValue: "false",
|
||||||
|
|
||||||
secret: false,
|
secret: false,
|
||||||
},
|
},
|
||||||
shareRecipientsSubject: {
|
shareRecipientsSubject: {
|
||||||
description:
|
|
||||||
"Subject of the email which gets sent to the share recipients.",
|
|
||||||
type: "string",
|
type: "string",
|
||||||
defaultValue: "Files shared with you",
|
defaultValue: "Files shared with you",
|
||||||
},
|
},
|
||||||
shareRecipientsMessage: {
|
shareRecipientsMessage: {
|
||||||
description:
|
|
||||||
"Message which gets sent to the share recipients.\n\nAvailable variables:\n{creator} - The username of the creator of the share\n{shareUrl} - The URL of the share\n{desc} - The description of the share\n{expires} - The expiration date of the share\n\nVariables will be replaced with the actual values.",
|
|
||||||
type: "text",
|
type: "text",
|
||||||
defaultValue:
|
defaultValue:
|
||||||
"Hey!\n\n{creator} shared some files with you, view or download the files with this link: {shareUrl}\n\nThe share will expire {expires}.\n\nNote: {desc}\n\nShared securely with Pingvin Share 🐧",
|
"Hey!\n\n{creator} shared some files with you, view or download the files with this link: {shareUrl}\n\nThe share will expire {expires}.\n\nNote: {desc}\n\nShared securely with Pingvin Share 🐧",
|
||||||
},
|
},
|
||||||
reverseShareSubject: {
|
reverseShareSubject: {
|
||||||
description:
|
|
||||||
"Subject of the email which gets sent when someone created a share with your reverse share link.",
|
|
||||||
type: "string",
|
type: "string",
|
||||||
defaultValue: "Reverse share link used",
|
defaultValue: "Reverse share link used",
|
||||||
},
|
},
|
||||||
reverseShareMessage: {
|
reverseShareMessage: {
|
||||||
description:
|
|
||||||
"Message which gets sent when someone created a share with your reverse share link. {shareUrl} will be replaced with the creator's name and the share URL.",
|
|
||||||
type: "text",
|
type: "text",
|
||||||
defaultValue:
|
defaultValue:
|
||||||
"Hey!\n\nA share was just created with your reverse share link: {shareUrl}\n\nShared securely with Pingvin Share 🐧",
|
"Hey!\n\nA share was just created with your reverse share link: {shareUrl}\n\nShared securely with Pingvin Share 🐧",
|
||||||
},
|
},
|
||||||
resetPasswordSubject: {
|
resetPasswordSubject: {
|
||||||
description:
|
|
||||||
"Subject of the email which gets sent when a user requests a password reset.",
|
|
||||||
type: "string",
|
type: "string",
|
||||||
defaultValue: "Pingvin Share password reset",
|
defaultValue: "Pingvin Share password reset",
|
||||||
},
|
},
|
||||||
resetPasswordMessage: {
|
resetPasswordMessage: {
|
||||||
description:
|
|
||||||
"Message which gets sent when a user requests a password reset. {url} will be replaced with the reset password URL.",
|
|
||||||
type: "text",
|
type: "text",
|
||||||
defaultValue:
|
defaultValue:
|
||||||
"Hey!\n\nYou requested a password reset. Click this link to reset your password: {url}\nThe link expires in a hour.\n\nPingvin Share 🐧",
|
"Hey!\n\nYou requested a password reset. Click this link to reset your password: {url}\nThe link expires in a hour.\n\nPingvin Share 🐧",
|
||||||
},
|
},
|
||||||
inviteSubject: {
|
inviteSubject: {
|
||||||
description:
|
|
||||||
"Subject of the email which gets sent when an admin invites an user.",
|
|
||||||
type: "string",
|
type: "string",
|
||||||
defaultValue: "Pingvin Share invite",
|
defaultValue: "Pingvin Share invite",
|
||||||
},
|
},
|
||||||
inviteMessage: {
|
inviteMessage: {
|
||||||
description:
|
|
||||||
"Message which gets sent when an admin invites an user. {url} will be replaced with the invite URL and {password} with the password.",
|
|
||||||
type: "text",
|
type: "text",
|
||||||
defaultValue:
|
defaultValue:
|
||||||
"Hey!\n\nYou were invited to Pingvin Share. Click this link to accept the invite: {url}\n\nYour password is: {password}\n\nPingvin Share 🐧",
|
"Hey!\n\nYou were invited to Pingvin Share. Click this link to accept the invite: {url}\n\nYour password is: {password}\n\nPingvin Share 🐧",
|
||||||
|
@ -117,34 +92,27 @@ const configVariables: ConfigVariables = {
|
||||||
},
|
},
|
||||||
smtp: {
|
smtp: {
|
||||||
enabled: {
|
enabled: {
|
||||||
description:
|
|
||||||
"Whether SMTP is enabled. Only set this to true if you entered the host, port, email, user and password of your SMTP server.",
|
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
defaultValue: "false",
|
defaultValue: "false",
|
||||||
secret: false,
|
secret: false,
|
||||||
},
|
},
|
||||||
host: {
|
host: {
|
||||||
description: "Host of the SMTP server",
|
|
||||||
type: "string",
|
type: "string",
|
||||||
defaultValue: "",
|
defaultValue: "",
|
||||||
},
|
},
|
||||||
port: {
|
port: {
|
||||||
description: "Port of the SMTP server",
|
|
||||||
type: "number",
|
type: "number",
|
||||||
defaultValue: "0",
|
defaultValue: "0",
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
description: "Email address which the emails get sent from",
|
|
||||||
type: "string",
|
type: "string",
|
||||||
defaultValue: "",
|
defaultValue: "",
|
||||||
},
|
},
|
||||||
username: {
|
username: {
|
||||||
description: "Username of the SMTP server",
|
|
||||||
type: "string",
|
type: "string",
|
||||||
defaultValue: "",
|
defaultValue: "",
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
description: "Password of the SMTP server",
|
|
||||||
type: "string",
|
type: "string",
|
||||||
defaultValue: "",
|
defaultValue: "",
|
||||||
obscured: true,
|
obscured: true,
|
||||||
|
|
|
@ -14,9 +14,6 @@ export class AdminConfigDTO extends ConfigDTO {
|
||||||
@Expose()
|
@Expose()
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|
||||||
@Expose()
|
|
||||||
description: string;
|
|
||||||
|
|
||||||
@Expose()
|
@Expose()
|
||||||
obscured: boolean;
|
obscured: boolean;
|
||||||
|
|
||||||
|
|
3
crowdin.yml
Normal file
3
crowdin.yml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
files:
|
||||||
|
- source: /frontend/src/i18n/translations/en.ts
|
||||||
|
translation: /%original_path%/%two_letters_code%.ts
|
|
@ -1,5 +1,10 @@
|
||||||
{
|
{
|
||||||
"extends": ["eslint-config-next", "eslint:recommended", "prettier"],
|
"extends": [
|
||||||
|
"next/babel",
|
||||||
|
"eslint-config-next",
|
||||||
|
"eslint:recommended",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
"plugins": ["react"],
|
"plugins": ["react"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"quotes": ["warn", "double", { "allowTemplateLiterals": true }],
|
"quotes": ["warn", "double", { "allowTemplateLiterals": true }],
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
|
|
||||||
const { version } = require('./package.json');
|
const { version } = require('./package.json');
|
||||||
|
|
||||||
const withPWA = require("next-pwa")({
|
const withPWA = require("next-pwa")({
|
||||||
|
|
266
frontend/package-lock.json
generated
266
frontend/package-lock.json
generated
|
@ -32,6 +32,7 @@
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-icons": "^4.8.0",
|
"react-icons": "^4.8.0",
|
||||||
|
"react-intl": "^6.3.1",
|
||||||
"sharp": "^0.31.3",
|
"sharp": "^0.31.3",
|
||||||
"yup": "^1.0.2"
|
"yup": "^1.0.2"
|
||||||
},
|
},
|
||||||
|
@ -1860,6 +1861,92 @@
|
||||||
"react-dom": ">=16.8.0"
|
"react-dom": ">=16.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@formatjs/ecma402-abstract": {
|
||||||
|
"version": "1.14.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.14.3.tgz",
|
||||||
|
"integrity": "sha512-SlsbRC/RX+/zg4AApWIFNDdkLtFbkq3LNoZWXZCE/nHVKqoIJyaoQyge/I0Y38vLxowUn9KTtXgusLD91+orbg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/intl-localematcher": "0.2.32",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@formatjs/fast-memoize": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-U88W/qhEs5ZuX+Inw/DZHjA6w2YCTWTNzTkprzNznyWoGl8h+XtlOCW3nM78+VX7lSbvpMdnaHmWLnDnjJjuwg==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@formatjs/icu-messageformat-parser": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-xqtlqYAbfJDF4b6e4O828LBNOWXrFcuYadqAbYORlDRwhyJ2bH+xpUBPldZbzRGUN2mxlZ4Ykhm7jvERtmI8NQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/ecma402-abstract": "1.14.3",
|
||||||
|
"@formatjs/icu-skeleton-parser": "1.3.18",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@formatjs/icu-skeleton-parser": {
|
||||||
|
"version": "1.3.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.18.tgz",
|
||||||
|
"integrity": "sha512-ND1ZkZfmLPcHjAH1sVpkpQxA+QYfOX3py3SjKWMUVGDow18gZ0WPqz3F+pJLYQMpS2LnnQ5zYR2jPVYTbRwMpg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/ecma402-abstract": "1.14.3",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@formatjs/intl": {
|
||||||
|
"version": "2.6.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-2.6.8.tgz",
|
||||||
|
"integrity": "sha512-lWfFUBGvYfeQoIaW5/6TW7WdyiqfWg5wPFVnsb1eKs4JPmAz2nYBUPuKbyvBiRV87QCSNZ+tZTViFBU+bIHeqQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/ecma402-abstract": "1.14.3",
|
||||||
|
"@formatjs/fast-memoize": "2.0.0",
|
||||||
|
"@formatjs/icu-messageformat-parser": "2.3.0",
|
||||||
|
"@formatjs/intl-displaynames": "6.2.6",
|
||||||
|
"@formatjs/intl-listformat": "7.1.9",
|
||||||
|
"intl-messageformat": "10.3.2",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^4.7"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@formatjs/intl-displaynames": {
|
||||||
|
"version": "6.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-6.2.6.tgz",
|
||||||
|
"integrity": "sha512-scf5AQTk9EjpvPhboo5sizVOvidTdMOnajv9z+0cejvl7JNl9bl/aMrNBgC72UH+bP3l45usPUKAGskV6sNIrA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/ecma402-abstract": "1.14.3",
|
||||||
|
"@formatjs/intl-localematcher": "0.2.32",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@formatjs/intl-listformat": {
|
||||||
|
"version": "7.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-7.1.9.tgz",
|
||||||
|
"integrity": "sha512-5YikxwRqRXTVWVujhswDOTCq6gs+m9IcNbNZLa6FLtyBStAjEsuE2vAU+lPsbz9ZTST57D5fodjIh2JXT6sMWQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/ecma402-abstract": "1.14.3",
|
||||||
|
"@formatjs/intl-localematcher": "0.2.32",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@formatjs/intl-localematcher": {
|
||||||
|
"version": "0.2.32",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.32.tgz",
|
||||||
|
"integrity": "sha512-k/MEBstff4sttohyEpXxCmC3MqbUn9VvHGlZ8fauLzkbwXmVrEeyzS+4uhrvAk9DWU9/7otYWxyDox4nT/KVLQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanwhocodes/config-array": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.11.8",
|
"version": "0.11.8",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
|
||||||
|
@ -2625,6 +2712,15 @@
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/hoist-non-react-statics": {
|
||||||
|
"version": "3.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
|
||||||
|
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"hoist-non-react-statics": "^3.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/http-proxy": {
|
"node_modules/@types/http-proxy": {
|
||||||
"version": "1.17.3",
|
"version": "1.17.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.3.tgz",
|
||||||
|
@ -2668,14 +2764,12 @@
|
||||||
"node_modules/@types/prop-types": {
|
"node_modules/@types/prop-types": {
|
||||||
"version": "15.7.5",
|
"version": "15.7.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
|
||||||
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==",
|
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
|
||||||
"devOptional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "18.0.28",
|
"version": "18.0.28",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.28.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.28.tgz",
|
||||||
"integrity": "sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==",
|
"integrity": "sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==",
|
||||||
"devOptional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"@types/scheduler": "*",
|
"@types/scheduler": "*",
|
||||||
|
@ -2702,8 +2796,7 @@
|
||||||
"node_modules/@types/scheduler": {
|
"node_modules/@types/scheduler": {
|
||||||
"version": "0.16.2",
|
"version": "0.16.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
|
||||||
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
|
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
|
||||||
"devOptional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/trusted-types": {
|
"node_modules/@types/trusted-types": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
|
@ -5349,6 +5442,17 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/intl-messageformat": {
|
||||||
|
"version": "10.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.3.2.tgz",
|
||||||
|
"integrity": "sha512-kGY1KrpxPGbWX/yz6rpWQahBh5bJC6pIbq/cTzVYlmAYjRVzP+l2MulagbZf/5mABbcLT/0RJbZC46Iw6Mhmtw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/ecma402-abstract": "1.14.3",
|
||||||
|
"@formatjs/fast-memoize": "2.0.0",
|
||||||
|
"@formatjs/icu-messageformat-parser": "2.3.0",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/invariant": {
|
"node_modules/invariant": {
|
||||||
"version": "2.2.4",
|
"version": "2.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||||
|
@ -6863,6 +6967,32 @@
|
||||||
"react": "*"
|
"react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-intl": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-intl/-/react-intl-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-wI7YSxS2xQRYrHPMFSHXSWNuOLrgt9L0Dg8G35xJNfDRpcyebE9H1k2/DDPxgbV4ImeG/qKJMN5SrZV7fuB/Ag==",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/ecma402-abstract": "1.14.3",
|
||||||
|
"@formatjs/icu-messageformat-parser": "2.3.0",
|
||||||
|
"@formatjs/intl": "2.6.8",
|
||||||
|
"@formatjs/intl-displaynames": "6.2.6",
|
||||||
|
"@formatjs/intl-listformat": "7.1.9",
|
||||||
|
"@types/hoist-non-react-statics": "^3.3.1",
|
||||||
|
"@types/react": "16 || 17 || 18",
|
||||||
|
"hoist-non-react-statics": "^3.3.2",
|
||||||
|
"intl-messageformat": "10.3.2",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.6.0 || 17 || 18",
|
||||||
|
"typescript": "^4.7"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
@ -7988,7 +8118,7 @@
|
||||||
"version": "4.9.5",
|
"version": "4.9.5",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
@ -9950,6 +10080,84 @@
|
||||||
"@floating-ui/dom": "^1.2.1"
|
"@floating-ui/dom": "^1.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@formatjs/ecma402-abstract": {
|
||||||
|
"version": "1.14.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.14.3.tgz",
|
||||||
|
"integrity": "sha512-SlsbRC/RX+/zg4AApWIFNDdkLtFbkq3LNoZWXZCE/nHVKqoIJyaoQyge/I0Y38vLxowUn9KTtXgusLD91+orbg==",
|
||||||
|
"requires": {
|
||||||
|
"@formatjs/intl-localematcher": "0.2.32",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@formatjs/fast-memoize": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-U88W/qhEs5ZuX+Inw/DZHjA6w2YCTWTNzTkprzNznyWoGl8h+XtlOCW3nM78+VX7lSbvpMdnaHmWLnDnjJjuwg==",
|
||||||
|
"requires": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@formatjs/icu-messageformat-parser": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-xqtlqYAbfJDF4b6e4O828LBNOWXrFcuYadqAbYORlDRwhyJ2bH+xpUBPldZbzRGUN2mxlZ4Ykhm7jvERtmI8NQ==",
|
||||||
|
"requires": {
|
||||||
|
"@formatjs/ecma402-abstract": "1.14.3",
|
||||||
|
"@formatjs/icu-skeleton-parser": "1.3.18",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@formatjs/icu-skeleton-parser": {
|
||||||
|
"version": "1.3.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.18.tgz",
|
||||||
|
"integrity": "sha512-ND1ZkZfmLPcHjAH1sVpkpQxA+QYfOX3py3SjKWMUVGDow18gZ0WPqz3F+pJLYQMpS2LnnQ5zYR2jPVYTbRwMpg==",
|
||||||
|
"requires": {
|
||||||
|
"@formatjs/ecma402-abstract": "1.14.3",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@formatjs/intl": {
|
||||||
|
"version": "2.6.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-2.6.8.tgz",
|
||||||
|
"integrity": "sha512-lWfFUBGvYfeQoIaW5/6TW7WdyiqfWg5wPFVnsb1eKs4JPmAz2nYBUPuKbyvBiRV87QCSNZ+tZTViFBU+bIHeqQ==",
|
||||||
|
"requires": {
|
||||||
|
"@formatjs/ecma402-abstract": "1.14.3",
|
||||||
|
"@formatjs/fast-memoize": "2.0.0",
|
||||||
|
"@formatjs/icu-messageformat-parser": "2.3.0",
|
||||||
|
"@formatjs/intl-displaynames": "6.2.6",
|
||||||
|
"@formatjs/intl-listformat": "7.1.9",
|
||||||
|
"intl-messageformat": "10.3.2",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@formatjs/intl-displaynames": {
|
||||||
|
"version": "6.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-6.2.6.tgz",
|
||||||
|
"integrity": "sha512-scf5AQTk9EjpvPhboo5sizVOvidTdMOnajv9z+0cejvl7JNl9bl/aMrNBgC72UH+bP3l45usPUKAGskV6sNIrA==",
|
||||||
|
"requires": {
|
||||||
|
"@formatjs/ecma402-abstract": "1.14.3",
|
||||||
|
"@formatjs/intl-localematcher": "0.2.32",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@formatjs/intl-listformat": {
|
||||||
|
"version": "7.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-7.1.9.tgz",
|
||||||
|
"integrity": "sha512-5YikxwRqRXTVWVujhswDOTCq6gs+m9IcNbNZLa6FLtyBStAjEsuE2vAU+lPsbz9ZTST57D5fodjIh2JXT6sMWQ==",
|
||||||
|
"requires": {
|
||||||
|
"@formatjs/ecma402-abstract": "1.14.3",
|
||||||
|
"@formatjs/intl-localematcher": "0.2.32",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@formatjs/intl-localematcher": {
|
||||||
|
"version": "0.2.32",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.32.tgz",
|
||||||
|
"integrity": "sha512-k/MEBstff4sttohyEpXxCmC3MqbUn9VvHGlZ8fauLzkbwXmVrEeyzS+4uhrvAk9DWU9/7otYWxyDox4nT/KVLQ==",
|
||||||
|
"requires": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@humanwhocodes/config-array": {
|
"@humanwhocodes/config-array": {
|
||||||
"version": "0.11.8",
|
"version": "0.11.8",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
|
||||||
|
@ -10460,6 +10668,15 @@
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/hoist-non-react-statics": {
|
||||||
|
"version": "3.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
|
||||||
|
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
|
||||||
|
"requires": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"hoist-non-react-statics": "^3.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/http-proxy": {
|
"@types/http-proxy": {
|
||||||
"version": "1.17.3",
|
"version": "1.17.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.3.tgz",
|
||||||
|
@ -10503,14 +10720,12 @@
|
||||||
"@types/prop-types": {
|
"@types/prop-types": {
|
||||||
"version": "15.7.5",
|
"version": "15.7.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
|
||||||
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==",
|
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
|
||||||
"devOptional": true
|
|
||||||
},
|
},
|
||||||
"@types/react": {
|
"@types/react": {
|
||||||
"version": "18.0.28",
|
"version": "18.0.28",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.28.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.28.tgz",
|
||||||
"integrity": "sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==",
|
"integrity": "sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==",
|
||||||
"devOptional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"@types/scheduler": "*",
|
"@types/scheduler": "*",
|
||||||
|
@ -10537,8 +10752,7 @@
|
||||||
"@types/scheduler": {
|
"@types/scheduler": {
|
||||||
"version": "0.16.2",
|
"version": "0.16.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
|
||||||
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
|
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
|
||||||
"devOptional": true
|
|
||||||
},
|
},
|
||||||
"@types/trusted-types": {
|
"@types/trusted-types": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
|
@ -12506,6 +12720,17 @@
|
||||||
"side-channel": "^1.0.4"
|
"side-channel": "^1.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"intl-messageformat": {
|
||||||
|
"version": "10.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.3.2.tgz",
|
||||||
|
"integrity": "sha512-kGY1KrpxPGbWX/yz6rpWQahBh5bJC6pIbq/cTzVYlmAYjRVzP+l2MulagbZf/5mABbcLT/0RJbZC46Iw6Mhmtw==",
|
||||||
|
"requires": {
|
||||||
|
"@formatjs/ecma402-abstract": "1.14.3",
|
||||||
|
"@formatjs/fast-memoize": "2.0.0",
|
||||||
|
"@formatjs/icu-messageformat-parser": "2.3.0",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"invariant": {
|
"invariant": {
|
||||||
"version": "2.2.4",
|
"version": "2.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||||
|
@ -13565,6 +13790,23 @@
|
||||||
"integrity": "sha512-N6+kOLcihDiAnj5Czu637waJqSnwlMNROzVZMhfX68V/9bu9qHaMIJC4UdozWoOk57gahFCNHwVvWzm0MTzRjg==",
|
"integrity": "sha512-N6+kOLcihDiAnj5Czu637waJqSnwlMNROzVZMhfX68V/9bu9qHaMIJC4UdozWoOk57gahFCNHwVvWzm0MTzRjg==",
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
|
"react-intl": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-intl/-/react-intl-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-wI7YSxS2xQRYrHPMFSHXSWNuOLrgt9L0Dg8G35xJNfDRpcyebE9H1k2/DDPxgbV4ImeG/qKJMN5SrZV7fuB/Ag==",
|
||||||
|
"requires": {
|
||||||
|
"@formatjs/ecma402-abstract": "1.14.3",
|
||||||
|
"@formatjs/icu-messageformat-parser": "2.3.0",
|
||||||
|
"@formatjs/intl": "2.6.8",
|
||||||
|
"@formatjs/intl-displaynames": "6.2.6",
|
||||||
|
"@formatjs/intl-listformat": "7.1.9",
|
||||||
|
"@types/hoist-non-react-statics": "^3.3.1",
|
||||||
|
"@types/react": "16 || 17 || 18",
|
||||||
|
"hoist-non-react-statics": "^3.3.2",
|
||||||
|
"intl-messageformat": "10.3.2",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-is": {
|
"react-is": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
@ -14362,7 +14604,7 @@
|
||||||
"version": "4.9.5",
|
"version": "4.9.5",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||||
"dev": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"unbox-primitive": {
|
"unbox-primitive": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-icons": "^4.8.0",
|
"react-icons": "^4.8.0",
|
||||||
|
"react-intl": "^6.3.1",
|
||||||
"sharp": "^0.31.3",
|
"sharp": "^0.31.3",
|
||||||
"yup": "^1.0.2"
|
"yup": "^1.0.2"
|
||||||
},
|
},
|
||||||
|
|
33
frontend/src/components/account/LanguagePicker.tsx
Normal file
33
frontend/src/components/account/LanguagePicker.tsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { Select } from "@mantine/core";
|
||||||
|
import { getCookie, setCookie } from "cookies-next";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { LOCALES } from "../../i18n/locales";
|
||||||
|
|
||||||
|
const LanguagePicker = () => {
|
||||||
|
const [selectedLanguage, setSelectedLanguage] = useState(
|
||||||
|
getCookie("language")?.toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
const languages = Object.values(LOCALES).map((locale) => ({
|
||||||
|
value: locale.code,
|
||||||
|
label: locale.name,
|
||||||
|
}));
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
value={selectedLanguage}
|
||||||
|
onChange={(value) => {
|
||||||
|
setSelectedLanguage(value ?? "en");
|
||||||
|
setCookie("language", value, {
|
||||||
|
sameSite: "lax",
|
||||||
|
expires: new Date(
|
||||||
|
new Date().setFullYear(new Date().getFullYear() + 1)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
location.reload();
|
||||||
|
}}
|
||||||
|
data={languages}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LanguagePicker;
|
|
@ -9,12 +9,12 @@ import {
|
||||||
import { useColorScheme } from "@mantine/hooks";
|
import { useColorScheme } from "@mantine/hooks";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { TbDeviceLaptop, TbMoon, TbSun } from "react-icons/tb";
|
import { TbDeviceLaptop, TbMoon, TbSun } from "react-icons/tb";
|
||||||
import usePreferences from "../../hooks/usePreferences";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
import userPreferences from "../../utils/userPreferences.util";
|
||||||
|
|
||||||
const ThemeSwitcher = () => {
|
const ThemeSwitcher = () => {
|
||||||
const preferences = usePreferences();
|
|
||||||
const [colorScheme, setColorScheme] = useState(
|
const [colorScheme, setColorScheme] = useState(
|
||||||
preferences.get("colorScheme")
|
userPreferences.get("colorScheme")
|
||||||
);
|
);
|
||||||
const { toggleColorScheme } = useMantineColorScheme();
|
const { toggleColorScheme } = useMantineColorScheme();
|
||||||
const systemColorScheme = useColorScheme();
|
const systemColorScheme = useColorScheme();
|
||||||
|
@ -23,7 +23,7 @@ const ThemeSwitcher = () => {
|
||||||
<SegmentedControl
|
<SegmentedControl
|
||||||
value={colorScheme}
|
value={colorScheme}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
preferences.set("colorScheme", value);
|
userPreferences.set("colorScheme", value);
|
||||||
setColorScheme(value);
|
setColorScheme(value);
|
||||||
toggleColorScheme(
|
toggleColorScheme(
|
||||||
value == "system" ? systemColorScheme : (value as ColorScheme)
|
value == "system" ? systemColorScheme : (value as ColorScheme)
|
||||||
|
@ -34,7 +34,9 @@ const ThemeSwitcher = () => {
|
||||||
label: (
|
label: (
|
||||||
<Center>
|
<Center>
|
||||||
<TbMoon size={16} />
|
<TbMoon size={16} />
|
||||||
<Box ml={10}>Dark</Box>
|
<Box ml={10}>
|
||||||
|
<FormattedMessage id="account.theme.dark" />
|
||||||
|
</Box>
|
||||||
</Center>
|
</Center>
|
||||||
),
|
),
|
||||||
value: "dark",
|
value: "dark",
|
||||||
|
@ -43,7 +45,9 @@ const ThemeSwitcher = () => {
|
||||||
label: (
|
label: (
|
||||||
<Center>
|
<Center>
|
||||||
<TbSun size={16} />
|
<TbSun size={16} />
|
||||||
<Box ml={10}>Light</Box>
|
<Box ml={10}>
|
||||||
|
<FormattedMessage id="account.theme.light" />
|
||||||
|
</Box>
|
||||||
</Center>
|
</Center>
|
||||||
),
|
),
|
||||||
value: "light",
|
value: "light",
|
||||||
|
@ -52,7 +56,9 @@ const ThemeSwitcher = () => {
|
||||||
label: (
|
label: (
|
||||||
<Center>
|
<Center>
|
||||||
<TbDeviceLaptop size={16} />
|
<TbDeviceLaptop size={16} />
|
||||||
<Box ml={10}>System</Box>
|
<Box ml={10}>
|
||||||
|
<FormattedMessage id="account.theme.system" />
|
||||||
|
</Box>
|
||||||
</Center>
|
</Center>
|
||||||
),
|
),
|
||||||
value: "system",
|
value: "system",
|
||||||
|
|
|
@ -12,7 +12,11 @@ import {
|
||||||
import { useForm, yupResolver } from "@mantine/form";
|
import { useForm, yupResolver } from "@mantine/form";
|
||||||
import { useModals } from "@mantine/modals";
|
import { useModals } from "@mantine/modals";
|
||||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
import useTranslate, {
|
||||||
|
translateOutsideContext,
|
||||||
|
} from "../../hooks/useTranslate.hook";
|
||||||
import authService from "../../services/auth.service";
|
import authService from "../../services/auth.service";
|
||||||
import toast from "../../utils/toast.util";
|
import toast from "../../utils/toast.util";
|
||||||
|
|
||||||
|
@ -25,8 +29,9 @@ const showEnableTotpModal = (
|
||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
|
const t = translateOutsideContext();
|
||||||
return modals.openModal({
|
return modals.openModal({
|
||||||
title: "Enable TOTP",
|
title: t("account.modal.totp.title"),
|
||||||
children: (
|
children: (
|
||||||
<CreateEnableTotpModal options={options} refreshUser={refreshUser} />
|
<CreateEnableTotpModal options={options} refreshUser={refreshUser} />
|
||||||
),
|
),
|
||||||
|
@ -45,6 +50,7 @@ const CreateEnableTotpModal = ({
|
||||||
refreshUser: () => {};
|
refreshUser: () => {};
|
||||||
}) => {
|
}) => {
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
|
const t = useTranslate();
|
||||||
|
|
||||||
const validationSchema = yup.object().shape({
|
const validationSchema = yup.object().shape({
|
||||||
code: yup
|
code: yup
|
||||||
|
@ -66,14 +72,19 @@ const CreateEnableTotpModal = ({
|
||||||
<div>
|
<div>
|
||||||
<Center>
|
<Center>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text>Step 1: Add your authenticator</Text>
|
<Text>
|
||||||
|
<FormattedMessage id="account.modal.totp.step1" />
|
||||||
|
</Text>
|
||||||
<Image src={options.qrCode} alt="QR Code" />
|
<Image src={options.qrCode} alt="QR Code" />
|
||||||
|
|
||||||
<Center>
|
<Center>
|
||||||
<span>OR</span>
|
<span>
|
||||||
|
{" "}
|
||||||
|
<FormattedMessage id="common.text.or" />
|
||||||
|
</span>
|
||||||
</Center>
|
</Center>
|
||||||
|
|
||||||
<Tooltip label="Click to copy">
|
<Tooltip label={t("account.modal.totp.clickToCopy")}>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(options.secret);
|
navigator.clipboard.writeText(options.secret);
|
||||||
|
@ -84,17 +95,19 @@ const CreateEnableTotpModal = ({
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Center>
|
<Center>
|
||||||
<Text fz="xs">Enter manually</Text>
|
<Text fz="xs"></Text>
|
||||||
</Center>
|
</Center>
|
||||||
|
|
||||||
<Text>Step 2: Validate your code</Text>
|
<Text>
|
||||||
|
<FormattedMessage id="account.modal.totp.step2" />
|
||||||
|
</Text>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
onSubmit={form.onSubmit((values) => {
|
onSubmit={form.onSubmit((values) => {
|
||||||
authService
|
authService
|
||||||
.verifyTOTP(values.code, options.password)
|
.verifyTOTP(values.code, options.password)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Successfully enabled TOTP");
|
toast.success(t("account.notify.totp.enable"));
|
||||||
modals.closeAll();
|
modals.closeAll();
|
||||||
refreshUser();
|
refreshUser();
|
||||||
})
|
})
|
||||||
|
@ -105,14 +118,14 @@ const CreateEnableTotpModal = ({
|
||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<TextInput
|
<TextInput
|
||||||
variant="filled"
|
variant="filled"
|
||||||
label="Code"
|
label={t("account.modal.totp.code")}
|
||||||
placeholder="******"
|
placeholder="******"
|
||||||
{...form.getInputProps("code")}
|
{...form.getInputProps("code")}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={3}>
|
<Col xs={3}>
|
||||||
<Button variant="outline" type="submit">
|
<Button variant="outline" type="submit">
|
||||||
Verify
|
<FormattedMessage id="account.modal.totp.verify" />
|
||||||
</Button>
|
</Button>
|
||||||
</Col>
|
</Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
import { Stack, TextInput } from "@mantine/core";
|
import { Stack, TextInput } from "@mantine/core";
|
||||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
|
import { translateOutsideContext } from "../../hooks/useTranslate.hook";
|
||||||
|
|
||||||
const showReverseShareLinkModal = (
|
const showReverseShareLinkModal = (
|
||||||
modals: ModalsContextProps,
|
modals: ModalsContextProps,
|
||||||
reverseShareToken: string,
|
reverseShareToken: string,
|
||||||
appUrl: string
|
appUrl: string
|
||||||
) => {
|
) => {
|
||||||
|
const t = translateOutsideContext();
|
||||||
const link = `${appUrl}/upload/${reverseShareToken}`;
|
const link = `${appUrl}/upload/${reverseShareToken}`;
|
||||||
return modals.openModal({
|
return modals.openModal({
|
||||||
title: "Reverse share link",
|
title: t("account.reverseShares.modal.reverse-share-link"),
|
||||||
children: (
|
children: (
|
||||||
<Stack align="stretch">
|
<Stack align="stretch">
|
||||||
<TextInput variant="filled" value={link} />
|
<TextInput variant="filled" value={link} />
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import { Text, Divider, Progress, Stack, Group, Flex } from "@mantine/core";
|
import { Divider, Flex, Progress, Stack, Text } from "@mantine/core";
|
||||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
import { MyShare } from "../../types/share.type";
|
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
import { translateOutsideContext } from "../../hooks/useTranslate.hook";
|
||||||
|
import { FileMetaData } from "../../types/File.type";
|
||||||
|
import { MyShare } from "../../types/share.type";
|
||||||
import { byteToHumanSizeString } from "../../utils/fileSize.util";
|
import { byteToHumanSizeString } from "../../utils/fileSize.util";
|
||||||
import CopyTextField from "../upload/CopyTextField";
|
import CopyTextField from "../upload/CopyTextField";
|
||||||
import { FileMetaData } from "../../types/File.type";
|
|
||||||
|
|
||||||
const showShareInformationsModal = (
|
const showShareInformationsModal = (
|
||||||
modals: ModalsContextProps,
|
modals: ModalsContextProps,
|
||||||
|
@ -12,6 +14,7 @@ const showShareInformationsModal = (
|
||||||
appUrl: string,
|
appUrl: string,
|
||||||
maxShareSize: number
|
maxShareSize: number
|
||||||
) => {
|
) => {
|
||||||
|
const t = translateOutsideContext();
|
||||||
const link = `${appUrl}/share/${share.id}`;
|
const link = `${appUrl}/share/${share.id}`;
|
||||||
|
|
||||||
let shareSize: number = 0;
|
let shareSize: number = 0;
|
||||||
|
@ -29,34 +32,45 @@ const showShareInformationsModal = (
|
||||||
: moment(share.expiration).format("LLL");
|
: moment(share.expiration).format("LLL");
|
||||||
|
|
||||||
return modals.openModal({
|
return modals.openModal({
|
||||||
title: "Share informations",
|
title: t("account.shares.modal.share-informations"),
|
||||||
|
|
||||||
children: (
|
children: (
|
||||||
<Stack align="stretch" spacing="md">
|
<Stack align="stretch" spacing="md">
|
||||||
<Text size="sm" color="lightgray">
|
<Text size="sm" color="lightgray">
|
||||||
<b>ID:</b> {share.id}
|
<b>
|
||||||
|
<FormattedMessage id="account.shares.table.id" />:{" "}
|
||||||
|
</b>
|
||||||
|
{share.id}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text size="sm" color="lightgray">
|
<Text size="sm" color="lightgray">
|
||||||
<b>Description:</b> {share.description || "No description"}
|
<b>
|
||||||
|
<FormattedMessage id="account.shares.table.description" />:{" "}
|
||||||
|
</b>
|
||||||
|
{share.description || "No description"}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text size="sm" color="lightgray">
|
<Text size="sm" color="lightgray">
|
||||||
<b>Created at:</b> {formattedCreatedAt}
|
<b>
|
||||||
|
<FormattedMessage id="account.shares.table.createdAt" />:{" "}
|
||||||
|
</b>
|
||||||
|
{formattedCreatedAt}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text size="sm" color="lightgray">
|
<Text size="sm" color="lightgray">
|
||||||
<b>Expires at:</b> {formattedExpiration}
|
<b>
|
||||||
|
<FormattedMessage id="account.shares.table.expiresAt" />:{" "}
|
||||||
|
</b>
|
||||||
|
{formattedExpiration}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<CopyTextField link={link} />
|
<CopyTextField link={link} />
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Text size="sm" color="lightgray">
|
<Text size="sm" color="lightgray">
|
||||||
<b>Size:</b> {formattedShareSize} / {formattedMaxShareSize} (
|
<b>
|
||||||
|
<FormattedMessage id="account.shares.table.size" />:{" "}
|
||||||
|
</b>
|
||||||
|
{formattedShareSize} / {formattedMaxShareSize} (
|
||||||
{shareSizeProgress.toFixed(1)}%)
|
{shareSizeProgress.toFixed(1)}%)
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
import { Stack, TextInput } from "@mantine/core";
|
import { Stack, TextInput } from "@mantine/core";
|
||||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
|
import { translateOutsideContext } from "../../hooks/useTranslate.hook";
|
||||||
|
|
||||||
const showShareLinkModal = (
|
const showShareLinkModal = (
|
||||||
modals: ModalsContextProps,
|
modals: ModalsContextProps,
|
||||||
shareId: string,
|
shareId: string,
|
||||||
appUrl: string
|
appUrl: string
|
||||||
) => {
|
) => {
|
||||||
|
const t = translateOutsideContext();
|
||||||
const link = `${appUrl}/share/${shareId}`;
|
const link = `${appUrl}/share/${shareId}`;
|
||||||
return modals.openModal({
|
return modals.openModal({
|
||||||
title: "Share link",
|
title: t("account.shares.modal.share-link"),
|
||||||
children: (
|
children: (
|
||||||
<Stack align="stretch">
|
<Stack align="stretch">
|
||||||
<TextInput variant="filled" value={link} />
|
<TextInput variant="filled" value={link} />
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Dispatch, SetStateAction } from "react";
|
import { Dispatch, SetStateAction } from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import useConfig from "../../../hooks/config.hook";
|
import useConfig from "../../../hooks/config.hook";
|
||||||
import Logo from "../../Logo";
|
import Logo from "../../Logo";
|
||||||
|
|
||||||
|
@ -42,7 +43,7 @@ const ConfigurationHeader = ({
|
||||||
</Link>
|
</Link>
|
||||||
<MediaQuery smallerThan="sm" styles={{ display: "none" }}>
|
<MediaQuery smallerThan="sm" styles={{ display: "none" }}>
|
||||||
<Button variant="light" component={Link} href="/admin">
|
<Button variant="light" component={Link} href="/admin">
|
||||||
Go back
|
<FormattedMessage id="common.button.go-back" />
|
||||||
</Button>
|
</Button>
|
||||||
</MediaQuery>
|
</MediaQuery>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Dispatch, SetStateAction } from "react";
|
import { Dispatch, SetStateAction } from "react";
|
||||||
import { TbAt, TbMail, TbShare, TbSquare } from "react-icons/tb";
|
import { TbAt, TbMail, TbShare, TbSquare } from "react-icons/tb";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
const categories = [
|
const categories = [
|
||||||
{ name: "General", icon: <TbSquare /> },
|
{ name: "General", icon: <TbSquare /> },
|
||||||
|
@ -53,7 +54,7 @@ const ConfigurationNavBar = ({
|
||||||
>
|
>
|
||||||
<Navbar.Section>
|
<Navbar.Section>
|
||||||
<Text size="xs" color="dimmed" mb="sm">
|
<Text size="xs" color="dimmed" mb="sm">
|
||||||
Configuration
|
<FormattedMessage id="admin.config.title" />
|
||||||
</Text>
|
</Text>
|
||||||
<Stack spacing="xs">
|
<Stack spacing="xs">
|
||||||
{categories.map((category) => (
|
{categories.map((category) => (
|
||||||
|
@ -79,7 +80,11 @@ const ConfigurationNavBar = ({
|
||||||
>
|
>
|
||||||
{category.icon}
|
{category.icon}
|
||||||
</ThemeIcon>
|
</ThemeIcon>
|
||||||
<Text size="sm">{category.name}</Text>
|
<Text size="sm">
|
||||||
|
<FormattedMessage
|
||||||
|
id={`admin.config.category.${category.name.toLowerCase()}`}
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
|
@ -87,7 +92,7 @@ const ConfigurationNavBar = ({
|
||||||
</Navbar.Section>
|
</Navbar.Section>
|
||||||
<MediaQuery largerThan="sm" styles={{ display: "none" }}>
|
<MediaQuery largerThan="sm" styles={{ display: "none" }}>
|
||||||
<Button mt="xl" variant="light" component={Link} href="/admin">
|
<Button mt="xl" variant="light" component={Link} href="/admin">
|
||||||
Go back
|
<FormattedMessage id="common.button.go-back" />
|
||||||
</Button>
|
</Button>
|
||||||
</MediaQuery>
|
</MediaQuery>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
|
|
|
@ -2,6 +2,8 @@ import { Box, FileInput, Group, Stack, Text, Title } from "@mantine/core";
|
||||||
import { useMediaQuery } from "@mantine/hooks";
|
import { useMediaQuery } from "@mantine/hooks";
|
||||||
import { Dispatch, SetStateAction } from "react";
|
import { Dispatch, SetStateAction } from "react";
|
||||||
import { TbUpload } from "react-icons/tb";
|
import { TbUpload } from "react-icons/tb";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
import useTranslate from "../../../hooks/useTranslate.hook";
|
||||||
|
|
||||||
const LogoConfigInput = ({
|
const LogoConfigInput = ({
|
||||||
logo,
|
logo,
|
||||||
|
@ -11,14 +13,16 @@ const LogoConfigInput = ({
|
||||||
setLogo: Dispatch<SetStateAction<File | null>>;
|
setLogo: Dispatch<SetStateAction<File | null>>;
|
||||||
}) => {
|
}) => {
|
||||||
const isMobile = useMediaQuery("(max-width: 560px)");
|
const isMobile = useMediaQuery("(max-width: 560px)");
|
||||||
|
const t = useTranslate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group position="apart">
|
<Group position="apart">
|
||||||
<Stack style={{ maxWidth: isMobile ? "100%" : "40%" }} spacing={0}>
|
<Stack style={{ maxWidth: isMobile ? "100%" : "40%" }} spacing={0}>
|
||||||
<Title order={6}>Logo</Title>
|
<Title order={6}>
|
||||||
|
<FormattedMessage id="admin.config.general.logo" />
|
||||||
|
</Title>
|
||||||
<Text color="dimmed" size="sm" mb="xs">
|
<Text color="dimmed" size="sm" mb="xs">
|
||||||
Change your logo by uploading a new image. The image must be a PNG and
|
<FormattedMessage id="admin.config.general.logo.description" />
|
||||||
should have the format 1:1.
|
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack></Stack>
|
<Stack></Stack>
|
||||||
|
@ -29,7 +33,7 @@ const LogoConfigInput = ({
|
||||||
value={logo}
|
value={logo}
|
||||||
onChange={(v) => setLogo(v)}
|
onChange={(v) => setLogo(v)}
|
||||||
accept=".png"
|
accept=".png"
|
||||||
placeholder="Pick image"
|
placeholder={t("admin.config.general.logo.placeholder")}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Button, Stack, Text, Textarea } from "@mantine/core";
|
import { Button, Stack, Text, Textarea } from "@mantine/core";
|
||||||
import { useModals } from "@mantine/modals";
|
import { useModals } from "@mantine/modals";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import useUser from "../../../hooks/user.hook";
|
import useUser from "../../../hooks/user.hook";
|
||||||
import configService from "../../../services/config.service";
|
import configService from "../../../services/config.service";
|
||||||
import toast from "../../../utils/toast.util";
|
import toast from "../../../utils/toast.util";
|
||||||
|
@ -65,7 +66,7 @@ const TestEmailButton = ({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Send test email
|
<FormattedMessage id="admin.config.smtp.button.test" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { useModals } from "@mantine/modals";
|
||||||
import { TbCheck, TbEdit, TbTrash } from "react-icons/tb";
|
import { TbCheck, TbEdit, TbTrash } from "react-icons/tb";
|
||||||
import User from "../../../types/user.type";
|
import User from "../../../types/user.type";
|
||||||
import showUpdateUserModal from "./showUpdateUserModal";
|
import showUpdateUserModal from "./showUpdateUserModal";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
|
||||||
const ManageUserTable = ({
|
const ManageUserTable = ({
|
||||||
users,
|
users,
|
||||||
|
@ -22,9 +23,15 @@ const ManageUserTable = ({
|
||||||
<Table verticalSpacing="sm">
|
<Table verticalSpacing="sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Username</th>
|
<th>
|
||||||
<th>Email</th>
|
<FormattedMessage id="admin.users.table.username" />
|
||||||
<th>Admin</th>
|
</th>
|
||||||
|
<th>
|
||||||
|
<FormattedMessage id="admin.users.table.email" />
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<FormattedMessage id="admin.users.table.admin" />
|
||||||
|
</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
|
@ -8,7 +8,9 @@ import {
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useForm, yupResolver } from "@mantine/form";
|
import { useForm, yupResolver } from "@mantine/form";
|
||||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
import useTranslate from "../../../hooks/useTranslate.hook";
|
||||||
import userService from "../../../services/user.service";
|
import userService from "../../../services/user.service";
|
||||||
import toast from "../../../utils/toast.util";
|
import toast from "../../../utils/toast.util";
|
||||||
|
|
||||||
|
@ -34,6 +36,7 @@ const Body = ({
|
||||||
smtpEnabled: boolean;
|
smtpEnabled: boolean;
|
||||||
getUsers: () => void;
|
getUsers: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const t = useTranslate();
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
username: "",
|
username: "",
|
||||||
|
@ -44,9 +47,14 @@ const Body = ({
|
||||||
},
|
},
|
||||||
validate: yupResolver(
|
validate: yupResolver(
|
||||||
yup.object().shape({
|
yup.object().shape({
|
||||||
email: yup.string().email(),
|
email: yup.string().email(t("common.error.invalid-email")),
|
||||||
username: yup.string().min(3),
|
username: yup
|
||||||
password: yup.string().min(8).optional(),
|
.string()
|
||||||
|
.min(3, t("common.error.too-short", { length: 3 })),
|
||||||
|
password: yup
|
||||||
|
.string()
|
||||||
|
.min(8, t("common.error.too-short", { length: 8 }))
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
@ -65,14 +73,22 @@ const Body = ({
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Stack>
|
<Stack>
|
||||||
<TextInput label="Username" {...form.getInputProps("username")} />
|
<TextInput
|
||||||
<TextInput label="Email" {...form.getInputProps("email")} />
|
label={t("admin.users.modal.create.username")}
|
||||||
|
{...form.getInputProps("username")}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label={t("admin.users.modal.create.email")}
|
||||||
|
{...form.getInputProps("email")}
|
||||||
|
/>
|
||||||
{smtpEnabled && (
|
{smtpEnabled && (
|
||||||
<Switch
|
<Switch
|
||||||
mt="xs"
|
mt="xs"
|
||||||
labelPosition="left"
|
labelPosition="left"
|
||||||
label="Set password manually"
|
label={t("admin.users.modal.create.manual-password")}
|
||||||
description="If not checked, the user will receive an email with a link to set their password."
|
description={t(
|
||||||
|
"admin.users.modal.create.manual-password.description"
|
||||||
|
)}
|
||||||
{...form.getInputProps("setPasswordManually", {
|
{...form.getInputProps("setPasswordManually", {
|
||||||
type: "checkbox",
|
type: "checkbox",
|
||||||
})}
|
})}
|
||||||
|
@ -80,7 +96,7 @@ const Body = ({
|
||||||
)}
|
)}
|
||||||
{(form.values.setPasswordManually || !smtpEnabled) && (
|
{(form.values.setPasswordManually || !smtpEnabled) && (
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
label="Password"
|
label={t("admin.users.modal.create.password")}
|
||||||
{...form.getInputProps("password")}
|
{...form.getInputProps("password")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -93,12 +109,14 @@ const Body = ({
|
||||||
}}
|
}}
|
||||||
mt="xs"
|
mt="xs"
|
||||||
labelPosition="left"
|
labelPosition="left"
|
||||||
label="Admin privileges"
|
label={t("admin.users.modal.create.admin")}
|
||||||
description="If checked, the user will be able to access the admin panel."
|
description={t("admin.users.modal.create.admin.description")}
|
||||||
{...form.getInputProps("isAdmin", { type: "checkbox" })}
|
{...form.getInputProps("isAdmin", { type: "checkbox" })}
|
||||||
/>
|
/>
|
||||||
<Group position="right">
|
<Group position="right">
|
||||||
<Button type="submit">Create</Button>
|
<Button type="submit">
|
||||||
|
<FormattedMessage id="common.button.create" />
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -9,7 +9,11 @@ import {
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useForm, yupResolver } from "@mantine/form";
|
import { useForm, yupResolver } from "@mantine/form";
|
||||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
import useTranslate, {
|
||||||
|
translateOutsideContext,
|
||||||
|
} from "../../../hooks/useTranslate.hook";
|
||||||
import userService from "../../../services/user.service";
|
import userService from "../../../services/user.service";
|
||||||
import User from "../../../types/user.type";
|
import User from "../../../types/user.type";
|
||||||
import toast from "../../../utils/toast.util";
|
import toast from "../../../utils/toast.util";
|
||||||
|
@ -19,8 +23,9 @@ const showUpdateUserModal = (
|
||||||
user: User,
|
user: User,
|
||||||
getUsers: () => void
|
getUsers: () => void
|
||||||
) => {
|
) => {
|
||||||
|
const t = translateOutsideContext();
|
||||||
return modals.openModal({
|
return modals.openModal({
|
||||||
title: `Update ${user.username}`,
|
title: t("admin.users.edit.update.title", { username: user.username }),
|
||||||
children: <Body user={user} modals={modals} getUsers={getUsers} />,
|
children: <Body user={user} modals={modals} getUsers={getUsers} />,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -34,6 +39,8 @@ const Body = ({
|
||||||
user: User;
|
user: User;
|
||||||
getUsers: () => void;
|
getUsers: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const t = useTranslate();
|
||||||
|
|
||||||
const accountForm = useForm({
|
const accountForm = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
username: user.username,
|
username: user.username,
|
||||||
|
@ -42,8 +49,10 @@ const Body = ({
|
||||||
},
|
},
|
||||||
validate: yupResolver(
|
validate: yupResolver(
|
||||||
yup.object().shape({
|
yup.object().shape({
|
||||||
email: yup.string().email(),
|
email: yup.string().email(t("common.error.invalid-email")),
|
||||||
username: yup.string().min(3),
|
username: yup
|
||||||
|
.string()
|
||||||
|
.min(3, t("common.error.too-short", { length: 3 })),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
@ -54,7 +63,9 @@ const Body = ({
|
||||||
},
|
},
|
||||||
validate: yupResolver(
|
validate: yupResolver(
|
||||||
yup.object().shape({
|
yup.object().shape({
|
||||||
password: yup.string().min(8),
|
password: yup
|
||||||
|
.string()
|
||||||
|
.min(8, t("common.error.too-short", { length: 8 })),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
@ -75,21 +86,26 @@ const Body = ({
|
||||||
>
|
>
|
||||||
<Stack>
|
<Stack>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Username"
|
label={t("admin.users.table.username")}
|
||||||
{...accountForm.getInputProps("username")}
|
{...accountForm.getInputProps("username")}
|
||||||
/>
|
/>
|
||||||
<TextInput label="Email" {...accountForm.getInputProps("email")} />
|
<TextInput
|
||||||
|
label={t("admin.users.table.email")}
|
||||||
|
{...accountForm.getInputProps("email")}
|
||||||
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
mt="xs"
|
mt="xs"
|
||||||
labelPosition="left"
|
labelPosition="left"
|
||||||
label="Admin privileges"
|
label={t("admin.users.edit.update.admin-privileges")}
|
||||||
{...accountForm.getInputProps("isAdmin", { type: "checkbox" })}
|
{...accountForm.getInputProps("isAdmin", { type: "checkbox" })}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
<Accordion>
|
<Accordion>
|
||||||
<Accordion.Item sx={{ borderBottom: "none" }} value="changePassword">
|
<Accordion.Item sx={{ borderBottom: "none" }} value="changePassword">
|
||||||
<Accordion.Control px={0}>Change password</Accordion.Control>
|
<Accordion.Control px={0}>
|
||||||
|
<FormattedMessage id="admin.users.edit.update.change-password.title" />
|
||||||
|
</Accordion.Control>
|
||||||
<Accordion.Panel>
|
<Accordion.Panel>
|
||||||
<form
|
<form
|
||||||
onSubmit={passwordForm.onSubmit(async (values) => {
|
onSubmit={passwordForm.onSubmit(async (values) => {
|
||||||
|
@ -97,17 +113,21 @@ const Body = ({
|
||||||
.update(user.id, {
|
.update(user.id, {
|
||||||
password: values.password,
|
password: values.password,
|
||||||
})
|
})
|
||||||
.then(() => toast.success("Password changed successfully"))
|
.then(() =>
|
||||||
|
toast.success(
|
||||||
|
t("admin.users.edit.update.notify.password.success")
|
||||||
|
)
|
||||||
|
)
|
||||||
.catch(toast.axiosError);
|
.catch(toast.axiosError);
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Stack>
|
<Stack>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
label="New password"
|
label={t("admin.users.edit.update.change-password.field")}
|
||||||
{...passwordForm.getInputProps("password")}
|
{...passwordForm.getInputProps("password")}
|
||||||
/>
|
/>
|
||||||
<Button variant="light" type="submit">
|
<Button variant="light" type="submit">
|
||||||
Save new password
|
<FormattedMessage id="admin.users.edit.update.change-password.button" />
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
|
@ -116,7 +136,7 @@ const Body = ({
|
||||||
</Accordion>
|
</Accordion>
|
||||||
<Group position="right">
|
<Group position="right">
|
||||||
<Button type="submit" form="accountForm">
|
<Button type="submit" form="accountForm">
|
||||||
Save
|
<FormattedMessage id="common.button.save" />
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
@ -15,8 +15,10 @@ import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { TbInfoCircle } from "react-icons/tb";
|
import { TbInfoCircle } from "react-icons/tb";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
import useConfig from "../../hooks/config.hook";
|
import useConfig from "../../hooks/config.hook";
|
||||||
|
import useTranslate from "../../hooks/useTranslate.hook";
|
||||||
import useUser from "../../hooks/user.hook";
|
import useUser from "../../hooks/user.hook";
|
||||||
import authService from "../../services/auth.service";
|
import authService from "../../services/auth.service";
|
||||||
import toast from "../../utils/toast.util";
|
import toast from "../../utils/toast.util";
|
||||||
|
@ -24,14 +26,18 @@ import toast from "../../utils/toast.util";
|
||||||
const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
|
const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const t = useTranslate();
|
||||||
const { refreshUser } = useUser();
|
const { refreshUser } = useUser();
|
||||||
|
|
||||||
const [showTotp, setShowTotp] = React.useState(false);
|
const [showTotp, setShowTotp] = React.useState(false);
|
||||||
const [loginToken, setLoginToken] = React.useState("");
|
const [loginToken, setLoginToken] = React.useState("");
|
||||||
|
|
||||||
const validationSchema = yup.object().shape({
|
const validationSchema = yup.object().shape({
|
||||||
emailOrUsername: yup.string().required(),
|
emailOrUsername: yup.string().required(t("common.error.field-required")),
|
||||||
password: yup.string().min(8).required(),
|
password: yup
|
||||||
|
.string()
|
||||||
|
.min(8, t("common.error.too-short", { length: 8 }))
|
||||||
|
.required(t("common.error.field-required")),
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
|
@ -54,8 +60,8 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
|
||||||
icon: <TbInfoCircle />,
|
icon: <TbInfoCircle />,
|
||||||
color: "blue",
|
color: "blue",
|
||||||
radius: "md",
|
radius: "md",
|
||||||
title: "Two-factor authentication required",
|
title: t("signIn.notify.totp-required.title"),
|
||||||
message: "Please enter your two-factor authentication code",
|
message: t("signIn.notify.totp-required.description"),
|
||||||
});
|
});
|
||||||
setLoginToken(response.data["loginToken"]);
|
setLoginToken(response.data["loginToken"]);
|
||||||
} else {
|
} else {
|
||||||
|
@ -88,13 +94,13 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
|
||||||
return (
|
return (
|
||||||
<Container size={420} my={40}>
|
<Container size={420} my={40}>
|
||||||
<Title order={2} align="center" weight={900}>
|
<Title order={2} align="center" weight={900}>
|
||||||
Welcome back
|
<FormattedMessage id="signin.title" />
|
||||||
</Title>
|
</Title>
|
||||||
{config.get("share.allowRegistration") && (
|
{config.get("share.allowRegistration") && (
|
||||||
<Text color="dimmed" size="sm" align="center" mt={5}>
|
<Text color="dimmed" size="sm" align="center" mt={5}>
|
||||||
You don't have an account yet?{" "}
|
<FormattedMessage id="signin.description" />{" "}
|
||||||
<Anchor component={Link} href={"signUp"} size="sm">
|
<Anchor component={Link} href={"signUp"} size="sm">
|
||||||
{"Sign up"}
|
<FormattedMessage id="signin.button.signup" />
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
@ -107,20 +113,20 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Email or username"
|
label={t("signin.input.email-or-username")}
|
||||||
placeholder="Your email or username"
|
placeholder={t("signin.input.email-or-username.placeholder")}
|
||||||
{...form.getInputProps("emailOrUsername")}
|
{...form.getInputProps("emailOrUsername")}
|
||||||
/>
|
/>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
label="Password"
|
label={t("signin.input.password")}
|
||||||
placeholder="Your password"
|
placeholder={t("signin.input.password.placeholder")}
|
||||||
mt="md"
|
mt="md"
|
||||||
{...form.getInputProps("password")}
|
{...form.getInputProps("password")}
|
||||||
/>
|
/>
|
||||||
{showTotp && (
|
{showTotp && (
|
||||||
<TextInput
|
<TextInput
|
||||||
variant="filled"
|
variant="filled"
|
||||||
label="Code"
|
label={t("account.modal.totp.code")}
|
||||||
placeholder="******"
|
placeholder="******"
|
||||||
mt="md"
|
mt="md"
|
||||||
{...form.getInputProps("totp")}
|
{...form.getInputProps("totp")}
|
||||||
|
@ -129,12 +135,12 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
|
||||||
{config.get("smtp.enabled") && (
|
{config.get("smtp.enabled") && (
|
||||||
<Group position="right" mt="xs">
|
<Group position="right" mt="xs">
|
||||||
<Anchor component={Link} href="/auth/resetPassword" size="xs">
|
<Anchor component={Link} href="/auth/resetPassword" size="xs">
|
||||||
Forgot password?
|
<FormattedMessage id="resetPassword.title" />
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</Group>
|
</Group>
|
||||||
)}
|
)}
|
||||||
<Button fullWidth mt="xl" type="submit">
|
<Button fullWidth mt="xl" type="submit">
|
||||||
Sign in
|
<FormattedMessage id="signin.button.submit" />
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
|
@ -11,8 +11,10 @@ import {
|
||||||
import { useForm, yupResolver } from "@mantine/form";
|
import { useForm, yupResolver } from "@mantine/form";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
import useConfig from "../../hooks/config.hook";
|
import useConfig from "../../hooks/config.hook";
|
||||||
|
import useTranslate from "../../hooks/useTranslate.hook";
|
||||||
import useUser from "../../hooks/user.hook";
|
import useUser from "../../hooks/user.hook";
|
||||||
import authService from "../../services/auth.service";
|
import authService from "../../services/auth.service";
|
||||||
import toast from "../../utils/toast.util";
|
import toast from "../../utils/toast.util";
|
||||||
|
@ -20,12 +22,19 @@ import toast from "../../utils/toast.util";
|
||||||
const SignUpForm = () => {
|
const SignUpForm = () => {
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const t = useTranslate();
|
||||||
const { refreshUser } = useUser();
|
const { refreshUser } = useUser();
|
||||||
|
|
||||||
const validationSchema = yup.object().shape({
|
const validationSchema = yup.object().shape({
|
||||||
email: yup.string().email().required(),
|
email: yup.string().email(t("common.error.invalid-email")).required(),
|
||||||
username: yup.string().min(3).required(),
|
username: yup
|
||||||
password: yup.string().min(8).required(),
|
.string()
|
||||||
|
.min(3, t("common.error.too-short", { length: 3 }))
|
||||||
|
.required(t("common.error.field-required")),
|
||||||
|
password: yup
|
||||||
|
.string()
|
||||||
|
.min(8, t("common.error.too-short", { length: 8 }))
|
||||||
|
.required(t("common.error.field-required")),
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
|
@ -54,13 +63,13 @@ const SignUpForm = () => {
|
||||||
return (
|
return (
|
||||||
<Container size={420} my={40}>
|
<Container size={420} my={40}>
|
||||||
<Title order={2} align="center" weight={900}>
|
<Title order={2} align="center" weight={900}>
|
||||||
Sign up
|
<FormattedMessage id="signup.title" />
|
||||||
</Title>
|
</Title>
|
||||||
{config.get("share.allowRegistration") && (
|
{config.get("share.allowRegistration") && (
|
||||||
<Text color="dimmed" size="sm" align="center" mt={5}>
|
<Text color="dimmed" size="sm" align="center" mt={5}>
|
||||||
You have an account already?{" "}
|
<FormattedMessage id="signup.description" />{" "}
|
||||||
<Anchor component={Link} href={"signIn"} size="sm">
|
<Anchor component={Link} href={"signIn"} size="sm">
|
||||||
Sign in
|
<FormattedMessage id="signup.button.signin" />
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
@ -71,24 +80,24 @@ const SignUpForm = () => {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Username"
|
label={t("signup.input.username")}
|
||||||
placeholder="Your username"
|
placeholder={t("signup.input.username.placeholder")}
|
||||||
{...form.getInputProps("username")}
|
{...form.getInputProps("username")}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Email"
|
label={t("signup.input.email")}
|
||||||
placeholder="Your email"
|
placeholder={t("signup.input.email.placeholder")}
|
||||||
mt="md"
|
mt="md"
|
||||||
{...form.getInputProps("email")}
|
{...form.getInputProps("email")}
|
||||||
/>
|
/>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
label="Password"
|
label={t("signin.input.password")}
|
||||||
placeholder="Your password"
|
placeholder={t("signin.input.password.placeholder")}
|
||||||
mt="md"
|
mt="md"
|
||||||
{...form.getInputProps("password")}
|
{...form.getInputProps("password")}
|
||||||
/>
|
/>
|
||||||
<Button fullWidth mt="xl" type="submit">
|
<Button fullWidth mt="xl" type="submit">
|
||||||
Let's get started
|
<FormattedMessage id="signup.button.submit" />
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import Link from "next/link";
|
||||||
import { TbDoorExit, TbSettings, TbUser } from "react-icons/tb";
|
import { TbDoorExit, TbSettings, TbUser } from "react-icons/tb";
|
||||||
import useUser from "../../hooks/user.hook";
|
import useUser from "../../hooks/user.hook";
|
||||||
import authService from "../../services/auth.service";
|
import authService from "../../services/auth.service";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
|
||||||
const ActionAvatar = () => {
|
const ActionAvatar = () => {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
@ -16,7 +17,7 @@ const ActionAvatar = () => {
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
<Menu.Item component={Link} href="/account" icon={<TbUser size={14} />}>
|
<Menu.Item component={Link} href="/account" icon={<TbUser size={14} />}>
|
||||||
My account
|
<FormattedMessage id="navbar.avatar.account" />
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
{user!.isAdmin && (
|
{user!.isAdmin && (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
|
@ -24,7 +25,7 @@ const ActionAvatar = () => {
|
||||||
href="/admin"
|
href="/admin"
|
||||||
icon={<TbSettings size={14} />}
|
icon={<TbSettings size={14} />}
|
||||||
>
|
>
|
||||||
Administration
|
<FormattedMessage id="navbar.avatar.admin" />
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -34,7 +35,7 @@ const ActionAvatar = () => {
|
||||||
}}
|
}}
|
||||||
icon={<TbDoorExit size={14} />}
|
icon={<TbDoorExit size={14} />}
|
||||||
>
|
>
|
||||||
Sign out
|
<FormattedMessage id="navbar.avatar.signout" />
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { useRouter } from "next/router";
|
||||||
import { ReactNode, useEffect, useState } from "react";
|
import { ReactNode, useEffect, useState } from "react";
|
||||||
import useConfig from "../../hooks/config.hook";
|
import useConfig from "../../hooks/config.hook";
|
||||||
import useUser from "../../hooks/user.hook";
|
import useUser from "../../hooks/user.hook";
|
||||||
|
import useTranslate from "../../hooks/useTranslate.hook";
|
||||||
import Logo from "../Logo";
|
import Logo from "../Logo";
|
||||||
import ActionAvatar from "./ActionAvatar";
|
import ActionAvatar from "./ActionAvatar";
|
||||||
import NavbarShareMenu from "./NavbarShareMenu";
|
import NavbarShareMenu from "./NavbarShareMenu";
|
||||||
|
@ -112,6 +113,7 @@ const Header = () => {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
|
const t = useTranslate();
|
||||||
|
|
||||||
const [opened, toggleOpened] = useDisclosure(false);
|
const [opened, toggleOpened] = useDisclosure(false);
|
||||||
|
|
||||||
|
@ -124,7 +126,7 @@ const Header = () => {
|
||||||
const authenticatedLinks: NavLink[] = [
|
const authenticatedLinks: NavLink[] = [
|
||||||
{
|
{
|
||||||
link: "/upload",
|
link: "/upload",
|
||||||
label: "Upload",
|
label: t("navbar.upload"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: <NavbarShareMenu />,
|
component: <NavbarShareMenu />,
|
||||||
|
@ -137,27 +139,27 @@ const Header = () => {
|
||||||
let unauthenticatedLinks: NavLink[] = [
|
let unauthenticatedLinks: NavLink[] = [
|
||||||
{
|
{
|
||||||
link: "/auth/signIn",
|
link: "/auth/signIn",
|
||||||
label: "Sign in",
|
label: t("navbar.signin"),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (config.get("share.allowUnauthenticatedShares")) {
|
if (config.get("share.allowUnauthenticatedShares")) {
|
||||||
unauthenticatedLinks.unshift({
|
unauthenticatedLinks.unshift({
|
||||||
link: "/upload",
|
link: "/upload",
|
||||||
label: "Upload",
|
label: t("navbar.upload"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.get("general.showHomePage"))
|
if (config.get("general.showHomePage"))
|
||||||
unauthenticatedLinks.unshift({
|
unauthenticatedLinks.unshift({
|
||||||
link: "/",
|
link: "/",
|
||||||
label: "Home",
|
label: t("navbar.home"),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (config.get("share.allowRegistration"))
|
if (config.get("share.allowRegistration"))
|
||||||
unauthenticatedLinks.push({
|
unauthenticatedLinks.push({
|
||||||
link: "/auth/signUp",
|
link: "/auth/signUp",
|
||||||
label: "Sign up",
|
label: t("navbar.signup"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { classes, cx } = useStyles();
|
const { classes, cx } = useStyles();
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { ActionIcon, Menu } from "@mantine/core";
|
import { ActionIcon, Menu } from "@mantine/core";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { TbArrowLoopLeft, TbLink } from "react-icons/tb";
|
import { TbArrowLoopLeft, TbLink } from "react-icons/tb";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
const NavbarShareMneu = () => {
|
const NavbarShareMneu = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -12,14 +13,14 @@ const NavbarShareMneu = () => {
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
<Menu.Item component={Link} href="/account/shares" icon={<TbLink />}>
|
<Menu.Item component={Link} href="/account/shares" icon={<TbLink />}>
|
||||||
My shares
|
<FormattedMessage id="navbar.links.shares" />
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
component={Link}
|
component={Link}
|
||||||
href="/account/reverseShares"
|
href="/account/reverseShares"
|
||||||
icon={<TbArrowLoopLeft />}
|
icon={<TbArrowLoopLeft />}
|
||||||
>
|
>
|
||||||
Reverse shares
|
<FormattedMessage id="navbar.links.reverse" />
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import { Button } from "@mantine/core";
|
import { Button } from "@mantine/core";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
import useTranslate from "../../hooks/useTranslate.hook";
|
||||||
import shareService from "../../services/share.service";
|
import shareService from "../../services/share.service";
|
||||||
import toast from "../../utils/toast.util";
|
import toast from "../../utils/toast.util";
|
||||||
|
|
||||||
const DownloadAllButton = ({ shareId }: { shareId: string }) => {
|
const DownloadAllButton = ({ shareId }: { shareId: string }) => {
|
||||||
const [isZipReady, setIsZipReady] = useState(false);
|
const [isZipReady, setIsZipReady] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const t = useTranslate();
|
||||||
|
|
||||||
const downloadAll = async () => {
|
const downloadAll = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
await shareService
|
await shareService
|
||||||
|
@ -39,13 +43,13 @@ const DownloadAllButton = ({ shareId }: { shareId: string }) => {
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!isZipReady) {
|
if (!isZipReady) {
|
||||||
toast.error("The share is preparing. Try again in a few minutes.");
|
toast.error(t("share.notify.download-all-preparing"));
|
||||||
} else {
|
} else {
|
||||||
downloadAll();
|
downloadAll();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Download all
|
<FormattedMessage id="share.download-all" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,6 +19,8 @@ import { byteToHumanSizeString } from "../../utils/fileSize.util";
|
||||||
import toast from "../../utils/toast.util";
|
import toast from "../../utils/toast.util";
|
||||||
import TableSortIcon, { TableSort } from "../core/SortIcon";
|
import TableSortIcon, { TableSort } from "../core/SortIcon";
|
||||||
import showFilePreviewModal from "./modals/showFilePreviewModal";
|
import showFilePreviewModal from "./modals/showFilePreviewModal";
|
||||||
|
import useTranslate from "../../hooks/useTranslate.hook";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
const FileList = ({
|
const FileList = ({
|
||||||
files,
|
files,
|
||||||
|
@ -34,6 +36,7 @@ const FileList = ({
|
||||||
const clipboard = useClipboard();
|
const clipboard = useClipboard();
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
|
const t = useTranslate();
|
||||||
|
|
||||||
const [sort, setSort] = useState<TableSort>({
|
const [sort, setSort] = useState<TableSort>({
|
||||||
property: undefined,
|
property: undefined,
|
||||||
|
@ -68,10 +71,10 @@ const FileList = ({
|
||||||
|
|
||||||
if (window.isSecureContext) {
|
if (window.isSecureContext) {
|
||||||
clipboard.copy(link);
|
clipboard.copy(link);
|
||||||
toast.success("Your file link was copied to the keyboard.");
|
toast.success(t("common.notify.copied"));
|
||||||
} else {
|
} else {
|
||||||
modals.openModal({
|
modals.openModal({
|
||||||
title: "File link",
|
title: t("share.modal.file-link"),
|
||||||
children: (
|
children: (
|
||||||
<Stack align="stretch">
|
<Stack align="stretch">
|
||||||
<TextInput variant="filled" value={link} />
|
<TextInput variant="filled" value={link} />
|
||||||
|
@ -90,13 +93,13 @@ const FileList = ({
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
<Group spacing="xs">
|
<Group spacing="xs">
|
||||||
Name
|
<FormattedMessage id="share.table.name" />
|
||||||
<TableSortIcon sort={sort} setSort={setSort} property="name" />
|
<TableSortIcon sort={sort} setSort={setSort} property="name" />
|
||||||
</Group>
|
</Group>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<Group spacing="xs">
|
<Group spacing="xs">
|
||||||
Size
|
<FormattedMessage id="share.table.size" />
|
||||||
<TableSortIcon sort={sort} setSort={setSort} property="size" />
|
<TableSortIcon sort={sort} setSort={setSort} property="size" />
|
||||||
</Group>
|
</Group>
|
||||||
</th>
|
</th>
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { Button, Center, Stack, Text, Title } from "@mantine/core";
|
||||||
import { modals } from "@mantine/modals";
|
import { modals } from "@mantine/modals";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import React, { Dispatch, SetStateAction, useEffect, useState } from "react";
|
import React, { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import api from "../../services/api.service";
|
import api from "../../services/api.service";
|
||||||
|
|
||||||
const FilePreviewContext = React.createContext<{
|
const FilePreviewContext = React.createContext<{
|
||||||
|
@ -144,10 +145,11 @@ const UnSupportedFile = () => {
|
||||||
return (
|
return (
|
||||||
<Center style={{ minHeight: 200 }}>
|
<Center style={{ minHeight: 200 }}>
|
||||||
<Stack align="center" spacing={10}>
|
<Stack align="center" spacing={10}>
|
||||||
<Title order={3}>Preview not supported</Title>
|
<Title order={3}>
|
||||||
|
<FormattedMessage id="share.modal.file-preview.error.not-supported.title" />
|
||||||
|
</Title>
|
||||||
<Text>
|
<Text>
|
||||||
A preview for thise file type is unsupported. Please download the file
|
<FormattedMessage id="share.modal.file-preview.error.not-supported.description" />
|
||||||
to view it.
|
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Center>
|
</Center>
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { Button, Stack } from "@mantine/core";
|
import { Button, Stack } from "@mantine/core";
|
||||||
import { useModals } from "@mantine/modals";
|
import { useModals } from "@mantine/modals";
|
||||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
import { translateOutsideContext } from "../../../hooks/useTranslate.hook";
|
||||||
import CopyTextField from "../../upload/CopyTextField";
|
import CopyTextField from "../../upload/CopyTextField";
|
||||||
|
|
||||||
const showCompletedReverseShareModal = (
|
const showCompletedReverseShareModal = (
|
||||||
|
@ -8,11 +10,12 @@ const showCompletedReverseShareModal = (
|
||||||
link: string,
|
link: string,
|
||||||
getReverseShares: () => void
|
getReverseShares: () => void
|
||||||
) => {
|
) => {
|
||||||
|
const t = translateOutsideContext();
|
||||||
return modals.openModal({
|
return modals.openModal({
|
||||||
closeOnClickOutside: false,
|
closeOnClickOutside: false,
|
||||||
withCloseButton: false,
|
withCloseButton: false,
|
||||||
closeOnEscape: false,
|
closeOnEscape: false,
|
||||||
title: "Reverse share link",
|
title: t("account.reverseShares.modal.reverse-share-link"),
|
||||||
children: <Body link={link} getReverseShares={getReverseShares} />,
|
children: <Body link={link} getReverseShares={getReverseShares} />,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -36,7 +39,7 @@ const Body = ({
|
||||||
getReverseShares();
|
getReverseShares();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Done
|
<FormattedMessage id="common.button.done" />
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|
|
@ -12,6 +12,8 @@ import {
|
||||||
import { useForm } from "@mantine/form";
|
import { useForm } from "@mantine/form";
|
||||||
import { useModals } from "@mantine/modals";
|
import { useModals } from "@mantine/modals";
|
||||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
import useTranslate from "../../../hooks/useTranslate.hook";
|
||||||
import shareService from "../../../services/share.service";
|
import shareService from "../../../services/share.service";
|
||||||
import { getExpirationPreview } from "../../../utils/date.util";
|
import { getExpirationPreview } from "../../../utils/date.util";
|
||||||
import toast from "../../../utils/toast.util";
|
import toast from "../../../utils/toast.util";
|
||||||
|
@ -42,6 +44,7 @@ const Body = ({
|
||||||
showSendEmailNotificationOption: boolean;
|
showSendEmailNotificationOption: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
|
const t = useTranslate();
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
|
@ -79,7 +82,7 @@ const Body = ({
|
||||||
max={99999}
|
max={99999}
|
||||||
precision={0}
|
precision={0}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
label="Share expiration"
|
label={t("account.reverseShares.modal.expiration.label")}
|
||||||
{...form.getInputProps("expiration_num")}
|
{...form.getInputProps("expiration_num")}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -91,27 +94,44 @@ const Body = ({
|
||||||
{
|
{
|
||||||
value: "-minutes",
|
value: "-minutes",
|
||||||
label:
|
label:
|
||||||
"Minute" + (form.values.expiration_num == 1 ? "" : "s"),
|
form.values.expiration_num == 1
|
||||||
|
? t("upload.modal.expires.minute-singular")
|
||||||
|
: t("upload.modal.expires.minute-plural"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "-hours",
|
value: "-hours",
|
||||||
label:
|
label:
|
||||||
"Hour" + (form.values.expiration_num == 1 ? "" : "s"),
|
form.values.expiration_num == 1
|
||||||
|
? t("upload.modal.expires.hour-singular")
|
||||||
|
: t("upload.modal.expires.hour-plural"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "-days",
|
value: "-days",
|
||||||
label:
|
label:
|
||||||
"Day" + (form.values.expiration_num == 1 ? "" : "s"),
|
form.values.expiration_num == 1
|
||||||
|
? t("upload.modal.expires.day-singular")
|
||||||
|
: t("upload.modal.expires.day-plural"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "-weeks",
|
value: "-weeks",
|
||||||
label:
|
label:
|
||||||
"Week" + (form.values.expiration_num == 1 ? "" : "s"),
|
form.values.expiration_num == 1
|
||||||
|
? t("upload.modal.expires.week-singular")
|
||||||
|
: t("upload.modal.expires.week-plural"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "-months",
|
value: "-months",
|
||||||
label:
|
label:
|
||||||
"Month" + (form.values.expiration_num == 1 ? "" : "s"),
|
form.values.expiration_num == 1
|
||||||
|
? t("upload.modal.expires.month-singular")
|
||||||
|
: t("upload.modal.expires.month-plural"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "-years",
|
||||||
|
label:
|
||||||
|
form.values.expiration_num == 1
|
||||||
|
? t("upload.modal.expires.year-singular")
|
||||||
|
: t("upload.modal.expires.year-plural"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
@ -125,11 +145,17 @@ const Body = ({
|
||||||
color: theme.colors.gray[6],
|
color: theme.colors.gray[6],
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{getExpirationPreview("reverse share", form)}
|
{getExpirationPreview(
|
||||||
|
{
|
||||||
|
expiresOn: t("account.reverseShare.expires-on"),
|
||||||
|
neverExpires: t("account.reverseShare.never-expires"),
|
||||||
|
},
|
||||||
|
form
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<FileSizeInput
|
<FileSizeInput
|
||||||
label="Max share size"
|
label={t("account.reverseShares.modal.max-size.label")}
|
||||||
value={form.values.maxShareSize}
|
value={form.values.maxShareSize}
|
||||||
onChange={(number) => form.setFieldValue("maxShareSize", number)}
|
onChange={(number) => form.setFieldValue("maxShareSize", number)}
|
||||||
/>
|
/>
|
||||||
|
@ -138,16 +164,18 @@ const Body = ({
|
||||||
max={1000}
|
max={1000}
|
||||||
precision={0}
|
precision={0}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
label="Max use count"
|
label={t("account.reverseShares.modal.max-use.label")}
|
||||||
description="The maximum number of times this reverse share link can be used"
|
description={t("account.reverseShares.modal.max-use.description")}
|
||||||
{...form.getInputProps("maxUseCount")}
|
{...form.getInputProps("maxUseCount")}
|
||||||
/>
|
/>
|
||||||
{showSendEmailNotificationOption && (
|
{showSendEmailNotificationOption && (
|
||||||
<Switch
|
<Switch
|
||||||
mt="xs"
|
mt="xs"
|
||||||
labelPosition="left"
|
labelPosition="left"
|
||||||
label="Send email notification"
|
label={t("account.reverseShares.modal.send-email")}
|
||||||
description="Send an email notification when a share is created with this reverse share link"
|
description={t(
|
||||||
|
"account.reverseShares.modal.send-email.description"
|
||||||
|
)}
|
||||||
{...form.getInputProps("sendEmailNotification", {
|
{...form.getInputProps("sendEmailNotification", {
|
||||||
type: "checkbox",
|
type: "checkbox",
|
||||||
})}
|
})}
|
||||||
|
@ -155,7 +183,7 @@ const Body = ({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button mt="md" type="submit">
|
<Button mt="md" type="submit">
|
||||||
Create
|
<FormattedMessage id="common.button.create" />
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
import { Button, PasswordInput, Stack, Text } from "@mantine/core";
|
import { Button, PasswordInput, Stack, Text } from "@mantine/core";
|
||||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
import useTranslate, {
|
||||||
|
translateOutsideContext,
|
||||||
|
} from "../../hooks/useTranslate.hook";
|
||||||
|
|
||||||
const showEnterPasswordModal = (
|
const showEnterPasswordModal = (
|
||||||
modals: ModalsContextProps,
|
modals: ModalsContextProps,
|
||||||
submitCallback: (password: string) => Promise<void>
|
submitCallback: (password: string) => Promise<void>
|
||||||
) => {
|
) => {
|
||||||
|
const t = translateOutsideContext();
|
||||||
return modals.openModal({
|
return modals.openModal({
|
||||||
closeOnClickOutside: false,
|
closeOnClickOutside: false,
|
||||||
withCloseButton: false,
|
withCloseButton: false,
|
||||||
closeOnEscape: false,
|
closeOnEscape: false,
|
||||||
title: "Password required",
|
title: t("share.modal.password.title"),
|
||||||
children: <Body submitCallback={submitCallback} />,
|
children: <Body submitCallback={submitCallback} />,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -22,10 +27,11 @@ const Body = ({
|
||||||
}) => {
|
}) => {
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [passwordWrong, setPasswordWrong] = useState(false);
|
const [passwordWrong, setPasswordWrong] = useState(false);
|
||||||
|
const t = useTranslate();
|
||||||
return (
|
return (
|
||||||
<Stack align="stretch">
|
<Stack align="stretch">
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
This access this share please enter the password for the share.
|
<FormattedMessage id="share.modal.password.description" />
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
|
@ -37,13 +43,15 @@ const Body = ({
|
||||||
<Stack>
|
<Stack>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
variant="filled"
|
variant="filled"
|
||||||
placeholder="Password"
|
placeholder={t("share.modal.password")}
|
||||||
error={passwordWrong && "Wrong password"}
|
error={passwordWrong && t("share.modal.error.invalid-password")}
|
||||||
onFocus={() => setPasswordWrong(false)}
|
onFocus={() => setPasswordWrong(false)}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
value={password}
|
value={password}
|
||||||
/>
|
/>
|
||||||
<Button type="submit">Submit</Button>
|
<Button type="submit">
|
||||||
|
<FormattedMessage id="common.button.submit" />
|
||||||
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { Button, Stack, Text } from "@mantine/core";
|
||||||
import { useModals } from "@mantine/modals";
|
import { useModals } from "@mantine/modals";
|
||||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
const showErrorModal = (
|
const showErrorModal = (
|
||||||
modals: ModalsContextProps,
|
modals: ModalsContextProps,
|
||||||
|
@ -31,7 +32,7 @@ const Body = ({ text }: { text: string }) => {
|
||||||
router.back();
|
router.back();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Go back
|
<FormattedMessage id="common.button.go-back" />
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -2,10 +2,13 @@ import { ActionIcon, TextInput } from "@mantine/core";
|
||||||
import { useClipboard } from "@mantine/hooks";
|
import { useClipboard } from "@mantine/hooks";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { TbCheck, TbCopy } from "react-icons/tb";
|
import { TbCheck, TbCopy } from "react-icons/tb";
|
||||||
|
import useTranslate from "../../hooks/useTranslate.hook";
|
||||||
import toast from "../../utils/toast.util";
|
import toast from "../../utils/toast.util";
|
||||||
|
|
||||||
function CopyTextField(props: { link: string }) {
|
function CopyTextField(props: { link: string }) {
|
||||||
const clipboard = useClipboard({ timeout: 500 });
|
const clipboard = useClipboard({ timeout: 500 });
|
||||||
|
const t = useTranslate();
|
||||||
|
|
||||||
const [checkState, setCheckState] = useState(false);
|
const [checkState, setCheckState] = useState(false);
|
||||||
const [textClicked, setTextClicked] = useState(false);
|
const [textClicked, setTextClicked] = useState(false);
|
||||||
const timerRef = useRef<number | ReturnType<typeof setTimeout> | undefined>(
|
const timerRef = useRef<number | ReturnType<typeof setTimeout> | undefined>(
|
||||||
|
@ -14,7 +17,7 @@ function CopyTextField(props: { link: string }) {
|
||||||
|
|
||||||
const copyLink = () => {
|
const copyLink = () => {
|
||||||
clipboard.copy(props.link);
|
clipboard.copy(props.link);
|
||||||
toast.success("The link was copied to your clipboard.");
|
toast.success(t("common.notify.copied"));
|
||||||
if (timerRef.current) clearTimeout(timerRef.current);
|
if (timerRef.current) clearTimeout(timerRef.current);
|
||||||
timerRef.current = setTimeout(() => {
|
timerRef.current = setTimeout(() => {
|
||||||
setCheckState(false);
|
setCheckState(false);
|
||||||
|
@ -25,7 +28,7 @@ function CopyTextField(props: { link: string }) {
|
||||||
return (
|
return (
|
||||||
<TextInput
|
<TextInput
|
||||||
readOnly
|
readOnly
|
||||||
label="Link"
|
label={t("common.text.link")}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
value={props.link}
|
value={props.link}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
|
@ -2,7 +2,8 @@ import { Button, Center, createStyles, Group, Text } from "@mantine/core";
|
||||||
import { Dropzone as MantineDropzone } from "@mantine/dropzone";
|
import { Dropzone as MantineDropzone } from "@mantine/dropzone";
|
||||||
import { Dispatch, ForwardedRef, SetStateAction, useRef } from "react";
|
import { Dispatch, ForwardedRef, SetStateAction, useRef } from "react";
|
||||||
import { TbCloudUpload, TbUpload } from "react-icons/tb";
|
import { TbCloudUpload, TbUpload } from "react-icons/tb";
|
||||||
import useConfig from "../../hooks/config.hook";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
import useTranslate from "../../hooks/useTranslate.hook";
|
||||||
import { FileUpload } from "../../types/File.type";
|
import { FileUpload } from "../../types/File.type";
|
||||||
import { byteToHumanSizeString } from "../../utils/fileSize.util";
|
import { byteToHumanSizeString } from "../../utils/fileSize.util";
|
||||||
import toast from "../../utils/toast.util";
|
import toast from "../../utils/toast.util";
|
||||||
|
@ -42,7 +43,7 @@ const Dropzone = ({
|
||||||
files: FileUpload[];
|
files: FileUpload[];
|
||||||
setFiles: Dispatch<SetStateAction<FileUpload[]>>;
|
setFiles: Dispatch<SetStateAction<FileUpload[]>>;
|
||||||
}) => {
|
}) => {
|
||||||
const config = useConfig();
|
const t = useTranslate();
|
||||||
|
|
||||||
const { classes } = useStyles();
|
const { classes } = useStyles();
|
||||||
const openRef = useRef<() => void>();
|
const openRef = useRef<() => void>();
|
||||||
|
@ -62,9 +63,9 @@ const Dropzone = ({
|
||||||
|
|
||||||
if (fileSizeSum > maxShareSize) {
|
if (fileSizeSum > maxShareSize) {
|
||||||
toast.error(
|
toast.error(
|
||||||
`Your files exceed the maximum share size of ${byteToHumanSizeString(
|
t("upload.dropzone.notify.file-too-big", {
|
||||||
maxShareSize
|
maxSize: byteToHumanSizeString(maxShareSize),
|
||||||
)}.`
|
})
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
newFiles = newFiles.map((newFile) => {
|
newFiles = newFiles.map((newFile) => {
|
||||||
|
@ -82,12 +83,13 @@ const Dropzone = ({
|
||||||
<TbCloudUpload size={50} />
|
<TbCloudUpload size={50} />
|
||||||
</Group>
|
</Group>
|
||||||
<Text align="center" weight={700} size="lg" mt="xl">
|
<Text align="center" weight={700} size="lg" mt="xl">
|
||||||
Upload files
|
<FormattedMessage id="upload.dropzone.title" />
|
||||||
</Text>
|
</Text>
|
||||||
<Text align="center" size="sm" mt="xs" color="dimmed">
|
<Text align="center" size="sm" mt="xs" color="dimmed">
|
||||||
Drag'n'drop files here to start your share. We can accept
|
<FormattedMessage
|
||||||
only files that are less than {byteToHumanSizeString(maxShareSize)}{" "}
|
id="upload.dropzone.description"
|
||||||
in total.
|
values={{ maxSize: byteToHumanSizeString(maxShareSize) }}
|
||||||
|
/>
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</MantineDropzone>
|
</MantineDropzone>
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { TbTrash } from "react-icons/tb";
|
||||||
import { FileUpload } from "../../types/File.type";
|
import { FileUpload } from "../../types/File.type";
|
||||||
import { byteToHumanSizeString } from "../../utils/fileSize.util";
|
import { byteToHumanSizeString } from "../../utils/fileSize.util";
|
||||||
import UploadProgressIndicator from "./UploadProgressIndicator";
|
import UploadProgressIndicator from "./UploadProgressIndicator";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
const FileList = ({
|
const FileList = ({
|
||||||
files,
|
files,
|
||||||
|
@ -41,8 +42,12 @@ const FileList = ({
|
||||||
<Table>
|
<Table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>
|
||||||
<th>Size</th>
|
<FormattedMessage id="upload.filelist.name" />
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<FormattedMessage id="upload.filelist.size" />
|
||||||
|
</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
|
@ -3,6 +3,10 @@ import { useModals } from "@mantine/modals";
|
||||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
import useTranslate, {
|
||||||
|
translateOutsideContext,
|
||||||
|
} from "../../../hooks/useTranslate.hook";
|
||||||
import { Share } from "../../../types/share.type";
|
import { Share } from "../../../types/share.type";
|
||||||
import CopyTextField from "../CopyTextField";
|
import CopyTextField from "../CopyTextField";
|
||||||
|
|
||||||
|
@ -11,11 +15,12 @@ const showCompletedUploadModal = (
|
||||||
share: Share,
|
share: Share,
|
||||||
appUrl: string
|
appUrl: string
|
||||||
) => {
|
) => {
|
||||||
|
const t = translateOutsideContext();
|
||||||
return modals.openModal({
|
return modals.openModal({
|
||||||
closeOnClickOutside: false,
|
closeOnClickOutside: false,
|
||||||
withCloseButton: false,
|
withCloseButton: false,
|
||||||
closeOnEscape: false,
|
closeOnEscape: false,
|
||||||
title: "Share ready",
|
title: t("upload.modal.completed.share-ready"),
|
||||||
children: <Body share={share} appUrl={appUrl} />,
|
children: <Body share={share} appUrl={appUrl} />,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -23,6 +28,7 @@ const showCompletedUploadModal = (
|
||||||
const Body = ({ share, appUrl }: { share: Share; appUrl: string }) => {
|
const Body = ({ share, appUrl }: { share: Share; appUrl: string }) => {
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const t = useTranslate();
|
||||||
|
|
||||||
const link = `${appUrl}/share/${share.id}`;
|
const link = `${appUrl}/share/${share.id}`;
|
||||||
|
|
||||||
|
@ -37,10 +43,10 @@ const Body = ({ share, appUrl }: { share: Share; appUrl: string }) => {
|
||||||
>
|
>
|
||||||
{/* If our share.expiration is timestamp 0, show a different message */}
|
{/* If our share.expiration is timestamp 0, show a different message */}
|
||||||
{moment(share.expiration).unix() === 0
|
{moment(share.expiration).unix() === 0
|
||||||
? "This share will never expire."
|
? t("upload.modal.completed.never-expires")
|
||||||
: `This share will expire on ${moment(share.expiration).format(
|
: t("upload.modal.completed.expires-on", {
|
||||||
"LLL"
|
expiration: moment(share.expiration).format("LLL"),
|
||||||
)}`}
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
@ -49,7 +55,7 @@ const Body = ({ share, appUrl }: { share: Share; appUrl: string }) => {
|
||||||
router.push("/upload");
|
router.push("/upload");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Done
|
<FormattedMessage id="common.button.done" />
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|
|
@ -13,14 +13,17 @@ import {
|
||||||
Text,
|
Text,
|
||||||
Textarea,
|
Textarea,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useForm, yupResolver } from "@mantine/form";
|
import { useForm, yupResolver } from "@mantine/form";
|
||||||
import { useModals } from "@mantine/modals";
|
import { useModals } from "@mantine/modals";
|
||||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { TbAlertCircle } from "react-icons/tb";
|
import { TbAlertCircle } from "react-icons/tb";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
import useTranslate, {
|
||||||
|
translateOutsideContext,
|
||||||
|
} from "../../../hooks/useTranslate.hook";
|
||||||
import shareService from "../../../services/share.service";
|
import shareService from "../../../services/share.service";
|
||||||
import { CreateShare } from "../../../types/share.type";
|
import { CreateShare } from "../../../types/share.type";
|
||||||
import { getExpirationPreview } from "../../../utils/date.util";
|
import { getExpirationPreview } from "../../../utils/date.util";
|
||||||
|
@ -36,8 +39,10 @@ const showCreateUploadModal = (
|
||||||
},
|
},
|
||||||
uploadCallback: (createShare: CreateShare) => void
|
uploadCallback: (createShare: CreateShare) => void
|
||||||
) => {
|
) => {
|
||||||
|
const t = translateOutsideContext();
|
||||||
|
|
||||||
return modals.openModal({
|
return modals.openModal({
|
||||||
title: "Share",
|
title: t("upload.modal.title"),
|
||||||
children: (
|
children: (
|
||||||
<CreateUploadModalBody
|
<CreateUploadModalBody
|
||||||
options={options}
|
options={options}
|
||||||
|
@ -61,6 +66,7 @@ const CreateUploadModalBody = ({
|
||||||
};
|
};
|
||||||
}) => {
|
}) => {
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
|
const t = useTranslate();
|
||||||
|
|
||||||
const generatedLink = Buffer.from(Math.random().toString(), "utf8")
|
const generatedLink = Buffer.from(Math.random().toString(), "utf8")
|
||||||
.toString("base64")
|
.toString("base64")
|
||||||
|
@ -71,11 +77,11 @@ const CreateUploadModalBody = ({
|
||||||
const validationSchema = yup.object().shape({
|
const validationSchema = yup.object().shape({
|
||||||
link: yup
|
link: yup
|
||||||
.string()
|
.string()
|
||||||
.required()
|
.required(t("common.error.field-required"))
|
||||||
.min(3)
|
.min(3, t("common.error.too-short", { length: 3 }))
|
||||||
.max(50)
|
.max(50, t("common.error.too-long", { length: 50 }))
|
||||||
.matches(new RegExp("^[a-zA-Z0-9_-]*$"), {
|
.matches(new RegExp("^[a-zA-Z0-9_-]*$"), {
|
||||||
message: "Can only contain letters, numbers, underscores and hyphens",
|
message: t("upload.modal.link.error.invalid"),
|
||||||
}),
|
}),
|
||||||
password: yup.string().min(3).max(30),
|
password: yup.string().min(3).max(30),
|
||||||
maxViews: yup.number().min(1),
|
maxViews: yup.number().min(1),
|
||||||
|
@ -100,20 +106,19 @@ const CreateUploadModalBody = ({
|
||||||
withCloseButton
|
withCloseButton
|
||||||
onClose={() => setShowNotSignedInAlert(false)}
|
onClose={() => setShowNotSignedInAlert(false)}
|
||||||
icon={<TbAlertCircle size={16} />}
|
icon={<TbAlertCircle size={16} />}
|
||||||
title="You're not signed in"
|
title={t("upload.modal.not-signed-in")}
|
||||||
color="yellow"
|
color="yellow"
|
||||||
>
|
>
|
||||||
You will be unable to delete your share manually and view the visitor
|
<FormattedMessage id="upload.modal.not-signed-in-description" />
|
||||||
count.
|
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
<form
|
<form
|
||||||
onSubmit={form.onSubmit(async (values) => {
|
onSubmit={form.onSubmit(async (values) => {
|
||||||
if (!(await shareService.isShareIdAvailable(values.link))) {
|
if (!(await shareService.isShareIdAvailable(values.link))) {
|
||||||
form.setFieldError("link", "This link is already in use");
|
form.setFieldError("link", t("upload.modal.link.error.taken"));
|
||||||
} else {
|
} else {
|
||||||
const expiration = form.values.never_expires
|
const expiration = form.values.never_expires
|
||||||
? "never"
|
? t("upload.modal.expires.never")
|
||||||
: form.values.expiration_num + form.values.expiration_unit;
|
: form.values.expiration_num + form.values.expiration_unit;
|
||||||
uploadCallback({
|
uploadCallback({
|
||||||
id: values.link,
|
id: values.link,
|
||||||
|
@ -151,7 +156,7 @@ const CreateUploadModalBody = ({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Generate
|
<FormattedMessage id="common.button.generate" />
|
||||||
</Button>
|
</Button>
|
||||||
</Col>
|
</Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -169,18 +174,6 @@ const CreateUploadModalBody = ({
|
||||||
{!options.isReverseShare && (
|
{!options.isReverseShare && (
|
||||||
<>
|
<>
|
||||||
<Grid align={form.errors.link ? "center" : "flex-end"}>
|
<Grid align={form.errors.link ? "center" : "flex-end"}>
|
||||||
<Col xs={6}>
|
|
||||||
<NumberInput
|
|
||||||
min={1}
|
|
||||||
max={99999}
|
|
||||||
precision={0}
|
|
||||||
variant="filled"
|
|
||||||
label="Expiration"
|
|
||||||
placeholder="n"
|
|
||||||
disabled={form.values.never_expires}
|
|
||||||
{...form.getInputProps("expiration_num")}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col xs={6}>
|
<Col xs={6}>
|
||||||
<Select
|
<Select
|
||||||
disabled={form.values.never_expires}
|
disabled={form.values.never_expires}
|
||||||
|
@ -190,41 +183,51 @@ const CreateUploadModalBody = ({
|
||||||
{
|
{
|
||||||
value: "-minutes",
|
value: "-minutes",
|
||||||
label:
|
label:
|
||||||
"Minute" +
|
form.values.expiration_num == 1
|
||||||
(form.values.expiration_num == 1 ? "" : "s"),
|
? t("upload.modal.expires.minute-singular")
|
||||||
|
: t("upload.modal.expires.minute-plural"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "-hours",
|
value: "-hours",
|
||||||
label:
|
label:
|
||||||
"Hour" + (form.values.expiration_num == 1 ? "" : "s"),
|
form.values.expiration_num == 1
|
||||||
|
? t("upload.modal.expires.hour-singular")
|
||||||
|
: t("upload.modal.expires.hour-plural"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "-days",
|
value: "-days",
|
||||||
label:
|
label:
|
||||||
"Day" + (form.values.expiration_num == 1 ? "" : "s"),
|
form.values.expiration_num == 1
|
||||||
|
? t("upload.modal.expires.day-singular")
|
||||||
|
: t("upload.modal.expires.day-plural"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "-weeks",
|
value: "-weeks",
|
||||||
label:
|
label:
|
||||||
"Week" + (form.values.expiration_num == 1 ? "" : "s"),
|
form.values.expiration_num == 1
|
||||||
|
? t("upload.modal.expires.week-singular")
|
||||||
|
: t("upload.modal.expires.week-plural"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "-months",
|
value: "-months",
|
||||||
label:
|
label:
|
||||||
"Month" +
|
form.values.expiration_num == 1
|
||||||
(form.values.expiration_num == 1 ? "" : "s"),
|
? t("upload.modal.expires.month-singular")
|
||||||
|
: t("upload.modal.expires.month-plural"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "-years",
|
value: "-years",
|
||||||
label:
|
label:
|
||||||
"Year" + (form.values.expiration_num == 1 ? "" : "s"),
|
form.values.expiration_num == 1
|
||||||
|
? t("upload.modal.expires.year-singular")
|
||||||
|
: t("upload.modal.expires.year-plural"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="Never Expires"
|
label={t("upload.modal.expires.never-long")}
|
||||||
{...form.getInputProps("never_expires")}
|
{...form.getInputProps("never_expires")}
|
||||||
/>
|
/>
|
||||||
<Text
|
<Text
|
||||||
|
@ -234,18 +237,28 @@ const CreateUploadModalBody = ({
|
||||||
color: theme.colors.gray[6],
|
color: theme.colors.gray[6],
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{getExpirationPreview("share", form)}
|
{getExpirationPreview(
|
||||||
|
{
|
||||||
|
neverExpires: t("upload.modal.completed.never-expires"),
|
||||||
|
expiresOn: t("upload.modal.completed.expires-on"),
|
||||||
|
},
|
||||||
|
form
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Accordion>
|
<Accordion>
|
||||||
<Accordion.Item value="description" sx={{ borderBottom: "none" }}>
|
<Accordion.Item value="description" sx={{ borderBottom: "none" }}>
|
||||||
<Accordion.Control>Description</Accordion.Control>
|
<Accordion.Control>
|
||||||
|
<FormattedMessage id="upload.modal.accordion.description.title" />
|
||||||
|
</Accordion.Control>
|
||||||
<Accordion.Panel>
|
<Accordion.Panel>
|
||||||
<Stack align="stretch">
|
<Stack align="stretch">
|
||||||
<Textarea
|
<Textarea
|
||||||
variant="filled"
|
variant="filled"
|
||||||
placeholder="Note for the recepients"
|
placeholder={t(
|
||||||
|
"upload.modal.accordion.description.placeholder"
|
||||||
|
)}
|
||||||
{...form.getInputProps("description")}
|
{...form.getInputProps("description")}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -253,11 +266,13 @@ const CreateUploadModalBody = ({
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
{options.enableEmailRecepients && (
|
{options.enableEmailRecepients && (
|
||||||
<Accordion.Item value="recipients" sx={{ borderBottom: "none" }}>
|
<Accordion.Item value="recipients" sx={{ borderBottom: "none" }}>
|
||||||
<Accordion.Control>Email recipients</Accordion.Control>
|
<Accordion.Control>
|
||||||
|
<FormattedMessage id="upload.modal.accordion.email.tile" />
|
||||||
|
</Accordion.Control>
|
||||||
<Accordion.Panel>
|
<Accordion.Panel>
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
data={form.values.recipients}
|
data={form.values.recipients}
|
||||||
placeholder="Enter email recipients"
|
placeholder={t("upload.modal.accordion.email.placeholder")}
|
||||||
searchable
|
searchable
|
||||||
{...form.getInputProps("recipients")}
|
{...form.getInputProps("recipients")}
|
||||||
creatable
|
creatable
|
||||||
|
@ -266,7 +281,7 @@ const CreateUploadModalBody = ({
|
||||||
if (!query.match(/^\S+@\S+\.\S+$/)) {
|
if (!query.match(/^\S+@\S+\.\S+$/)) {
|
||||||
form.setFieldError(
|
form.setFieldError(
|
||||||
"recipients",
|
"recipients",
|
||||||
"Invalid email address"
|
t("upload.modal.accordion.email.invalid-email")
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
form.setFieldError("recipients", null);
|
form.setFieldError("recipients", null);
|
||||||
|
@ -283,28 +298,36 @@ const CreateUploadModalBody = ({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Accordion.Item value="security" sx={{ borderBottom: "none" }}>
|
<Accordion.Item value="security" sx={{ borderBottom: "none" }}>
|
||||||
<Accordion.Control>Security options</Accordion.Control>
|
<Accordion.Control>
|
||||||
|
<FormattedMessage id="upload.modal.accordion.security.title" />
|
||||||
|
</Accordion.Control>
|
||||||
<Accordion.Panel>
|
<Accordion.Panel>
|
||||||
<Stack align="stretch">
|
<Stack align="stretch">
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
variant="filled"
|
variant="filled"
|
||||||
placeholder="No password"
|
placeholder={t(
|
||||||
label="Password protection"
|
"upload.modal.accordion.security.password.placeholder"
|
||||||
|
)}
|
||||||
|
label={t("upload.modal.accordion.security.password.label")}
|
||||||
{...form.getInputProps("password")}
|
{...form.getInputProps("password")}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
min={1}
|
min={1}
|
||||||
type="number"
|
type="number"
|
||||||
variant="filled"
|
variant="filled"
|
||||||
placeholder="No limit"
|
placeholder={t(
|
||||||
label="Maximal views"
|
"upload.modal.accordion.security.max-views.placeholder"
|
||||||
|
)}
|
||||||
|
label={t("upload.modal.accordion.security.max-views.label")}
|
||||||
{...form.getInputProps("maxViews")}
|
{...form.getInputProps("maxViews")}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Accordion.Panel>
|
</Accordion.Panel>
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
<Button type="submit">Share</Button>
|
<Button type="submit">
|
||||||
|
<FormattedMessage id="common.button.share" />
|
||||||
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
</>
|
</>
|
||||||
|
|
39
frontend/src/hooks/useTranslate.hook.ts
Normal file
39
frontend/src/hooks/useTranslate.hook.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { getCookie } from "cookies-next";
|
||||||
|
import { createIntl, createIntlCache, useIntl } from "react-intl";
|
||||||
|
import i18nUtil from "../utils/i18n.util";
|
||||||
|
|
||||||
|
const useTranslate = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
return (
|
||||||
|
id: string,
|
||||||
|
values?: Parameters<typeof intl.formatMessage>[1],
|
||||||
|
opts?: Parameters<typeof intl.formatMessage>[2]
|
||||||
|
) => {
|
||||||
|
return intl.formatMessage({ id }, values, opts) as string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const cache = createIntlCache();
|
||||||
|
|
||||||
|
export const translateOutsideContext = () => {
|
||||||
|
const locale =
|
||||||
|
getCookie("language")?.toString() ?? navigator.language.split("-")[0];
|
||||||
|
|
||||||
|
const intl = createIntl(
|
||||||
|
{
|
||||||
|
locale,
|
||||||
|
messages: i18nUtil.getLocaleByCode(locale)?.messages,
|
||||||
|
defaultLocale: "en",
|
||||||
|
},
|
||||||
|
cache
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
id: string,
|
||||||
|
values?: Parameters<typeof intl.formatMessage>[1],
|
||||||
|
opts?: Parameters<typeof intl.formatMessage>[2]
|
||||||
|
) => {
|
||||||
|
return intl.formatMessage({ id }, values, opts) as string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useTranslate;
|
51
frontend/src/i18n/locales.ts
Normal file
51
frontend/src/i18n/locales.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import danish from "./translations/da";
|
||||||
|
import german from "./translations/de";
|
||||||
|
import english from "./translations/en";
|
||||||
|
import spanish from "./translations/es";
|
||||||
|
import french from "./translations/fr";
|
||||||
|
import portugueze from "./translations/pt";
|
||||||
|
import thai from "./translations/th";
|
||||||
|
import chineseSimplified from "./translations/zh-CN";
|
||||||
|
|
||||||
|
export const LOCALES = {
|
||||||
|
ENGLISH: {
|
||||||
|
name: "English",
|
||||||
|
code: "en",
|
||||||
|
messages: english,
|
||||||
|
},
|
||||||
|
GERMAN: {
|
||||||
|
name: "Deutsch",
|
||||||
|
code: "de",
|
||||||
|
messages: german,
|
||||||
|
},
|
||||||
|
FRENCH: {
|
||||||
|
name: "Français",
|
||||||
|
code: "fr",
|
||||||
|
messages: french,
|
||||||
|
},
|
||||||
|
PORTUGUEZE: {
|
||||||
|
name: "Português",
|
||||||
|
code: "pt",
|
||||||
|
messages: portugueze,
|
||||||
|
},
|
||||||
|
DANISH: {
|
||||||
|
name: "Dansk",
|
||||||
|
code: "da",
|
||||||
|
messages: danish,
|
||||||
|
},
|
||||||
|
THAI: {
|
||||||
|
name: "ไทย",
|
||||||
|
code: "th",
|
||||||
|
messages: thai,
|
||||||
|
},
|
||||||
|
SPANISH: {
|
||||||
|
name: "Español",
|
||||||
|
code: "es",
|
||||||
|
messages: spanish,
|
||||||
|
},
|
||||||
|
CHINESE_SIMPLIFIED: {
|
||||||
|
name: "简体中文",
|
||||||
|
code: "zh-CN",
|
||||||
|
messages: chineseSimplified,
|
||||||
|
},
|
||||||
|
};
|
435
frontend/src/i18n/translations/da.ts
Normal file
435
frontend/src/i18n/translations/da.ts
Normal file
|
@ -0,0 +1,435 @@
|
||||||
|
export default {
|
||||||
|
// Navbar
|
||||||
|
"navbar.upload": "Upload",
|
||||||
|
"navbar.signin": "Sign in",
|
||||||
|
"navbar.home": "Home",
|
||||||
|
"navbar.signup": "Sign Up",
|
||||||
|
|
||||||
|
"navbar.links.shares": "My shares",
|
||||||
|
"navbar.links.reverse": "Reverse shares",
|
||||||
|
|
||||||
|
"navbar.avatar.account": "My account",
|
||||||
|
"navbar.avatar.admin": "Administration",
|
||||||
|
"navbar.avatar.signout": "Sign out",
|
||||||
|
// END navbar
|
||||||
|
|
||||||
|
// /
|
||||||
|
"home.title": "A <h>self-hosted</h> file sharing platform.",
|
||||||
|
|
||||||
|
"home.description":
|
||||||
|
"Do you really want to give your personal files in the hand of third parties like WeTransfer?",
|
||||||
|
"home.bullet.a.name": "Self-Hosted",
|
||||||
|
"home.bullet.a.description": "Host Pingvin Share on your own machine.",
|
||||||
|
"home.bullet.b.name": "Privacy",
|
||||||
|
"home.bullet.b.description":
|
||||||
|
"Your files are your files and should never get into the hands of third parties.",
|
||||||
|
"home.bullet.c.name": "No annoying file size limit",
|
||||||
|
"home.bullet.c.description":
|
||||||
|
"Upload as big files as you want. Only your hard drive will be your limit.",
|
||||||
|
|
||||||
|
"home.button.start": "Get started",
|
||||||
|
"home.button.source": "Source code",
|
||||||
|
// END /
|
||||||
|
|
||||||
|
// /auth/signin
|
||||||
|
"signin.title": "Welcome back",
|
||||||
|
"signin.description": "You don't have an account yet?",
|
||||||
|
"signin.button.signup": "Sign up",
|
||||||
|
"signin.input.email-or-username": "Email or username",
|
||||||
|
"signin.input.email-or-username.placeholder": "Your email or username",
|
||||||
|
"signin.input.password": "Password",
|
||||||
|
"signin.input.password.placeholder": "Your password",
|
||||||
|
"signin.button.submit": "Sign in",
|
||||||
|
"signIn.notify.totp-required.title": "Two-factor authentication required",
|
||||||
|
"signIn.notify.totp-required.description":
|
||||||
|
"Please enter your two-factor authentication code",
|
||||||
|
|
||||||
|
// END /auth/signin
|
||||||
|
|
||||||
|
// /auth/signup
|
||||||
|
"signup.title": "Create an account",
|
||||||
|
"signup.description": "Already have an account?",
|
||||||
|
"signup.button.signin": "Sign in",
|
||||||
|
"signup.input.username": "Username",
|
||||||
|
"signup.input.username.placeholder": "Your username",
|
||||||
|
"signup.input.email": "Email",
|
||||||
|
"signup.input.email.placeholder": "Your email",
|
||||||
|
"signup.button.submit": "Let's get started",
|
||||||
|
|
||||||
|
// END /auth/signup
|
||||||
|
|
||||||
|
// /auth/reset-password
|
||||||
|
"resetPassword.title": "Forgot your password?",
|
||||||
|
"resetPassword.description": "Enter your email to reset your password.",
|
||||||
|
"resetPassword.notify.success":
|
||||||
|
"An email has been sent with a link to reset your password.",
|
||||||
|
"resetPassword.button.back": "Back to sign in page",
|
||||||
|
"resetPassword.text.resetPassword": "Reset password",
|
||||||
|
"resetPassword.text.enterNewPassword": "Enter your new password",
|
||||||
|
"resetPassword.input.password": "New password",
|
||||||
|
"resetPassword.notify.passwordReset":
|
||||||
|
"Your password has been reset successfully.",
|
||||||
|
|
||||||
|
// /account
|
||||||
|
"account.title": "My account",
|
||||||
|
|
||||||
|
"account.card.info.title": "Account info",
|
||||||
|
"account.card.info.username": "Username",
|
||||||
|
"account.card.info.email": "Email",
|
||||||
|
"account.notify.info.success": "Account updated successfully",
|
||||||
|
|
||||||
|
"account.card.password.title": "Password",
|
||||||
|
"account.card.password.old": "Old password",
|
||||||
|
"account.card.password.new": "New password",
|
||||||
|
"account.notify.password.success": "Password changed successfully",
|
||||||
|
|
||||||
|
"account.card.security.title": "Security",
|
||||||
|
"account.card.security.totp.enable.description":
|
||||||
|
"Enter your current password to start enabling TOTP",
|
||||||
|
"account.card.security.totp.disable.description":
|
||||||
|
"Enter your current password to start enabling TOTP",
|
||||||
|
"account.card.security.totp.button.start": "Start",
|
||||||
|
"account.modal.totp.title": "Enable TOTP",
|
||||||
|
"account.modal.totp.step1": "Step 1: Add your authenticator",
|
||||||
|
"account.modal.totp.step2": "Step 2: Validate your code",
|
||||||
|
"account.modal.totp.enterManually": "Enter manually",
|
||||||
|
"account.modal.totp.code": "Code",
|
||||||
|
"account.modal.totp.clickToCopy": "Click to copy",
|
||||||
|
"account.modal.totp.verify": "Verify",
|
||||||
|
"account.notify.totp.disable": "TOTP disabled successfully",
|
||||||
|
"account.notify.totp.enable": "TOTP enabled successfully",
|
||||||
|
|
||||||
|
"account.card.language.title": "Language",
|
||||||
|
"account.card.color.title": "Color scheme",
|
||||||
|
|
||||||
|
// ThemeSwitcher.tsx
|
||||||
|
"account.theme.dark": "Dark",
|
||||||
|
"account.theme.light": "Light",
|
||||||
|
"account.theme.system": "System",
|
||||||
|
|
||||||
|
"account.button.delete": "Delete Account",
|
||||||
|
"account.modal.delete.title": "Delete Account",
|
||||||
|
"account.modal.delete.description":
|
||||||
|
"Do you really want to delete your account including all your active shares?",
|
||||||
|
// END /account
|
||||||
|
|
||||||
|
// /account/shares
|
||||||
|
"account.shares.title": "My shares",
|
||||||
|
"account.shares.title.empty": "It's empty here 👀",
|
||||||
|
"account.shares.description.empty": "You don't have any shares.",
|
||||||
|
"account.shares.button.create": "Create one",
|
||||||
|
|
||||||
|
"account.shares.info.title": "Share informatons",
|
||||||
|
"account.shares.table.id": "ID",
|
||||||
|
"account.shares.table.name": "Name",
|
||||||
|
"account.shares.table.description": "Description",
|
||||||
|
"account.shares.table.visitors": "Visitors",
|
||||||
|
"account.shares.table.expiresAt": "Expires at",
|
||||||
|
"account.shares.table.createdAt": "Created at",
|
||||||
|
"account.shares.table.size": "Size",
|
||||||
|
|
||||||
|
"account.shares.modal.share-link": "Share link",
|
||||||
|
|
||||||
|
"account.shares.modal.delete.title": "Delete {share}",
|
||||||
|
"account.shares.modal.delete.description":
|
||||||
|
"Do you really want to delete this share?",
|
||||||
|
|
||||||
|
// END /account/shares
|
||||||
|
|
||||||
|
// /account/reverseShares
|
||||||
|
"account.reverseShares.title": "Reverse shares",
|
||||||
|
"account.reverseShares.description":
|
||||||
|
"A reverse share allows you to generate a unique URL that allows external users to create a share.",
|
||||||
|
|
||||||
|
"account.reverseShares.title.empty": "It's empty here 👀",
|
||||||
|
"account.reverseShares.description.empty":
|
||||||
|
"You don't have any reverse shares.",
|
||||||
|
|
||||||
|
// showCreateReverseShareModal.tsx
|
||||||
|
"account.reverseShares.modal.expiration.label": "Expiration",
|
||||||
|
"account.reverseShares.modal.expiration.minute-singular": "Minute",
|
||||||
|
"account.reverseShares.modal.expiration.minute-plural": "Minutes",
|
||||||
|
"account.reverseShares.modal.expiration.hour-singular": "Hour",
|
||||||
|
"account.reverseShares.modal.expiration.hour-plural": "Hours",
|
||||||
|
"account.reverseShares.modal.expiration.day-singular": "Day",
|
||||||
|
"account.reverseShares.modal.expiration.day-plural": "Days",
|
||||||
|
"account.reverseShares.modal.expiration.week-singular": "Week",
|
||||||
|
"account.reverseShares.modal.expiration.week-plural": "Weeks",
|
||||||
|
"account.reverseShares.modal.expiration.month-singular": "Month",
|
||||||
|
"account.reverseShares.modal.expiration.month-plural": "Months",
|
||||||
|
"account.reverseShares.modal.expiration.year-singular": "Year",
|
||||||
|
"account.reverseShares.modal.expiration.year-plural": "Years",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.max-size.label": "Max share size",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.send-email": "Send email notification",
|
||||||
|
"account.reverseShares.modal.send-email.description":
|
||||||
|
"Send an email notification when a share is created with this reverse share link.",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.max-use.label": "Max uses",
|
||||||
|
"account.reverseShares.modal.max-use.description":
|
||||||
|
"The maximum amount of times this URL can be used to create a share.",
|
||||||
|
"account.reverseShare.never-expires": "This reverse share will never expire.",
|
||||||
|
"account.reverseShare.expires-on":
|
||||||
|
"This reverse share will expire on {expiration}.",
|
||||||
|
|
||||||
|
"account.reverseShares.table.no-shares": "No shares created yet",
|
||||||
|
"account.reverseShares.table.count.singular": "share",
|
||||||
|
"account.reverseShares.table.count.plural": "shares",
|
||||||
|
"account.reverseShares.table.shares": "Shares",
|
||||||
|
"account.reverseShares.table.remaining": "Remaining uses",
|
||||||
|
"account.reverseShares.table.max-size": "Max share size",
|
||||||
|
"account.reverseShares.table.expires": "Expires at",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.reverse-share-link": "Reverse share link",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.delete.title":
|
||||||
|
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
|
||||||
|
"account.reverseShares.modal.delete.description":
|
||||||
|
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
|
||||||
|
|
||||||
|
// END /account/reverseShares
|
||||||
|
|
||||||
|
// /admin
|
||||||
|
"admin.title": "Administration",
|
||||||
|
"admin.button.users": "User management",
|
||||||
|
"admin.button.config": "Configuration",
|
||||||
|
"admin.version": "Version",
|
||||||
|
// END /admin
|
||||||
|
|
||||||
|
// /admin/users
|
||||||
|
"admin.users.title": "User management",
|
||||||
|
"admin.users.table.username": "Username",
|
||||||
|
"admin.users.table.email": "Email",
|
||||||
|
"admin.users.table.admin": "Admin",
|
||||||
|
|
||||||
|
"admin.users.edit.update.title": "Update user {username}",
|
||||||
|
"admin.users.edit.update.admin-privileges": "Admin privileges",
|
||||||
|
"admin.users.edit.update.change-password.title": "Change password",
|
||||||
|
"admin.users.edit.update.change-password.field": "New password",
|
||||||
|
"admin.users.edit.update.change-password.button": "Save new password",
|
||||||
|
"admin.users.edit.update.notify.password.success":
|
||||||
|
"Password changed successfully",
|
||||||
|
|
||||||
|
"admin.users.edit.delete.title": "Delete user {username}",
|
||||||
|
"admin.users.edit.delete.description":
|
||||||
|
"Do you really want to delete this user and all his shares?",
|
||||||
|
|
||||||
|
// showCreateUserModal.tsx
|
||||||
|
"admin.users.modal.create.title": "Create user",
|
||||||
|
"admin.users.modal.create.username": "Username",
|
||||||
|
"admin.users.modal.create.email": "Email",
|
||||||
|
"admin.users.modal.create.password": "Password",
|
||||||
|
"admin.users.modal.create.manual-password": "Set password manually",
|
||||||
|
"admin.users.modal.create.manual-password.description":
|
||||||
|
"If not checked, the user will receive an email with a link to set their password.",
|
||||||
|
"admin.users.modal.create.admin": "Admin privileges",
|
||||||
|
"admin.users.modal.create.admin.description":
|
||||||
|
"If checked, the user will be able to access the admin panel.",
|
||||||
|
|
||||||
|
// END /admin/users
|
||||||
|
|
||||||
|
// /upload
|
||||||
|
"upload.title": "Upload",
|
||||||
|
|
||||||
|
"upload.notify.generic-error":
|
||||||
|
"An error occurred while finishing your share.",
|
||||||
|
"upload.notify.count-failed": "{count} filed failed to upload. Trying again.",
|
||||||
|
|
||||||
|
// Dropzone.tsx
|
||||||
|
"upload.dropzone.title": "Upload files",
|
||||||
|
"upload.dropzone.description":
|
||||||
|
"Drag'n'drop files here to start your share. We can accept only files that are less than {maxSize} in total.",
|
||||||
|
"upload.dropzone.notify.file-too-big":
|
||||||
|
"Your files exceed the maximum share size of {maxSize}.",
|
||||||
|
|
||||||
|
// FileList.tsx
|
||||||
|
"upload.filelist.name": "Name",
|
||||||
|
"upload.filelist.size": "Size",
|
||||||
|
|
||||||
|
// showCreateUploadModal.tsx
|
||||||
|
"upload.modal.title": "Create Share",
|
||||||
|
"upload.modal.link.error.invalid":
|
||||||
|
"Can only contain letters, numbers, underscores, and hyphens",
|
||||||
|
"upload.modal.link.error.taken": "This link is already in use",
|
||||||
|
"upload.modal.not-signed-in": "You're not signed in",
|
||||||
|
"upload.modal.not-signed-in-description":
|
||||||
|
"You will be unable to delete your share manually and view the visitor count.",
|
||||||
|
|
||||||
|
"upload.modal.expires.never": "never",
|
||||||
|
"upload.modal.expires.never-long": "Never Expires",
|
||||||
|
|
||||||
|
"upload.modal.link.label": "Link",
|
||||||
|
"upload.modal.link.placeholder": "myAwesomeShare",
|
||||||
|
|
||||||
|
"upload.modal.expires.label": "Expiration",
|
||||||
|
"upload.modal.expires.minute-singular": "Minute",
|
||||||
|
"upload.modal.expires.minute-plural": "Minutes",
|
||||||
|
"upload.modal.expires.hour-singular": "Hour",
|
||||||
|
"upload.modal.expires.hour-plural": "Hours",
|
||||||
|
"upload.modal.expires.day-singular": "Day",
|
||||||
|
"upload.modal.expires.day-plural": "Days",
|
||||||
|
"upload.modal.expires.week-singular": "Week",
|
||||||
|
"upload.modal.expires.week-plural": "Weeks",
|
||||||
|
"upload.modal.expires.month-singular": "Month",
|
||||||
|
"upload.modal.expires.month-plural": "Months",
|
||||||
|
"upload.modal.expires.year-singular": "Year",
|
||||||
|
"upload.modal.expires.year-plural": "Years",
|
||||||
|
|
||||||
|
"upload.modal.accordion.description.title": "Description",
|
||||||
|
"upload.modal.accordion.description.placeholder":
|
||||||
|
"Note for the recipients of this share",
|
||||||
|
|
||||||
|
"upload.modal.accordion.email.title": "Email recipients",
|
||||||
|
"upload.modal.accordion.email.placeholder": "Enter email recipients",
|
||||||
|
"upload.modal.accordion.email.invalid-email": "Invalid email address",
|
||||||
|
|
||||||
|
"upload.modal.accordion.security.title": "Security options",
|
||||||
|
"upload.modal.accordion.security.password.label": "Password protection",
|
||||||
|
"upload.modal.accordion.security.password.placeholder": "No password",
|
||||||
|
"upload.modal.accordion.security.max-views.label": "Maximum views",
|
||||||
|
"upload.modal.accordion.security.max-views.placeholder": "No limit",
|
||||||
|
|
||||||
|
// showCompletedUploadModal.tsx
|
||||||
|
"upload.modal.completed.never-expires": "This share will never expire.",
|
||||||
|
"upload.modal.completed.expires-on":
|
||||||
|
"This share will expire on {expiration}.",
|
||||||
|
"upload.modal.completed.share-ready": "Share ready",
|
||||||
|
|
||||||
|
// END /upload
|
||||||
|
|
||||||
|
// /share/[id]
|
||||||
|
"share.title": "Share {shareId}",
|
||||||
|
"share.description": "Look what I've shared with you!",
|
||||||
|
"share.error.visitor-limit-exceeded.title": "Visitor limit exceeded",
|
||||||
|
"share.error.visitor-limit-exceeded.description":
|
||||||
|
"The visitor limit from this share has been exceeded.",
|
||||||
|
"share.error.removed.title": "Share removed",
|
||||||
|
"share.error.not-found.title": "Share not found",
|
||||||
|
"share.error.not-found.description":
|
||||||
|
"The share you're looking for doesn't exist.",
|
||||||
|
|
||||||
|
"share.modal.password.title": "Password required",
|
||||||
|
"share.modal.password.description":
|
||||||
|
"To access this share please enter the password for the share.",
|
||||||
|
"share.modal.password": "Password",
|
||||||
|
"share.modal.error.invalid-password": "Invalid password",
|
||||||
|
|
||||||
|
"share.button.download-all": "Download all",
|
||||||
|
"share.notify.download-all-preparing":
|
||||||
|
"The share is preparing. Try again in a few minutes.",
|
||||||
|
|
||||||
|
"share.modal.file-link": "File link",
|
||||||
|
"share.table.name": "Name",
|
||||||
|
"share.table.size": "Size",
|
||||||
|
|
||||||
|
"share.modal.file-preview.error.not-supported.title": "Preview not supported",
|
||||||
|
"share.modal.file-preview.error.not-supported.description":
|
||||||
|
"A preview for thise file type is unsupported. Please download the file to view it.",
|
||||||
|
|
||||||
|
// END /share/[id]
|
||||||
|
|
||||||
|
// /admin/config
|
||||||
|
"admin.config.title": "Configuration",
|
||||||
|
"admin.config.category.general": "General",
|
||||||
|
"admin.config.category.share": "Share",
|
||||||
|
"admin.config.category.email": "Email",
|
||||||
|
"admin.config.category.smtp": "SMTP",
|
||||||
|
|
||||||
|
"admin.config.general.app-name": "App name",
|
||||||
|
"admin.config.general.app-name.description": "Name of the application",
|
||||||
|
"admin.config.general.app-url": "App URL",
|
||||||
|
"admin.config.general.app-url.description":
|
||||||
|
"On which URL Pingvin Share is available",
|
||||||
|
"admin.config.general.show-home-page": "Show home page",
|
||||||
|
"admin.config.general.show-home-page.description":
|
||||||
|
"Whether to show the home page",
|
||||||
|
"admin.config.general.logo": "Logo",
|
||||||
|
"admin.config.general.logo.description":
|
||||||
|
"Change your logo by uploading a new image. The image must be a PNG and should have the format 1:1.",
|
||||||
|
"admin.config.general.logo.placeholder": "Pick image",
|
||||||
|
|
||||||
|
"admin.config.email.enable-share-email-recipients":
|
||||||
|
"Enable share email recipients",
|
||||||
|
"admin.config.email.enable-share-email-recipients.description":
|
||||||
|
"Whether to allow emails to share recipients. Only enable this if you have enabled SMTP.",
|
||||||
|
"admin.config.email.share-recipients-subject": "Share recipients subject",
|
||||||
|
"admin.config.email.share-recipients-subject.description":
|
||||||
|
"Subject of the email which gets sent to the share recipients.",
|
||||||
|
"admin.config.email.share-recipients-message": "Share recipients message",
|
||||||
|
"admin.config.email.share-recipients-message.description":
|
||||||
|
"Message which gets sent to the share recipients. Available variables:\n {creator} - The username of the creator of the share\n {shareUrl} - The URL of the share\n {desc} - The description of the share\n {expires} - The expiration date of the share\n The variables will be replaced with the actual values.",
|
||||||
|
"admin.config.email.reverse-share-subject": "Reverse share subject",
|
||||||
|
"admin.config.email.reverse-share-subject.description":
|
||||||
|
"Subject of the email which gets sent when someone created a share with your reverse share link.",
|
||||||
|
"admin.config.email.reverse-share-message": "Reverse share message",
|
||||||
|
"admin.config.email.reverse-share-message.description":
|
||||||
|
"Message which gets sent when someone created a share with your reverse share link. {shareUrl} will be replaced with the creator's name and the share URL.",
|
||||||
|
"admin.config.email.reset-password-subject": "Reset password subject",
|
||||||
|
"admin.config.email.reset-password-subject.description":
|
||||||
|
"Subject of the email which gets sent when a user requests a password reset.",
|
||||||
|
"admin.config.email.reset-password-message": "Reset password message",
|
||||||
|
"admin.config.email.reset-password-message.description":
|
||||||
|
"Message which gets sent when a user requests a password reset. {url} will be replaced with the reset password URL.",
|
||||||
|
"admin.config.email.invite-subject": "Invite subject",
|
||||||
|
"admin.config.email.invite-subject.description":
|
||||||
|
"Subject of the email which gets sent when an admin invites a user.",
|
||||||
|
"admin.config.email.invite-message": "Invite message",
|
||||||
|
"admin.config.email.invite-message.description":
|
||||||
|
"Message which gets sent when an admin invites a user. {url} will be replaced with the invite URL and {password} with the password.",
|
||||||
|
"admin.config.share.allow-registration": "Allow registration",
|
||||||
|
"admin.config.share.allow-registration.description":
|
||||||
|
"Whether registration is allowed",
|
||||||
|
"admin.config.share.allow-unauthenticated-shares":
|
||||||
|
"Allow unauthenticated shares",
|
||||||
|
"admin.config.share.allow-unauthenticated-shares.description":
|
||||||
|
"Whether unauthorized users can create shares",
|
||||||
|
"admin.config.share.max-size": "Max size",
|
||||||
|
"admin.config.share.max-size.description": "Maximum share size in bytes",
|
||||||
|
|
||||||
|
"admin.config.smtp.enabled": "Enabled",
|
||||||
|
"admin.config.smtp.enabled.description":
|
||||||
|
"Whether SMTP is enabled. Only set this to true if you entered the host, port, email, user and password of your SMTP server.",
|
||||||
|
"admin.config.smtp.host": "Host",
|
||||||
|
"admin.config.smtp.host.description": "Host of the SMTP server",
|
||||||
|
"admin.config.smtp.port": "Port",
|
||||||
|
"admin.config.smtp.port.description": "Port of the SMTP server",
|
||||||
|
"admin.config.smtp.email": "Email",
|
||||||
|
"admin.config.smtp.email.description":
|
||||||
|
"Email address which the emails get sent from",
|
||||||
|
"admin.config.smtp.username": "Username",
|
||||||
|
"admin.config.smtp.username.description": "Username of the SMTP server",
|
||||||
|
"admin.config.smtp.password": "Password",
|
||||||
|
"admin.config.smtp.password.description": "Password of the SMTP server",
|
||||||
|
"admin.config.smtp.button.test": "Send test email",
|
||||||
|
|
||||||
|
// 404
|
||||||
|
"404.title": "404",
|
||||||
|
"404.description": "Oops this page doesn't exist.",
|
||||||
|
"404.button.home": "Gring me back home",
|
||||||
|
|
||||||
|
// Common translations
|
||||||
|
"common.button.save": "Save",
|
||||||
|
"common.button.create": "Create",
|
||||||
|
"common.button.submit": "Submit",
|
||||||
|
"common.button.delete": "Delete",
|
||||||
|
"common.button.cancel": "Cancel",
|
||||||
|
"common.button.confirm": "Confirm",
|
||||||
|
"common.button.disable": "Disable",
|
||||||
|
"common.button.share": "Share",
|
||||||
|
"common.button.generate": "Generate",
|
||||||
|
"common.button.done": "Done",
|
||||||
|
"common.text.link": "Link",
|
||||||
|
"common.text.or": "or",
|
||||||
|
"common.button.go-back": "Go back",
|
||||||
|
"common.notify.copied": "Your link was copied to the clipboard",
|
||||||
|
|
||||||
|
"common.error": "Error",
|
||||||
|
"common.error.unknown": "An unknown error occurred",
|
||||||
|
"common.error.invalid-email": "Invalid email address",
|
||||||
|
"common.error.too-short": "Must be at least {length} characters",
|
||||||
|
"common.error.too-long": "Must be at most {length} characters",
|
||||||
|
"common.error.exact-length": "Must be exactly {length} characters",
|
||||||
|
"common.error.invalid-number": "Must be a number",
|
||||||
|
"common.error.field-required": "This field is required",
|
||||||
|
};
|
435
frontend/src/i18n/translations/de.ts
Normal file
435
frontend/src/i18n/translations/de.ts
Normal file
|
@ -0,0 +1,435 @@
|
||||||
|
export default {
|
||||||
|
// Navbar
|
||||||
|
"navbar.upload": "Upload",
|
||||||
|
"navbar.signin": "Sign in",
|
||||||
|
"navbar.home": "Home",
|
||||||
|
"navbar.signup": "Sign Up",
|
||||||
|
|
||||||
|
"navbar.links.shares": "My shares",
|
||||||
|
"navbar.links.reverse": "Reverse shares",
|
||||||
|
|
||||||
|
"navbar.avatar.account": "My account",
|
||||||
|
"navbar.avatar.admin": "Administration",
|
||||||
|
"navbar.avatar.signout": "Sign out",
|
||||||
|
// END navbar
|
||||||
|
|
||||||
|
// /
|
||||||
|
"home.title": "A <h>self-hosted</h> file sharing platform.",
|
||||||
|
|
||||||
|
"home.description":
|
||||||
|
"Do you really want to give your personal files in the hand of third parties like WeTransfer?",
|
||||||
|
"home.bullet.a.name": "Self-Hosted",
|
||||||
|
"home.bullet.a.description": "Host Pingvin Share on your own machine.",
|
||||||
|
"home.bullet.b.name": "Privacy",
|
||||||
|
"home.bullet.b.description":
|
||||||
|
"Your files are your files and should never get into the hands of third parties.",
|
||||||
|
"home.bullet.c.name": "No annoying file size limit",
|
||||||
|
"home.bullet.c.description":
|
||||||
|
"Upload as big files as you want. Only your hard drive will be your limit.",
|
||||||
|
|
||||||
|
"home.button.start": "Get started",
|
||||||
|
"home.button.source": "Source code",
|
||||||
|
// END /
|
||||||
|
|
||||||
|
// /auth/signin
|
||||||
|
"signin.title": "Welcome back",
|
||||||
|
"signin.description": "You don't have an account yet?",
|
||||||
|
"signin.button.signup": "Sign up",
|
||||||
|
"signin.input.email-or-username": "Email or username",
|
||||||
|
"signin.input.email-or-username.placeholder": "Your email or username",
|
||||||
|
"signin.input.password": "Password",
|
||||||
|
"signin.input.password.placeholder": "Your password",
|
||||||
|
"signin.button.submit": "Sign in",
|
||||||
|
"signIn.notify.totp-required.title": "Two-factor authentication required",
|
||||||
|
"signIn.notify.totp-required.description":
|
||||||
|
"Please enter your two-factor authentication code",
|
||||||
|
|
||||||
|
// END /auth/signin
|
||||||
|
|
||||||
|
// /auth/signup
|
||||||
|
"signup.title": "Create an account",
|
||||||
|
"signup.description": "Already have an account?",
|
||||||
|
"signup.button.signin": "Sign in",
|
||||||
|
"signup.input.username": "Username",
|
||||||
|
"signup.input.username.placeholder": "Your username",
|
||||||
|
"signup.input.email": "Email",
|
||||||
|
"signup.input.email.placeholder": "Your email",
|
||||||
|
"signup.button.submit": "Let's get started",
|
||||||
|
|
||||||
|
// END /auth/signup
|
||||||
|
|
||||||
|
// /auth/reset-password
|
||||||
|
"resetPassword.title": "Forgot your password?",
|
||||||
|
"resetPassword.description": "Enter your email to reset your password.",
|
||||||
|
"resetPassword.notify.success":
|
||||||
|
"An email has been sent with a link to reset your password.",
|
||||||
|
"resetPassword.button.back": "Back to sign in page",
|
||||||
|
"resetPassword.text.resetPassword": "Reset password",
|
||||||
|
"resetPassword.text.enterNewPassword": "Enter your new password",
|
||||||
|
"resetPassword.input.password": "New password",
|
||||||
|
"resetPassword.notify.passwordReset":
|
||||||
|
"Your password has been reset successfully.",
|
||||||
|
|
||||||
|
// /account
|
||||||
|
"account.title": "My account",
|
||||||
|
|
||||||
|
"account.card.info.title": "Account info",
|
||||||
|
"account.card.info.username": "Username",
|
||||||
|
"account.card.info.email": "Email",
|
||||||
|
"account.notify.info.success": "Account updated successfully",
|
||||||
|
|
||||||
|
"account.card.password.title": "Password",
|
||||||
|
"account.card.password.old": "Old password",
|
||||||
|
"account.card.password.new": "New password",
|
||||||
|
"account.notify.password.success": "Password changed successfully",
|
||||||
|
|
||||||
|
"account.card.security.title": "Security",
|
||||||
|
"account.card.security.totp.enable.description":
|
||||||
|
"Enter your current password to start enabling TOTP",
|
||||||
|
"account.card.security.totp.disable.description":
|
||||||
|
"Enter your current password to start enabling TOTP",
|
||||||
|
"account.card.security.totp.button.start": "Start",
|
||||||
|
"account.modal.totp.title": "Enable TOTP",
|
||||||
|
"account.modal.totp.step1": "Step 1: Add your authenticator",
|
||||||
|
"account.modal.totp.step2": "Step 2: Validate your code",
|
||||||
|
"account.modal.totp.enterManually": "Enter manually",
|
||||||
|
"account.modal.totp.code": "Code",
|
||||||
|
"account.modal.totp.clickToCopy": "Click to copy",
|
||||||
|
"account.modal.totp.verify": "Verify",
|
||||||
|
"account.notify.totp.disable": "TOTP disabled successfully",
|
||||||
|
"account.notify.totp.enable": "TOTP enabled successfully",
|
||||||
|
|
||||||
|
"account.card.language.title": "Language",
|
||||||
|
"account.card.color.title": "Color scheme",
|
||||||
|
|
||||||
|
// ThemeSwitcher.tsx
|
||||||
|
"account.theme.dark": "Dark",
|
||||||
|
"account.theme.light": "Light",
|
||||||
|
"account.theme.system": "System",
|
||||||
|
|
||||||
|
"account.button.delete": "Delete Account",
|
||||||
|
"account.modal.delete.title": "Delete Account",
|
||||||
|
"account.modal.delete.description":
|
||||||
|
"Do you really want to delete your account including all your active shares?",
|
||||||
|
// END /account
|
||||||
|
|
||||||
|
// /account/shares
|
||||||
|
"account.shares.title": "My shares",
|
||||||
|
"account.shares.title.empty": "It's empty here 👀",
|
||||||
|
"account.shares.description.empty": "You don't have any shares.",
|
||||||
|
"account.shares.button.create": "Create one",
|
||||||
|
|
||||||
|
"account.shares.info.title": "Share informatons",
|
||||||
|
"account.shares.table.id": "ID",
|
||||||
|
"account.shares.table.name": "Name",
|
||||||
|
"account.shares.table.description": "Description",
|
||||||
|
"account.shares.table.visitors": "Visitors",
|
||||||
|
"account.shares.table.expiresAt": "Expires at",
|
||||||
|
"account.shares.table.createdAt": "Created at",
|
||||||
|
"account.shares.table.size": "Size",
|
||||||
|
|
||||||
|
"account.shares.modal.share-link": "Share link",
|
||||||
|
|
||||||
|
"account.shares.modal.delete.title": "Delete {share}",
|
||||||
|
"account.shares.modal.delete.description":
|
||||||
|
"Do you really want to delete this share?",
|
||||||
|
|
||||||
|
// END /account/shares
|
||||||
|
|
||||||
|
// /account/reverseShares
|
||||||
|
"account.reverseShares.title": "Reverse shares",
|
||||||
|
"account.reverseShares.description":
|
||||||
|
"A reverse share allows you to generate a unique URL that allows external users to create a share.",
|
||||||
|
|
||||||
|
"account.reverseShares.title.empty": "It's empty here 👀",
|
||||||
|
"account.reverseShares.description.empty":
|
||||||
|
"You don't have any reverse shares.",
|
||||||
|
|
||||||
|
// showCreateReverseShareModal.tsx
|
||||||
|
"account.reverseShares.modal.expiration.label": "Expiration",
|
||||||
|
"account.reverseShares.modal.expiration.minute-singular": "Minute",
|
||||||
|
"account.reverseShares.modal.expiration.minute-plural": "Minutes",
|
||||||
|
"account.reverseShares.modal.expiration.hour-singular": "Hour",
|
||||||
|
"account.reverseShares.modal.expiration.hour-plural": "Hours",
|
||||||
|
"account.reverseShares.modal.expiration.day-singular": "Day",
|
||||||
|
"account.reverseShares.modal.expiration.day-plural": "Days",
|
||||||
|
"account.reverseShares.modal.expiration.week-singular": "Week",
|
||||||
|
"account.reverseShares.modal.expiration.week-plural": "Weeks",
|
||||||
|
"account.reverseShares.modal.expiration.month-singular": "Month",
|
||||||
|
"account.reverseShares.modal.expiration.month-plural": "Months",
|
||||||
|
"account.reverseShares.modal.expiration.year-singular": "Year",
|
||||||
|
"account.reverseShares.modal.expiration.year-plural": "Years",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.max-size.label": "Max share size",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.send-email": "Send email notification",
|
||||||
|
"account.reverseShares.modal.send-email.description":
|
||||||
|
"Send an email notification when a share is created with this reverse share link.",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.max-use.label": "Max uses",
|
||||||
|
"account.reverseShares.modal.max-use.description":
|
||||||
|
"The maximum amount of times this URL can be used to create a share.",
|
||||||
|
"account.reverseShare.never-expires": "This reverse share will never expire.",
|
||||||
|
"account.reverseShare.expires-on":
|
||||||
|
"This reverse share will expire on {expiration}.",
|
||||||
|
|
||||||
|
"account.reverseShares.table.no-shares": "No shares created yet",
|
||||||
|
"account.reverseShares.table.count.singular": "share",
|
||||||
|
"account.reverseShares.table.count.plural": "shares",
|
||||||
|
"account.reverseShares.table.shares": "Shares",
|
||||||
|
"account.reverseShares.table.remaining": "Remaining uses",
|
||||||
|
"account.reverseShares.table.max-size": "Max share size",
|
||||||
|
"account.reverseShares.table.expires": "Expires at",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.reverse-share-link": "Reverse share link",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.delete.title":
|
||||||
|
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
|
||||||
|
"account.reverseShares.modal.delete.description":
|
||||||
|
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
|
||||||
|
|
||||||
|
// END /account/reverseShares
|
||||||
|
|
||||||
|
// /admin
|
||||||
|
"admin.title": "Administration",
|
||||||
|
"admin.button.users": "User management",
|
||||||
|
"admin.button.config": "Configuration",
|
||||||
|
"admin.version": "Version",
|
||||||
|
// END /admin
|
||||||
|
|
||||||
|
// /admin/users
|
||||||
|
"admin.users.title": "User management",
|
||||||
|
"admin.users.table.username": "Username",
|
||||||
|
"admin.users.table.email": "Email",
|
||||||
|
"admin.users.table.admin": "Admin",
|
||||||
|
|
||||||
|
"admin.users.edit.update.title": "Update user {username}",
|
||||||
|
"admin.users.edit.update.admin-privileges": "Admin privileges",
|
||||||
|
"admin.users.edit.update.change-password.title": "Change password",
|
||||||
|
"admin.users.edit.update.change-password.field": "New password",
|
||||||
|
"admin.users.edit.update.change-password.button": "Save new password",
|
||||||
|
"admin.users.edit.update.notify.password.success":
|
||||||
|
"Password changed successfully",
|
||||||
|
|
||||||
|
"admin.users.edit.delete.title": "Delete user {username}",
|
||||||
|
"admin.users.edit.delete.description":
|
||||||
|
"Do you really want to delete this user and all his shares?",
|
||||||
|
|
||||||
|
// showCreateUserModal.tsx
|
||||||
|
"admin.users.modal.create.title": "Create user",
|
||||||
|
"admin.users.modal.create.username": "Username",
|
||||||
|
"admin.users.modal.create.email": "Email",
|
||||||
|
"admin.users.modal.create.password": "Password",
|
||||||
|
"admin.users.modal.create.manual-password": "Set password manually",
|
||||||
|
"admin.users.modal.create.manual-password.description":
|
||||||
|
"If not checked, the user will receive an email with a link to set their password.",
|
||||||
|
"admin.users.modal.create.admin": "Admin privileges",
|
||||||
|
"admin.users.modal.create.admin.description":
|
||||||
|
"If checked, the user will be able to access the admin panel.",
|
||||||
|
|
||||||
|
// END /admin/users
|
||||||
|
|
||||||
|
// /upload
|
||||||
|
"upload.title": "Upload",
|
||||||
|
|
||||||
|
"upload.notify.generic-error":
|
||||||
|
"An error occurred while finishing your share.",
|
||||||
|
"upload.notify.count-failed": "{count} filed failed to upload. Trying again.",
|
||||||
|
|
||||||
|
// Dropzone.tsx
|
||||||
|
"upload.dropzone.title": "Upload files",
|
||||||
|
"upload.dropzone.description":
|
||||||
|
"Drag'n'drop files here to start your share. We can accept only files that are less than {maxSize} in total.",
|
||||||
|
"upload.dropzone.notify.file-too-big":
|
||||||
|
"Your files exceed the maximum share size of {maxSize}.",
|
||||||
|
|
||||||
|
// FileList.tsx
|
||||||
|
"upload.filelist.name": "Name",
|
||||||
|
"upload.filelist.size": "Size",
|
||||||
|
|
||||||
|
// showCreateUploadModal.tsx
|
||||||
|
"upload.modal.title": "Create Share",
|
||||||
|
"upload.modal.link.error.invalid":
|
||||||
|
"Can only contain letters, numbers, underscores, and hyphens",
|
||||||
|
"upload.modal.link.error.taken": "This link is already in use",
|
||||||
|
"upload.modal.not-signed-in": "You're not signed in",
|
||||||
|
"upload.modal.not-signed-in-description":
|
||||||
|
"You will be unable to delete your share manually and view the visitor count.",
|
||||||
|
|
||||||
|
"upload.modal.expires.never": "never",
|
||||||
|
"upload.modal.expires.never-long": "Never Expires",
|
||||||
|
|
||||||
|
"upload.modal.link.label": "Link",
|
||||||
|
"upload.modal.link.placeholder": "myAwesomeShare",
|
||||||
|
|
||||||
|
"upload.modal.expires.label": "Expiration",
|
||||||
|
"upload.modal.expires.minute-singular": "Minute",
|
||||||
|
"upload.modal.expires.minute-plural": "Minutes",
|
||||||
|
"upload.modal.expires.hour-singular": "Hour",
|
||||||
|
"upload.modal.expires.hour-plural": "Hours",
|
||||||
|
"upload.modal.expires.day-singular": "Day",
|
||||||
|
"upload.modal.expires.day-plural": "Days",
|
||||||
|
"upload.modal.expires.week-singular": "Week",
|
||||||
|
"upload.modal.expires.week-plural": "Weeks",
|
||||||
|
"upload.modal.expires.month-singular": "Month",
|
||||||
|
"upload.modal.expires.month-plural": "Months",
|
||||||
|
"upload.modal.expires.year-singular": "Year",
|
||||||
|
"upload.modal.expires.year-plural": "Years",
|
||||||
|
|
||||||
|
"upload.modal.accordion.description.title": "Description",
|
||||||
|
"upload.modal.accordion.description.placeholder":
|
||||||
|
"Note for the recipients of this share",
|
||||||
|
|
||||||
|
"upload.modal.accordion.email.title": "Email recipients",
|
||||||
|
"upload.modal.accordion.email.placeholder": "Enter email recipients",
|
||||||
|
"upload.modal.accordion.email.invalid-email": "Invalid email address",
|
||||||
|
|
||||||
|
"upload.modal.accordion.security.title": "Security options",
|
||||||
|
"upload.modal.accordion.security.password.label": "Password protection",
|
||||||
|
"upload.modal.accordion.security.password.placeholder": "No password",
|
||||||
|
"upload.modal.accordion.security.max-views.label": "Maximum views",
|
||||||
|
"upload.modal.accordion.security.max-views.placeholder": "No limit",
|
||||||
|
|
||||||
|
// showCompletedUploadModal.tsx
|
||||||
|
"upload.modal.completed.never-expires": "This share will never expire.",
|
||||||
|
"upload.modal.completed.expires-on":
|
||||||
|
"This share will expire on {expiration}.",
|
||||||
|
"upload.modal.completed.share-ready": "Share ready",
|
||||||
|
|
||||||
|
// END /upload
|
||||||
|
|
||||||
|
// /share/[id]
|
||||||
|
"share.title": "Share {shareId}",
|
||||||
|
"share.description": "Look what I've shared with you!",
|
||||||
|
"share.error.visitor-limit-exceeded.title": "Visitor limit exceeded",
|
||||||
|
"share.error.visitor-limit-exceeded.description":
|
||||||
|
"The visitor limit from this share has been exceeded.",
|
||||||
|
"share.error.removed.title": "Share removed",
|
||||||
|
"share.error.not-found.title": "Share not found",
|
||||||
|
"share.error.not-found.description":
|
||||||
|
"The share you're looking for doesn't exist.",
|
||||||
|
|
||||||
|
"share.modal.password.title": "Password required",
|
||||||
|
"share.modal.password.description":
|
||||||
|
"To access this share please enter the password for the share.",
|
||||||
|
"share.modal.password": "Password",
|
||||||
|
"share.modal.error.invalid-password": "Invalid password",
|
||||||
|
|
||||||
|
"share.button.download-all": "Download all",
|
||||||
|
"share.notify.download-all-preparing":
|
||||||
|
"The share is preparing. Try again in a few minutes.",
|
||||||
|
|
||||||
|
"share.modal.file-link": "File link",
|
||||||
|
"share.table.name": "Name",
|
||||||
|
"share.table.size": "Size",
|
||||||
|
|
||||||
|
"share.modal.file-preview.error.not-supported.title": "Preview not supported",
|
||||||
|
"share.modal.file-preview.error.not-supported.description":
|
||||||
|
"A preview for thise file type is unsupported. Please download the file to view it.",
|
||||||
|
|
||||||
|
// END /share/[id]
|
||||||
|
|
||||||
|
// /admin/config
|
||||||
|
"admin.config.title": "Configuration",
|
||||||
|
"admin.config.category.general": "General",
|
||||||
|
"admin.config.category.share": "Share",
|
||||||
|
"admin.config.category.email": "Email",
|
||||||
|
"admin.config.category.smtp": "SMTP",
|
||||||
|
|
||||||
|
"admin.config.general.app-name": "App name",
|
||||||
|
"admin.config.general.app-name.description": "Name of the application",
|
||||||
|
"admin.config.general.app-url": "App URL",
|
||||||
|
"admin.config.general.app-url.description":
|
||||||
|
"On which URL Pingvin Share is available",
|
||||||
|
"admin.config.general.show-home-page": "Show home page",
|
||||||
|
"admin.config.general.show-home-page.description":
|
||||||
|
"Whether to show the home page",
|
||||||
|
"admin.config.general.logo": "Logo",
|
||||||
|
"admin.config.general.logo.description":
|
||||||
|
"Change your logo by uploading a new image. The image must be a PNG and should have the format 1:1.",
|
||||||
|
"admin.config.general.logo.placeholder": "Pick image",
|
||||||
|
|
||||||
|
"admin.config.email.enable-share-email-recipients":
|
||||||
|
"Enable share email recipients",
|
||||||
|
"admin.config.email.enable-share-email-recipients.description":
|
||||||
|
"Whether to allow emails to share recipients. Only enable this if you have enabled SMTP.",
|
||||||
|
"admin.config.email.share-recipients-subject": "Share recipients subject",
|
||||||
|
"admin.config.email.share-recipients-subject.description":
|
||||||
|
"Subject of the email which gets sent to the share recipients.",
|
||||||
|
"admin.config.email.share-recipients-message": "Share recipients message",
|
||||||
|
"admin.config.email.share-recipients-message.description":
|
||||||
|
"Message which gets sent to the share recipients. Available variables:\n {creator} - The username of the creator of the share\n {shareUrl} - The URL of the share\n {desc} - The description of the share\n {expires} - The expiration date of the share\n The variables will be replaced with the actual values.",
|
||||||
|
"admin.config.email.reverse-share-subject": "Reverse share subject",
|
||||||
|
"admin.config.email.reverse-share-subject.description":
|
||||||
|
"Subject of the email which gets sent when someone created a share with your reverse share link.",
|
||||||
|
"admin.config.email.reverse-share-message": "Reverse share message",
|
||||||
|
"admin.config.email.reverse-share-message.description":
|
||||||
|
"Message which gets sent when someone created a share with your reverse share link. {shareUrl} will be replaced with the creator's name and the share URL.",
|
||||||
|
"admin.config.email.reset-password-subject": "Reset password subject",
|
||||||
|
"admin.config.email.reset-password-subject.description":
|
||||||
|
"Subject of the email which gets sent when a user requests a password reset.",
|
||||||
|
"admin.config.email.reset-password-message": "Reset password message",
|
||||||
|
"admin.config.email.reset-password-message.description":
|
||||||
|
"Message which gets sent when a user requests a password reset. {url} will be replaced with the reset password URL.",
|
||||||
|
"admin.config.email.invite-subject": "Invite subject",
|
||||||
|
"admin.config.email.invite-subject.description":
|
||||||
|
"Subject of the email which gets sent when an admin invites a user.",
|
||||||
|
"admin.config.email.invite-message": "Invite message",
|
||||||
|
"admin.config.email.invite-message.description":
|
||||||
|
"Message which gets sent when an admin invites a user. {url} will be replaced with the invite URL and {password} with the password.",
|
||||||
|
"admin.config.share.allow-registration": "Allow registration",
|
||||||
|
"admin.config.share.allow-registration.description":
|
||||||
|
"Whether registration is allowed",
|
||||||
|
"admin.config.share.allow-unauthenticated-shares":
|
||||||
|
"Allow unauthenticated shares",
|
||||||
|
"admin.config.share.allow-unauthenticated-shares.description":
|
||||||
|
"Whether unauthorized users can create shares",
|
||||||
|
"admin.config.share.max-size": "Max size",
|
||||||
|
"admin.config.share.max-size.description": "Maximum share size in bytes",
|
||||||
|
|
||||||
|
"admin.config.smtp.enabled": "Enabled",
|
||||||
|
"admin.config.smtp.enabled.description":
|
||||||
|
"Whether SMTP is enabled. Only set this to true if you entered the host, port, email, user and password of your SMTP server.",
|
||||||
|
"admin.config.smtp.host": "Host",
|
||||||
|
"admin.config.smtp.host.description": "Host of the SMTP server",
|
||||||
|
"admin.config.smtp.port": "Port",
|
||||||
|
"admin.config.smtp.port.description": "Port of the SMTP server",
|
||||||
|
"admin.config.smtp.email": "Email",
|
||||||
|
"admin.config.smtp.email.description":
|
||||||
|
"Email address which the emails get sent from",
|
||||||
|
"admin.config.smtp.username": "Username",
|
||||||
|
"admin.config.smtp.username.description": "Username of the SMTP server",
|
||||||
|
"admin.config.smtp.password": "Password",
|
||||||
|
"admin.config.smtp.password.description": "Password of the SMTP server",
|
||||||
|
"admin.config.smtp.button.test": "Send test email",
|
||||||
|
|
||||||
|
// 404
|
||||||
|
"404.title": "404",
|
||||||
|
"404.description": "Oops this page doesn't exist.",
|
||||||
|
"404.button.home": "Gring me back home",
|
||||||
|
|
||||||
|
// Common translations
|
||||||
|
"common.button.save": "Save",
|
||||||
|
"common.button.create": "Create",
|
||||||
|
"common.button.submit": "Submit",
|
||||||
|
"common.button.delete": "Delete",
|
||||||
|
"common.button.cancel": "Cancel",
|
||||||
|
"common.button.confirm": "Confirm",
|
||||||
|
"common.button.disable": "Disable",
|
||||||
|
"common.button.share": "Share",
|
||||||
|
"common.button.generate": "Generate",
|
||||||
|
"common.button.done": "Done",
|
||||||
|
"common.text.link": "Link",
|
||||||
|
"common.text.or": "or",
|
||||||
|
"common.button.go-back": "Go back",
|
||||||
|
"common.notify.copied": "Your link was copied to the clipboard",
|
||||||
|
|
||||||
|
"common.error": "Error",
|
||||||
|
"common.error.unknown": "An unknown error occurred",
|
||||||
|
"common.error.invalid-email": "Invalid email address",
|
||||||
|
"common.error.too-short": "Must be at least {length} characters",
|
||||||
|
"common.error.too-long": "Must be at most {length} characters",
|
||||||
|
"common.error.exact-length": "Must be exactly {length} characters",
|
||||||
|
"common.error.invalid-number": "Must be a number",
|
||||||
|
"common.error.field-required": "This field is required",
|
||||||
|
};
|
435
frontend/src/i18n/translations/en.ts
Normal file
435
frontend/src/i18n/translations/en.ts
Normal file
|
@ -0,0 +1,435 @@
|
||||||
|
export default {
|
||||||
|
// Navbar
|
||||||
|
"navbar.upload": "Upload",
|
||||||
|
"navbar.signin": "Sign in",
|
||||||
|
"navbar.home": "Home",
|
||||||
|
"navbar.signup": "Sign Up",
|
||||||
|
|
||||||
|
"navbar.links.shares": "My shares",
|
||||||
|
"navbar.links.reverse": "Reverse shares",
|
||||||
|
|
||||||
|
"navbar.avatar.account": "My account",
|
||||||
|
"navbar.avatar.admin": "Administration",
|
||||||
|
"navbar.avatar.signout": "Sign out",
|
||||||
|
// END navbar
|
||||||
|
|
||||||
|
// /
|
||||||
|
"home.title": "A <h>self-hosted</h> file sharing platform.",
|
||||||
|
|
||||||
|
"home.description":
|
||||||
|
"Do you really want to give your personal files in the hand of third parties like WeTransfer?",
|
||||||
|
"home.bullet.a.name": "Self-Hosted",
|
||||||
|
"home.bullet.a.description": "Host Pingvin Share on your own machine.",
|
||||||
|
"home.bullet.b.name": "Privacy",
|
||||||
|
"home.bullet.b.description":
|
||||||
|
"Your files are your files and should never get into the hands of third parties.",
|
||||||
|
"home.bullet.c.name": "No annoying file size limit",
|
||||||
|
"home.bullet.c.description":
|
||||||
|
"Upload as big files as you want. Only your hard drive will be your limit.",
|
||||||
|
|
||||||
|
"home.button.start": "Get started",
|
||||||
|
"home.button.source": "Source code",
|
||||||
|
// END /
|
||||||
|
|
||||||
|
// /auth/signin
|
||||||
|
"signin.title": "Welcome back",
|
||||||
|
"signin.description": "You don't have an account yet?",
|
||||||
|
"signin.button.signup": "Sign up",
|
||||||
|
"signin.input.email-or-username": "Email or username",
|
||||||
|
"signin.input.email-or-username.placeholder": "Your email or username",
|
||||||
|
"signin.input.password": "Password",
|
||||||
|
"signin.input.password.placeholder": "Your password",
|
||||||
|
"signin.button.submit": "Sign in",
|
||||||
|
"signIn.notify.totp-required.title": "Two-factor authentication required",
|
||||||
|
"signIn.notify.totp-required.description":
|
||||||
|
"Please enter your two-factor authentication code",
|
||||||
|
|
||||||
|
// END /auth/signin
|
||||||
|
|
||||||
|
// /auth/signup
|
||||||
|
"signup.title": "Create an account",
|
||||||
|
"signup.description": "Already have an account?",
|
||||||
|
"signup.button.signin": "Sign in",
|
||||||
|
"signup.input.username": "Username",
|
||||||
|
"signup.input.username.placeholder": "Your username",
|
||||||
|
"signup.input.email": "Email",
|
||||||
|
"signup.input.email.placeholder": "Your email",
|
||||||
|
"signup.button.submit": "Let's get started",
|
||||||
|
|
||||||
|
// END /auth/signup
|
||||||
|
|
||||||
|
// /auth/reset-password
|
||||||
|
"resetPassword.title": "Forgot your password?",
|
||||||
|
"resetPassword.description": "Enter your email to reset your password.",
|
||||||
|
"resetPassword.notify.success":
|
||||||
|
"An email has been sent with a link to reset your password.",
|
||||||
|
"resetPassword.button.back": "Back to sign in page",
|
||||||
|
"resetPassword.text.resetPassword": "Reset password",
|
||||||
|
"resetPassword.text.enterNewPassword": "Enter your new password",
|
||||||
|
"resetPassword.input.password": "New password",
|
||||||
|
"resetPassword.notify.passwordReset":
|
||||||
|
"Your password has been reset successfully.",
|
||||||
|
|
||||||
|
// /account
|
||||||
|
"account.title": "My account",
|
||||||
|
|
||||||
|
"account.card.info.title": "Account info",
|
||||||
|
"account.card.info.username": "Username",
|
||||||
|
"account.card.info.email": "Email",
|
||||||
|
"account.notify.info.success": "Account updated successfully",
|
||||||
|
|
||||||
|
"account.card.password.title": "Password",
|
||||||
|
"account.card.password.old": "Old password",
|
||||||
|
"account.card.password.new": "New password",
|
||||||
|
"account.notify.password.success": "Password changed successfully",
|
||||||
|
|
||||||
|
"account.card.security.title": "Security",
|
||||||
|
"account.card.security.totp.enable.description":
|
||||||
|
"Enter your current password to start enabling TOTP",
|
||||||
|
"account.card.security.totp.disable.description":
|
||||||
|
"Enter your current password to start enabling TOTP",
|
||||||
|
"account.card.security.totp.button.start": "Start",
|
||||||
|
"account.modal.totp.title": "Enable TOTP",
|
||||||
|
"account.modal.totp.step1": "Step 1: Add your authenticator",
|
||||||
|
"account.modal.totp.step2": "Step 2: Validate your code",
|
||||||
|
"account.modal.totp.enterManually": "Enter manually",
|
||||||
|
"account.modal.totp.code": "Code",
|
||||||
|
"account.modal.totp.clickToCopy": "Click to copy",
|
||||||
|
"account.modal.totp.verify": "Verify",
|
||||||
|
"account.notify.totp.disable": "TOTP disabled successfully",
|
||||||
|
"account.notify.totp.enable": "TOTP enabled successfully",
|
||||||
|
|
||||||
|
"account.card.language.title": "Language",
|
||||||
|
"account.card.color.title": "Color scheme",
|
||||||
|
|
||||||
|
// ThemeSwitcher.tsx
|
||||||
|
"account.theme.dark": "Dark",
|
||||||
|
"account.theme.light": "Light",
|
||||||
|
"account.theme.system": "System",
|
||||||
|
|
||||||
|
"account.button.delete": "Delete Account",
|
||||||
|
"account.modal.delete.title": "Delete Account",
|
||||||
|
"account.modal.delete.description":
|
||||||
|
"Do you really want to delete your account including all your active shares?",
|
||||||
|
// END /account
|
||||||
|
|
||||||
|
// /account/shares
|
||||||
|
"account.shares.title": "My shares",
|
||||||
|
"account.shares.title.empty": "It's empty here 👀",
|
||||||
|
"account.shares.description.empty": "You don't have any shares.",
|
||||||
|
"account.shares.button.create": "Create one",
|
||||||
|
|
||||||
|
"account.shares.info.title": "Share informatons",
|
||||||
|
"account.shares.table.id": "ID",
|
||||||
|
"account.shares.table.name": "Name",
|
||||||
|
"account.shares.table.description": "Description",
|
||||||
|
"account.shares.table.visitors": "Visitors",
|
||||||
|
"account.shares.table.expiresAt": "Expires at",
|
||||||
|
"account.shares.table.createdAt": "Created at",
|
||||||
|
"account.shares.table.size": "Size",
|
||||||
|
|
||||||
|
"account.shares.modal.share-link": "Share link",
|
||||||
|
|
||||||
|
"account.shares.modal.delete.title": "Delete {share}",
|
||||||
|
"account.shares.modal.delete.description":
|
||||||
|
"Do you really want to delete this share?",
|
||||||
|
|
||||||
|
// END /account/shares
|
||||||
|
|
||||||
|
// /account/reverseShares
|
||||||
|
"account.reverseShares.title": "Reverse shares",
|
||||||
|
"account.reverseShares.description":
|
||||||
|
"A reverse share allows you to generate a unique URL that allows external users to create a share.",
|
||||||
|
|
||||||
|
"account.reverseShares.title.empty": "It's empty here 👀",
|
||||||
|
"account.reverseShares.description.empty":
|
||||||
|
"You don't have any reverse shares.",
|
||||||
|
|
||||||
|
// showCreateReverseShareModal.tsx
|
||||||
|
"account.reverseShares.modal.expiration.label": "Expiration",
|
||||||
|
"account.reverseShares.modal.expiration.minute-singular": "Minute",
|
||||||
|
"account.reverseShares.modal.expiration.minute-plural": "Minutes",
|
||||||
|
"account.reverseShares.modal.expiration.hour-singular": "Hour",
|
||||||
|
"account.reverseShares.modal.expiration.hour-plural": "Hours",
|
||||||
|
"account.reverseShares.modal.expiration.day-singular": "Day",
|
||||||
|
"account.reverseShares.modal.expiration.day-plural": "Days",
|
||||||
|
"account.reverseShares.modal.expiration.week-singular": "Week",
|
||||||
|
"account.reverseShares.modal.expiration.week-plural": "Weeks",
|
||||||
|
"account.reverseShares.modal.expiration.month-singular": "Month",
|
||||||
|
"account.reverseShares.modal.expiration.month-plural": "Months",
|
||||||
|
"account.reverseShares.modal.expiration.year-singular": "Year",
|
||||||
|
"account.reverseShares.modal.expiration.year-plural": "Years",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.max-size.label": "Max share size",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.send-email": "Send email notification",
|
||||||
|
"account.reverseShares.modal.send-email.description":
|
||||||
|
"Send an email notification when a share is created with this reverse share link.",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.max-use.label": "Max uses",
|
||||||
|
"account.reverseShares.modal.max-use.description":
|
||||||
|
"The maximum amount of times this URL can be used to create a share.",
|
||||||
|
"account.reverseShare.never-expires": "This reverse share will never expire.",
|
||||||
|
"account.reverseShare.expires-on":
|
||||||
|
"This reverse share will expire on {expiration}.",
|
||||||
|
|
||||||
|
"account.reverseShares.table.no-shares": "No shares created yet",
|
||||||
|
"account.reverseShares.table.count.singular": "share",
|
||||||
|
"account.reverseShares.table.count.plural": "shares",
|
||||||
|
"account.reverseShares.table.shares": "Shares",
|
||||||
|
"account.reverseShares.table.remaining": "Remaining uses",
|
||||||
|
"account.reverseShares.table.max-size": "Max share size",
|
||||||
|
"account.reverseShares.table.expires": "Expires at",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.reverse-share-link": "Reverse share link",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.delete.title":
|
||||||
|
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
|
||||||
|
"account.reverseShares.modal.delete.description":
|
||||||
|
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
|
||||||
|
|
||||||
|
// END /account/reverseShares
|
||||||
|
|
||||||
|
// /admin
|
||||||
|
"admin.title": "Administration",
|
||||||
|
"admin.button.users": "User management",
|
||||||
|
"admin.button.config": "Configuration",
|
||||||
|
"admin.version": "Version",
|
||||||
|
// END /admin
|
||||||
|
|
||||||
|
// /admin/users
|
||||||
|
"admin.users.title": "User management",
|
||||||
|
"admin.users.table.username": "Username",
|
||||||
|
"admin.users.table.email": "Email",
|
||||||
|
"admin.users.table.admin": "Admin",
|
||||||
|
|
||||||
|
"admin.users.edit.update.title": "Update user {username}",
|
||||||
|
"admin.users.edit.update.admin-privileges": "Admin privileges",
|
||||||
|
"admin.users.edit.update.change-password.title": "Change password",
|
||||||
|
"admin.users.edit.update.change-password.field": "New password",
|
||||||
|
"admin.users.edit.update.change-password.button": "Save new password",
|
||||||
|
"admin.users.edit.update.notify.password.success":
|
||||||
|
"Password changed successfully",
|
||||||
|
|
||||||
|
"admin.users.edit.delete.title": "Delete user {username}",
|
||||||
|
"admin.users.edit.delete.description":
|
||||||
|
"Do you really want to delete this user and all his shares?",
|
||||||
|
|
||||||
|
// showCreateUserModal.tsx
|
||||||
|
"admin.users.modal.create.title": "Create user",
|
||||||
|
"admin.users.modal.create.username": "Username",
|
||||||
|
"admin.users.modal.create.email": "Email",
|
||||||
|
"admin.users.modal.create.password": "Password",
|
||||||
|
"admin.users.modal.create.manual-password": "Set password manually",
|
||||||
|
"admin.users.modal.create.manual-password.description":
|
||||||
|
"If not checked, the user will receive an email with a link to set their password.",
|
||||||
|
"admin.users.modal.create.admin": "Admin privileges",
|
||||||
|
"admin.users.modal.create.admin.description":
|
||||||
|
"If checked, the user will be able to access the admin panel.",
|
||||||
|
|
||||||
|
// END /admin/users
|
||||||
|
|
||||||
|
// /upload
|
||||||
|
"upload.title": "Upload",
|
||||||
|
|
||||||
|
"upload.notify.generic-error":
|
||||||
|
"An error occurred while finishing your share.",
|
||||||
|
"upload.notify.count-failed": "{count} filed failed to upload. Trying again.",
|
||||||
|
|
||||||
|
// Dropzone.tsx
|
||||||
|
"upload.dropzone.title": "Upload files",
|
||||||
|
"upload.dropzone.description":
|
||||||
|
"Drag'n'drop files here to start your share. We can accept only files that are less than {maxSize} in total.",
|
||||||
|
"upload.dropzone.notify.file-too-big":
|
||||||
|
"Your files exceed the maximum share size of {maxSize}.",
|
||||||
|
|
||||||
|
// FileList.tsx
|
||||||
|
"upload.filelist.name": "Name",
|
||||||
|
"upload.filelist.size": "Size",
|
||||||
|
|
||||||
|
// showCreateUploadModal.tsx
|
||||||
|
"upload.modal.title": "Create Share",
|
||||||
|
"upload.modal.link.error.invalid":
|
||||||
|
"Can only contain letters, numbers, underscores, and hyphens",
|
||||||
|
"upload.modal.link.error.taken": "This link is already in use",
|
||||||
|
"upload.modal.not-signed-in": "You're not signed in",
|
||||||
|
"upload.modal.not-signed-in-description":
|
||||||
|
"You will be unable to delete your share manually and view the visitor count.",
|
||||||
|
|
||||||
|
"upload.modal.expires.never": "never",
|
||||||
|
"upload.modal.expires.never-long": "Never Expires",
|
||||||
|
|
||||||
|
"upload.modal.link.label": "Link",
|
||||||
|
"upload.modal.link.placeholder": "myAwesomeShare",
|
||||||
|
|
||||||
|
"upload.modal.expires.label": "Expiration",
|
||||||
|
"upload.modal.expires.minute-singular": "Minute",
|
||||||
|
"upload.modal.expires.minute-plural": "Minutes",
|
||||||
|
"upload.modal.expires.hour-singular": "Hour",
|
||||||
|
"upload.modal.expires.hour-plural": "Hours",
|
||||||
|
"upload.modal.expires.day-singular": "Day",
|
||||||
|
"upload.modal.expires.day-plural": "Days",
|
||||||
|
"upload.modal.expires.week-singular": "Week",
|
||||||
|
"upload.modal.expires.week-plural": "Weeks",
|
||||||
|
"upload.modal.expires.month-singular": "Month",
|
||||||
|
"upload.modal.expires.month-plural": "Months",
|
||||||
|
"upload.modal.expires.year-singular": "Year",
|
||||||
|
"upload.modal.expires.year-plural": "Years",
|
||||||
|
|
||||||
|
"upload.modal.accordion.description.title": "Description",
|
||||||
|
"upload.modal.accordion.description.placeholder":
|
||||||
|
"Note for the recipients of this share",
|
||||||
|
|
||||||
|
"upload.modal.accordion.email.title": "Email recipients",
|
||||||
|
"upload.modal.accordion.email.placeholder": "Enter email recipients",
|
||||||
|
"upload.modal.accordion.email.invalid-email": "Invalid email address",
|
||||||
|
|
||||||
|
"upload.modal.accordion.security.title": "Security options",
|
||||||
|
"upload.modal.accordion.security.password.label": "Password protection",
|
||||||
|
"upload.modal.accordion.security.password.placeholder": "No password",
|
||||||
|
"upload.modal.accordion.security.max-views.label": "Maximum views",
|
||||||
|
"upload.modal.accordion.security.max-views.placeholder": "No limit",
|
||||||
|
|
||||||
|
// showCompletedUploadModal.tsx
|
||||||
|
"upload.modal.completed.never-expires": "This share will never expire.",
|
||||||
|
"upload.modal.completed.expires-on":
|
||||||
|
"This share will expire on {expiration}.",
|
||||||
|
"upload.modal.completed.share-ready": "Share ready",
|
||||||
|
|
||||||
|
// END /upload
|
||||||
|
|
||||||
|
// /share/[id]
|
||||||
|
"share.title": "Share {shareId}",
|
||||||
|
"share.description": "Look what I've shared with you!",
|
||||||
|
"share.error.visitor-limit-exceeded.title": "Visitor limit exceeded",
|
||||||
|
"share.error.visitor-limit-exceeded.description":
|
||||||
|
"The visitor limit from this share has been exceeded.",
|
||||||
|
"share.error.removed.title": "Share removed",
|
||||||
|
"share.error.not-found.title": "Share not found",
|
||||||
|
"share.error.not-found.description":
|
||||||
|
"The share you're looking for doesn't exist.",
|
||||||
|
|
||||||
|
"share.modal.password.title": "Password required",
|
||||||
|
"share.modal.password.description":
|
||||||
|
"To access this share please enter the password for the share.",
|
||||||
|
"share.modal.password": "Password",
|
||||||
|
"share.modal.error.invalid-password": "Invalid password",
|
||||||
|
|
||||||
|
"share.button.download-all": "Download all",
|
||||||
|
"share.notify.download-all-preparing":
|
||||||
|
"The share is preparing. Try again in a few minutes.",
|
||||||
|
|
||||||
|
"share.modal.file-link": "File link",
|
||||||
|
"share.table.name": "Name",
|
||||||
|
"share.table.size": "Size",
|
||||||
|
|
||||||
|
"share.modal.file-preview.error.not-supported.title": "Preview not supported",
|
||||||
|
"share.modal.file-preview.error.not-supported.description":
|
||||||
|
"A preview for thise file type is unsupported. Please download the file to view it.",
|
||||||
|
|
||||||
|
// END /share/[id]
|
||||||
|
|
||||||
|
// /admin/config
|
||||||
|
"admin.config.title": "Configuration",
|
||||||
|
"admin.config.category.general": "General",
|
||||||
|
"admin.config.category.share": "Share",
|
||||||
|
"admin.config.category.email": "Email",
|
||||||
|
"admin.config.category.smtp": "SMTP",
|
||||||
|
|
||||||
|
"admin.config.general.app-name": "App name",
|
||||||
|
"admin.config.general.app-name.description": "Name of the application",
|
||||||
|
"admin.config.general.app-url": "App URL",
|
||||||
|
"admin.config.general.app-url.description":
|
||||||
|
"On which URL Pingvin Share is available",
|
||||||
|
"admin.config.general.show-home-page": "Show home page",
|
||||||
|
"admin.config.general.show-home-page.description":
|
||||||
|
"Whether to show the home page",
|
||||||
|
"admin.config.general.logo": "Logo",
|
||||||
|
"admin.config.general.logo.description":
|
||||||
|
"Change your logo by uploading a new image. The image must be a PNG and should have the format 1:1.",
|
||||||
|
"admin.config.general.logo.placeholder": "Pick image",
|
||||||
|
|
||||||
|
"admin.config.email.enable-share-email-recipients":
|
||||||
|
"Enable share email recipients",
|
||||||
|
"admin.config.email.enable-share-email-recipients.description":
|
||||||
|
"Whether to allow emails to share recipients. Only enable this if you have enabled SMTP.",
|
||||||
|
"admin.config.email.share-recipients-subject": "Share recipients subject",
|
||||||
|
"admin.config.email.share-recipients-subject.description":
|
||||||
|
"Subject of the email which gets sent to the share recipients.",
|
||||||
|
"admin.config.email.share-recipients-message": "Share recipients message",
|
||||||
|
"admin.config.email.share-recipients-message.description":
|
||||||
|
"Message which gets sent to the share recipients. Available variables:\n {creator} - The username of the creator of the share\n {shareUrl} - The URL of the share\n {desc} - The description of the share\n {expires} - The expiration date of the share\n The variables will be replaced with the actual values.",
|
||||||
|
"admin.config.email.reverse-share-subject": "Reverse share subject",
|
||||||
|
"admin.config.email.reverse-share-subject.description":
|
||||||
|
"Subject of the email which gets sent when someone created a share with your reverse share link.",
|
||||||
|
"admin.config.email.reverse-share-message": "Reverse share message",
|
||||||
|
"admin.config.email.reverse-share-message.description":
|
||||||
|
"Message which gets sent when someone created a share with your reverse share link. {shareUrl} will be replaced with the creator's name and the share URL.",
|
||||||
|
"admin.config.email.reset-password-subject": "Reset password subject",
|
||||||
|
"admin.config.email.reset-password-subject.description":
|
||||||
|
"Subject of the email which gets sent when a user requests a password reset.",
|
||||||
|
"admin.config.email.reset-password-message": "Reset password message",
|
||||||
|
"admin.config.email.reset-password-message.description":
|
||||||
|
"Message which gets sent when a user requests a password reset. {url} will be replaced with the reset password URL.",
|
||||||
|
"admin.config.email.invite-subject": "Invite subject",
|
||||||
|
"admin.config.email.invite-subject.description":
|
||||||
|
"Subject of the email which gets sent when an admin invites a user.",
|
||||||
|
"admin.config.email.invite-message": "Invite message",
|
||||||
|
"admin.config.email.invite-message.description":
|
||||||
|
"Message which gets sent when an admin invites a user. {url} will be replaced with the invite URL and {password} with the password.",
|
||||||
|
"admin.config.share.allow-registration": "Allow registration",
|
||||||
|
"admin.config.share.allow-registration.description":
|
||||||
|
"Whether registration is allowed",
|
||||||
|
"admin.config.share.allow-unauthenticated-shares":
|
||||||
|
"Allow unauthenticated shares",
|
||||||
|
"admin.config.share.allow-unauthenticated-shares.description":
|
||||||
|
"Whether unauthorized users can create shares",
|
||||||
|
"admin.config.share.max-size": "Max size",
|
||||||
|
"admin.config.share.max-size.description": "Maximum share size in bytes",
|
||||||
|
|
||||||
|
"admin.config.smtp.enabled": "Enabled",
|
||||||
|
"admin.config.smtp.enabled.description":
|
||||||
|
"Whether SMTP is enabled. Only set this to true if you entered the host, port, email, user and password of your SMTP server.",
|
||||||
|
"admin.config.smtp.host": "Host",
|
||||||
|
"admin.config.smtp.host.description": "Host of the SMTP server",
|
||||||
|
"admin.config.smtp.port": "Port",
|
||||||
|
"admin.config.smtp.port.description": "Port of the SMTP server",
|
||||||
|
"admin.config.smtp.email": "Email",
|
||||||
|
"admin.config.smtp.email.description":
|
||||||
|
"Email address which the emails get sent from",
|
||||||
|
"admin.config.smtp.username": "Username",
|
||||||
|
"admin.config.smtp.username.description": "Username of the SMTP server",
|
||||||
|
"admin.config.smtp.password": "Password",
|
||||||
|
"admin.config.smtp.password.description": "Password of the SMTP server",
|
||||||
|
"admin.config.smtp.button.test": "Send test email",
|
||||||
|
|
||||||
|
// 404
|
||||||
|
"404.title": "404",
|
||||||
|
"404.description": "Oops this page doesn't exist.",
|
||||||
|
"404.button.home": "Gring me back home",
|
||||||
|
|
||||||
|
// Common translations
|
||||||
|
"common.button.save": "Save",
|
||||||
|
"common.button.create": "Create",
|
||||||
|
"common.button.submit": "Submit",
|
||||||
|
"common.button.delete": "Delete",
|
||||||
|
"common.button.cancel": "Cancel",
|
||||||
|
"common.button.confirm": "Confirm",
|
||||||
|
"common.button.disable": "Disable",
|
||||||
|
"common.button.share": "Share",
|
||||||
|
"common.button.generate": "Generate",
|
||||||
|
"common.button.done": "Done",
|
||||||
|
"common.text.link": "Link",
|
||||||
|
"common.text.or": "or",
|
||||||
|
"common.button.go-back": "Go back",
|
||||||
|
"common.notify.copied": "Your link was copied to the clipboard",
|
||||||
|
|
||||||
|
"common.error": "Error",
|
||||||
|
"common.error.unknown": "An unknown error occurred",
|
||||||
|
"common.error.invalid-email": "Invalid email address",
|
||||||
|
"common.error.too-short": "Must be at least {length} characters",
|
||||||
|
"common.error.too-long": "Must be at most {length} characters",
|
||||||
|
"common.error.exact-length": "Must be exactly {length} characters",
|
||||||
|
"common.error.invalid-number": "Must be a number",
|
||||||
|
"common.error.field-required": "This field is required",
|
||||||
|
};
|
435
frontend/src/i18n/translations/es.ts
Normal file
435
frontend/src/i18n/translations/es.ts
Normal file
|
@ -0,0 +1,435 @@
|
||||||
|
export default {
|
||||||
|
// Navbar
|
||||||
|
"navbar.upload": "Upload",
|
||||||
|
"navbar.signin": "Sign in",
|
||||||
|
"navbar.home": "Home",
|
||||||
|
"navbar.signup": "Sign Up",
|
||||||
|
|
||||||
|
"navbar.links.shares": "My shares",
|
||||||
|
"navbar.links.reverse": "Reverse shares",
|
||||||
|
|
||||||
|
"navbar.avatar.account": "My account",
|
||||||
|
"navbar.avatar.admin": "Administration",
|
||||||
|
"navbar.avatar.signout": "Sign out",
|
||||||
|
// END navbar
|
||||||
|
|
||||||
|
// /
|
||||||
|
"home.title": "A <h>self-hosted</h> file sharing platform.",
|
||||||
|
|
||||||
|
"home.description":
|
||||||
|
"Do you really want to give your personal files in the hand of third parties like WeTransfer?",
|
||||||
|
"home.bullet.a.name": "Self-Hosted",
|
||||||
|
"home.bullet.a.description": "Host Pingvin Share on your own machine.",
|
||||||
|
"home.bullet.b.name": "Privacy",
|
||||||
|
"home.bullet.b.description":
|
||||||
|
"Your files are your files and should never get into the hands of third parties.",
|
||||||
|
"home.bullet.c.name": "No annoying file size limit",
|
||||||
|
"home.bullet.c.description":
|
||||||
|
"Upload as big files as you want. Only your hard drive will be your limit.",
|
||||||
|
|
||||||
|
"home.button.start": "Get started",
|
||||||
|
"home.button.source": "Source code",
|
||||||
|
// END /
|
||||||
|
|
||||||
|
// /auth/signin
|
||||||
|
"signin.title": "Welcome back",
|
||||||
|
"signin.description": "You don't have an account yet?",
|
||||||
|
"signin.button.signup": "Sign up",
|
||||||
|
"signin.input.email-or-username": "Email or username",
|
||||||
|
"signin.input.email-or-username.placeholder": "Your email or username",
|
||||||
|
"signin.input.password": "Password",
|
||||||
|
"signin.input.password.placeholder": "Your password",
|
||||||
|
"signin.button.submit": "Sign in",
|
||||||
|
"signIn.notify.totp-required.title": "Two-factor authentication required",
|
||||||
|
"signIn.notify.totp-required.description":
|
||||||
|
"Please enter your two-factor authentication code",
|
||||||
|
|
||||||
|
// END /auth/signin
|
||||||
|
|
||||||
|
// /auth/signup
|
||||||
|
"signup.title": "Create an account",
|
||||||
|
"signup.description": "Already have an account?",
|
||||||
|
"signup.button.signin": "Sign in",
|
||||||
|
"signup.input.username": "Username",
|
||||||
|
"signup.input.username.placeholder": "Your username",
|
||||||
|
"signup.input.email": "Email",
|
||||||
|
"signup.input.email.placeholder": "Your email",
|
||||||
|
"signup.button.submit": "Let's get started",
|
||||||
|
|
||||||
|
// END /auth/signup
|
||||||
|
|
||||||
|
// /auth/reset-password
|
||||||
|
"resetPassword.title": "Forgot your password?",
|
||||||
|
"resetPassword.description": "Enter your email to reset your password.",
|
||||||
|
"resetPassword.notify.success":
|
||||||
|
"An email has been sent with a link to reset your password.",
|
||||||
|
"resetPassword.button.back": "Back to sign in page",
|
||||||
|
"resetPassword.text.resetPassword": "Reset password",
|
||||||
|
"resetPassword.text.enterNewPassword": "Enter your new password",
|
||||||
|
"resetPassword.input.password": "New password",
|
||||||
|
"resetPassword.notify.passwordReset":
|
||||||
|
"Your password has been reset successfully.",
|
||||||
|
|
||||||
|
// /account
|
||||||
|
"account.title": "My account",
|
||||||
|
|
||||||
|
"account.card.info.title": "Account info",
|
||||||
|
"account.card.info.username": "Username",
|
||||||
|
"account.card.info.email": "Email",
|
||||||
|
"account.notify.info.success": "Account updated successfully",
|
||||||
|
|
||||||
|
"account.card.password.title": "Password",
|
||||||
|
"account.card.password.old": "Old password",
|
||||||
|
"account.card.password.new": "New password",
|
||||||
|
"account.notify.password.success": "Password changed successfully",
|
||||||
|
|
||||||
|
"account.card.security.title": "Security",
|
||||||
|
"account.card.security.totp.enable.description":
|
||||||
|
"Enter your current password to start enabling TOTP",
|
||||||
|
"account.card.security.totp.disable.description":
|
||||||
|
"Enter your current password to start enabling TOTP",
|
||||||
|
"account.card.security.totp.button.start": "Start",
|
||||||
|
"account.modal.totp.title": "Enable TOTP",
|
||||||
|
"account.modal.totp.step1": "Step 1: Add your authenticator",
|
||||||
|
"account.modal.totp.step2": "Step 2: Validate your code",
|
||||||
|
"account.modal.totp.enterManually": "Enter manually",
|
||||||
|
"account.modal.totp.code": "Code",
|
||||||
|
"account.modal.totp.clickToCopy": "Click to copy",
|
||||||
|
"account.modal.totp.verify": "Verify",
|
||||||
|
"account.notify.totp.disable": "TOTP disabled successfully",
|
||||||
|
"account.notify.totp.enable": "TOTP enabled successfully",
|
||||||
|
|
||||||
|
"account.card.language.title": "Language",
|
||||||
|
"account.card.color.title": "Color scheme",
|
||||||
|
|
||||||
|
// ThemeSwitcher.tsx
|
||||||
|
"account.theme.dark": "Dark",
|
||||||
|
"account.theme.light": "Light",
|
||||||
|
"account.theme.system": "System",
|
||||||
|
|
||||||
|
"account.button.delete": "Delete Account",
|
||||||
|
"account.modal.delete.title": "Delete Account",
|
||||||
|
"account.modal.delete.description":
|
||||||
|
"Do you really want to delete your account including all your active shares?",
|
||||||
|
// END /account
|
||||||
|
|
||||||
|
// /account/shares
|
||||||
|
"account.shares.title": "My shares",
|
||||||
|
"account.shares.title.empty": "It's empty here 👀",
|
||||||
|
"account.shares.description.empty": "You don't have any shares.",
|
||||||
|
"account.shares.button.create": "Create one",
|
||||||
|
|
||||||
|
"account.shares.info.title": "Share informatons",
|
||||||
|
"account.shares.table.id": "ID",
|
||||||
|
"account.shares.table.name": "Name",
|
||||||
|
"account.shares.table.description": "Description",
|
||||||
|
"account.shares.table.visitors": "Visitors",
|
||||||
|
"account.shares.table.expiresAt": "Expires at",
|
||||||
|
"account.shares.table.createdAt": "Created at",
|
||||||
|
"account.shares.table.size": "Size",
|
||||||
|
|
||||||
|
"account.shares.modal.share-link": "Share link",
|
||||||
|
|
||||||
|
"account.shares.modal.delete.title": "Delete {share}",
|
||||||
|
"account.shares.modal.delete.description":
|
||||||
|
"Do you really want to delete this share?",
|
||||||
|
|
||||||
|
// END /account/shares
|
||||||
|
|
||||||
|
// /account/reverseShares
|
||||||
|
"account.reverseShares.title": "Reverse shares",
|
||||||
|
"account.reverseShares.description":
|
||||||
|
"A reverse share allows you to generate a unique URL that allows external users to create a share.",
|
||||||
|
|
||||||
|
"account.reverseShares.title.empty": "It's empty here 👀",
|
||||||
|
"account.reverseShares.description.empty":
|
||||||
|
"You don't have any reverse shares.",
|
||||||
|
|
||||||
|
// showCreateReverseShareModal.tsx
|
||||||
|
"account.reverseShares.modal.expiration.label": "Expiration",
|
||||||
|
"account.reverseShares.modal.expiration.minute-singular": "Minute",
|
||||||
|
"account.reverseShares.modal.expiration.minute-plural": "Minutes",
|
||||||
|
"account.reverseShares.modal.expiration.hour-singular": "Hour",
|
||||||
|
"account.reverseShares.modal.expiration.hour-plural": "Hours",
|
||||||
|
"account.reverseShares.modal.expiration.day-singular": "Day",
|
||||||
|
"account.reverseShares.modal.expiration.day-plural": "Days",
|
||||||
|
"account.reverseShares.modal.expiration.week-singular": "Week",
|
||||||
|
"account.reverseShares.modal.expiration.week-plural": "Weeks",
|
||||||
|
"account.reverseShares.modal.expiration.month-singular": "Month",
|
||||||
|
"account.reverseShares.modal.expiration.month-plural": "Months",
|
||||||
|
"account.reverseShares.modal.expiration.year-singular": "Year",
|
||||||
|
"account.reverseShares.modal.expiration.year-plural": "Years",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.max-size.label": "Max share size",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.send-email": "Send email notification",
|
||||||
|
"account.reverseShares.modal.send-email.description":
|
||||||
|
"Send an email notification when a share is created with this reverse share link.",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.max-use.label": "Max uses",
|
||||||
|
"account.reverseShares.modal.max-use.description":
|
||||||
|
"The maximum amount of times this URL can be used to create a share.",
|
||||||
|
"account.reverseShare.never-expires": "This reverse share will never expire.",
|
||||||
|
"account.reverseShare.expires-on":
|
||||||
|
"This reverse share will expire on {expiration}.",
|
||||||
|
|
||||||
|
"account.reverseShares.table.no-shares": "No shares created yet",
|
||||||
|
"account.reverseShares.table.count.singular": "share",
|
||||||
|
"account.reverseShares.table.count.plural": "shares",
|
||||||
|
"account.reverseShares.table.shares": "Shares",
|
||||||
|
"account.reverseShares.table.remaining": "Remaining uses",
|
||||||
|
"account.reverseShares.table.max-size": "Max share size",
|
||||||
|
"account.reverseShares.table.expires": "Expires at",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.reverse-share-link": "Reverse share link",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.delete.title":
|
||||||
|
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
|
||||||
|
"account.reverseShares.modal.delete.description":
|
||||||
|
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
|
||||||
|
|
||||||
|
// END /account/reverseShares
|
||||||
|
|
||||||
|
// /admin
|
||||||
|
"admin.title": "Administration",
|
||||||
|
"admin.button.users": "User management",
|
||||||
|
"admin.button.config": "Configuration",
|
||||||
|
"admin.version": "Version",
|
||||||
|
// END /admin
|
||||||
|
|
||||||
|
// /admin/users
|
||||||
|
"admin.users.title": "User management",
|
||||||
|
"admin.users.table.username": "Username",
|
||||||
|
"admin.users.table.email": "Email",
|
||||||
|
"admin.users.table.admin": "Admin",
|
||||||
|
|
||||||
|
"admin.users.edit.update.title": "Update user {username}",
|
||||||
|
"admin.users.edit.update.admin-privileges": "Admin privileges",
|
||||||
|
"admin.users.edit.update.change-password.title": "Change password",
|
||||||
|
"admin.users.edit.update.change-password.field": "New password",
|
||||||
|
"admin.users.edit.update.change-password.button": "Save new password",
|
||||||
|
"admin.users.edit.update.notify.password.success":
|
||||||
|
"Password changed successfully",
|
||||||
|
|
||||||
|
"admin.users.edit.delete.title": "Delete user {username}",
|
||||||
|
"admin.users.edit.delete.description":
|
||||||
|
"Do you really want to delete this user and all his shares?",
|
||||||
|
|
||||||
|
// showCreateUserModal.tsx
|
||||||
|
"admin.users.modal.create.title": "Create user",
|
||||||
|
"admin.users.modal.create.username": "Username",
|
||||||
|
"admin.users.modal.create.email": "Email",
|
||||||
|
"admin.users.modal.create.password": "Password",
|
||||||
|
"admin.users.modal.create.manual-password": "Set password manually",
|
||||||
|
"admin.users.modal.create.manual-password.description":
|
||||||
|
"If not checked, the user will receive an email with a link to set their password.",
|
||||||
|
"admin.users.modal.create.admin": "Admin privileges",
|
||||||
|
"admin.users.modal.create.admin.description":
|
||||||
|
"If checked, the user will be able to access the admin panel.",
|
||||||
|
|
||||||
|
// END /admin/users
|
||||||
|
|
||||||
|
// /upload
|
||||||
|
"upload.title": "Upload",
|
||||||
|
|
||||||
|
"upload.notify.generic-error":
|
||||||
|
"An error occurred while finishing your share.",
|
||||||
|
"upload.notify.count-failed": "{count} filed failed to upload. Trying again.",
|
||||||
|
|
||||||
|
// Dropzone.tsx
|
||||||
|
"upload.dropzone.title": "Upload files",
|
||||||
|
"upload.dropzone.description":
|
||||||
|
"Drag'n'drop files here to start your share. We can accept only files that are less than {maxSize} in total.",
|
||||||
|
"upload.dropzone.notify.file-too-big":
|
||||||
|
"Your files exceed the maximum share size of {maxSize}.",
|
||||||
|
|
||||||
|
// FileList.tsx
|
||||||
|
"upload.filelist.name": "Name",
|
||||||
|
"upload.filelist.size": "Size",
|
||||||
|
|
||||||
|
// showCreateUploadModal.tsx
|
||||||
|
"upload.modal.title": "Create Share",
|
||||||
|
"upload.modal.link.error.invalid":
|
||||||
|
"Can only contain letters, numbers, underscores, and hyphens",
|
||||||
|
"upload.modal.link.error.taken": "This link is already in use",
|
||||||
|
"upload.modal.not-signed-in": "You're not signed in",
|
||||||
|
"upload.modal.not-signed-in-description":
|
||||||
|
"You will be unable to delete your share manually and view the visitor count.",
|
||||||
|
|
||||||
|
"upload.modal.expires.never": "never",
|
||||||
|
"upload.modal.expires.never-long": "Never Expires",
|
||||||
|
|
||||||
|
"upload.modal.link.label": "Link",
|
||||||
|
"upload.modal.link.placeholder": "myAwesomeShare",
|
||||||
|
|
||||||
|
"upload.modal.expires.label": "Expiration",
|
||||||
|
"upload.modal.expires.minute-singular": "Minute",
|
||||||
|
"upload.modal.expires.minute-plural": "Minutes",
|
||||||
|
"upload.modal.expires.hour-singular": "Hour",
|
||||||
|
"upload.modal.expires.hour-plural": "Hours",
|
||||||
|
"upload.modal.expires.day-singular": "Day",
|
||||||
|
"upload.modal.expires.day-plural": "Days",
|
||||||
|
"upload.modal.expires.week-singular": "Week",
|
||||||
|
"upload.modal.expires.week-plural": "Weeks",
|
||||||
|
"upload.modal.expires.month-singular": "Month",
|
||||||
|
"upload.modal.expires.month-plural": "Months",
|
||||||
|
"upload.modal.expires.year-singular": "Year",
|
||||||
|
"upload.modal.expires.year-plural": "Years",
|
||||||
|
|
||||||
|
"upload.modal.accordion.description.title": "Description",
|
||||||
|
"upload.modal.accordion.description.placeholder":
|
||||||
|
"Note for the recipients of this share",
|
||||||
|
|
||||||
|
"upload.modal.accordion.email.title": "Email recipients",
|
||||||
|
"upload.modal.accordion.email.placeholder": "Enter email recipients",
|
||||||
|
"upload.modal.accordion.email.invalid-email": "Invalid email address",
|
||||||
|
|
||||||
|
"upload.modal.accordion.security.title": "Security options",
|
||||||
|
"upload.modal.accordion.security.password.label": "Password protection",
|
||||||
|
"upload.modal.accordion.security.password.placeholder": "No password",
|
||||||
|
"upload.modal.accordion.security.max-views.label": "Maximum views",
|
||||||
|
"upload.modal.accordion.security.max-views.placeholder": "No limit",
|
||||||
|
|
||||||
|
// showCompletedUploadModal.tsx
|
||||||
|
"upload.modal.completed.never-expires": "This share will never expire.",
|
||||||
|
"upload.modal.completed.expires-on":
|
||||||
|
"This share will expire on {expiration}.",
|
||||||
|
"upload.modal.completed.share-ready": "Share ready",
|
||||||
|
|
||||||
|
// END /upload
|
||||||
|
|
||||||
|
// /share/[id]
|
||||||
|
"share.title": "Share {shareId}",
|
||||||
|
"share.description": "Look what I've shared with you!",
|
||||||
|
"share.error.visitor-limit-exceeded.title": "Visitor limit exceeded",
|
||||||
|
"share.error.visitor-limit-exceeded.description":
|
||||||
|
"The visitor limit from this share has been exceeded.",
|
||||||
|
"share.error.removed.title": "Share removed",
|
||||||
|
"share.error.not-found.title": "Share not found",
|
||||||
|
"share.error.not-found.description":
|
||||||
|
"The share you're looking for doesn't exist.",
|
||||||
|
|
||||||
|
"share.modal.password.title": "Password required",
|
||||||
|
"share.modal.password.description":
|
||||||
|
"To access this share please enter the password for the share.",
|
||||||
|
"share.modal.password": "Password",
|
||||||
|
"share.modal.error.invalid-password": "Invalid password",
|
||||||
|
|
||||||
|
"share.button.download-all": "Download all",
|
||||||
|
"share.notify.download-all-preparing":
|
||||||
|
"The share is preparing. Try again in a few minutes.",
|
||||||
|
|
||||||
|
"share.modal.file-link": "File link",
|
||||||
|
"share.table.name": "Name",
|
||||||
|
"share.table.size": "Size",
|
||||||
|
|
||||||
|
"share.modal.file-preview.error.not-supported.title": "Preview not supported",
|
||||||
|
"share.modal.file-preview.error.not-supported.description":
|
||||||
|
"A preview for thise file type is unsupported. Please download the file to view it.",
|
||||||
|
|
||||||
|
// END /share/[id]
|
||||||
|
|
||||||
|
// /admin/config
|
||||||
|
"admin.config.title": "Configuration",
|
||||||
|
"admin.config.category.general": "General",
|
||||||
|
"admin.config.category.share": "Share",
|
||||||
|
"admin.config.category.email": "Email",
|
||||||
|
"admin.config.category.smtp": "SMTP",
|
||||||
|
|
||||||
|
"admin.config.general.app-name": "App name",
|
||||||
|
"admin.config.general.app-name.description": "Name of the application",
|
||||||
|
"admin.config.general.app-url": "App URL",
|
||||||
|
"admin.config.general.app-url.description":
|
||||||
|
"On which URL Pingvin Share is available",
|
||||||
|
"admin.config.general.show-home-page": "Show home page",
|
||||||
|
"admin.config.general.show-home-page.description":
|
||||||
|
"Whether to show the home page",
|
||||||
|
"admin.config.general.logo": "Logo",
|
||||||
|
"admin.config.general.logo.description":
|
||||||
|
"Change your logo by uploading a new image. The image must be a PNG and should have the format 1:1.",
|
||||||
|
"admin.config.general.logo.placeholder": "Pick image",
|
||||||
|
|
||||||
|
"admin.config.email.enable-share-email-recipients":
|
||||||
|
"Enable share email recipients",
|
||||||
|
"admin.config.email.enable-share-email-recipients.description":
|
||||||
|
"Whether to allow emails to share recipients. Only enable this if you have enabled SMTP.",
|
||||||
|
"admin.config.email.share-recipients-subject": "Share recipients subject",
|
||||||
|
"admin.config.email.share-recipients-subject.description":
|
||||||
|
"Subject of the email which gets sent to the share recipients.",
|
||||||
|
"admin.config.email.share-recipients-message": "Share recipients message",
|
||||||
|
"admin.config.email.share-recipients-message.description":
|
||||||
|
"Message which gets sent to the share recipients. Available variables:\n {creator} - The username of the creator of the share\n {shareUrl} - The URL of the share\n {desc} - The description of the share\n {expires} - The expiration date of the share\n The variables will be replaced with the actual values.",
|
||||||
|
"admin.config.email.reverse-share-subject": "Reverse share subject",
|
||||||
|
"admin.config.email.reverse-share-subject.description":
|
||||||
|
"Subject of the email which gets sent when someone created a share with your reverse share link.",
|
||||||
|
"admin.config.email.reverse-share-message": "Reverse share message",
|
||||||
|
"admin.config.email.reverse-share-message.description":
|
||||||
|
"Message which gets sent when someone created a share with your reverse share link. {shareUrl} will be replaced with the creator's name and the share URL.",
|
||||||
|
"admin.config.email.reset-password-subject": "Reset password subject",
|
||||||
|
"admin.config.email.reset-password-subject.description":
|
||||||
|
"Subject of the email which gets sent when a user requests a password reset.",
|
||||||
|
"admin.config.email.reset-password-message": "Reset password message",
|
||||||
|
"admin.config.email.reset-password-message.description":
|
||||||
|
"Message which gets sent when a user requests a password reset. {url} will be replaced with the reset password URL.",
|
||||||
|
"admin.config.email.invite-subject": "Invite subject",
|
||||||
|
"admin.config.email.invite-subject.description":
|
||||||
|
"Subject of the email which gets sent when an admin invites a user.",
|
||||||
|
"admin.config.email.invite-message": "Invite message",
|
||||||
|
"admin.config.email.invite-message.description":
|
||||||
|
"Message which gets sent when an admin invites a user. {url} will be replaced with the invite URL and {password} with the password.",
|
||||||
|
"admin.config.share.allow-registration": "Allow registration",
|
||||||
|
"admin.config.share.allow-registration.description":
|
||||||
|
"Whether registration is allowed",
|
||||||
|
"admin.config.share.allow-unauthenticated-shares":
|
||||||
|
"Allow unauthenticated shares",
|
||||||
|
"admin.config.share.allow-unauthenticated-shares.description":
|
||||||
|
"Whether unauthorized users can create shares",
|
||||||
|
"admin.config.share.max-size": "Max size",
|
||||||
|
"admin.config.share.max-size.description": "Maximum share size in bytes",
|
||||||
|
|
||||||
|
"admin.config.smtp.enabled": "Enabled",
|
||||||
|
"admin.config.smtp.enabled.description":
|
||||||
|
"Whether SMTP is enabled. Only set this to true if you entered the host, port, email, user and password of your SMTP server.",
|
||||||
|
"admin.config.smtp.host": "Host",
|
||||||
|
"admin.config.smtp.host.description": "Host of the SMTP server",
|
||||||
|
"admin.config.smtp.port": "Port",
|
||||||
|
"admin.config.smtp.port.description": "Port of the SMTP server",
|
||||||
|
"admin.config.smtp.email": "Email",
|
||||||
|
"admin.config.smtp.email.description":
|
||||||
|
"Email address which the emails get sent from",
|
||||||
|
"admin.config.smtp.username": "Username",
|
||||||
|
"admin.config.smtp.username.description": "Username of the SMTP server",
|
||||||
|
"admin.config.smtp.password": "Password",
|
||||||
|
"admin.config.smtp.password.description": "Password of the SMTP server",
|
||||||
|
"admin.config.smtp.button.test": "Send test email",
|
||||||
|
|
||||||
|
// 404
|
||||||
|
"404.title": "404",
|
||||||
|
"404.description": "Oops this page doesn't exist.",
|
||||||
|
"404.button.home": "Gring me back home",
|
||||||
|
|
||||||
|
// Common translations
|
||||||
|
"common.button.save": "Save",
|
||||||
|
"common.button.create": "Create",
|
||||||
|
"common.button.submit": "Submit",
|
||||||
|
"common.button.delete": "Delete",
|
||||||
|
"common.button.cancel": "Cancel",
|
||||||
|
"common.button.confirm": "Confirm",
|
||||||
|
"common.button.disable": "Disable",
|
||||||
|
"common.button.share": "Share",
|
||||||
|
"common.button.generate": "Generate",
|
||||||
|
"common.button.done": "Done",
|
||||||
|
"common.text.link": "Link",
|
||||||
|
"common.text.or": "or",
|
||||||
|
"common.button.go-back": "Go back",
|
||||||
|
"common.notify.copied": "Your link was copied to the clipboard",
|
||||||
|
|
||||||
|
"common.error": "Error",
|
||||||
|
"common.error.unknown": "An unknown error occurred",
|
||||||
|
"common.error.invalid-email": "Invalid email address",
|
||||||
|
"common.error.too-short": "Must be at least {length} characters",
|
||||||
|
"common.error.too-long": "Must be at most {length} characters",
|
||||||
|
"common.error.exact-length": "Must be exactly {length} characters",
|
||||||
|
"common.error.invalid-number": "Must be a number",
|
||||||
|
"common.error.field-required": "This field is required",
|
||||||
|
};
|
435
frontend/src/i18n/translations/fr.ts
Normal file
435
frontend/src/i18n/translations/fr.ts
Normal file
|
@ -0,0 +1,435 @@
|
||||||
|
export default {
|
||||||
|
// Navbar
|
||||||
|
"navbar.upload": "Upload",
|
||||||
|
"navbar.signin": "Sign in",
|
||||||
|
"navbar.home": "Home",
|
||||||
|
"navbar.signup": "Sign Up",
|
||||||
|
|
||||||
|
"navbar.links.shares": "My shares",
|
||||||
|
"navbar.links.reverse": "Reverse shares",
|
||||||
|
|
||||||
|
"navbar.avatar.account": "My account",
|
||||||
|
"navbar.avatar.admin": "Administration",
|
||||||
|
"navbar.avatar.signout": "Sign out",
|
||||||
|
// END navbar
|
||||||
|
|
||||||
|
// /
|
||||||
|
"home.title": "A <h>self-hosted</h> file sharing platform.",
|
||||||
|
|
||||||
|
"home.description":
|
||||||
|
"Do you really want to give your personal files in the hand of third parties like WeTransfer?",
|
||||||
|
"home.bullet.a.name": "Self-Hosted",
|
||||||
|
"home.bullet.a.description": "Host Pingvin Share on your own machine.",
|
||||||
|
"home.bullet.b.name": "Privacy",
|
||||||
|
"home.bullet.b.description":
|
||||||
|
"Your files are your files and should never get into the hands of third parties.",
|
||||||
|
"home.bullet.c.name": "No annoying file size limit",
|
||||||
|
"home.bullet.c.description":
|
||||||
|
"Upload as big files as you want. Only your hard drive will be your limit.",
|
||||||
|
|
||||||
|
"home.button.start": "Get started",
|
||||||
|
"home.button.source": "Source code",
|
||||||
|
// END /
|
||||||
|
|
||||||
|
// /auth/signin
|
||||||
|
"signin.title": "Welcome back",
|
||||||
|
"signin.description": "You don't have an account yet?",
|
||||||
|
"signin.button.signup": "Sign up",
|
||||||
|
"signin.input.email-or-username": "Email or username",
|
||||||
|
"signin.input.email-or-username.placeholder": "Your email or username",
|
||||||
|
"signin.input.password": "Password",
|
||||||
|
"signin.input.password.placeholder": "Your password",
|
||||||
|
"signin.button.submit": "Sign in",
|
||||||
|
"signIn.notify.totp-required.title": "Two-factor authentication required",
|
||||||
|
"signIn.notify.totp-required.description":
|
||||||
|
"Please enter your two-factor authentication code",
|
||||||
|
|
||||||
|
// END /auth/signin
|
||||||
|
|
||||||
|
// /auth/signup
|
||||||
|
"signup.title": "Create an account",
|
||||||
|
"signup.description": "Already have an account?",
|
||||||
|
"signup.button.signin": "Sign in",
|
||||||
|
"signup.input.username": "Username",
|
||||||
|
"signup.input.username.placeholder": "Your username",
|
||||||
|
"signup.input.email": "Email",
|
||||||
|
"signup.input.email.placeholder": "Your email",
|
||||||
|
"signup.button.submit": "Let's get started",
|
||||||
|
|
||||||
|
// END /auth/signup
|
||||||
|
|
||||||
|
// /auth/reset-password
|
||||||
|
"resetPassword.title": "Forgot your password?",
|
||||||
|
"resetPassword.description": "Enter your email to reset your password.",
|
||||||
|
"resetPassword.notify.success":
|
||||||
|
"An email has been sent with a link to reset your password.",
|
||||||
|
"resetPassword.button.back": "Back to sign in page",
|
||||||
|
"resetPassword.text.resetPassword": "Reset password",
|
||||||
|
"resetPassword.text.enterNewPassword": "Enter your new password",
|
||||||
|
"resetPassword.input.password": "New password",
|
||||||
|
"resetPassword.notify.passwordReset":
|
||||||
|
"Your password has been reset successfully.",
|
||||||
|
|
||||||
|
// /account
|
||||||
|
"account.title": "My account",
|
||||||
|
|
||||||
|
"account.card.info.title": "Account info",
|
||||||
|
"account.card.info.username": "Username",
|
||||||
|
"account.card.info.email": "Email",
|
||||||
|
"account.notify.info.success": "Account updated successfully",
|
||||||
|
|
||||||
|
"account.card.password.title": "Password",
|
||||||
|
"account.card.password.old": "Old password",
|
||||||
|
"account.card.password.new": "New password",
|
||||||
|
"account.notify.password.success": "Password changed successfully",
|
||||||
|
|
||||||
|
"account.card.security.title": "Security",
|
||||||
|
"account.card.security.totp.enable.description":
|
||||||
|
"Enter your current password to start enabling TOTP",
|
||||||
|
"account.card.security.totp.disable.description":
|
||||||
|
"Enter your current password to start enabling TOTP",
|
||||||
|
"account.card.security.totp.button.start": "Start",
|
||||||
|
"account.modal.totp.title": "Enable TOTP",
|
||||||
|
"account.modal.totp.step1": "Step 1: Add your authenticator",
|
||||||
|
"account.modal.totp.step2": "Step 2: Validate your code",
|
||||||
|
"account.modal.totp.enterManually": "Enter manually",
|
||||||
|
"account.modal.totp.code": "Code",
|
||||||
|
"account.modal.totp.clickToCopy": "Click to copy",
|
||||||
|
"account.modal.totp.verify": "Verify",
|
||||||
|
"account.notify.totp.disable": "TOTP disabled successfully",
|
||||||
|
"account.notify.totp.enable": "TOTP enabled successfully",
|
||||||
|
|
||||||
|
"account.card.language.title": "Language",
|
||||||
|
"account.card.color.title": "Color scheme",
|
||||||
|
|
||||||
|
// ThemeSwitcher.tsx
|
||||||
|
"account.theme.dark": "Dark",
|
||||||
|
"account.theme.light": "Light",
|
||||||
|
"account.theme.system": "System",
|
||||||
|
|
||||||
|
"account.button.delete": "Delete Account",
|
||||||
|
"account.modal.delete.title": "Delete Account",
|
||||||
|
"account.modal.delete.description":
|
||||||
|
"Do you really want to delete your account including all your active shares?",
|
||||||
|
// END /account
|
||||||
|
|
||||||
|
// /account/shares
|
||||||
|
"account.shares.title": "My shares",
|
||||||
|
"account.shares.title.empty": "It's empty here 👀",
|
||||||
|
"account.shares.description.empty": "You don't have any shares.",
|
||||||
|
"account.shares.button.create": "Create one",
|
||||||
|
|
||||||
|
"account.shares.info.title": "Share informatons",
|
||||||
|
"account.shares.table.id": "ID",
|
||||||
|
"account.shares.table.name": "Name",
|
||||||
|
"account.shares.table.description": "Description",
|
||||||
|
"account.shares.table.visitors": "Visitors",
|
||||||
|
"account.shares.table.expiresAt": "Expires at",
|
||||||
|
"account.shares.table.createdAt": "Created at",
|
||||||
|
"account.shares.table.size": "Size",
|
||||||
|
|
||||||
|
"account.shares.modal.share-link": "Share link",
|
||||||
|
|
||||||
|
"account.shares.modal.delete.title": "Delete {share}",
|
||||||
|
"account.shares.modal.delete.description":
|
||||||
|
"Do you really want to delete this share?",
|
||||||
|
|
||||||
|
// END /account/shares
|
||||||
|
|
||||||
|
// /account/reverseShares
|
||||||
|
"account.reverseShares.title": "Reverse shares",
|
||||||
|
"account.reverseShares.description":
|
||||||
|
"A reverse share allows you to generate a unique URL that allows external users to create a share.",
|
||||||
|
|
||||||
|
"account.reverseShares.title.empty": "It's empty here 👀",
|
||||||
|
"account.reverseShares.description.empty":
|
||||||
|
"You don't have any reverse shares.",
|
||||||
|
|
||||||
|
// showCreateReverseShareModal.tsx
|
||||||
|
"account.reverseShares.modal.expiration.label": "Expiration",
|
||||||
|
"account.reverseShares.modal.expiration.minute-singular": "Minute",
|
||||||
|
"account.reverseShares.modal.expiration.minute-plural": "Minutes",
|
||||||
|
"account.reverseShares.modal.expiration.hour-singular": "Hour",
|
||||||
|
"account.reverseShares.modal.expiration.hour-plural": "Hours",
|
||||||
|
"account.reverseShares.modal.expiration.day-singular": "Day",
|
||||||
|
"account.reverseShares.modal.expiration.day-plural": "Days",
|
||||||
|
"account.reverseShares.modal.expiration.week-singular": "Week",
|
||||||
|
"account.reverseShares.modal.expiration.week-plural": "Weeks",
|
||||||
|
"account.reverseShares.modal.expiration.month-singular": "Month",
|
||||||
|
"account.reverseShares.modal.expiration.month-plural": "Months",
|
||||||
|
"account.reverseShares.modal.expiration.year-singular": "Year",
|
||||||
|
"account.reverseShares.modal.expiration.year-plural": "Years",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.max-size.label": "Max share size",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.send-email": "Send email notification",
|
||||||
|
"account.reverseShares.modal.send-email.description":
|
||||||
|
"Send an email notification when a share is created with this reverse share link.",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.max-use.label": "Max uses",
|
||||||
|
"account.reverseShares.modal.max-use.description":
|
||||||
|
"The maximum amount of times this URL can be used to create a share.",
|
||||||
|
"account.reverseShare.never-expires": "This reverse share will never expire.",
|
||||||
|
"account.reverseShare.expires-on":
|
||||||
|
"This reverse share will expire on {expiration}.",
|
||||||
|
|
||||||
|
"account.reverseShares.table.no-shares": "No shares created yet",
|
||||||
|
"account.reverseShares.table.count.singular": "share",
|
||||||
|
"account.reverseShares.table.count.plural": "shares",
|
||||||
|
"account.reverseShares.table.shares": "Shares",
|
||||||
|
"account.reverseShares.table.remaining": "Remaining uses",
|
||||||
|
"account.reverseShares.table.max-size": "Max share size",
|
||||||
|
"account.reverseShares.table.expires": "Expires at",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.reverse-share-link": "Reverse share link",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.delete.title":
|
||||||
|
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
|
||||||
|
"account.reverseShares.modal.delete.description":
|
||||||
|
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
|
||||||
|
|
||||||
|
// END /account/reverseShares
|
||||||
|
|
||||||
|
// /admin
|
||||||
|
"admin.title": "Administration",
|
||||||
|
"admin.button.users": "User management",
|
||||||
|
"admin.button.config": "Configuration",
|
||||||
|
"admin.version": "Version",
|
||||||
|
// END /admin
|
||||||
|
|
||||||
|
// /admin/users
|
||||||
|
"admin.users.title": "User management",
|
||||||
|
"admin.users.table.username": "Username",
|
||||||
|
"admin.users.table.email": "Email",
|
||||||
|
"admin.users.table.admin": "Admin",
|
||||||
|
|
||||||
|
"admin.users.edit.update.title": "Update user {username}",
|
||||||
|
"admin.users.edit.update.admin-privileges": "Admin privileges",
|
||||||
|
"admin.users.edit.update.change-password.title": "Change password",
|
||||||
|
"admin.users.edit.update.change-password.field": "New password",
|
||||||
|
"admin.users.edit.update.change-password.button": "Save new password",
|
||||||
|
"admin.users.edit.update.notify.password.success":
|
||||||
|
"Password changed successfully",
|
||||||
|
|
||||||
|
"admin.users.edit.delete.title": "Delete user {username}",
|
||||||
|
"admin.users.edit.delete.description":
|
||||||
|
"Do you really want to delete this user and all his shares?",
|
||||||
|
|
||||||
|
// showCreateUserModal.tsx
|
||||||
|
"admin.users.modal.create.title": "Create user",
|
||||||
|
"admin.users.modal.create.username": "Username",
|
||||||
|
"admin.users.modal.create.email": "Email",
|
||||||
|
"admin.users.modal.create.password": "Password",
|
||||||
|
"admin.users.modal.create.manual-password": "Set password manually",
|
||||||
|
"admin.users.modal.create.manual-password.description":
|
||||||
|
"If not checked, the user will receive an email with a link to set their password.",
|
||||||
|
"admin.users.modal.create.admin": "Admin privileges",
|
||||||
|
"admin.users.modal.create.admin.description":
|
||||||
|
"If checked, the user will be able to access the admin panel.",
|
||||||
|
|
||||||
|
// END /admin/users
|
||||||
|
|
||||||
|
// /upload
|
||||||
|
"upload.title": "Upload",
|
||||||
|
|
||||||
|
"upload.notify.generic-error":
|
||||||
|
"An error occurred while finishing your share.",
|
||||||
|
"upload.notify.count-failed": "{count} filed failed to upload. Trying again.",
|
||||||
|
|
||||||
|
// Dropzone.tsx
|
||||||
|
"upload.dropzone.title": "Upload files",
|
||||||
|
"upload.dropzone.description":
|
||||||
|
"Drag'n'drop files here to start your share. We can accept only files that are less than {maxSize} in total.",
|
||||||
|
"upload.dropzone.notify.file-too-big":
|
||||||
|
"Your files exceed the maximum share size of {maxSize}.",
|
||||||
|
|
||||||
|
// FileList.tsx
|
||||||
|
"upload.filelist.name": "Name",
|
||||||
|
"upload.filelist.size": "Size",
|
||||||
|
|
||||||
|
// showCreateUploadModal.tsx
|
||||||
|
"upload.modal.title": "Create Share",
|
||||||
|
"upload.modal.link.error.invalid":
|
||||||
|
"Can only contain letters, numbers, underscores, and hyphens",
|
||||||
|
"upload.modal.link.error.taken": "This link is already in use",
|
||||||
|
"upload.modal.not-signed-in": "You're not signed in",
|
||||||
|
"upload.modal.not-signed-in-description":
|
||||||
|
"You will be unable to delete your share manually and view the visitor count.",
|
||||||
|
|
||||||
|
"upload.modal.expires.never": "never",
|
||||||
|
"upload.modal.expires.never-long": "Never Expires",
|
||||||
|
|
||||||
|
"upload.modal.link.label": "Link",
|
||||||
|
"upload.modal.link.placeholder": "myAwesomeShare",
|
||||||
|
|
||||||
|
"upload.modal.expires.label": "Expiration",
|
||||||
|
"upload.modal.expires.minute-singular": "Minute",
|
||||||
|
"upload.modal.expires.minute-plural": "Minutes",
|
||||||
|
"upload.modal.expires.hour-singular": "Hour",
|
||||||
|
"upload.modal.expires.hour-plural": "Hours",
|
||||||
|
"upload.modal.expires.day-singular": "Day",
|
||||||
|
"upload.modal.expires.day-plural": "Days",
|
||||||
|
"upload.modal.expires.week-singular": "Week",
|
||||||
|
"upload.modal.expires.week-plural": "Weeks",
|
||||||
|
"upload.modal.expires.month-singular": "Month",
|
||||||
|
"upload.modal.expires.month-plural": "Months",
|
||||||
|
"upload.modal.expires.year-singular": "Year",
|
||||||
|
"upload.modal.expires.year-plural": "Years",
|
||||||
|
|
||||||
|
"upload.modal.accordion.description.title": "Description",
|
||||||
|
"upload.modal.accordion.description.placeholder":
|
||||||
|
"Note for the recipients of this share",
|
||||||
|
|
||||||
|
"upload.modal.accordion.email.title": "Email recipients",
|
||||||
|
"upload.modal.accordion.email.placeholder": "Enter email recipients",
|
||||||
|
"upload.modal.accordion.email.invalid-email": "Invalid email address",
|
||||||
|
|
||||||
|
"upload.modal.accordion.security.title": "Security options",
|
||||||
|
"upload.modal.accordion.security.password.label": "Password protection",
|
||||||
|
"upload.modal.accordion.security.password.placeholder": "No password",
|
||||||
|
"upload.modal.accordion.security.max-views.label": "Maximum views",
|
||||||
|
"upload.modal.accordion.security.max-views.placeholder": "No limit",
|
||||||
|
|
||||||
|
// showCompletedUploadModal.tsx
|
||||||
|
"upload.modal.completed.never-expires": "This share will never expire.",
|
||||||
|
"upload.modal.completed.expires-on":
|
||||||
|
"This share will expire on {expiration}.",
|
||||||
|
"upload.modal.completed.share-ready": "Share ready",
|
||||||
|
|
||||||
|
// END /upload
|
||||||
|
|
||||||
|
// /share/[id]
|
||||||
|
"share.title": "Share {shareId}",
|
||||||
|
"share.description": "Look what I've shared with you!",
|
||||||
|
"share.error.visitor-limit-exceeded.title": "Visitor limit exceeded",
|
||||||
|
"share.error.visitor-limit-exceeded.description":
|
||||||
|
"The visitor limit from this share has been exceeded.",
|
||||||
|
"share.error.removed.title": "Share removed",
|
||||||
|
"share.error.not-found.title": "Share not found",
|
||||||
|
"share.error.not-found.description":
|
||||||
|
"The share you're looking for doesn't exist.",
|
||||||
|
|
||||||
|
"share.modal.password.title": "Password required",
|
||||||
|
"share.modal.password.description":
|
||||||
|
"To access this share please enter the password for the share.",
|
||||||
|
"share.modal.password": "Password",
|
||||||
|
"share.modal.error.invalid-password": "Invalid password",
|
||||||
|
|
||||||
|
"share.button.download-all": "Download all",
|
||||||
|
"share.notify.download-all-preparing":
|
||||||
|
"The share is preparing. Try again in a few minutes.",
|
||||||
|
|
||||||
|
"share.modal.file-link": "File link",
|
||||||
|
"share.table.name": "Name",
|
||||||
|
"share.table.size": "Size",
|
||||||
|
|
||||||
|
"share.modal.file-preview.error.not-supported.title": "Preview not supported",
|
||||||
|
"share.modal.file-preview.error.not-supported.description":
|
||||||
|
"A preview for thise file type is unsupported. Please download the file to view it.",
|
||||||
|
|
||||||
|
// END /share/[id]
|
||||||
|
|
||||||
|
// /admin/config
|
||||||
|
"admin.config.title": "Configuration",
|
||||||
|
"admin.config.category.general": "General",
|
||||||
|
"admin.config.category.share": "Share",
|
||||||
|
"admin.config.category.email": "Email",
|
||||||
|
"admin.config.category.smtp": "SMTP",
|
||||||
|
|
||||||
|
"admin.config.general.app-name": "App name",
|
||||||
|
"admin.config.general.app-name.description": "Name of the application",
|
||||||
|
"admin.config.general.app-url": "App URL",
|
||||||
|
"admin.config.general.app-url.description":
|
||||||
|
"On which URL Pingvin Share is available",
|
||||||
|
"admin.config.general.show-home-page": "Show home page",
|
||||||
|
"admin.config.general.show-home-page.description":
|
||||||
|
"Whether to show the home page",
|
||||||
|
"admin.config.general.logo": "Logo",
|
||||||
|
"admin.config.general.logo.description":
|
||||||
|
"Change your logo by uploading a new image. The image must be a PNG and should have the format 1:1.",
|
||||||
|
"admin.config.general.logo.placeholder": "Pick image",
|
||||||
|
|
||||||
|
"admin.config.email.enable-share-email-recipients":
|
||||||
|
"Enable share email recipients",
|
||||||
|
"admin.config.email.enable-share-email-recipients.description":
|
||||||
|
"Whether to allow emails to share recipients. Only enable this if you have enabled SMTP.",
|
||||||
|
"admin.config.email.share-recipients-subject": "Share recipients subject",
|
||||||
|
"admin.config.email.share-recipients-subject.description":
|
||||||
|
"Subject of the email which gets sent to the share recipients.",
|
||||||
|
"admin.config.email.share-recipients-message": "Share recipients message",
|
||||||
|
"admin.config.email.share-recipients-message.description":
|
||||||
|
"Message which gets sent to the share recipients. Available variables:\n {creator} - The username of the creator of the share\n {shareUrl} - The URL of the share\n {desc} - The description of the share\n {expires} - The expiration date of the share\n The variables will be replaced with the actual values.",
|
||||||
|
"admin.config.email.reverse-share-subject": "Reverse share subject",
|
||||||
|
"admin.config.email.reverse-share-subject.description":
|
||||||
|
"Subject of the email which gets sent when someone created a share with your reverse share link.",
|
||||||
|
"admin.config.email.reverse-share-message": "Reverse share message",
|
||||||
|
"admin.config.email.reverse-share-message.description":
|
||||||
|
"Message which gets sent when someone created a share with your reverse share link. {shareUrl} will be replaced with the creator's name and the share URL.",
|
||||||
|
"admin.config.email.reset-password-subject": "Reset password subject",
|
||||||
|
"admin.config.email.reset-password-subject.description":
|
||||||
|
"Subject of the email which gets sent when a user requests a password reset.",
|
||||||
|
"admin.config.email.reset-password-message": "Reset password message",
|
||||||
|
"admin.config.email.reset-password-message.description":
|
||||||
|
"Message which gets sent when a user requests a password reset. {url} will be replaced with the reset password URL.",
|
||||||
|
"admin.config.email.invite-subject": "Invite subject",
|
||||||
|
"admin.config.email.invite-subject.description":
|
||||||
|
"Subject of the email which gets sent when an admin invites a user.",
|
||||||
|
"admin.config.email.invite-message": "Invite message",
|
||||||
|
"admin.config.email.invite-message.description":
|
||||||
|
"Message which gets sent when an admin invites a user. {url} will be replaced with the invite URL and {password} with the password.",
|
||||||
|
"admin.config.share.allow-registration": "Allow registration",
|
||||||
|
"admin.config.share.allow-registration.description":
|
||||||
|
"Whether registration is allowed",
|
||||||
|
"admin.config.share.allow-unauthenticated-shares":
|
||||||
|
"Allow unauthenticated shares",
|
||||||
|
"admin.config.share.allow-unauthenticated-shares.description":
|
||||||
|
"Whether unauthorized users can create shares",
|
||||||
|
"admin.config.share.max-size": "Max size",
|
||||||
|
"admin.config.share.max-size.description": "Maximum share size in bytes",
|
||||||
|
|
||||||
|
"admin.config.smtp.enabled": "Enabled",
|
||||||
|
"admin.config.smtp.enabled.description":
|
||||||
|
"Whether SMTP is enabled. Only set this to true if you entered the host, port, email, user and password of your SMTP server.",
|
||||||
|
"admin.config.smtp.host": "Host",
|
||||||
|
"admin.config.smtp.host.description": "Host of the SMTP server",
|
||||||
|
"admin.config.smtp.port": "Port",
|
||||||
|
"admin.config.smtp.port.description": "Port of the SMTP server",
|
||||||
|
"admin.config.smtp.email": "Email",
|
||||||
|
"admin.config.smtp.email.description":
|
||||||
|
"Email address which the emails get sent from",
|
||||||
|
"admin.config.smtp.username": "Username",
|
||||||
|
"admin.config.smtp.username.description": "Username of the SMTP server",
|
||||||
|
"admin.config.smtp.password": "Password",
|
||||||
|
"admin.config.smtp.password.description": "Password of the SMTP server",
|
||||||
|
"admin.config.smtp.button.test": "Send test email",
|
||||||
|
|
||||||
|
// 404
|
||||||
|
"404.title": "404",
|
||||||
|
"404.description": "Oops this page doesn't exist.",
|
||||||
|
"404.button.home": "Gring me back home",
|
||||||
|
|
||||||
|
// Common translations
|
||||||
|
"common.button.save": "Save",
|
||||||
|
"common.button.create": "Create",
|
||||||
|
"common.button.submit": "Submit",
|
||||||
|
"common.button.delete": "Delete",
|
||||||
|
"common.button.cancel": "Cancel",
|
||||||
|
"common.button.confirm": "Confirm",
|
||||||
|
"common.button.disable": "Disable",
|
||||||
|
"common.button.share": "Share",
|
||||||
|
"common.button.generate": "Generate",
|
||||||
|
"common.button.done": "Done",
|
||||||
|
"common.text.link": "Link",
|
||||||
|
"common.text.or": "or",
|
||||||
|
"common.button.go-back": "Go back",
|
||||||
|
"common.notify.copied": "Your link was copied to the clipboard",
|
||||||
|
|
||||||
|
"common.error": "Error",
|
||||||
|
"common.error.unknown": "An unknown error occurred",
|
||||||
|
"common.error.invalid-email": "Invalid email address",
|
||||||
|
"common.error.too-short": "Must be at least {length} characters",
|
||||||
|
"common.error.too-long": "Must be at most {length} characters",
|
||||||
|
"common.error.exact-length": "Must be exactly {length} characters",
|
||||||
|
"common.error.invalid-number": "Must be a number",
|
||||||
|
"common.error.field-required": "This field is required",
|
||||||
|
};
|
435
frontend/src/i18n/translations/pt.ts
Normal file
435
frontend/src/i18n/translations/pt.ts
Normal file
|
@ -0,0 +1,435 @@
|
||||||
|
export default {
|
||||||
|
// Navbar
|
||||||
|
"navbar.upload": "Upload",
|
||||||
|
"navbar.signin": "Sign in",
|
||||||
|
"navbar.home": "Home",
|
||||||
|
"navbar.signup": "Sign Up",
|
||||||
|
|
||||||
|
"navbar.links.shares": "My shares",
|
||||||
|
"navbar.links.reverse": "Reverse shares",
|
||||||
|
|
||||||
|
"navbar.avatar.account": "My account",
|
||||||
|
"navbar.avatar.admin": "Administration",
|
||||||
|
"navbar.avatar.signout": "Sign out",
|
||||||
|
// END navbar
|
||||||
|
|
||||||
|
// /
|
||||||
|
"home.title": "A <h>self-hosted</h> file sharing platform.",
|
||||||
|
|
||||||
|
"home.description":
|
||||||
|
"Do you really want to give your personal files in the hand of third parties like WeTransfer?",
|
||||||
|
"home.bullet.a.name": "Self-Hosted",
|
||||||
|
"home.bullet.a.description": "Host Pingvin Share on your own machine.",
|
||||||
|
"home.bullet.b.name": "Privacy",
|
||||||
|
"home.bullet.b.description":
|
||||||
|
"Your files are your files and should never get into the hands of third parties.",
|
||||||
|
"home.bullet.c.name": "No annoying file size limit",
|
||||||
|
"home.bullet.c.description":
|
||||||
|
"Upload as big files as you want. Only your hard drive will be your limit.",
|
||||||
|
|
||||||
|
"home.button.start": "Get started",
|
||||||
|
"home.button.source": "Source code",
|
||||||
|
// END /
|
||||||
|
|
||||||
|
// /auth/signin
|
||||||
|
"signin.title": "Welcome back",
|
||||||
|
"signin.description": "You don't have an account yet?",
|
||||||
|
"signin.button.signup": "Sign up",
|
||||||
|
"signin.input.email-or-username": "Email or username",
|
||||||
|
"signin.input.email-or-username.placeholder": "Your email or username",
|
||||||
|
"signin.input.password": "Password",
|
||||||
|
"signin.input.password.placeholder": "Your password",
|
||||||
|
"signin.button.submit": "Sign in",
|
||||||
|
"signIn.notify.totp-required.title": "Two-factor authentication required",
|
||||||
|
"signIn.notify.totp-required.description":
|
||||||
|
"Please enter your two-factor authentication code",
|
||||||
|
|
||||||
|
// END /auth/signin
|
||||||
|
|
||||||
|
// /auth/signup
|
||||||
|
"signup.title": "Create an account",
|
||||||
|
"signup.description": "Already have an account?",
|
||||||
|
"signup.button.signin": "Sign in",
|
||||||
|
"signup.input.username": "Username",
|
||||||
|
"signup.input.username.placeholder": "Your username",
|
||||||
|
"signup.input.email": "Email",
|
||||||
|
"signup.input.email.placeholder": "Your email",
|
||||||
|
"signup.button.submit": "Let's get started",
|
||||||
|
|
||||||
|
// END /auth/signup
|
||||||
|
|
||||||
|
// /auth/reset-password
|
||||||
|
"resetPassword.title": "Forgot your password?",
|
||||||
|
"resetPassword.description": "Enter your email to reset your password.",
|
||||||
|
"resetPassword.notify.success":
|
||||||
|
"An email has been sent with a link to reset your password.",
|
||||||
|
"resetPassword.button.back": "Back to sign in page",
|
||||||
|
"resetPassword.text.resetPassword": "Reset password",
|
||||||
|
"resetPassword.text.enterNewPassword": "Enter your new password",
|
||||||
|
"resetPassword.input.password": "New password",
|
||||||
|
"resetPassword.notify.passwordReset":
|
||||||
|
"Your password has been reset successfully.",
|
||||||
|
|
||||||
|
// /account
|
||||||
|
"account.title": "My account",
|
||||||
|
|
||||||
|
"account.card.info.title": "Account info",
|
||||||
|
"account.card.info.username": "Username",
|
||||||
|
"account.card.info.email": "Email",
|
||||||
|
"account.notify.info.success": "Account updated successfully",
|
||||||
|
|
||||||
|
"account.card.password.title": "Password",
|
||||||
|
"account.card.password.old": "Old password",
|
||||||
|
"account.card.password.new": "New password",
|
||||||
|
"account.notify.password.success": "Password changed successfully",
|
||||||
|
|
||||||
|
"account.card.security.title": "Security",
|
||||||
|
"account.card.security.totp.enable.description":
|
||||||
|
"Enter your current password to start enabling TOTP",
|
||||||
|
"account.card.security.totp.disable.description":
|
||||||
|
"Enter your current password to start enabling TOTP",
|
||||||
|
"account.card.security.totp.button.start": "Start",
|
||||||
|
"account.modal.totp.title": "Enable TOTP",
|
||||||
|
"account.modal.totp.step1": "Step 1: Add your authenticator",
|
||||||
|
"account.modal.totp.step2": "Step 2: Validate your code",
|
||||||
|
"account.modal.totp.enterManually": "Enter manually",
|
||||||
|
"account.modal.totp.code": "Code",
|
||||||
|
"account.modal.totp.clickToCopy": "Click to copy",
|
||||||
|
"account.modal.totp.verify": "Verify",
|
||||||
|
"account.notify.totp.disable": "TOTP disabled successfully",
|
||||||
|
"account.notify.totp.enable": "TOTP enabled successfully",
|
||||||
|
|
||||||
|
"account.card.language.title": "Language",
|
||||||
|
"account.card.color.title": "Color scheme",
|
||||||
|
|
||||||
|
// ThemeSwitcher.tsx
|
||||||
|
"account.theme.dark": "Dark",
|
||||||
|
"account.theme.light": "Light",
|
||||||
|
"account.theme.system": "System",
|
||||||
|
|
||||||
|
"account.button.delete": "Delete Account",
|
||||||
|
"account.modal.delete.title": "Delete Account",
|
||||||
|
"account.modal.delete.description":
|
||||||
|
"Do you really want to delete your account including all your active shares?",
|
||||||
|
// END /account
|
||||||
|
|
||||||
|
// /account/shares
|
||||||
|
"account.shares.title": "My shares",
|
||||||
|
"account.shares.title.empty": "It's empty here 👀",
|
||||||
|
"account.shares.description.empty": "You don't have any shares.",
|
||||||
|
"account.shares.button.create": "Create one",
|
||||||
|
|
||||||
|
"account.shares.info.title": "Share informatons",
|
||||||
|
"account.shares.table.id": "ID",
|
||||||
|
"account.shares.table.name": "Name",
|
||||||
|
"account.shares.table.description": "Description",
|
||||||
|
"account.shares.table.visitors": "Visitors",
|
||||||
|
"account.shares.table.expiresAt": "Expires at",
|
||||||
|
"account.shares.table.createdAt": "Created at",
|
||||||
|
"account.shares.table.size": "Size",
|
||||||
|
|
||||||
|
"account.shares.modal.share-link": "Share link",
|
||||||
|
|
||||||
|
"account.shares.modal.delete.title": "Delete {share}",
|
||||||
|
"account.shares.modal.delete.description":
|
||||||
|
"Do you really want to delete this share?",
|
||||||
|
|
||||||
|
// END /account/shares
|
||||||
|
|
||||||
|
// /account/reverseShares
|
||||||
|
"account.reverseShares.title": "Reverse shares",
|
||||||
|
"account.reverseShares.description":
|
||||||
|
"A reverse share allows you to generate a unique URL that allows external users to create a share.",
|
||||||
|
|
||||||
|
"account.reverseShares.title.empty": "It's empty here 👀",
|
||||||
|
"account.reverseShares.description.empty":
|
||||||
|
"You don't have any reverse shares.",
|
||||||
|
|
||||||
|
// showCreateReverseShareModal.tsx
|
||||||
|
"account.reverseShares.modal.expiration.label": "Expiration",
|
||||||
|
"account.reverseShares.modal.expiration.minute-singular": "Minute",
|
||||||
|
"account.reverseShares.modal.expiration.minute-plural": "Minutes",
|
||||||
|
"account.reverseShares.modal.expiration.hour-singular": "Hour",
|
||||||
|
"account.reverseShares.modal.expiration.hour-plural": "Hours",
|
||||||
|
"account.reverseShares.modal.expiration.day-singular": "Day",
|
||||||
|
"account.reverseShares.modal.expiration.day-plural": "Days",
|
||||||
|
"account.reverseShares.modal.expiration.week-singular": "Week",
|
||||||
|
"account.reverseShares.modal.expiration.week-plural": "Weeks",
|
||||||
|
"account.reverseShares.modal.expiration.month-singular": "Month",
|
||||||
|
"account.reverseShares.modal.expiration.month-plural": "Months",
|
||||||
|
"account.reverseShares.modal.expiration.year-singular": "Year",
|
||||||
|
"account.reverseShares.modal.expiration.year-plural": "Years",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.max-size.label": "Max share size",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.send-email": "Send email notification",
|
||||||
|
"account.reverseShares.modal.send-email.description":
|
||||||
|
"Send an email notification when a share is created with this reverse share link.",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.max-use.label": "Max uses",
|
||||||
|
"account.reverseShares.modal.max-use.description":
|
||||||
|
"The maximum amount of times this URL can be used to create a share.",
|
||||||
|
"account.reverseShare.never-expires": "This reverse share will never expire.",
|
||||||
|
"account.reverseShare.expires-on":
|
||||||
|
"This reverse share will expire on {expiration}.",
|
||||||
|
|
||||||
|
"account.reverseShares.table.no-shares": "No shares created yet",
|
||||||
|
"account.reverseShares.table.count.singular": "share",
|
||||||
|
"account.reverseShares.table.count.plural": "shares",
|
||||||
|
"account.reverseShares.table.shares": "Shares",
|
||||||
|
"account.reverseShares.table.remaining": "Remaining uses",
|
||||||
|
"account.reverseShares.table.max-size": "Max share size",
|
||||||
|
"account.reverseShares.table.expires": "Expires at",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.reverse-share-link": "Reverse share link",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.delete.title":
|
||||||
|
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
|
||||||
|
"account.reverseShares.modal.delete.description":
|
||||||
|
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
|
||||||
|
|
||||||
|
// END /account/reverseShares
|
||||||
|
|
||||||
|
// /admin
|
||||||
|
"admin.title": "Administration",
|
||||||
|
"admin.button.users": "User management",
|
||||||
|
"admin.button.config": "Configuration",
|
||||||
|
"admin.version": "Version",
|
||||||
|
// END /admin
|
||||||
|
|
||||||
|
// /admin/users
|
||||||
|
"admin.users.title": "User management",
|
||||||
|
"admin.users.table.username": "Username",
|
||||||
|
"admin.users.table.email": "Email",
|
||||||
|
"admin.users.table.admin": "Admin",
|
||||||
|
|
||||||
|
"admin.users.edit.update.title": "Update user {username}",
|
||||||
|
"admin.users.edit.update.admin-privileges": "Admin privileges",
|
||||||
|
"admin.users.edit.update.change-password.title": "Change password",
|
||||||
|
"admin.users.edit.update.change-password.field": "New password",
|
||||||
|
"admin.users.edit.update.change-password.button": "Save new password",
|
||||||
|
"admin.users.edit.update.notify.password.success":
|
||||||
|
"Password changed successfully",
|
||||||
|
|
||||||
|
"admin.users.edit.delete.title": "Delete user {username}",
|
||||||
|
"admin.users.edit.delete.description":
|
||||||
|
"Do you really want to delete this user and all his shares?",
|
||||||
|
|
||||||
|
// showCreateUserModal.tsx
|
||||||
|
"admin.users.modal.create.title": "Create user",
|
||||||
|
"admin.users.modal.create.username": "Username",
|
||||||
|
"admin.users.modal.create.email": "Email",
|
||||||
|
"admin.users.modal.create.password": "Password",
|
||||||
|
"admin.users.modal.create.manual-password": "Set password manually",
|
||||||
|
"admin.users.modal.create.manual-password.description":
|
||||||
|
"If not checked, the user will receive an email with a link to set their password.",
|
||||||
|
"admin.users.modal.create.admin": "Admin privileges",
|
||||||
|
"admin.users.modal.create.admin.description":
|
||||||
|
"If checked, the user will be able to access the admin panel.",
|
||||||
|
|
||||||
|
// END /admin/users
|
||||||
|
|
||||||
|
// /upload
|
||||||
|
"upload.title": "Upload",
|
||||||
|
|
||||||
|
"upload.notify.generic-error":
|
||||||
|
"An error occurred while finishing your share.",
|
||||||
|
"upload.notify.count-failed": "{count} filed failed to upload. Trying again.",
|
||||||
|
|
||||||
|
// Dropzone.tsx
|
||||||
|
"upload.dropzone.title": "Upload files",
|
||||||
|
"upload.dropzone.description":
|
||||||
|
"Drag'n'drop files here to start your share. We can accept only files that are less than {maxSize} in total.",
|
||||||
|
"upload.dropzone.notify.file-too-big":
|
||||||
|
"Your files exceed the maximum share size of {maxSize}.",
|
||||||
|
|
||||||
|
// FileList.tsx
|
||||||
|
"upload.filelist.name": "Name",
|
||||||
|
"upload.filelist.size": "Size",
|
||||||
|
|
||||||
|
// showCreateUploadModal.tsx
|
||||||
|
"upload.modal.title": "Create Share",
|
||||||
|
"upload.modal.link.error.invalid":
|
||||||
|
"Can only contain letters, numbers, underscores, and hyphens",
|
||||||
|
"upload.modal.link.error.taken": "This link is already in use",
|
||||||
|
"upload.modal.not-signed-in": "You're not signed in",
|
||||||
|
"upload.modal.not-signed-in-description":
|
||||||
|
"You will be unable to delete your share manually and view the visitor count.",
|
||||||
|
|
||||||
|
"upload.modal.expires.never": "never",
|
||||||
|
"upload.modal.expires.never-long": "Never Expires",
|
||||||
|
|
||||||
|
"upload.modal.link.label": "Link",
|
||||||
|
"upload.modal.link.placeholder": "myAwesomeShare",
|
||||||
|
|
||||||
|
"upload.modal.expires.label": "Expiration",
|
||||||
|
"upload.modal.expires.minute-singular": "Minute",
|
||||||
|
"upload.modal.expires.minute-plural": "Minutes",
|
||||||
|
"upload.modal.expires.hour-singular": "Hour",
|
||||||
|
"upload.modal.expires.hour-plural": "Hours",
|
||||||
|
"upload.modal.expires.day-singular": "Day",
|
||||||
|
"upload.modal.expires.day-plural": "Days",
|
||||||
|
"upload.modal.expires.week-singular": "Week",
|
||||||
|
"upload.modal.expires.week-plural": "Weeks",
|
||||||
|
"upload.modal.expires.month-singular": "Month",
|
||||||
|
"upload.modal.expires.month-plural": "Months",
|
||||||
|
"upload.modal.expires.year-singular": "Year",
|
||||||
|
"upload.modal.expires.year-plural": "Years",
|
||||||
|
|
||||||
|
"upload.modal.accordion.description.title": "Description",
|
||||||
|
"upload.modal.accordion.description.placeholder":
|
||||||
|
"Note for the recipients of this share",
|
||||||
|
|
||||||
|
"upload.modal.accordion.email.title": "Email recipients",
|
||||||
|
"upload.modal.accordion.email.placeholder": "Enter email recipients",
|
||||||
|
"upload.modal.accordion.email.invalid-email": "Invalid email address",
|
||||||
|
|
||||||
|
"upload.modal.accordion.security.title": "Security options",
|
||||||
|
"upload.modal.accordion.security.password.label": "Password protection",
|
||||||
|
"upload.modal.accordion.security.password.placeholder": "No password",
|
||||||
|
"upload.modal.accordion.security.max-views.label": "Maximum views",
|
||||||
|
"upload.modal.accordion.security.max-views.placeholder": "No limit",
|
||||||
|
|
||||||
|
// showCompletedUploadModal.tsx
|
||||||
|
"upload.modal.completed.never-expires": "This share will never expire.",
|
||||||
|
"upload.modal.completed.expires-on":
|
||||||
|
"This share will expire on {expiration}.",
|
||||||
|
"upload.modal.completed.share-ready": "Share ready",
|
||||||
|
|
||||||
|
// END /upload
|
||||||
|
|
||||||
|
// /share/[id]
|
||||||
|
"share.title": "Share {shareId}",
|
||||||
|
"share.description": "Look what I've shared with you!",
|
||||||
|
"share.error.visitor-limit-exceeded.title": "Visitor limit exceeded",
|
||||||
|
"share.error.visitor-limit-exceeded.description":
|
||||||
|
"The visitor limit from this share has been exceeded.",
|
||||||
|
"share.error.removed.title": "Share removed",
|
||||||
|
"share.error.not-found.title": "Share not found",
|
||||||
|
"share.error.not-found.description":
|
||||||
|
"The share you're looking for doesn't exist.",
|
||||||
|
|
||||||
|
"share.modal.password.title": "Password required",
|
||||||
|
"share.modal.password.description":
|
||||||
|
"To access this share please enter the password for the share.",
|
||||||
|
"share.modal.password": "Password",
|
||||||
|
"share.modal.error.invalid-password": "Invalid password",
|
||||||
|
|
||||||
|
"share.button.download-all": "Download all",
|
||||||
|
"share.notify.download-all-preparing":
|
||||||
|
"The share is preparing. Try again in a few minutes.",
|
||||||
|
|
||||||
|
"share.modal.file-link": "File link",
|
||||||
|
"share.table.name": "Name",
|
||||||
|
"share.table.size": "Size",
|
||||||
|
|
||||||
|
"share.modal.file-preview.error.not-supported.title": "Preview not supported",
|
||||||
|
"share.modal.file-preview.error.not-supported.description":
|
||||||
|
"A preview for thise file type is unsupported. Please download the file to view it.",
|
||||||
|
|
||||||
|
// END /share/[id]
|
||||||
|
|
||||||
|
// /admin/config
|
||||||
|
"admin.config.title": "Configuration",
|
||||||
|
"admin.config.category.general": "General",
|
||||||
|
"admin.config.category.share": "Share",
|
||||||
|
"admin.config.category.email": "Email",
|
||||||
|
"admin.config.category.smtp": "SMTP",
|
||||||
|
|
||||||
|
"admin.config.general.app-name": "App name",
|
||||||
|
"admin.config.general.app-name.description": "Name of the application",
|
||||||
|
"admin.config.general.app-url": "App URL",
|
||||||
|
"admin.config.general.app-url.description":
|
||||||
|
"On which URL Pingvin Share is available",
|
||||||
|
"admin.config.general.show-home-page": "Show home page",
|
||||||
|
"admin.config.general.show-home-page.description":
|
||||||
|
"Whether to show the home page",
|
||||||
|
"admin.config.general.logo": "Logo",
|
||||||
|
"admin.config.general.logo.description":
|
||||||
|
"Change your logo by uploading a new image. The image must be a PNG and should have the format 1:1.",
|
||||||
|
"admin.config.general.logo.placeholder": "Pick image",
|
||||||
|
|
||||||
|
"admin.config.email.enable-share-email-recipients":
|
||||||
|
"Enable share email recipients",
|
||||||
|
"admin.config.email.enable-share-email-recipients.description":
|
||||||
|
"Whether to allow emails to share recipients. Only enable this if you have enabled SMTP.",
|
||||||
|
"admin.config.email.share-recipients-subject": "Share recipients subject",
|
||||||
|
"admin.config.email.share-recipients-subject.description":
|
||||||
|
"Subject of the email which gets sent to the share recipients.",
|
||||||
|
"admin.config.email.share-recipients-message": "Share recipients message",
|
||||||
|
"admin.config.email.share-recipients-message.description":
|
||||||
|
"Message which gets sent to the share recipients. Available variables:\n {creator} - The username of the creator of the share\n {shareUrl} - The URL of the share\n {desc} - The description of the share\n {expires} - The expiration date of the share\n The variables will be replaced with the actual values.",
|
||||||
|
"admin.config.email.reverse-share-subject": "Reverse share subject",
|
||||||
|
"admin.config.email.reverse-share-subject.description":
|
||||||
|
"Subject of the email which gets sent when someone created a share with your reverse share link.",
|
||||||
|
"admin.config.email.reverse-share-message": "Reverse share message",
|
||||||
|
"admin.config.email.reverse-share-message.description":
|
||||||
|
"Message which gets sent when someone created a share with your reverse share link. {shareUrl} will be replaced with the creator's name and the share URL.",
|
||||||
|
"admin.config.email.reset-password-subject": "Reset password subject",
|
||||||
|
"admin.config.email.reset-password-subject.description":
|
||||||
|
"Subject of the email which gets sent when a user requests a password reset.",
|
||||||
|
"admin.config.email.reset-password-message": "Reset password message",
|
||||||
|
"admin.config.email.reset-password-message.description":
|
||||||
|
"Message which gets sent when a user requests a password reset. {url} will be replaced with the reset password URL.",
|
||||||
|
"admin.config.email.invite-subject": "Invite subject",
|
||||||
|
"admin.config.email.invite-subject.description":
|
||||||
|
"Subject of the email which gets sent when an admin invites a user.",
|
||||||
|
"admin.config.email.invite-message": "Invite message",
|
||||||
|
"admin.config.email.invite-message.description":
|
||||||
|
"Message which gets sent when an admin invites a user. {url} will be replaced with the invite URL and {password} with the password.",
|
||||||
|
"admin.config.share.allow-registration": "Allow registration",
|
||||||
|
"admin.config.share.allow-registration.description":
|
||||||
|
"Whether registration is allowed",
|
||||||
|
"admin.config.share.allow-unauthenticated-shares":
|
||||||
|
"Allow unauthenticated shares",
|
||||||
|
"admin.config.share.allow-unauthenticated-shares.description":
|
||||||
|
"Whether unauthorized users can create shares",
|
||||||
|
"admin.config.share.max-size": "Max size",
|
||||||
|
"admin.config.share.max-size.description": "Maximum share size in bytes",
|
||||||
|
|
||||||
|
"admin.config.smtp.enabled": "Enabled",
|
||||||
|
"admin.config.smtp.enabled.description":
|
||||||
|
"Whether SMTP is enabled. Only set this to true if you entered the host, port, email, user and password of your SMTP server.",
|
||||||
|
"admin.config.smtp.host": "Host",
|
||||||
|
"admin.config.smtp.host.description": "Host of the SMTP server",
|
||||||
|
"admin.config.smtp.port": "Port",
|
||||||
|
"admin.config.smtp.port.description": "Port of the SMTP server",
|
||||||
|
"admin.config.smtp.email": "Email",
|
||||||
|
"admin.config.smtp.email.description":
|
||||||
|
"Email address which the emails get sent from",
|
||||||
|
"admin.config.smtp.username": "Username",
|
||||||
|
"admin.config.smtp.username.description": "Username of the SMTP server",
|
||||||
|
"admin.config.smtp.password": "Password",
|
||||||
|
"admin.config.smtp.password.description": "Password of the SMTP server",
|
||||||
|
"admin.config.smtp.button.test": "Send test email",
|
||||||
|
|
||||||
|
// 404
|
||||||
|
"404.title": "404",
|
||||||
|
"404.description": "Oops this page doesn't exist.",
|
||||||
|
"404.button.home": "Gring me back home",
|
||||||
|
|
||||||
|
// Common translations
|
||||||
|
"common.button.save": "Save",
|
||||||
|
"common.button.create": "Create",
|
||||||
|
"common.button.submit": "Submit",
|
||||||
|
"common.button.delete": "Delete",
|
||||||
|
"common.button.cancel": "Cancel",
|
||||||
|
"common.button.confirm": "Confirm",
|
||||||
|
"common.button.disable": "Disable",
|
||||||
|
"common.button.share": "Share",
|
||||||
|
"common.button.generate": "Generate",
|
||||||
|
"common.button.done": "Done",
|
||||||
|
"common.text.link": "Link",
|
||||||
|
"common.text.or": "or",
|
||||||
|
"common.button.go-back": "Go back",
|
||||||
|
"common.notify.copied": "Your link was copied to the clipboard",
|
||||||
|
|
||||||
|
"common.error": "Error",
|
||||||
|
"common.error.unknown": "An unknown error occurred",
|
||||||
|
"common.error.invalid-email": "Invalid email address",
|
||||||
|
"common.error.too-short": "Must be at least {length} characters",
|
||||||
|
"common.error.too-long": "Must be at most {length} characters",
|
||||||
|
"common.error.exact-length": "Must be exactly {length} characters",
|
||||||
|
"common.error.invalid-number": "Must be a number",
|
||||||
|
"common.error.field-required": "This field is required",
|
||||||
|
};
|
435
frontend/src/i18n/translations/th.ts
Normal file
435
frontend/src/i18n/translations/th.ts
Normal file
|
@ -0,0 +1,435 @@
|
||||||
|
export default {
|
||||||
|
// Navbar
|
||||||
|
"navbar.upload": "Upload",
|
||||||
|
"navbar.signin": "Sign in",
|
||||||
|
"navbar.home": "Home",
|
||||||
|
"navbar.signup": "Sign Up",
|
||||||
|
|
||||||
|
"navbar.links.shares": "My shares",
|
||||||
|
"navbar.links.reverse": "Reverse shares",
|
||||||
|
|
||||||
|
"navbar.avatar.account": "My account",
|
||||||
|
"navbar.avatar.admin": "Administration",
|
||||||
|
"navbar.avatar.signout": "Sign out",
|
||||||
|
// END navbar
|
||||||
|
|
||||||
|
// /
|
||||||
|
"home.title": "A <h>self-hosted</h> file sharing platform.",
|
||||||
|
|
||||||
|
"home.description":
|
||||||
|
"Do you really want to give your personal files in the hand of third parties like WeTransfer?",
|
||||||
|
"home.bullet.a.name": "Self-Hosted",
|
||||||
|
"home.bullet.a.description": "Host Pingvin Share on your own machine.",
|
||||||
|
"home.bullet.b.name": "Privacy",
|
||||||
|
"home.bullet.b.description":
|
||||||
|
"Your files are your files and should never get into the hands of third parties.",
|
||||||
|
"home.bullet.c.name": "No annoying file size limit",
|
||||||
|
"home.bullet.c.description":
|
||||||
|
"Upload as big files as you want. Only your hard drive will be your limit.",
|
||||||
|
|
||||||
|
"home.button.start": "Get started",
|
||||||
|
"home.button.source": "Source code",
|
||||||
|
// END /
|
||||||
|
|
||||||
|
// /auth/signin
|
||||||
|
"signin.title": "Welcome back",
|
||||||
|
"signin.description": "You don't have an account yet?",
|
||||||
|
"signin.button.signup": "Sign up",
|
||||||
|
"signin.input.email-or-username": "Email or username",
|
||||||
|
"signin.input.email-or-username.placeholder": "Your email or username",
|
||||||
|
"signin.input.password": "Password",
|
||||||
|
"signin.input.password.placeholder": "Your password",
|
||||||
|
"signin.button.submit": "Sign in",
|
||||||
|
"signIn.notify.totp-required.title": "Two-factor authentication required",
|
||||||
|
"signIn.notify.totp-required.description":
|
||||||
|
"Please enter your two-factor authentication code",
|
||||||
|
|
||||||
|
// END /auth/signin
|
||||||
|
|
||||||
|
// /auth/signup
|
||||||
|
"signup.title": "Create an account",
|
||||||
|
"signup.description": "Already have an account?",
|
||||||
|
"signup.button.signin": "Sign in",
|
||||||
|
"signup.input.username": "Username",
|
||||||
|
"signup.input.username.placeholder": "Your username",
|
||||||
|
"signup.input.email": "Email",
|
||||||
|
"signup.input.email.placeholder": "Your email",
|
||||||
|
"signup.button.submit": "Let's get started",
|
||||||
|
|
||||||
|
// END /auth/signup
|
||||||
|
|
||||||
|
// /auth/reset-password
|
||||||
|
"resetPassword.title": "Forgot your password?",
|
||||||
|
"resetPassword.description": "Enter your email to reset your password.",
|
||||||
|
"resetPassword.notify.success":
|
||||||
|
"An email has been sent with a link to reset your password.",
|
||||||
|
"resetPassword.button.back": "Back to sign in page",
|
||||||
|
"resetPassword.text.resetPassword": "Reset password",
|
||||||
|
"resetPassword.text.enterNewPassword": "Enter your new password",
|
||||||
|
"resetPassword.input.password": "New password",
|
||||||
|
"resetPassword.notify.passwordReset":
|
||||||
|
"Your password has been reset successfully.",
|
||||||
|
|
||||||
|
// /account
|
||||||
|
"account.title": "My account",
|
||||||
|
|
||||||
|
"account.card.info.title": "Account info",
|
||||||
|
"account.card.info.username": "Username",
|
||||||
|
"account.card.info.email": "Email",
|
||||||
|
"account.notify.info.success": "Account updated successfully",
|
||||||
|
|
||||||
|
"account.card.password.title": "Password",
|
||||||
|
"account.card.password.old": "Old password",
|
||||||
|
"account.card.password.new": "New password",
|
||||||
|
"account.notify.password.success": "Password changed successfully",
|
||||||
|
|
||||||
|
"account.card.security.title": "Security",
|
||||||
|
"account.card.security.totp.enable.description":
|
||||||
|
"Enter your current password to start enabling TOTP",
|
||||||
|
"account.card.security.totp.disable.description":
|
||||||
|
"Enter your current password to start enabling TOTP",
|
||||||
|
"account.card.security.totp.button.start": "Start",
|
||||||
|
"account.modal.totp.title": "Enable TOTP",
|
||||||
|
"account.modal.totp.step1": "Step 1: Add your authenticator",
|
||||||
|
"account.modal.totp.step2": "Step 2: Validate your code",
|
||||||
|
"account.modal.totp.enterManually": "Enter manually",
|
||||||
|
"account.modal.totp.code": "Code",
|
||||||
|
"account.modal.totp.clickToCopy": "Click to copy",
|
||||||
|
"account.modal.totp.verify": "Verify",
|
||||||
|
"account.notify.totp.disable": "TOTP disabled successfully",
|
||||||
|
"account.notify.totp.enable": "TOTP enabled successfully",
|
||||||
|
|
||||||
|
"account.card.language.title": "Language",
|
||||||
|
"account.card.color.title": "Color scheme",
|
||||||
|
|
||||||
|
// ThemeSwitcher.tsx
|
||||||
|
"account.theme.dark": "Dark",
|
||||||
|
"account.theme.light": "Light",
|
||||||
|
"account.theme.system": "System",
|
||||||
|
|
||||||
|
"account.button.delete": "Delete Account",
|
||||||
|
"account.modal.delete.title": "Delete Account",
|
||||||
|
"account.modal.delete.description":
|
||||||
|
"Do you really want to delete your account including all your active shares?",
|
||||||
|
// END /account
|
||||||
|
|
||||||
|
// /account/shares
|
||||||
|
"account.shares.title": "My shares",
|
||||||
|
"account.shares.title.empty": "It's empty here 👀",
|
||||||
|
"account.shares.description.empty": "You don't have any shares.",
|
||||||
|
"account.shares.button.create": "Create one",
|
||||||
|
|
||||||
|
"account.shares.info.title": "Share informatons",
|
||||||
|
"account.shares.table.id": "ID",
|
||||||
|
"account.shares.table.name": "Name",
|
||||||
|
"account.shares.table.description": "Description",
|
||||||
|
"account.shares.table.visitors": "Visitors",
|
||||||
|
"account.shares.table.expiresAt": "Expires at",
|
||||||
|
"account.shares.table.createdAt": "Created at",
|
||||||
|
"account.shares.table.size": "Size",
|
||||||
|
|
||||||
|
"account.shares.modal.share-link": "Share link",
|
||||||
|
|
||||||
|
"account.shares.modal.delete.title": "Delete {share}",
|
||||||
|
"account.shares.modal.delete.description":
|
||||||
|
"Do you really want to delete this share?",
|
||||||
|
|
||||||
|
// END /account/shares
|
||||||
|
|
||||||
|
// /account/reverseShares
|
||||||
|
"account.reverseShares.title": "Reverse shares",
|
||||||
|
"account.reverseShares.description":
|
||||||
|
"A reverse share allows you to generate a unique URL that allows external users to create a share.",
|
||||||
|
|
||||||
|
"account.reverseShares.title.empty": "It's empty here 👀",
|
||||||
|
"account.reverseShares.description.empty":
|
||||||
|
"You don't have any reverse shares.",
|
||||||
|
|
||||||
|
// showCreateReverseShareModal.tsx
|
||||||
|
"account.reverseShares.modal.expiration.label": "Expiration",
|
||||||
|
"account.reverseShares.modal.expiration.minute-singular": "Minute",
|
||||||
|
"account.reverseShares.modal.expiration.minute-plural": "Minutes",
|
||||||
|
"account.reverseShares.modal.expiration.hour-singular": "Hour",
|
||||||
|
"account.reverseShares.modal.expiration.hour-plural": "Hours",
|
||||||
|
"account.reverseShares.modal.expiration.day-singular": "Day",
|
||||||
|
"account.reverseShares.modal.expiration.day-plural": "Days",
|
||||||
|
"account.reverseShares.modal.expiration.week-singular": "Week",
|
||||||
|
"account.reverseShares.modal.expiration.week-plural": "Weeks",
|
||||||
|
"account.reverseShares.modal.expiration.month-singular": "Month",
|
||||||
|
"account.reverseShares.modal.expiration.month-plural": "Months",
|
||||||
|
"account.reverseShares.modal.expiration.year-singular": "Year",
|
||||||
|
"account.reverseShares.modal.expiration.year-plural": "Years",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.max-size.label": "Max share size",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.send-email": "Send email notification",
|
||||||
|
"account.reverseShares.modal.send-email.description":
|
||||||
|
"Send an email notification when a share is created with this reverse share link.",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.max-use.label": "Max uses",
|
||||||
|
"account.reverseShares.modal.max-use.description":
|
||||||
|
"The maximum amount of times this URL can be used to create a share.",
|
||||||
|
"account.reverseShare.never-expires": "This reverse share will never expire.",
|
||||||
|
"account.reverseShare.expires-on":
|
||||||
|
"This reverse share will expire on {expiration}.",
|
||||||
|
|
||||||
|
"account.reverseShares.table.no-shares": "No shares created yet",
|
||||||
|
"account.reverseShares.table.count.singular": "share",
|
||||||
|
"account.reverseShares.table.count.plural": "shares",
|
||||||
|
"account.reverseShares.table.shares": "Shares",
|
||||||
|
"account.reverseShares.table.remaining": "Remaining uses",
|
||||||
|
"account.reverseShares.table.max-size": "Max share size",
|
||||||
|
"account.reverseShares.table.expires": "Expires at",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.reverse-share-link": "Reverse share link",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.delete.title":
|
||||||
|
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
|
||||||
|
"account.reverseShares.modal.delete.description":
|
||||||
|
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
|
||||||
|
|
||||||
|
// END /account/reverseShares
|
||||||
|
|
||||||
|
// /admin
|
||||||
|
"admin.title": "Administration",
|
||||||
|
"admin.button.users": "User management",
|
||||||
|
"admin.button.config": "Configuration",
|
||||||
|
"admin.version": "Version",
|
||||||
|
// END /admin
|
||||||
|
|
||||||
|
// /admin/users
|
||||||
|
"admin.users.title": "User management",
|
||||||
|
"admin.users.table.username": "Username",
|
||||||
|
"admin.users.table.email": "Email",
|
||||||
|
"admin.users.table.admin": "Admin",
|
||||||
|
|
||||||
|
"admin.users.edit.update.title": "Update user {username}",
|
||||||
|
"admin.users.edit.update.admin-privileges": "Admin privileges",
|
||||||
|
"admin.users.edit.update.change-password.title": "Change password",
|
||||||
|
"admin.users.edit.update.change-password.field": "New password",
|
||||||
|
"admin.users.edit.update.change-password.button": "Save new password",
|
||||||
|
"admin.users.edit.update.notify.password.success":
|
||||||
|
"Password changed successfully",
|
||||||
|
|
||||||
|
"admin.users.edit.delete.title": "Delete user {username}",
|
||||||
|
"admin.users.edit.delete.description":
|
||||||
|
"Do you really want to delete this user and all his shares?",
|
||||||
|
|
||||||
|
// showCreateUserModal.tsx
|
||||||
|
"admin.users.modal.create.title": "Create user",
|
||||||
|
"admin.users.modal.create.username": "Username",
|
||||||
|
"admin.users.modal.create.email": "Email",
|
||||||
|
"admin.users.modal.create.password": "Password",
|
||||||
|
"admin.users.modal.create.manual-password": "Set password manually",
|
||||||
|
"admin.users.modal.create.manual-password.description":
|
||||||
|
"If not checked, the user will receive an email with a link to set their password.",
|
||||||
|
"admin.users.modal.create.admin": "Admin privileges",
|
||||||
|
"admin.users.modal.create.admin.description":
|
||||||
|
"If checked, the user will be able to access the admin panel.",
|
||||||
|
|
||||||
|
// END /admin/users
|
||||||
|
|
||||||
|
// /upload
|
||||||
|
"upload.title": "Upload",
|
||||||
|
|
||||||
|
"upload.notify.generic-error":
|
||||||
|
"An error occurred while finishing your share.",
|
||||||
|
"upload.notify.count-failed": "{count} filed failed to upload. Trying again.",
|
||||||
|
|
||||||
|
// Dropzone.tsx
|
||||||
|
"upload.dropzone.title": "Upload files",
|
||||||
|
"upload.dropzone.description":
|
||||||
|
"Drag'n'drop files here to start your share. We can accept only files that are less than {maxSize} in total.",
|
||||||
|
"upload.dropzone.notify.file-too-big":
|
||||||
|
"Your files exceed the maximum share size of {maxSize}.",
|
||||||
|
|
||||||
|
// FileList.tsx
|
||||||
|
"upload.filelist.name": "Name",
|
||||||
|
"upload.filelist.size": "Size",
|
||||||
|
|
||||||
|
// showCreateUploadModal.tsx
|
||||||
|
"upload.modal.title": "Create Share",
|
||||||
|
"upload.modal.link.error.invalid":
|
||||||
|
"Can only contain letters, numbers, underscores, and hyphens",
|
||||||
|
"upload.modal.link.error.taken": "This link is already in use",
|
||||||
|
"upload.modal.not-signed-in": "You're not signed in",
|
||||||
|
"upload.modal.not-signed-in-description":
|
||||||
|
"You will be unable to delete your share manually and view the visitor count.",
|
||||||
|
|
||||||
|
"upload.modal.expires.never": "never",
|
||||||
|
"upload.modal.expires.never-long": "Never Expires",
|
||||||
|
|
||||||
|
"upload.modal.link.label": "Link",
|
||||||
|
"upload.modal.link.placeholder": "myAwesomeShare",
|
||||||
|
|
||||||
|
"upload.modal.expires.label": "Expiration",
|
||||||
|
"upload.modal.expires.minute-singular": "Minute",
|
||||||
|
"upload.modal.expires.minute-plural": "Minutes",
|
||||||
|
"upload.modal.expires.hour-singular": "Hour",
|
||||||
|
"upload.modal.expires.hour-plural": "Hours",
|
||||||
|
"upload.modal.expires.day-singular": "Day",
|
||||||
|
"upload.modal.expires.day-plural": "Days",
|
||||||
|
"upload.modal.expires.week-singular": "Week",
|
||||||
|
"upload.modal.expires.week-plural": "Weeks",
|
||||||
|
"upload.modal.expires.month-singular": "Month",
|
||||||
|
"upload.modal.expires.month-plural": "Months",
|
||||||
|
"upload.modal.expires.year-singular": "Year",
|
||||||
|
"upload.modal.expires.year-plural": "Years",
|
||||||
|
|
||||||
|
"upload.modal.accordion.description.title": "Description",
|
||||||
|
"upload.modal.accordion.description.placeholder":
|
||||||
|
"Note for the recipients of this share",
|
||||||
|
|
||||||
|
"upload.modal.accordion.email.title": "Email recipients",
|
||||||
|
"upload.modal.accordion.email.placeholder": "Enter email recipients",
|
||||||
|
"upload.modal.accordion.email.invalid-email": "Invalid email address",
|
||||||
|
|
||||||
|
"upload.modal.accordion.security.title": "Security options",
|
||||||
|
"upload.modal.accordion.security.password.label": "Password protection",
|
||||||
|
"upload.modal.accordion.security.password.placeholder": "No password",
|
||||||
|
"upload.modal.accordion.security.max-views.label": "Maximum views",
|
||||||
|
"upload.modal.accordion.security.max-views.placeholder": "No limit",
|
||||||
|
|
||||||
|
// showCompletedUploadModal.tsx
|
||||||
|
"upload.modal.completed.never-expires": "This share will never expire.",
|
||||||
|
"upload.modal.completed.expires-on":
|
||||||
|
"This share will expire on {expiration}.",
|
||||||
|
"upload.modal.completed.share-ready": "Share ready",
|
||||||
|
|
||||||
|
// END /upload
|
||||||
|
|
||||||
|
// /share/[id]
|
||||||
|
"share.title": "Share {shareId}",
|
||||||
|
"share.description": "Look what I've shared with you!",
|
||||||
|
"share.error.visitor-limit-exceeded.title": "Visitor limit exceeded",
|
||||||
|
"share.error.visitor-limit-exceeded.description":
|
||||||
|
"The visitor limit from this share has been exceeded.",
|
||||||
|
"share.error.removed.title": "Share removed",
|
||||||
|
"share.error.not-found.title": "Share not found",
|
||||||
|
"share.error.not-found.description":
|
||||||
|
"The share you're looking for doesn't exist.",
|
||||||
|
|
||||||
|
"share.modal.password.title": "Password required",
|
||||||
|
"share.modal.password.description":
|
||||||
|
"To access this share please enter the password for the share.",
|
||||||
|
"share.modal.password": "Password",
|
||||||
|
"share.modal.error.invalid-password": "Invalid password",
|
||||||
|
|
||||||
|
"share.button.download-all": "Download all",
|
||||||
|
"share.notify.download-all-preparing":
|
||||||
|
"The share is preparing. Try again in a few minutes.",
|
||||||
|
|
||||||
|
"share.modal.file-link": "File link",
|
||||||
|
"share.table.name": "Name",
|
||||||
|
"share.table.size": "Size",
|
||||||
|
|
||||||
|
"share.modal.file-preview.error.not-supported.title": "Preview not supported",
|
||||||
|
"share.modal.file-preview.error.not-supported.description":
|
||||||
|
"A preview for thise file type is unsupported. Please download the file to view it.",
|
||||||
|
|
||||||
|
// END /share/[id]
|
||||||
|
|
||||||
|
// /admin/config
|
||||||
|
"admin.config.title": "Configuration",
|
||||||
|
"admin.config.category.general": "General",
|
||||||
|
"admin.config.category.share": "Share",
|
||||||
|
"admin.config.category.email": "Email",
|
||||||
|
"admin.config.category.smtp": "SMTP",
|
||||||
|
|
||||||
|
"admin.config.general.app-name": "App name",
|
||||||
|
"admin.config.general.app-name.description": "Name of the application",
|
||||||
|
"admin.config.general.app-url": "App URL",
|
||||||
|
"admin.config.general.app-url.description":
|
||||||
|
"On which URL Pingvin Share is available",
|
||||||
|
"admin.config.general.show-home-page": "Show home page",
|
||||||
|
"admin.config.general.show-home-page.description":
|
||||||
|
"Whether to show the home page",
|
||||||
|
"admin.config.general.logo": "Logo",
|
||||||
|
"admin.config.general.logo.description":
|
||||||
|
"Change your logo by uploading a new image. The image must be a PNG and should have the format 1:1.",
|
||||||
|
"admin.config.general.logo.placeholder": "Pick image",
|
||||||
|
|
||||||
|
"admin.config.email.enable-share-email-recipients":
|
||||||
|
"Enable share email recipients",
|
||||||
|
"admin.config.email.enable-share-email-recipients.description":
|
||||||
|
"Whether to allow emails to share recipients. Only enable this if you have enabled SMTP.",
|
||||||
|
"admin.config.email.share-recipients-subject": "Share recipients subject",
|
||||||
|
"admin.config.email.share-recipients-subject.description":
|
||||||
|
"Subject of the email which gets sent to the share recipients.",
|
||||||
|
"admin.config.email.share-recipients-message": "Share recipients message",
|
||||||
|
"admin.config.email.share-recipients-message.description":
|
||||||
|
"Message which gets sent to the share recipients. Available variables:\n {creator} - The username of the creator of the share\n {shareUrl} - The URL of the share\n {desc} - The description of the share\n {expires} - The expiration date of the share\n The variables will be replaced with the actual values.",
|
||||||
|
"admin.config.email.reverse-share-subject": "Reverse share subject",
|
||||||
|
"admin.config.email.reverse-share-subject.description":
|
||||||
|
"Subject of the email which gets sent when someone created a share with your reverse share link.",
|
||||||
|
"admin.config.email.reverse-share-message": "Reverse share message",
|
||||||
|
"admin.config.email.reverse-share-message.description":
|
||||||
|
"Message which gets sent when someone created a share with your reverse share link. {shareUrl} will be replaced with the creator's name and the share URL.",
|
||||||
|
"admin.config.email.reset-password-subject": "Reset password subject",
|
||||||
|
"admin.config.email.reset-password-subject.description":
|
||||||
|
"Subject of the email which gets sent when a user requests a password reset.",
|
||||||
|
"admin.config.email.reset-password-message": "Reset password message",
|
||||||
|
"admin.config.email.reset-password-message.description":
|
||||||
|
"Message which gets sent when a user requests a password reset. {url} will be replaced with the reset password URL.",
|
||||||
|
"admin.config.email.invite-subject": "Invite subject",
|
||||||
|
"admin.config.email.invite-subject.description":
|
||||||
|
"Subject of the email which gets sent when an admin invites a user.",
|
||||||
|
"admin.config.email.invite-message": "Invite message",
|
||||||
|
"admin.config.email.invite-message.description":
|
||||||
|
"Message which gets sent when an admin invites a user. {url} will be replaced with the invite URL and {password} with the password.",
|
||||||
|
"admin.config.share.allow-registration": "Allow registration",
|
||||||
|
"admin.config.share.allow-registration.description":
|
||||||
|
"Whether registration is allowed",
|
||||||
|
"admin.config.share.allow-unauthenticated-shares":
|
||||||
|
"Allow unauthenticated shares",
|
||||||
|
"admin.config.share.allow-unauthenticated-shares.description":
|
||||||
|
"Whether unauthorized users can create shares",
|
||||||
|
"admin.config.share.max-size": "Max size",
|
||||||
|
"admin.config.share.max-size.description": "Maximum share size in bytes",
|
||||||
|
|
||||||
|
"admin.config.smtp.enabled": "Enabled",
|
||||||
|
"admin.config.smtp.enabled.description":
|
||||||
|
"Whether SMTP is enabled. Only set this to true if you entered the host, port, email, user and password of your SMTP server.",
|
||||||
|
"admin.config.smtp.host": "Host",
|
||||||
|
"admin.config.smtp.host.description": "Host of the SMTP server",
|
||||||
|
"admin.config.smtp.port": "Port",
|
||||||
|
"admin.config.smtp.port.description": "Port of the SMTP server",
|
||||||
|
"admin.config.smtp.email": "Email",
|
||||||
|
"admin.config.smtp.email.description":
|
||||||
|
"Email address which the emails get sent from",
|
||||||
|
"admin.config.smtp.username": "Username",
|
||||||
|
"admin.config.smtp.username.description": "Username of the SMTP server",
|
||||||
|
"admin.config.smtp.password": "Password",
|
||||||
|
"admin.config.smtp.password.description": "Password of the SMTP server",
|
||||||
|
"admin.config.smtp.button.test": "Send test email",
|
||||||
|
|
||||||
|
// 404
|
||||||
|
"404.title": "404",
|
||||||
|
"404.description": "Oops this page doesn't exist.",
|
||||||
|
"404.button.home": "Gring me back home",
|
||||||
|
|
||||||
|
// Common translations
|
||||||
|
"common.button.save": "Save",
|
||||||
|
"common.button.create": "Create",
|
||||||
|
"common.button.submit": "Submit",
|
||||||
|
"common.button.delete": "Delete",
|
||||||
|
"common.button.cancel": "Cancel",
|
||||||
|
"common.button.confirm": "Confirm",
|
||||||
|
"common.button.disable": "Disable",
|
||||||
|
"common.button.share": "Share",
|
||||||
|
"common.button.generate": "Generate",
|
||||||
|
"common.button.done": "Done",
|
||||||
|
"common.text.link": "Link",
|
||||||
|
"common.text.or": "or",
|
||||||
|
"common.button.go-back": "Go back",
|
||||||
|
"common.notify.copied": "Your link was copied to the clipboard",
|
||||||
|
|
||||||
|
"common.error": "Error",
|
||||||
|
"common.error.unknown": "An unknown error occurred",
|
||||||
|
"common.error.invalid-email": "Invalid email address",
|
||||||
|
"common.error.too-short": "Must be at least {length} characters",
|
||||||
|
"common.error.too-long": "Must be at most {length} characters",
|
||||||
|
"common.error.exact-length": "Must be exactly {length} characters",
|
||||||
|
"common.error.invalid-number": "Must be a number",
|
||||||
|
"common.error.field-required": "This field is required",
|
||||||
|
};
|
435
frontend/src/i18n/translations/zh-CN.ts
Normal file
435
frontend/src/i18n/translations/zh-CN.ts
Normal file
|
@ -0,0 +1,435 @@
|
||||||
|
export default {
|
||||||
|
// Navbar
|
||||||
|
"navbar.upload": "Upload",
|
||||||
|
"navbar.signin": "Sign in",
|
||||||
|
"navbar.home": "Home",
|
||||||
|
"navbar.signup": "Sign Up",
|
||||||
|
|
||||||
|
"navbar.links.shares": "My shares",
|
||||||
|
"navbar.links.reverse": "Reverse shares",
|
||||||
|
|
||||||
|
"navbar.avatar.account": "My account",
|
||||||
|
"navbar.avatar.admin": "Administration",
|
||||||
|
"navbar.avatar.signout": "Sign out",
|
||||||
|
// END navbar
|
||||||
|
|
||||||
|
// /
|
||||||
|
"home.title": "A <h>self-hosted</h> file sharing platform.",
|
||||||
|
|
||||||
|
"home.description":
|
||||||
|
"Do you really want to give your personal files in the hand of third parties like WeTransfer?",
|
||||||
|
"home.bullet.a.name": "Self-Hosted",
|
||||||
|
"home.bullet.a.description": "Host Pingvin Share on your own machine.",
|
||||||
|
"home.bullet.b.name": "Privacy",
|
||||||
|
"home.bullet.b.description":
|
||||||
|
"Your files are your files and should never get into the hands of third parties.",
|
||||||
|
"home.bullet.c.name": "No annoying file size limit",
|
||||||
|
"home.bullet.c.description":
|
||||||
|
"Upload as big files as you want. Only your hard drive will be your limit.",
|
||||||
|
|
||||||
|
"home.button.start": "Get started",
|
||||||
|
"home.button.source": "Source code",
|
||||||
|
// END /
|
||||||
|
|
||||||
|
// /auth/signin
|
||||||
|
"signin.title": "Welcome back",
|
||||||
|
"signin.description": "You don't have an account yet?",
|
||||||
|
"signin.button.signup": "Sign up",
|
||||||
|
"signin.input.email-or-username": "Email or username",
|
||||||
|
"signin.input.email-or-username.placeholder": "Your email or username",
|
||||||
|
"signin.input.password": "Password",
|
||||||
|
"signin.input.password.placeholder": "Your password",
|
||||||
|
"signin.button.submit": "Sign in",
|
||||||
|
"signIn.notify.totp-required.title": "Two-factor authentication required",
|
||||||
|
"signIn.notify.totp-required.description":
|
||||||
|
"Please enter your two-factor authentication code",
|
||||||
|
|
||||||
|
// END /auth/signin
|
||||||
|
|
||||||
|
// /auth/signup
|
||||||
|
"signup.title": "Create an account",
|
||||||
|
"signup.description": "Already have an account?",
|
||||||
|
"signup.button.signin": "Sign in",
|
||||||
|
"signup.input.username": "Username",
|
||||||
|
"signup.input.username.placeholder": "Your username",
|
||||||
|
"signup.input.email": "Email",
|
||||||
|
"signup.input.email.placeholder": "Your email",
|
||||||
|
"signup.button.submit": "Let's get started",
|
||||||
|
|
||||||
|
// END /auth/signup
|
||||||
|
|
||||||
|
// /auth/reset-password
|
||||||
|
"resetPassword.title": "Forgot your password?",
|
||||||
|
"resetPassword.description": "Enter your email to reset your password.",
|
||||||
|
"resetPassword.notify.success":
|
||||||
|
"An email has been sent with a link to reset your password.",
|
||||||
|
"resetPassword.button.back": "Back to sign in page",
|
||||||
|
"resetPassword.text.resetPassword": "Reset password",
|
||||||
|
"resetPassword.text.enterNewPassword": "Enter your new password",
|
||||||
|
"resetPassword.input.password": "New password",
|
||||||
|
"resetPassword.notify.passwordReset":
|
||||||
|
"Your password has been reset successfully.",
|
||||||
|
|
||||||
|
// /account
|
||||||
|
"account.title": "My account",
|
||||||
|
|
||||||
|
"account.card.info.title": "Account info",
|
||||||
|
"account.card.info.username": "Username",
|
||||||
|
"account.card.info.email": "Email",
|
||||||
|
"account.notify.info.success": "Account updated successfully",
|
||||||
|
|
||||||
|
"account.card.password.title": "Password",
|
||||||
|
"account.card.password.old": "Old password",
|
||||||
|
"account.card.password.new": "New password",
|
||||||
|
"account.notify.password.success": "Password changed successfully",
|
||||||
|
|
||||||
|
"account.card.security.title": "Security",
|
||||||
|
"account.card.security.totp.enable.description":
|
||||||
|
"Enter your current password to start enabling TOTP",
|
||||||
|
"account.card.security.totp.disable.description":
|
||||||
|
"Enter your current password to start enabling TOTP",
|
||||||
|
"account.card.security.totp.button.start": "Start",
|
||||||
|
"account.modal.totp.title": "Enable TOTP",
|
||||||
|
"account.modal.totp.step1": "Step 1: Add your authenticator",
|
||||||
|
"account.modal.totp.step2": "Step 2: Validate your code",
|
||||||
|
"account.modal.totp.enterManually": "Enter manually",
|
||||||
|
"account.modal.totp.code": "Code",
|
||||||
|
"account.modal.totp.clickToCopy": "Click to copy",
|
||||||
|
"account.modal.totp.verify": "Verify",
|
||||||
|
"account.notify.totp.disable": "TOTP disabled successfully",
|
||||||
|
"account.notify.totp.enable": "TOTP enabled successfully",
|
||||||
|
|
||||||
|
"account.card.language.title": "Language",
|
||||||
|
"account.card.color.title": "Color scheme",
|
||||||
|
|
||||||
|
// ThemeSwitcher.tsx
|
||||||
|
"account.theme.dark": "Dark",
|
||||||
|
"account.theme.light": "Light",
|
||||||
|
"account.theme.system": "System",
|
||||||
|
|
||||||
|
"account.button.delete": "Delete Account",
|
||||||
|
"account.modal.delete.title": "Delete Account",
|
||||||
|
"account.modal.delete.description":
|
||||||
|
"Do you really want to delete your account including all your active shares?",
|
||||||
|
// END /account
|
||||||
|
|
||||||
|
// /account/shares
|
||||||
|
"account.shares.title": "My shares",
|
||||||
|
"account.shares.title.empty": "It's empty here 👀",
|
||||||
|
"account.shares.description.empty": "You don't have any shares.",
|
||||||
|
"account.shares.button.create": "Create one",
|
||||||
|
|
||||||
|
"account.shares.info.title": "Share informatons",
|
||||||
|
"account.shares.table.id": "ID",
|
||||||
|
"account.shares.table.name": "Name",
|
||||||
|
"account.shares.table.description": "Description",
|
||||||
|
"account.shares.table.visitors": "Visitors",
|
||||||
|
"account.shares.table.expiresAt": "Expires at",
|
||||||
|
"account.shares.table.createdAt": "Created at",
|
||||||
|
"account.shares.table.size": "Size",
|
||||||
|
|
||||||
|
"account.shares.modal.share-link": "Share link",
|
||||||
|
|
||||||
|
"account.shares.modal.delete.title": "Delete {share}",
|
||||||
|
"account.shares.modal.delete.description":
|
||||||
|
"Do you really want to delete this share?",
|
||||||
|
|
||||||
|
// END /account/shares
|
||||||
|
|
||||||
|
// /account/reverseShares
|
||||||
|
"account.reverseShares.title": "Reverse shares",
|
||||||
|
"account.reverseShares.description":
|
||||||
|
"A reverse share allows you to generate a unique URL that allows external users to create a share.",
|
||||||
|
|
||||||
|
"account.reverseShares.title.empty": "It's empty here 👀",
|
||||||
|
"account.reverseShares.description.empty":
|
||||||
|
"You don't have any reverse shares.",
|
||||||
|
|
||||||
|
// showCreateReverseShareModal.tsx
|
||||||
|
"account.reverseShares.modal.expiration.label": "Expiration",
|
||||||
|
"account.reverseShares.modal.expiration.minute-singular": "Minute",
|
||||||
|
"account.reverseShares.modal.expiration.minute-plural": "Minutes",
|
||||||
|
"account.reverseShares.modal.expiration.hour-singular": "Hour",
|
||||||
|
"account.reverseShares.modal.expiration.hour-plural": "Hours",
|
||||||
|
"account.reverseShares.modal.expiration.day-singular": "Day",
|
||||||
|
"account.reverseShares.modal.expiration.day-plural": "Days",
|
||||||
|
"account.reverseShares.modal.expiration.week-singular": "Week",
|
||||||
|
"account.reverseShares.modal.expiration.week-plural": "Weeks",
|
||||||
|
"account.reverseShares.modal.expiration.month-singular": "Month",
|
||||||
|
"account.reverseShares.modal.expiration.month-plural": "Months",
|
||||||
|
"account.reverseShares.modal.expiration.year-singular": "Year",
|
||||||
|
"account.reverseShares.modal.expiration.year-plural": "Years",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.max-size.label": "Max share size",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.send-email": "Send email notification",
|
||||||
|
"account.reverseShares.modal.send-email.description":
|
||||||
|
"Send an email notification when a share is created with this reverse share link.",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.max-use.label": "Max uses",
|
||||||
|
"account.reverseShares.modal.max-use.description":
|
||||||
|
"The maximum amount of times this URL can be used to create a share.",
|
||||||
|
"account.reverseShare.never-expires": "This reverse share will never expire.",
|
||||||
|
"account.reverseShare.expires-on":
|
||||||
|
"This reverse share will expire on {expiration}.",
|
||||||
|
|
||||||
|
"account.reverseShares.table.no-shares": "No shares created yet",
|
||||||
|
"account.reverseShares.table.count.singular": "share",
|
||||||
|
"account.reverseShares.table.count.plural": "shares",
|
||||||
|
"account.reverseShares.table.shares": "Shares",
|
||||||
|
"account.reverseShares.table.remaining": "Remaining uses",
|
||||||
|
"account.reverseShares.table.max-size": "Max share size",
|
||||||
|
"account.reverseShares.table.expires": "Expires at",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.reverse-share-link": "Reverse share link",
|
||||||
|
|
||||||
|
"account.reverseShares.modal.delete.title":
|
||||||
|
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
|
||||||
|
"account.reverseShares.modal.delete.description":
|
||||||
|
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
|
||||||
|
|
||||||
|
// END /account/reverseShares
|
||||||
|
|
||||||
|
// /admin
|
||||||
|
"admin.title": "Administration",
|
||||||
|
"admin.button.users": "User management",
|
||||||
|
"admin.button.config": "Configuration",
|
||||||
|
"admin.version": "Version",
|
||||||
|
// END /admin
|
||||||
|
|
||||||
|
// /admin/users
|
||||||
|
"admin.users.title": "User management",
|
||||||
|
"admin.users.table.username": "Username",
|
||||||
|
"admin.users.table.email": "Email",
|
||||||
|
"admin.users.table.admin": "Admin",
|
||||||
|
|
||||||
|
"admin.users.edit.update.title": "Update user {username}",
|
||||||
|
"admin.users.edit.update.admin-privileges": "Admin privileges",
|
||||||
|
"admin.users.edit.update.change-password.title": "Change password",
|
||||||
|
"admin.users.edit.update.change-password.field": "New password",
|
||||||
|
"admin.users.edit.update.change-password.button": "Save new password",
|
||||||
|
"admin.users.edit.update.notify.password.success":
|
||||||
|
"Password changed successfully",
|
||||||
|
|
||||||
|
"admin.users.edit.delete.title": "Delete user {username}",
|
||||||
|
"admin.users.edit.delete.description":
|
||||||
|
"Do you really want to delete this user and all his shares?",
|
||||||
|
|
||||||
|
// showCreateUserModal.tsx
|
||||||
|
"admin.users.modal.create.title": "Create user",
|
||||||
|
"admin.users.modal.create.username": "Username",
|
||||||
|
"admin.users.modal.create.email": "Email",
|
||||||
|
"admin.users.modal.create.password": "Password",
|
||||||
|
"admin.users.modal.create.manual-password": "Set password manually",
|
||||||
|
"admin.users.modal.create.manual-password.description":
|
||||||
|
"If not checked, the user will receive an email with a link to set their password.",
|
||||||
|
"admin.users.modal.create.admin": "Admin privileges",
|
||||||
|
"admin.users.modal.create.admin.description":
|
||||||
|
"If checked, the user will be able to access the admin panel.",
|
||||||
|
|
||||||
|
// END /admin/users
|
||||||
|
|
||||||
|
// /upload
|
||||||
|
"upload.title": "Upload",
|
||||||
|
|
||||||
|
"upload.notify.generic-error":
|
||||||
|
"An error occurred while finishing your share.",
|
||||||
|
"upload.notify.count-failed": "{count} filed failed to upload. Trying again.",
|
||||||
|
|
||||||
|
// Dropzone.tsx
|
||||||
|
"upload.dropzone.title": "Upload files",
|
||||||
|
"upload.dropzone.description":
|
||||||
|
"Drag'n'drop files here to start your share. We can accept only files that are less than {maxSize} in total.",
|
||||||
|
"upload.dropzone.notify.file-too-big":
|
||||||
|
"Your files exceed the maximum share size of {maxSize}.",
|
||||||
|
|
||||||
|
// FileList.tsx
|
||||||
|
"upload.filelist.name": "Name",
|
||||||
|
"upload.filelist.size": "Size",
|
||||||
|
|
||||||
|
// showCreateUploadModal.tsx
|
||||||
|
"upload.modal.title": "Create Share",
|
||||||
|
"upload.modal.link.error.invalid":
|
||||||
|
"Can only contain letters, numbers, underscores, and hyphens",
|
||||||
|
"upload.modal.link.error.taken": "This link is already in use",
|
||||||
|
"upload.modal.not-signed-in": "You're not signed in",
|
||||||
|
"upload.modal.not-signed-in-description":
|
||||||
|
"You will be unable to delete your share manually and view the visitor count.",
|
||||||
|
|
||||||
|
"upload.modal.expires.never": "never",
|
||||||
|
"upload.modal.expires.never-long": "Never Expires",
|
||||||
|
|
||||||
|
"upload.modal.link.label": "Link",
|
||||||
|
"upload.modal.link.placeholder": "myAwesomeShare",
|
||||||
|
|
||||||
|
"upload.modal.expires.label": "Expiration",
|
||||||
|
"upload.modal.expires.minute-singular": "Minute",
|
||||||
|
"upload.modal.expires.minute-plural": "Minutes",
|
||||||
|
"upload.modal.expires.hour-singular": "Hour",
|
||||||
|
"upload.modal.expires.hour-plural": "Hours",
|
||||||
|
"upload.modal.expires.day-singular": "Day",
|
||||||
|
"upload.modal.expires.day-plural": "Days",
|
||||||
|
"upload.modal.expires.week-singular": "Week",
|
||||||
|
"upload.modal.expires.week-plural": "Weeks",
|
||||||
|
"upload.modal.expires.month-singular": "Month",
|
||||||
|
"upload.modal.expires.month-plural": "Months",
|
||||||
|
"upload.modal.expires.year-singular": "Year",
|
||||||
|
"upload.modal.expires.year-plural": "Years",
|
||||||
|
|
||||||
|
"upload.modal.accordion.description.title": "Description",
|
||||||
|
"upload.modal.accordion.description.placeholder":
|
||||||
|
"Note for the recipients of this share",
|
||||||
|
|
||||||
|
"upload.modal.accordion.email.title": "Email recipients",
|
||||||
|
"upload.modal.accordion.email.placeholder": "Enter email recipients",
|
||||||
|
"upload.modal.accordion.email.invalid-email": "Invalid email address",
|
||||||
|
|
||||||
|
"upload.modal.accordion.security.title": "Security options",
|
||||||
|
"upload.modal.accordion.security.password.label": "Password protection",
|
||||||
|
"upload.modal.accordion.security.password.placeholder": "No password",
|
||||||
|
"upload.modal.accordion.security.max-views.label": "Maximum views",
|
||||||
|
"upload.modal.accordion.security.max-views.placeholder": "No limit",
|
||||||
|
|
||||||
|
// showCompletedUploadModal.tsx
|
||||||
|
"upload.modal.completed.never-expires": "This share will never expire.",
|
||||||
|
"upload.modal.completed.expires-on":
|
||||||
|
"This share will expire on {expiration}.",
|
||||||
|
"upload.modal.completed.share-ready": "Share ready",
|
||||||
|
|
||||||
|
// END /upload
|
||||||
|
|
||||||
|
// /share/[id]
|
||||||
|
"share.title": "Share {shareId}",
|
||||||
|
"share.description": "Look what I've shared with you!",
|
||||||
|
"share.error.visitor-limit-exceeded.title": "Visitor limit exceeded",
|
||||||
|
"share.error.visitor-limit-exceeded.description":
|
||||||
|
"The visitor limit from this share has been exceeded.",
|
||||||
|
"share.error.removed.title": "Share removed",
|
||||||
|
"share.error.not-found.title": "Share not found",
|
||||||
|
"share.error.not-found.description":
|
||||||
|
"The share you're looking for doesn't exist.",
|
||||||
|
|
||||||
|
"share.modal.password.title": "Password required",
|
||||||
|
"share.modal.password.description":
|
||||||
|
"To access this share please enter the password for the share.",
|
||||||
|
"share.modal.password": "Password",
|
||||||
|
"share.modal.error.invalid-password": "Invalid password",
|
||||||
|
|
||||||
|
"share.button.download-all": "Download all",
|
||||||
|
"share.notify.download-all-preparing":
|
||||||
|
"The share is preparing. Try again in a few minutes.",
|
||||||
|
|
||||||
|
"share.modal.file-link": "File link",
|
||||||
|
"share.table.name": "Name",
|
||||||
|
"share.table.size": "Size",
|
||||||
|
|
||||||
|
"share.modal.file-preview.error.not-supported.title": "Preview not supported",
|
||||||
|
"share.modal.file-preview.error.not-supported.description":
|
||||||
|
"A preview for thise file type is unsupported. Please download the file to view it.",
|
||||||
|
|
||||||
|
// END /share/[id]
|
||||||
|
|
||||||
|
// /admin/config
|
||||||
|
"admin.config.title": "Configuration",
|
||||||
|
"admin.config.category.general": "General",
|
||||||
|
"admin.config.category.share": "Share",
|
||||||
|
"admin.config.category.email": "Email",
|
||||||
|
"admin.config.category.smtp": "SMTP",
|
||||||
|
|
||||||
|
"admin.config.general.app-name": "App name",
|
||||||
|
"admin.config.general.app-name.description": "Name of the application",
|
||||||
|
"admin.config.general.app-url": "App URL",
|
||||||
|
"admin.config.general.app-url.description":
|
||||||
|
"On which URL Pingvin Share is available",
|
||||||
|
"admin.config.general.show-home-page": "Show home page",
|
||||||
|
"admin.config.general.show-home-page.description":
|
||||||
|
"Whether to show the home page",
|
||||||
|
"admin.config.general.logo": "Logo",
|
||||||
|
"admin.config.general.logo.description":
|
||||||
|
"Change your logo by uploading a new image. The image must be a PNG and should have the format 1:1.",
|
||||||
|
"admin.config.general.logo.placeholder": "Pick image",
|
||||||
|
|
||||||
|
"admin.config.email.enable-share-email-recipients":
|
||||||
|
"Enable share email recipients",
|
||||||
|
"admin.config.email.enable-share-email-recipients.description":
|
||||||
|
"Whether to allow emails to share recipients. Only enable this if you have enabled SMTP.",
|
||||||
|
"admin.config.email.share-recipients-subject": "Share recipients subject",
|
||||||
|
"admin.config.email.share-recipients-subject.description":
|
||||||
|
"Subject of the email which gets sent to the share recipients.",
|
||||||
|
"admin.config.email.share-recipients-message": "Share recipients message",
|
||||||
|
"admin.config.email.share-recipients-message.description":
|
||||||
|
"Message which gets sent to the share recipients. Available variables:\n {creator} - The username of the creator of the share\n {shareUrl} - The URL of the share\n {desc} - The description of the share\n {expires} - The expiration date of the share\n The variables will be replaced with the actual values.",
|
||||||
|
"admin.config.email.reverse-share-subject": "Reverse share subject",
|
||||||
|
"admin.config.email.reverse-share-subject.description":
|
||||||
|
"Subject of the email which gets sent when someone created a share with your reverse share link.",
|
||||||
|
"admin.config.email.reverse-share-message": "Reverse share message",
|
||||||
|
"admin.config.email.reverse-share-message.description":
|
||||||
|
"Message which gets sent when someone created a share with your reverse share link. {shareUrl} will be replaced with the creator's name and the share URL.",
|
||||||
|
"admin.config.email.reset-password-subject": "Reset password subject",
|
||||||
|
"admin.config.email.reset-password-subject.description":
|
||||||
|
"Subject of the email which gets sent when a user requests a password reset.",
|
||||||
|
"admin.config.email.reset-password-message": "Reset password message",
|
||||||
|
"admin.config.email.reset-password-message.description":
|
||||||
|
"Message which gets sent when a user requests a password reset. {url} will be replaced with the reset password URL.",
|
||||||
|
"admin.config.email.invite-subject": "Invite subject",
|
||||||
|
"admin.config.email.invite-subject.description":
|
||||||
|
"Subject of the email which gets sent when an admin invites a user.",
|
||||||
|
"admin.config.email.invite-message": "Invite message",
|
||||||
|
"admin.config.email.invite-message.description":
|
||||||
|
"Message which gets sent when an admin invites a user. {url} will be replaced with the invite URL and {password} with the password.",
|
||||||
|
"admin.config.share.allow-registration": "Allow registration",
|
||||||
|
"admin.config.share.allow-registration.description":
|
||||||
|
"Whether registration is allowed",
|
||||||
|
"admin.config.share.allow-unauthenticated-shares":
|
||||||
|
"Allow unauthenticated shares",
|
||||||
|
"admin.config.share.allow-unauthenticated-shares.description":
|
||||||
|
"Whether unauthorized users can create shares",
|
||||||
|
"admin.config.share.max-size": "Max size",
|
||||||
|
"admin.config.share.max-size.description": "Maximum share size in bytes",
|
||||||
|
|
||||||
|
"admin.config.smtp.enabled": "Enabled",
|
||||||
|
"admin.config.smtp.enabled.description":
|
||||||
|
"Whether SMTP is enabled. Only set this to true if you entered the host, port, email, user and password of your SMTP server.",
|
||||||
|
"admin.config.smtp.host": "Host",
|
||||||
|
"admin.config.smtp.host.description": "Host of the SMTP server",
|
||||||
|
"admin.config.smtp.port": "Port",
|
||||||
|
"admin.config.smtp.port.description": "Port of the SMTP server",
|
||||||
|
"admin.config.smtp.email": "Email",
|
||||||
|
"admin.config.smtp.email.description":
|
||||||
|
"Email address which the emails get sent from",
|
||||||
|
"admin.config.smtp.username": "Username",
|
||||||
|
"admin.config.smtp.username.description": "Username of the SMTP server",
|
||||||
|
"admin.config.smtp.password": "Password",
|
||||||
|
"admin.config.smtp.password.description": "Password of the SMTP server",
|
||||||
|
"admin.config.smtp.button.test": "Send test email",
|
||||||
|
|
||||||
|
// 404
|
||||||
|
"404.title": "404",
|
||||||
|
"404.description": "Oops this page doesn't exist.",
|
||||||
|
"404.button.home": "Gring me back home",
|
||||||
|
|
||||||
|
// Common translations
|
||||||
|
"common.button.save": "Save",
|
||||||
|
"common.button.create": "Create",
|
||||||
|
"common.button.submit": "Submit",
|
||||||
|
"common.button.delete": "Delete",
|
||||||
|
"common.button.cancel": "Cancel",
|
||||||
|
"common.button.confirm": "Confirm",
|
||||||
|
"common.button.disable": "Disable",
|
||||||
|
"common.button.share": "Share",
|
||||||
|
"common.button.generate": "Generate",
|
||||||
|
"common.button.done": "Done",
|
||||||
|
"common.text.link": "Link",
|
||||||
|
"common.text.or": "or",
|
||||||
|
"common.button.go-back": "Go back",
|
||||||
|
"common.notify.copied": "Your link was copied to the clipboard",
|
||||||
|
|
||||||
|
"common.error": "Error",
|
||||||
|
"common.error.unknown": "An unknown error occurred",
|
||||||
|
"common.error.invalid-email": "Invalid email address",
|
||||||
|
"common.error.too-short": "Must be at least {length} characters",
|
||||||
|
"common.error.too-long": "Must be at most {length} characters",
|
||||||
|
"common.error.exact-length": "Must be exactly {length} characters",
|
||||||
|
"common.error.invalid-number": "Must be a number",
|
||||||
|
"common.error.field-required": "This field is required",
|
||||||
|
};
|
|
@ -8,6 +8,7 @@ import {
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Meta from "../components/Meta";
|
import Meta from "../components/Meta";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
const useStyles = createStyles((theme) => ({
|
const useStyles = createStyles((theme) => ({
|
||||||
root: {
|
root: {
|
||||||
|
@ -42,9 +43,11 @@ const ErrorNotFound = () => {
|
||||||
<>
|
<>
|
||||||
<Meta title="Not found" />
|
<Meta title="Not found" />
|
||||||
<Container className={classes.root}>
|
<Container className={classes.root}>
|
||||||
<div className={classes.label}>404</div>
|
<div className={classes.label}>
|
||||||
|
<FormattedMessage id="404.title" />
|
||||||
|
</div>
|
||||||
<Title align="center" order={3}>
|
<Title align="center" order={3}>
|
||||||
Oops this page doesn't exist.
|
<FormattedMessage id="404.description" />
|
||||||
</Title>
|
</Title>
|
||||||
<Text
|
<Text
|
||||||
color="dimmed"
|
color="dimmed"
|
||||||
|
@ -53,7 +56,7 @@ const ErrorNotFound = () => {
|
||||||
></Text>
|
></Text>
|
||||||
<Group position="center">
|
<Group position="center">
|
||||||
<Button component={Link} href="/" variant="light">
|
<Button component={Link} href="/" variant="light">
|
||||||
Bring me back
|
<FormattedMessage id="404.button.home" />
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
|
@ -13,11 +13,12 @@ import { GetServerSidePropsContext } from "next";
|
||||||
import type { AppProps } from "next/app";
|
import type { AppProps } from "next/app";
|
||||||
import getConfig from "next/config";
|
import getConfig from "next/config";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { IntlProvider } from "react-intl";
|
||||||
import Header from "../components/header/Header";
|
import Header from "../components/header/Header";
|
||||||
import { ConfigContext } from "../hooks/config.hook";
|
import { ConfigContext } from "../hooks/config.hook";
|
||||||
import usePreferences from "../hooks/usePreferences";
|
|
||||||
import { UserContext } from "../hooks/user.hook";
|
import { UserContext } from "../hooks/user.hook";
|
||||||
|
import { LOCALES } from "../i18n/locales";
|
||||||
import authService from "../services/auth.service";
|
import authService from "../services/auth.service";
|
||||||
import configService from "../services/config.service";
|
import configService from "../services/config.service";
|
||||||
import userService from "../services/user.service";
|
import userService from "../services/user.service";
|
||||||
|
@ -25,6 +26,8 @@ import GlobalStyle from "../styles/global.style";
|
||||||
import globalStyle from "../styles/mantine.style";
|
import globalStyle from "../styles/mantine.style";
|
||||||
import Config from "../types/config.type";
|
import Config from "../types/config.type";
|
||||||
import { CurrentUser } from "../types/user.type";
|
import { CurrentUser } from "../types/user.type";
|
||||||
|
import i18nUtil from "../utils/i18n.util";
|
||||||
|
import userPreferences from "../utils/userPreferences.util";
|
||||||
|
|
||||||
const excludeDefaultLayoutRoutes = ["/admin/config/[category]"];
|
const excludeDefaultLayoutRoutes = ["/admin/config/[category]"];
|
||||||
|
|
||||||
|
@ -33,7 +36,6 @@ function App({ Component, pageProps }: AppProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [colorScheme, setColorScheme] = useState<ColorScheme>(systemTheme);
|
const [colorScheme, setColorScheme] = useState<ColorScheme>(systemTheme);
|
||||||
const preferences = usePreferences();
|
|
||||||
|
|
||||||
const [user, setUser] = useState<CurrentUser | null>(pageProps.user);
|
const [user, setUser] = useState<CurrentUser | null>(pageProps.user);
|
||||||
const [route, setRoute] = useState<string>(pageProps.route);
|
const [route, setRoute] = useState<string>(pageProps.route);
|
||||||
|
@ -50,11 +52,20 @@ function App({ Component, pageProps }: AppProps) {
|
||||||
setInterval(async () => await authService.refreshAccessToken(), 30 * 1000);
|
setInterval(async () => await authService.refreshAccessToken(), 30 * 1000);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!pageProps.language) return;
|
||||||
|
const cookieLanguage = getCookie("language");
|
||||||
|
if (pageProps.language != cookieLanguage) {
|
||||||
|
i18nUtil.setLanguageCookie(pageProps.language);
|
||||||
|
if (cookieLanguage) location.reload();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const colorScheme =
|
const colorScheme =
|
||||||
preferences.get("colorScheme") == "system"
|
userPreferences.get("colorScheme") == "system"
|
||||||
? systemTheme
|
? systemTheme
|
||||||
: preferences.get("colorScheme");
|
: userPreferences.get("colorScheme");
|
||||||
|
|
||||||
toggleColorScheme(colorScheme);
|
toggleColorScheme(colorScheme);
|
||||||
}, [systemTheme]);
|
}, [systemTheme]);
|
||||||
|
@ -66,52 +77,60 @@ function App({ Component, pageProps }: AppProps) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const language = useRef(pageProps.language);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MantineProvider
|
<IntlProvider
|
||||||
withGlobalStyles
|
messages={i18nUtil.getLocaleByCode(language.current)?.messages}
|
||||||
withNormalizeCSS
|
locale={language.current}
|
||||||
theme={{ colorScheme, ...globalStyle }}
|
defaultLocale={LOCALES.ENGLISH.code}
|
||||||
>
|
>
|
||||||
<ColorSchemeProvider
|
<MantineProvider
|
||||||
colorScheme={colorScheme}
|
withGlobalStyles
|
||||||
toggleColorScheme={toggleColorScheme}
|
withNormalizeCSS
|
||||||
|
theme={{ colorScheme, ...globalStyle }}
|
||||||
>
|
>
|
||||||
<GlobalStyle />
|
<ColorSchemeProvider
|
||||||
<Notifications />
|
colorScheme={colorScheme}
|
||||||
<ModalsProvider>
|
toggleColorScheme={toggleColorScheme}
|
||||||
<ConfigContext.Provider
|
>
|
||||||
value={{
|
<GlobalStyle />
|
||||||
configVariables,
|
<Notifications />
|
||||||
refresh: async () => {
|
<ModalsProvider>
|
||||||
setConfigVariables(await configService.list());
|
<ConfigContext.Provider
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<UserContext.Provider
|
|
||||||
value={{
|
value={{
|
||||||
user,
|
configVariables,
|
||||||
refreshUser: async () => {
|
refresh: async () => {
|
||||||
const user = await userService.getCurrentUser();
|
setConfigVariables(await configService.list());
|
||||||
setUser(user);
|
|
||||||
return user;
|
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{excludeDefaultLayoutRoutes.includes(route) ? (
|
<UserContext.Provider
|
||||||
<Component {...pageProps} />
|
value={{
|
||||||
) : (
|
user,
|
||||||
<>
|
refreshUser: async () => {
|
||||||
<Header />
|
const user = await userService.getCurrentUser();
|
||||||
<Container>
|
setUser(user);
|
||||||
<Component {...pageProps} />
|
return user;
|
||||||
</Container>
|
},
|
||||||
</>
|
}}
|
||||||
)}
|
>
|
||||||
</UserContext.Provider>
|
{excludeDefaultLayoutRoutes.includes(route) ? (
|
||||||
</ConfigContext.Provider>
|
<Component {...pageProps} />
|
||||||
</ModalsProvider>
|
) : (
|
||||||
</ColorSchemeProvider>
|
<>
|
||||||
</MantineProvider>
|
<Header />
|
||||||
|
<Container>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</UserContext.Provider>
|
||||||
|
</ConfigContext.Provider>
|
||||||
|
</ModalsProvider>
|
||||||
|
</ColorSchemeProvider>
|
||||||
|
</MantineProvider>
|
||||||
|
</IntlProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,11 +144,13 @@ App.getInitialProps = async ({ ctx }: { ctx: GetServerSidePropsContext }) => {
|
||||||
configVariables?: Config[];
|
configVariables?: Config[];
|
||||||
route?: string;
|
route?: string;
|
||||||
colorScheme: ColorScheme;
|
colorScheme: ColorScheme;
|
||||||
|
language?: string;
|
||||||
} = {
|
} = {
|
||||||
route: ctx.resolvedUrl,
|
route: ctx.resolvedUrl,
|
||||||
colorScheme:
|
colorScheme:
|
||||||
(getCookie("mantine-color-scheme", ctx) as ColorScheme) ?? "light",
|
(getCookie("mantine-color-scheme", ctx) as ColorScheme) ?? "light",
|
||||||
};
|
};
|
||||||
|
|
||||||
if (ctx.req) {
|
if (ctx.req) {
|
||||||
const cookieHeader = ctx.req.headers.cookie;
|
const cookieHeader = ctx.req.headers.cookie;
|
||||||
|
|
||||||
|
@ -142,8 +163,13 @@ App.getInitialProps = async ({ ctx }: { ctx: GetServerSidePropsContext }) => {
|
||||||
pageProps.configVariables = (await axios(`${apiURL}/api/configs`)).data;
|
pageProps.configVariables = (await axios(`${apiURL}/api/configs`)).data;
|
||||||
|
|
||||||
pageProps.route = ctx.req.url;
|
pageProps.route = ctx.req.url;
|
||||||
}
|
|
||||||
|
|
||||||
|
const requestLanguage = i18nUtil.getLanguageFromAcceptHeader(
|
||||||
|
ctx.req.headers["accept-language"]
|
||||||
|
);
|
||||||
|
|
||||||
|
pageProps.language = ctx.req.cookies["language"] ?? requestLanguage;
|
||||||
|
}
|
||||||
return { pageProps };
|
return { pageProps };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -14,18 +14,22 @@ import {
|
||||||
import { useForm, yupResolver } from "@mantine/form";
|
import { useForm, yupResolver } from "@mantine/form";
|
||||||
import { useModals } from "@mantine/modals";
|
import { useModals } from "@mantine/modals";
|
||||||
import { Tb2Fa } from "react-icons/tb";
|
import { Tb2Fa } from "react-icons/tb";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
import showEnableTotpModal from "../../components/account/showEnableTotpModal";
|
|
||||||
import ThemeSwitcher from "../../components/account/ThemeSwitcher";
|
|
||||||
import Meta from "../../components/Meta";
|
import Meta from "../../components/Meta";
|
||||||
|
import ThemeSwitcher from "../../components/account/ThemeSwitcher";
|
||||||
|
import showEnableTotpModal from "../../components/account/showEnableTotpModal";
|
||||||
|
import useTranslate from "../../hooks/useTranslate.hook";
|
||||||
import useUser from "../../hooks/user.hook";
|
import useUser from "../../hooks/user.hook";
|
||||||
import authService from "../../services/auth.service";
|
import authService from "../../services/auth.service";
|
||||||
import userService from "../../services/user.service";
|
import userService from "../../services/user.service";
|
||||||
import toast from "../../utils/toast.util";
|
import toast from "../../utils/toast.util";
|
||||||
|
import LanguagePicker from "../../components/account/LanguagePicker";
|
||||||
|
|
||||||
const Account = () => {
|
const Account = () => {
|
||||||
const { user, refreshUser } = useUser();
|
const { user, refreshUser } = useUser();
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
|
const t = useTranslate();
|
||||||
|
|
||||||
const accountForm = useForm({
|
const accountForm = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
|
@ -34,8 +38,10 @@ const Account = () => {
|
||||||
},
|
},
|
||||||
validate: yupResolver(
|
validate: yupResolver(
|
||||||
yup.object().shape({
|
yup.object().shape({
|
||||||
email: yup.string().email(),
|
email: yup.string().email(t("common.error.invalid-email")),
|
||||||
username: yup.string().min(3),
|
username: yup
|
||||||
|
.string()
|
||||||
|
.min(3, t("common.error.too-short", { length: 3 })),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
@ -47,8 +53,14 @@ const Account = () => {
|
||||||
},
|
},
|
||||||
validate: yupResolver(
|
validate: yupResolver(
|
||||||
yup.object().shape({
|
yup.object().shape({
|
||||||
oldPassword: yup.string().min(8),
|
oldPassword: yup
|
||||||
password: yup.string().min(8),
|
.string()
|
||||||
|
.min(8, t("common.error.too-short", { length: 8 }))
|
||||||
|
.required(t("common.error.field-required")),
|
||||||
|
password: yup
|
||||||
|
.string()
|
||||||
|
.min(8, t("common.error.too-short", { length: 8 }))
|
||||||
|
.required(t("common.error.field-required")),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
@ -59,7 +71,10 @@ const Account = () => {
|
||||||
},
|
},
|
||||||
validate: yupResolver(
|
validate: yupResolver(
|
||||||
yup.object().shape({
|
yup.object().shape({
|
||||||
password: yup.string().min(8),
|
password: yup
|
||||||
|
.string()
|
||||||
|
.min(8, t("common.error.too-short", { length: 8 }))
|
||||||
|
.required(t("common.error.field-required")),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
@ -74,23 +89,23 @@ const Account = () => {
|
||||||
password: yup.string().min(8),
|
password: yup.string().min(8),
|
||||||
code: yup
|
code: yup
|
||||||
.string()
|
.string()
|
||||||
.min(6)
|
.min(6, t("common.error.exact-length", { length: 6 }))
|
||||||
.max(6)
|
.max(6, t("common.error.exact-length", { length: 6 }))
|
||||||
.matches(/^[0-9]+$/, { message: "Code must be a number" }),
|
.matches(/^[0-9]+$/, { message: t("common.error.invalid-number") }),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title="My account" />
|
<Meta title={t("account.title")} />
|
||||||
<Container size="sm">
|
<Container size="sm">
|
||||||
<Title order={3} mb="xs">
|
<Title order={3} mb="xs">
|
||||||
My account
|
<FormattedMessage id="account.title" />
|
||||||
</Title>
|
</Title>
|
||||||
<Paper withBorder p="xl">
|
<Paper withBorder p="xl">
|
||||||
<Title order={5} mb="xs">
|
<Title order={5} mb="xs">
|
||||||
Account Info
|
<FormattedMessage id="account.card.info.title" />
|
||||||
</Title>
|
</Title>
|
||||||
<form
|
<form
|
||||||
onSubmit={accountForm.onSubmit((values) =>
|
onSubmit={accountForm.onSubmit((values) =>
|
||||||
|
@ -99,35 +114,37 @@ const Account = () => {
|
||||||
username: values.username,
|
username: values.username,
|
||||||
email: values.email,
|
email: values.email,
|
||||||
})
|
})
|
||||||
.then(() => toast.success("User updated successfully"))
|
.then(() => toast.success(t("account.notify.info.success")))
|
||||||
.catch(toast.axiosError)
|
.catch(toast.axiosError)
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Stack>
|
<Stack>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Username"
|
label={t("account.card.info.username")}
|
||||||
{...accountForm.getInputProps("username")}
|
{...accountForm.getInputProps("username")}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Email"
|
label={t("account.card.info.email")}
|
||||||
{...accountForm.getInputProps("email")}
|
{...accountForm.getInputProps("email")}
|
||||||
/>
|
/>
|
||||||
<Group position="right">
|
<Group position="right">
|
||||||
<Button type="submit">Save</Button>
|
<Button type="submit">
|
||||||
|
<FormattedMessage id="common.button.save" />
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
</Paper>
|
</Paper>
|
||||||
<Paper withBorder p="xl" mt="lg">
|
<Paper withBorder p="xl" mt="lg">
|
||||||
<Title order={5} mb="xs">
|
<Title order={5} mb="xs">
|
||||||
Password
|
<FormattedMessage id="account.card.password.title" />
|
||||||
</Title>
|
</Title>
|
||||||
<form
|
<form
|
||||||
onSubmit={passwordForm.onSubmit((values) =>
|
onSubmit={passwordForm.onSubmit((values) =>
|
||||||
authService
|
authService
|
||||||
.updatePassword(values.oldPassword, values.password)
|
.updatePassword(values.oldPassword, values.password)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Password updated successfully");
|
toast.success(t("account.notify.password.success"));
|
||||||
passwordForm.reset();
|
passwordForm.reset();
|
||||||
})
|
})
|
||||||
.catch(toast.axiosError)
|
.catch(toast.axiosError)
|
||||||
|
@ -135,15 +152,17 @@ const Account = () => {
|
||||||
>
|
>
|
||||||
<Stack>
|
<Stack>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
label="Old password"
|
label={t("account.card.password.old")}
|
||||||
{...passwordForm.getInputProps("oldPassword")}
|
{...passwordForm.getInputProps("oldPassword")}
|
||||||
/>
|
/>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
label="New password"
|
label={t("account.card.password.new")}
|
||||||
{...passwordForm.getInputProps("password")}
|
{...passwordForm.getInputProps("password")}
|
||||||
/>
|
/>
|
||||||
<Group position="right">
|
<Group position="right">
|
||||||
<Button type="submit">Save</Button>
|
<Button type="submit">
|
||||||
|
<FormattedMessage id="common.button.save" />
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
|
@ -151,7 +170,7 @@ const Account = () => {
|
||||||
|
|
||||||
<Paper withBorder p="xl" mt="lg">
|
<Paper withBorder p="xl" mt="lg">
|
||||||
<Title order={5} mb="xs">
|
<Title order={5} mb="xs">
|
||||||
Security
|
<FormattedMessage id="account.card.security.title" />
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
<Tabs defaultValue="totp">
|
<Tabs defaultValue="totp">
|
||||||
|
@ -169,7 +188,7 @@ const Account = () => {
|
||||||
authService
|
authService
|
||||||
.disableTOTP(values.code, values.password)
|
.disableTOTP(values.code, values.password)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Successfully disabled TOTP");
|
toast.success(t("account.notify.totp.disable"));
|
||||||
values.password = "";
|
values.password = "";
|
||||||
values.code = "";
|
values.code = "";
|
||||||
refreshUser();
|
refreshUser();
|
||||||
|
@ -179,21 +198,23 @@ const Account = () => {
|
||||||
>
|
>
|
||||||
<Stack>
|
<Stack>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
description="Enter your current password to disable TOTP"
|
description={t(
|
||||||
label="Password"
|
"account.card.security.totp.disable.description"
|
||||||
|
)}
|
||||||
|
label={t("account.card.password.title")}
|
||||||
{...disableTotpForm.getInputProps("password")}
|
{...disableTotpForm.getInputProps("password")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
variant="filled"
|
variant="filled"
|
||||||
label="Code"
|
label={t("account.modal.totp.code")}
|
||||||
placeholder="******"
|
placeholder="******"
|
||||||
{...disableTotpForm.getInputProps("code")}
|
{...disableTotpForm.getInputProps("code")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Group position="right">
|
<Group position="right">
|
||||||
<Button color="red" type="submit">
|
<Button color="red" type="submit">
|
||||||
Disable
|
<FormattedMessage id="common.button.disable" />
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -218,12 +239,16 @@ const Account = () => {
|
||||||
>
|
>
|
||||||
<Stack>
|
<Stack>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
label="Password"
|
label={t("account.card.password.title")}
|
||||||
description="Enter your current password to start enabling TOTP"
|
description={t(
|
||||||
|
"account.card.security.totp.enable.description"
|
||||||
|
)}
|
||||||
{...enableTotpForm.getInputProps("password")}
|
{...enableTotpForm.getInputProps("password")}
|
||||||
/>
|
/>
|
||||||
<Group position="right">
|
<Group position="right">
|
||||||
<Button type="submit">Start</Button>
|
<Button type="submit">
|
||||||
|
<FormattedMessage id="account.card.security.totp.button.start" />
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
|
@ -234,7 +259,13 @@ const Account = () => {
|
||||||
</Paper>
|
</Paper>
|
||||||
<Paper withBorder p="xl" mt="lg">
|
<Paper withBorder p="xl" mt="lg">
|
||||||
<Title order={5} mb="xs">
|
<Title order={5} mb="xs">
|
||||||
Color scheme
|
<FormattedMessage id="account.card.language.title" />
|
||||||
|
</Title>
|
||||||
|
<LanguagePicker />
|
||||||
|
</Paper>
|
||||||
|
<Paper withBorder p="xl" mt="lg">
|
||||||
|
<Title order={5} mb="xs">
|
||||||
|
<FormattedMessage id="account.card.color.title" />
|
||||||
</Title>
|
</Title>
|
||||||
<ThemeSwitcher />
|
<ThemeSwitcher />
|
||||||
</Paper>
|
</Paper>
|
||||||
|
@ -245,15 +276,17 @@ const Account = () => {
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
modals.openConfirmModal({
|
modals.openConfirmModal({
|
||||||
title: "Account deletion",
|
title: t("account.modal.delete.title"),
|
||||||
children: (
|
children: (
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
Do you really want to delete your account including all
|
<FormattedMessage id="account.modal.delete.description" />
|
||||||
your active shares?
|
|
||||||
</Text>
|
</Text>
|
||||||
),
|
),
|
||||||
|
|
||||||
labels: { confirm: "Delete", cancel: "Cancel" },
|
labels: {
|
||||||
|
confirm: t("common.button.delete"),
|
||||||
|
cancel: t("common.button.cancel"),
|
||||||
|
},
|
||||||
confirmProps: { color: "red" },
|
confirmProps: { color: "red" },
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
await userService.removeCurrentUser();
|
await userService.removeCurrentUser();
|
||||||
|
@ -262,7 +295,7 @@ const Account = () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Delete Account
|
<FormattedMessage id="account.button.delete" />
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Center>
|
</Center>
|
||||||
|
|
|
@ -17,12 +17,14 @@ import { useModals } from "@mantine/modals";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { TbInfoCircle, TbLink, TbPlus, TbTrash } from "react-icons/tb";
|
import { TbInfoCircle, TbLink, TbPlus, TbTrash } from "react-icons/tb";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import Meta from "../../components/Meta";
|
import Meta from "../../components/Meta";
|
||||||
import showReverseShareLinkModal from "../../components/account/showReverseShareLinkModal";
|
import showReverseShareLinkModal from "../../components/account/showReverseShareLinkModal";
|
||||||
import showShareLinkModal from "../../components/account/showShareLinkModal";
|
import showShareLinkModal from "../../components/account/showShareLinkModal";
|
||||||
import CenterLoader from "../../components/core/CenterLoader";
|
import CenterLoader from "../../components/core/CenterLoader";
|
||||||
import showCreateReverseShareModal from "../../components/share/modals/showCreateReverseShareModal";
|
import showCreateReverseShareModal from "../../components/share/modals/showCreateReverseShareModal";
|
||||||
import useConfig from "../../hooks/config.hook";
|
import useConfig from "../../hooks/config.hook";
|
||||||
|
import useTranslate from "../../hooks/useTranslate.hook";
|
||||||
import shareService from "../../services/share.service";
|
import shareService from "../../services/share.service";
|
||||||
import { MyReverseShare } from "../../types/share.type";
|
import { MyReverseShare } from "../../types/share.type";
|
||||||
import { byteToHumanSizeString } from "../../utils/fileSize.util";
|
import { byteToHumanSizeString } from "../../utils/fileSize.util";
|
||||||
|
@ -31,13 +33,14 @@ import toast from "../../utils/toast.util";
|
||||||
const MyShares = () => {
|
const MyShares = () => {
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
const clipboard = useClipboard();
|
const clipboard = useClipboard();
|
||||||
|
const t = useTranslate();
|
||||||
|
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
|
|
||||||
const [reverseShares, setReverseShares] = useState<MyReverseShare[]>();
|
|
||||||
|
|
||||||
const appUrl = config.get("general.appUrl");
|
const appUrl = config.get("general.appUrl");
|
||||||
|
|
||||||
|
const [reverseShares, setReverseShares] = useState<MyReverseShare[]>();
|
||||||
|
|
||||||
const getReverseShares = () => {
|
const getReverseShares = () => {
|
||||||
shareService
|
shareService
|
||||||
.getMyReverseShares()
|
.getMyReverseShares()
|
||||||
|
@ -51,15 +54,17 @@ const MyShares = () => {
|
||||||
if (!reverseShares) return <CenterLoader />;
|
if (!reverseShares) return <CenterLoader />;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title="My shares" />
|
<Meta title={t("account.reverseShares.title")} />
|
||||||
<Group position="apart" align="baseline" mb={20}>
|
<Group position="apart" align="baseline" mb={20}>
|
||||||
<Group align="center" spacing={3} mb={30}>
|
<Group align="center" spacing={3} mb={30}>
|
||||||
<Title order={3}>My reverse shares</Title>
|
<Title order={3}>
|
||||||
|
<FormattedMessage id="account.reverseShares.title" />
|
||||||
|
</Title>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
position="bottom"
|
position="bottom"
|
||||||
multiline
|
multiline
|
||||||
width={220}
|
width={220}
|
||||||
label="A reverse share allows you to generate a unique URL that allows external users to create a share."
|
label={t("account.reverseShares.description")}
|
||||||
events={{ hover: true, focus: false, touch: true }}
|
events={{ hover: true, focus: false, touch: true }}
|
||||||
>
|
>
|
||||||
<ActionIcon>
|
<ActionIcon>
|
||||||
|
@ -77,14 +82,18 @@ const MyShares = () => {
|
||||||
}
|
}
|
||||||
leftIcon={<TbPlus size={20} />}
|
leftIcon={<TbPlus size={20} />}
|
||||||
>
|
>
|
||||||
Create
|
<FormattedMessage id="common.button.create" />
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
{reverseShares.length == 0 ? (
|
{reverseShares.length == 0 ? (
|
||||||
<Center style={{ height: "70vh" }}>
|
<Center style={{ height: "70vh" }}>
|
||||||
<Stack align="center" spacing={10}>
|
<Stack align="center" spacing={10}>
|
||||||
<Title order={3}>It's empty here 👀</Title>
|
<Title order={3}>
|
||||||
<Text>You don't have any reverse shares.</Text>
|
<FormattedMessage id="account.reverseShares.title.empty" />
|
||||||
|
</Title>
|
||||||
|
<Text>
|
||||||
|
<FormattedMessage id="account.reverseShares.description.empty" />
|
||||||
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Center>
|
</Center>
|
||||||
) : (
|
) : (
|
||||||
|
@ -92,10 +101,18 @@ const MyShares = () => {
|
||||||
<Table>
|
<Table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Shares</th>
|
<th>
|
||||||
<th>Remaining uses</th>
|
<FormattedMessage id="account.reverseShares.table.shares" />
|
||||||
<th>Max share size</th>
|
</th>
|
||||||
<th>Expires at</th>
|
<th>
|
||||||
|
<FormattedMessage id="account.reverseShares.table.remaining" />
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<FormattedMessage id="account.reverseShares.table.max-size" />
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<FormattedMessage id="account.reverseShares.table.expires" />
|
||||||
|
</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -105,7 +122,7 @@ const MyShares = () => {
|
||||||
<td style={{ width: 220 }}>
|
<td style={{ width: 220 }}>
|
||||||
{reverseShare.shares.length == 0 ? (
|
{reverseShare.shares.length == 0 ? (
|
||||||
<Text color="dimmed" size="sm">
|
<Text color="dimmed" size="sm">
|
||||||
No shares created yet
|
<FormattedMessage id="account.reverseShares.table.no-shares" />
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<Accordion>
|
<Accordion>
|
||||||
|
@ -115,9 +132,13 @@ const MyShares = () => {
|
||||||
>
|
>
|
||||||
<Accordion.Control p={0}>
|
<Accordion.Control p={0}>
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
{`${reverseShare.shares.length} share${
|
{reverseShare.shares.length == 1
|
||||||
reverseShare.shares.length > 1 ? "s" : ""
|
? `1 ${t(
|
||||||
}`}
|
"account.reverseShares.table.count.singular"
|
||||||
|
)}`
|
||||||
|
: `${reverseShare.shares.length} ${t(
|
||||||
|
"account.reverseShares.table.count.plural"
|
||||||
|
)}`}
|
||||||
</Text>
|
</Text>
|
||||||
</Accordion.Control>
|
</Accordion.Control>
|
||||||
<Accordion.Panel>
|
<Accordion.Panel>
|
||||||
|
@ -140,9 +161,7 @@ const MyShares = () => {
|
||||||
clipboard.copy(
|
clipboard.copy(
|
||||||
`${appUrl}/share/${share.id}`
|
`${appUrl}/share/${share.id}`
|
||||||
);
|
);
|
||||||
toast.success(
|
toast.success(t("common.notify.copied"));
|
||||||
"The share link was copied to the keyboard."
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
showShareLinkModal(
|
showShareLinkModal(
|
||||||
modals,
|
modals,
|
||||||
|
@ -183,9 +202,7 @@ const MyShares = () => {
|
||||||
reverseShare.token
|
reverseShare.token
|
||||||
}`
|
}`
|
||||||
);
|
);
|
||||||
toast.success(
|
toast.success(t("common.notify.copied"));
|
||||||
"The link was copied to your clipboard."
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
showReverseShareLinkModal(
|
showReverseShareLinkModal(
|
||||||
modals,
|
modals,
|
||||||
|
@ -203,18 +220,21 @@ const MyShares = () => {
|
||||||
size={25}
|
size={25}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
modals.openConfirmModal({
|
modals.openConfirmModal({
|
||||||
title: `Delete reverse share`,
|
title: t(
|
||||||
|
"account.reverseShares.modal.delete.title"
|
||||||
|
),
|
||||||
children: (
|
children: (
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
Do you really want to delete this reverse share?
|
<FormattedMessage id="account.reverseShares.modal.delete.description" />
|
||||||
If you do, the associated shares will be deleted
|
|
||||||
as well.
|
|
||||||
</Text>
|
</Text>
|
||||||
),
|
),
|
||||||
confirmProps: {
|
confirmProps: {
|
||||||
color: "red",
|
color: "red",
|
||||||
},
|
},
|
||||||
labels: { confirm: "Delete", cancel: "Cancel" },
|
labels: {
|
||||||
|
confirm: t("common.button.delete"),
|
||||||
|
cancel: t("common.button.cancel"),
|
||||||
|
},
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
shareService.removeReverseShare(reverseShare.id);
|
shareService.removeReverseShare(reverseShare.id);
|
||||||
setReverseShares(
|
setReverseShares(
|
||||||
|
|
|
@ -17,11 +17,13 @@ import moment from "moment";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { TbInfoCircle, TbLink, TbTrash } from "react-icons/tb";
|
import { TbInfoCircle, TbLink, TbTrash } from "react-icons/tb";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import Meta from "../../components/Meta";
|
import Meta from "../../components/Meta";
|
||||||
import showShareInformationsModal from "../../components/account/showShareInformationsModal";
|
import showShareInformationsModal from "../../components/account/showShareInformationsModal";
|
||||||
import showShareLinkModal from "../../components/account/showShareLinkModal";
|
import showShareLinkModal from "../../components/account/showShareLinkModal";
|
||||||
import CenterLoader from "../../components/core/CenterLoader";
|
import CenterLoader from "../../components/core/CenterLoader";
|
||||||
import useConfig from "../../hooks/config.hook";
|
import useConfig from "../../hooks/config.hook";
|
||||||
|
import useTranslate from "../../hooks/useTranslate.hook";
|
||||||
import shareService from "../../services/share.service";
|
import shareService from "../../services/share.service";
|
||||||
import { MyShare } from "../../types/share.type";
|
import { MyShare } from "../../types/share.type";
|
||||||
import toast from "../../utils/toast.util";
|
import toast from "../../utils/toast.util";
|
||||||
|
@ -30,6 +32,7 @@ const MyShares = () => {
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
const clipboard = useClipboard();
|
const clipboard = useClipboard();
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
|
const t = useTranslate();
|
||||||
|
|
||||||
const [shares, setShares] = useState<MyShare[]>();
|
const [shares, setShares] = useState<MyShare[]>();
|
||||||
|
|
||||||
|
@ -41,18 +44,22 @@ const MyShares = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title="My shares" />
|
<Meta title={t("account.shares.title")} />
|
||||||
<Title mb={30} order={3}>
|
<Title mb={30} order={3}>
|
||||||
My shares
|
<FormattedMessage id="account.shares.title" />
|
||||||
</Title>
|
</Title>
|
||||||
{shares.length == 0 ? (
|
{shares.length == 0 ? (
|
||||||
<Center style={{ height: "70vh" }}>
|
<Center style={{ height: "70vh" }}>
|
||||||
<Stack align="center" spacing={10}>
|
<Stack align="center" spacing={10}>
|
||||||
<Title order={3}>It's empty here 👀</Title>
|
<Title order={3}>
|
||||||
<Text>You don't have any shares.</Text>
|
<FormattedMessage id="account.shares.title.empty" />
|
||||||
|
</Title>
|
||||||
|
<Text>
|
||||||
|
<FormattedMessage id="account.shares.description.empty" />
|
||||||
|
</Text>
|
||||||
<Space h={5} />
|
<Space h={5} />
|
||||||
<Button component={Link} href="/upload" variant="light">
|
<Button component={Link} href="/upload" variant="light">
|
||||||
Create one
|
<FormattedMessage id="account.shares.button.create" />
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Center>
|
</Center>
|
||||||
|
@ -61,13 +68,21 @@ const MyShares = () => {
|
||||||
<Table>
|
<Table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>
|
||||||
|
<FormattedMessage id="account.shares.table.name" />
|
||||||
|
</th>
|
||||||
<MediaQuery smallerThan="md" styles={{ display: "none" }}>
|
<MediaQuery smallerThan="md" styles={{ display: "none" }}>
|
||||||
<th>Description</th>
|
<th>
|
||||||
|
<FormattedMessage id="account.shares.table.description" />
|
||||||
|
</th>
|
||||||
</MediaQuery>
|
</MediaQuery>
|
||||||
|
|
||||||
<th>Visitors</th>
|
<th>
|
||||||
<th>Expires at</th>
|
<FormattedMessage id="account.shares.table.visitors" />
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<FormattedMessage id="account.shares.table.expiresAt" />
|
||||||
|
</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -121,9 +136,7 @@ const MyShares = () => {
|
||||||
share.id
|
share.id
|
||||||
}`
|
}`
|
||||||
);
|
);
|
||||||
toast.success(
|
toast.success(t("common.notify.copied"));
|
||||||
"The link was copied to your clipboard."
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
showShareLinkModal(
|
showShareLinkModal(
|
||||||
modals,
|
modals,
|
||||||
|
@ -141,16 +154,21 @@ const MyShares = () => {
|
||||||
size={25}
|
size={25}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
modals.openConfirmModal({
|
modals.openConfirmModal({
|
||||||
title: `Delete share ${share.id}`,
|
title: t("account.shares.modal.delete.title", {
|
||||||
|
share: share.id,
|
||||||
|
}),
|
||||||
children: (
|
children: (
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
Do you really want to delete this share?
|
<FormattedMessage id="account.shares.modal.delete.description" />
|
||||||
</Text>
|
</Text>
|
||||||
),
|
),
|
||||||
confirmProps: {
|
confirmProps: {
|
||||||
color: "red",
|
color: "red",
|
||||||
},
|
},
|
||||||
labels: { confirm: "Confirm", cancel: "Cancel" },
|
labels: {
|
||||||
|
confirm: t("common.button.delete"),
|
||||||
|
cancel: t("common.button.cancel"),
|
||||||
|
},
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
shareService.remove(share.id);
|
shareService.remove(share.id);
|
||||||
setShares(
|
setShares(
|
||||||
|
|
|
@ -13,25 +13,28 @@ import { useMediaQuery } from "@mantine/hooks";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
import Meta from "../../../components/Meta";
|
||||||
import AdminConfigInput from "../../../components/admin/configuration/AdminConfigInput";
|
import AdminConfigInput from "../../../components/admin/configuration/AdminConfigInput";
|
||||||
import ConfigurationHeader from "../../../components/admin/configuration/ConfigurationHeader";
|
import ConfigurationHeader from "../../../components/admin/configuration/ConfigurationHeader";
|
||||||
import ConfigurationNavBar from "../../../components/admin/configuration/ConfigurationNavBar";
|
import ConfigurationNavBar from "../../../components/admin/configuration/ConfigurationNavBar";
|
||||||
import LogoConfigInput from "../../../components/admin/configuration/LogoConfigInput";
|
import LogoConfigInput from "../../../components/admin/configuration/LogoConfigInput";
|
||||||
import TestEmailButton from "../../../components/admin/configuration/TestEmailButton";
|
import TestEmailButton from "../../../components/admin/configuration/TestEmailButton";
|
||||||
import CenterLoader from "../../../components/core/CenterLoader";
|
import CenterLoader from "../../../components/core/CenterLoader";
|
||||||
import Meta from "../../../components/Meta";
|
|
||||||
import useConfig from "../../../hooks/config.hook";
|
import useConfig from "../../../hooks/config.hook";
|
||||||
import configService from "../../../services/config.service";
|
import configService from "../../../services/config.service";
|
||||||
import { AdminConfig, UpdateConfig } from "../../../types/config.type";
|
import { AdminConfig, UpdateConfig } from "../../../types/config.type";
|
||||||
import {
|
import {
|
||||||
|
camelToKebab,
|
||||||
capitalizeFirstLetter,
|
capitalizeFirstLetter,
|
||||||
configVariableToFriendlyName,
|
|
||||||
} from "../../../utils/string.util";
|
} from "../../../utils/string.util";
|
||||||
import toast from "../../../utils/toast.util";
|
import toast from "../../../utils/toast.util";
|
||||||
|
import useTranslate from "../../../hooks/useTranslate.hook";
|
||||||
|
|
||||||
export default function AppShellDemo() {
|
export default function AppShellDemo() {
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const t = useTranslate();
|
||||||
|
|
||||||
const [isMobileNavBarOpened, setIsMobileNavBarOpened] = useState(false);
|
const [isMobileNavBarOpened, setIsMobileNavBarOpened] = useState(false);
|
||||||
const isMobile = useMediaQuery("(max-width: 560px)");
|
const isMobile = useMediaQuery("(max-width: 560px)");
|
||||||
|
@ -94,7 +97,7 @@ export default function AppShellDemo() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title="Configuration" />
|
<Meta title={t("admin.config.title")} />
|
||||||
<AppShell
|
<AppShell
|
||||||
styles={{
|
styles={{
|
||||||
main: {
|
main: {
|
||||||
|
@ -134,26 +137,28 @@ export default function AppShellDemo() {
|
||||||
spacing={0}
|
spacing={0}
|
||||||
>
|
>
|
||||||
<Title order={6}>
|
<Title order={6}>
|
||||||
{configVariableToFriendlyName(configVariable.name)}
|
<FormattedMessage
|
||||||
|
id={`admin.config.${camelToKebab(
|
||||||
|
configVariable.key
|
||||||
|
)}`}
|
||||||
|
/>
|
||||||
</Title>
|
</Title>
|
||||||
{configVariable.description.split("\n").length == 1 ? (
|
|
||||||
<Text color="dimmed" size="sm" mb="xs">
|
<Text
|
||||||
{configVariable.description}
|
sx={{
|
||||||
</Text>
|
whiteSpace: "pre-line",
|
||||||
) : (
|
}}
|
||||||
configVariable.description.split("\n").map((line) => (
|
color="dimmed"
|
||||||
<Text
|
size="sm"
|
||||||
key={line}
|
mb="xs"
|
||||||
color="dimmed"
|
>
|
||||||
size="sm"
|
<FormattedMessage
|
||||||
style={{
|
id={`admin.config.${camelToKebab(
|
||||||
marginBottom: line === "" ? "1rem" : "0",
|
configVariable.key
|
||||||
}}
|
)}.description`}
|
||||||
>
|
values={{ br: <br /> }}
|
||||||
{line}
|
/>
|
||||||
</Text>
|
</Text>
|
||||||
))
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack></Stack>
|
<Stack></Stack>
|
||||||
<Box style={{ width: isMobile ? "100%" : "50%" }}>
|
<Box style={{ width: isMobile ? "100%" : "50%" }}>
|
||||||
|
@ -176,7 +181,9 @@ export default function AppShellDemo() {
|
||||||
saveConfigVariables={saveConfigVariables}
|
saveConfigVariables={saveConfigVariables}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Button onClick={saveConfigVariables}>Save</Button>
|
<Button onClick={saveConfigVariables}>
|
||||||
|
<FormattedMessage id="common.button.save" />
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -11,7 +11,9 @@ import {
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { TbRefresh, TbSettings, TbUsers } from "react-icons/tb";
|
import { TbRefresh, TbSettings, TbUsers } from "react-icons/tb";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import Meta from "../../components/Meta";
|
import Meta from "../../components/Meta";
|
||||||
|
import useTranslate from "../../hooks/useTranslate.hook";
|
||||||
import configService from "../../services/config.service";
|
import configService from "../../services/config.service";
|
||||||
|
|
||||||
const useStyles = createStyles((theme) => ({
|
const useStyles = createStyles((theme) => ({
|
||||||
|
@ -31,15 +33,16 @@ const useStyles = createStyles((theme) => ({
|
||||||
|
|
||||||
const Admin = () => {
|
const Admin = () => {
|
||||||
const { classes, theme } = useStyles();
|
const { classes, theme } = useStyles();
|
||||||
|
const t = useTranslate();
|
||||||
|
|
||||||
const [managementOptions, setManagementOptions] = useState([
|
const [managementOptions, setManagementOptions] = useState([
|
||||||
{
|
{
|
||||||
title: "User management",
|
title: t("admin.button.users"),
|
||||||
icon: TbUsers,
|
icon: TbUsers,
|
||||||
route: "/admin/users",
|
route: "/admin/users",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Configuration",
|
title: t("admin.button.config"),
|
||||||
icon: TbSettings,
|
icon: TbSettings,
|
||||||
route: "/admin/config/general",
|
route: "/admin/config/general",
|
||||||
},
|
},
|
||||||
|
@ -63,9 +66,9 @@ const Admin = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title="Administration" />
|
<Meta title={t("admin.title")} />
|
||||||
<Title mb={30} order={3}>
|
<Title mb={30} order={3}>
|
||||||
Administration
|
<FormattedMessage id="admin.title" />
|
||||||
</Title>
|
</Title>
|
||||||
<Stack justify="space-between" style={{ height: "calc(100vh - 180px)" }}>
|
<Stack justify="space-between" style={{ height: "calc(100vh - 180px)" }}>
|
||||||
<Paper withBorder p={40}>
|
<Paper withBorder p={40}>
|
||||||
|
@ -91,7 +94,7 @@ const Admin = () => {
|
||||||
|
|
||||||
<Center>
|
<Center>
|
||||||
<Text size="xs" color="dimmed">
|
<Text size="xs" color="dimmed">
|
||||||
Version {process.env.VERSION}
|
<FormattedMessage id="admin.version" /> {process.env.VERSION}
|
||||||
</Text>
|
</Text>
|
||||||
</Center>
|
</Center>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
@ -2,10 +2,12 @@ import { Button, Group, Space, Text, Title } from "@mantine/core";
|
||||||
import { useModals } from "@mantine/modals";
|
import { useModals } from "@mantine/modals";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { TbPlus } from "react-icons/tb";
|
import { TbPlus } from "react-icons/tb";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
import Meta from "../../components/Meta";
|
||||||
import ManageUserTable from "../../components/admin/users/ManageUserTable";
|
import ManageUserTable from "../../components/admin/users/ManageUserTable";
|
||||||
import showCreateUserModal from "../../components/admin/users/showCreateUserModal";
|
import showCreateUserModal from "../../components/admin/users/showCreateUserModal";
|
||||||
import Meta from "../../components/Meta";
|
|
||||||
import useConfig from "../../hooks/config.hook";
|
import useConfig from "../../hooks/config.hook";
|
||||||
|
import useTranslate from "../../hooks/useTranslate.hook";
|
||||||
import userService from "../../services/user.service";
|
import userService from "../../services/user.service";
|
||||||
import User from "../../types/user.type";
|
import User from "../../types/user.type";
|
||||||
import toast from "../../utils/toast.util";
|
import toast from "../../utils/toast.util";
|
||||||
|
@ -16,6 +18,7 @@ const Users = () => {
|
||||||
|
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
|
const t = useTranslate();
|
||||||
|
|
||||||
const getUsers = () => {
|
const getUsers = () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
@ -27,14 +30,18 @@ const Users = () => {
|
||||||
|
|
||||||
const deleteUser = (user: User) => {
|
const deleteUser = (user: User) => {
|
||||||
modals.openConfirmModal({
|
modals.openConfirmModal({
|
||||||
title: `Delete ${user.username}?`,
|
title: t("admin.users.edit.delete.title", {
|
||||||
|
username: user.username,
|
||||||
|
}),
|
||||||
children: (
|
children: (
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
Do you really want to delete <b>{user.username}</b> and all his
|
<FormattedMessage id="admin.users.edit.delete.description" />
|
||||||
shares?
|
|
||||||
</Text>
|
</Text>
|
||||||
),
|
),
|
||||||
labels: { confirm: "Delete", cancel: "Cancel" },
|
labels: {
|
||||||
|
confirm: t("common.button.delete"),
|
||||||
|
cancel: t("common.button.cancel"),
|
||||||
|
},
|
||||||
confirmProps: { color: "red" },
|
confirmProps: { color: "red" },
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
userService
|
userService
|
||||||
|
@ -51,10 +58,10 @@ const Users = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title="User management" />
|
<Meta title={t("admin.users.title")} />
|
||||||
<Group position="apart" align="baseline" mb={20}>
|
<Group position="apart" align="baseline" mb={20}>
|
||||||
<Title mb={30} order={3}>
|
<Title mb={30} order={3}>
|
||||||
User management
|
<FormattedMessage id="admin.users.title" />
|
||||||
</Title>
|
</Title>
|
||||||
<Button
|
<Button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
|
@ -62,7 +69,7 @@ const Users = () => {
|
||||||
}
|
}
|
||||||
leftIcon={<TbPlus size={20} />}
|
leftIcon={<TbPlus size={20} />}
|
||||||
>
|
>
|
||||||
Create
|
<FormattedMessage id="common.button.create" />
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,9 @@ import {
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useForm, yupResolver } from "@mantine/form";
|
import { useForm, yupResolver } from "@mantine/form";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
import useTranslate from "../../../hooks/useTranslate.hook";
|
||||||
import authService from "../../../services/auth.service";
|
import authService from "../../../services/auth.service";
|
||||||
import toast from "../../../utils/toast.util";
|
import toast from "../../../utils/toast.util";
|
||||||
|
|
||||||
|
@ -25,6 +27,7 @@ const useStyles = createStyles((theme) => ({
|
||||||
const ResetPassword = () => {
|
const ResetPassword = () => {
|
||||||
const { classes } = useStyles();
|
const { classes } = useStyles();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const t = useTranslate();
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
|
@ -32,7 +35,10 @@ const ResetPassword = () => {
|
||||||
},
|
},
|
||||||
validate: yupResolver(
|
validate: yupResolver(
|
||||||
yup.object().shape({
|
yup.object().shape({
|
||||||
password: yup.string().min(8).required(),
|
password: yup
|
||||||
|
.string()
|
||||||
|
.min(8, t("common.error.too-short", { length: 8 }))
|
||||||
|
.required(t("common.error.field-required")),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
@ -42,10 +48,10 @@ const ResetPassword = () => {
|
||||||
return (
|
return (
|
||||||
<Container size={460} my={30}>
|
<Container size={460} my={30}>
|
||||||
<Title order={2} weight={900} align="center">
|
<Title order={2} weight={900} align="center">
|
||||||
Reset password
|
<FormattedMessage id="resetPassword.text.resetPassword" />
|
||||||
</Title>
|
</Title>
|
||||||
<Text color="dimmed" size="sm" align="center">
|
<Text color="dimmed" size="sm" align="center">
|
||||||
Enter your new password
|
<FormattedMessage id="resetPassword.text.enterNewPassword" />
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Paper withBorder shadow="md" p={30} radius="md" mt="xl">
|
<Paper withBorder shadow="md" p={30} radius="md" mt="xl">
|
||||||
|
@ -54,7 +60,7 @@ const ResetPassword = () => {
|
||||||
authService
|
authService
|
||||||
.resetPassword(resetPasswordToken, values.password)
|
.resetPassword(resetPasswordToken, values.password)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Your password has been reset successfully.");
|
toast.success(t("resetPassword.notify.passwordReset"));
|
||||||
|
|
||||||
router.push("/auth/signIn");
|
router.push("/auth/signIn");
|
||||||
})
|
})
|
||||||
|
@ -62,13 +68,13 @@ const ResetPassword = () => {
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
label="New password"
|
label={t("resetPassword.text.password")}
|
||||||
placeholder="••••••••••"
|
placeholder="••••••••••"
|
||||||
{...form.getInputProps("password")}
|
{...form.getInputProps("password")}
|
||||||
/>
|
/>
|
||||||
<Group position="right" mt="lg">
|
<Group position="right" mt="lg">
|
||||||
<Button type="submit" className={classes.control}>
|
<Button type="submit" className={classes.control}>
|
||||||
Reset password
|
<FormattedMessage id="resetPassword.button.resetPassword" />
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -15,7 +15,9 @@ import { useForm, yupResolver } from "@mantine/form";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { TbArrowLeft } from "react-icons/tb";
|
import { TbArrowLeft } from "react-icons/tb";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
import useTranslate from "../../../hooks/useTranslate.hook";
|
||||||
import authService from "../../../services/auth.service";
|
import authService from "../../../services/auth.service";
|
||||||
import toast from "../../../utils/toast.util";
|
import toast from "../../../utils/toast.util";
|
||||||
|
|
||||||
|
@ -43,6 +45,7 @@ const useStyles = createStyles((theme) => ({
|
||||||
const ResetPassword = () => {
|
const ResetPassword = () => {
|
||||||
const { classes } = useStyles();
|
const { classes } = useStyles();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const t = useTranslate();
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
|
@ -50,7 +53,10 @@ const ResetPassword = () => {
|
||||||
},
|
},
|
||||||
validate: yupResolver(
|
validate: yupResolver(
|
||||||
yup.object().shape({
|
yup.object().shape({
|
||||||
email: yup.string().email().required(),
|
email: yup
|
||||||
|
.string()
|
||||||
|
.email(t("common.error.invalid-email"))
|
||||||
|
.required(t("common.error.field-required")),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
@ -58,10 +64,10 @@ const ResetPassword = () => {
|
||||||
return (
|
return (
|
||||||
<Container size={460} my={30}>
|
<Container size={460} my={30}>
|
||||||
<Title order={2} weight={900} align="center">
|
<Title order={2} weight={900} align="center">
|
||||||
Forgot your password?
|
<FormattedMessage id="resetPassword.title" />
|
||||||
</Title>
|
</Title>
|
||||||
<Text color="dimmed" size="sm" align="center">
|
<Text color="dimmed" size="sm" align="center">
|
||||||
Enter your email to get a reset link
|
<FormattedMessage id="resetPassword.description" />
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Paper withBorder shadow="md" p={30} radius="md" mt="xl">
|
<Paper withBorder shadow="md" p={30} radius="md" mt="xl">
|
||||||
|
@ -70,15 +76,15 @@ const ResetPassword = () => {
|
||||||
authService
|
authService
|
||||||
.requestResetPassword(values.email)
|
.requestResetPassword(values.email)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("The email has been sent.");
|
toast.success(t("resetPassword.notify.success"));
|
||||||
router.push("/auth/signIn");
|
router.push("/auth/signIn");
|
||||||
})
|
})
|
||||||
.catch(toast.axiosError)
|
.catch(toast.axiosError)
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Your email"
|
label={t("signup.input.email")}
|
||||||
placeholder="Your email"
|
placeholder={t("signup.input.email.placeholder")}
|
||||||
{...form.getInputProps("email")}
|
{...form.getInputProps("email")}
|
||||||
/>
|
/>
|
||||||
<Group position="apart" mt="lg" className={classes.controls}>
|
<Group position="apart" mt="lg" className={classes.controls}>
|
||||||
|
@ -91,11 +97,13 @@ const ResetPassword = () => {
|
||||||
>
|
>
|
||||||
<Center inline>
|
<Center inline>
|
||||||
<TbArrowLeft size={12} />
|
<TbArrowLeft size={12} />
|
||||||
<Box ml={5}>Back to login page</Box>
|
<Box ml={5}>
|
||||||
|
<FormattedMessage id="resetPassword.button.back" />
|
||||||
|
</Box>
|
||||||
</Center>
|
</Center>
|
||||||
</Anchor>
|
</Anchor>
|
||||||
<Button type="submit" className={classes.control}>
|
<Button type="submit" className={classes.control}>
|
||||||
Reset password
|
<FormattedMessage id="resetPassword.text.resetPassword" />
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { useEffect, useState } from "react";
|
||||||
import SignInForm from "../../components/auth/SignInForm";
|
import SignInForm from "../../components/auth/SignInForm";
|
||||||
import Meta from "../../components/Meta";
|
import Meta from "../../components/Meta";
|
||||||
import useUser from "../../hooks/user.hook";
|
import useUser from "../../hooks/user.hook";
|
||||||
|
import useTranslate from "../../hooks/useTranslate.hook";
|
||||||
|
|
||||||
export function getServerSideProps(context: GetServerSidePropsContext) {
|
export function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
return {
|
return {
|
||||||
|
@ -15,6 +16,7 @@ export function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
const SignIn = ({ redirectPath }: { redirectPath?: string }) => {
|
const SignIn = ({ redirectPath }: { redirectPath?: string }) => {
|
||||||
const { refreshUser } = useUser();
|
const { refreshUser } = useUser();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const t = useTranslate();
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(redirectPath ? true : false);
|
const [isLoading, setIsLoading] = useState(redirectPath ? true : false);
|
||||||
|
|
||||||
|
@ -34,7 +36,7 @@ const SignIn = ({ redirectPath }: { redirectPath?: string }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title="Sign In" />
|
<Meta title={t("signin.title")} />
|
||||||
<SignInForm redirectPath={redirectPath ?? "/upload"} />
|
<SignInForm redirectPath={redirectPath ?? "/upload"} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import SignUpForm from "../../components/auth/SignUpForm";
|
import SignUpForm from "../../components/auth/SignUpForm";
|
||||||
import Meta from "../../components/Meta";
|
import Meta from "../../components/Meta";
|
||||||
|
import useTranslate from "../../hooks/useTranslate.hook";
|
||||||
|
|
||||||
const SignUp = () => {
|
const SignUp = () => {
|
||||||
|
const t = useTranslate();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title="Sign Up" />
|
<Meta title={t("signup.title")} />
|
||||||
<SignUpForm />
|
<SignUpForm />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -12,6 +12,7 @@ import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { TbCheck } from "react-icons/tb";
|
import { TbCheck } from "react-icons/tb";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import Logo from "../components/Logo";
|
import Logo from "../components/Logo";
|
||||||
import Meta from "../components/Meta";
|
import Meta from "../components/Meta";
|
||||||
import useUser from "../hooks/user.hook";
|
import useUser from "../hooks/user.hook";
|
||||||
|
@ -89,12 +90,17 @@ export default function Home() {
|
||||||
<div className={classes.inner}>
|
<div className={classes.inner}>
|
||||||
<div className={classes.content}>
|
<div className={classes.content}>
|
||||||
<Title className={classes.title}>
|
<Title className={classes.title}>
|
||||||
A <span className={classes.highlight}>self-hosted</span> <br />{" "}
|
<FormattedMessage
|
||||||
file sharing platform.
|
id="home.title"
|
||||||
|
values={{
|
||||||
|
h: (chunks) => (
|
||||||
|
<span className={classes.highlight}>{chunks} </span>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Title>
|
</Title>
|
||||||
<Text color="dimmed" mt="md">
|
<Text color="dimmed" mt="md">
|
||||||
Do you really want to give your personal files in the hand of
|
<FormattedMessage id="home.description" />
|
||||||
third parties like WeTransfer?
|
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<List
|
<List
|
||||||
|
@ -109,19 +115,26 @@ export default function Home() {
|
||||||
>
|
>
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<div>
|
<div>
|
||||||
<b>Self-Hosted</b> - Host Pingvin Share on your own machine.
|
<b>
|
||||||
|
<FormattedMessage id="home.bullet.a.name" />
|
||||||
|
</b>{" "}
|
||||||
|
- <FormattedMessage id="home.bullet.a.description" />
|
||||||
</div>
|
</div>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<div>
|
<div>
|
||||||
<b>Privacy</b> - Your files are your files and should never
|
<b>
|
||||||
get into the hands of third parties.
|
<FormattedMessage id="home.bullet.b.name" />
|
||||||
|
</b>{" "}
|
||||||
|
- <FormattedMessage id="home.bullet.b.description" />
|
||||||
</div>
|
</div>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<div>
|
<div>
|
||||||
<b>No annoying file size limit</b> - Upload as big files as
|
<b>
|
||||||
you want. Only your hard drive will be your limit.
|
<FormattedMessage id="home.bullet.c.name" />
|
||||||
|
</b>{" "}
|
||||||
|
- <FormattedMessage id="home.bullet.c.description" />
|
||||||
</div>
|
</div>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
</List>
|
</List>
|
||||||
|
@ -134,7 +147,7 @@ export default function Home() {
|
||||||
size="md"
|
size="md"
|
||||||
className={classes.control}
|
className={classes.control}
|
||||||
>
|
>
|
||||||
Get started
|
<FormattedMessage id="home.button.start" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
component={Link}
|
component={Link}
|
||||||
|
@ -145,7 +158,7 @@ export default function Home() {
|
||||||
size="md"
|
size="md"
|
||||||
className={classes.control}
|
className={classes.control}
|
||||||
>
|
>
|
||||||
Source code
|
<FormattedMessage id="home.button.source" />
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,6 +7,7 @@ import DownloadAllButton from "../../../components/share/DownloadAllButton";
|
||||||
import FileList from "../../../components/share/FileList";
|
import FileList from "../../../components/share/FileList";
|
||||||
import showEnterPasswordModal from "../../../components/share/showEnterPasswordModal";
|
import showEnterPasswordModal from "../../../components/share/showEnterPasswordModal";
|
||||||
import showErrorModal from "../../../components/share/showErrorModal";
|
import showErrorModal from "../../../components/share/showErrorModal";
|
||||||
|
import useTranslate from "../../../hooks/useTranslate.hook";
|
||||||
import shareService from "../../../services/share.service";
|
import shareService from "../../../services/share.service";
|
||||||
import { Share as ShareType } from "../../../types/share.type";
|
import { Share as ShareType } from "../../../types/share.type";
|
||||||
import toast from "../../../utils/toast.util";
|
import toast from "../../../utils/toast.util";
|
||||||
|
@ -20,6 +21,7 @@ export function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
const Share = ({ shareId }: { shareId: string }) => {
|
const Share = ({ shareId }: { shareId: string }) => {
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
const [share, setShare] = useState<ShareType>();
|
const [share, setShare] = useState<ShareType>();
|
||||||
|
const t = useTranslate();
|
||||||
|
|
||||||
const getShareToken = async (password?: string) => {
|
const getShareToken = async (password?: string) => {
|
||||||
await shareService
|
await shareService
|
||||||
|
@ -33,8 +35,8 @@ const Share = ({ shareId }: { shareId: string }) => {
|
||||||
if (error == "share_max_views_exceeded") {
|
if (error == "share_max_views_exceeded") {
|
||||||
showErrorModal(
|
showErrorModal(
|
||||||
modals,
|
modals,
|
||||||
"Visitor limit exceeded",
|
t("share.error.visitor-limit-exceeded.title"),
|
||||||
"The visitor limit from this share has been exceeded."
|
t("share.error.visitor-limit-exceeded.description")
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
toast.axiosError(e);
|
toast.axiosError(e);
|
||||||
|
@ -52,12 +54,16 @@ const Share = ({ shareId }: { shareId: string }) => {
|
||||||
const { error } = e.response.data;
|
const { error } = e.response.data;
|
||||||
if (e.response.status == 404) {
|
if (e.response.status == 404) {
|
||||||
if (error == "share_removed") {
|
if (error == "share_removed") {
|
||||||
showErrorModal(modals, "Share removed", e.response.data.message);
|
showErrorModal(
|
||||||
|
modals,
|
||||||
|
t("share.error.removed.title"),
|
||||||
|
e.response.data.message
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
showErrorModal(
|
showErrorModal(
|
||||||
modals,
|
modals,
|
||||||
"Not found",
|
t("share.error.not-found.title"),
|
||||||
"This share can't be found. Please check your link."
|
t("share.error.not-found.description")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (error == "share_password_required") {
|
} else if (error == "share_password_required") {
|
||||||
|
@ -65,7 +71,7 @@ const Share = ({ shareId }: { shareId: string }) => {
|
||||||
} else if (error == "share_token_required") {
|
} else if (error == "share_token_required") {
|
||||||
getShareToken();
|
getShareToken();
|
||||||
} else {
|
} else {
|
||||||
showErrorModal(modals, "Error", "An unknown error occurred.");
|
showErrorModal(modals, t("common.error"), t("common.error.unknown"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -77,8 +83,8 @@ const Share = ({ shareId }: { shareId: string }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta
|
<Meta
|
||||||
title={`Share ${shareId}`}
|
title={t("share.title", { shareId })}
|
||||||
description="Look what I've shared with you."
|
description={t("share.description")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Group position="apart" mb="lg">
|
<Group position="apart" mb="lg">
|
||||||
|
|
|
@ -4,12 +4,14 @@ import { cleanNotifications } from "@mantine/notifications";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import pLimit from "p-limit";
|
import pLimit from "p-limit";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import Meta from "../../components/Meta";
|
import Meta from "../../components/Meta";
|
||||||
import Dropzone from "../../components/upload/Dropzone";
|
import Dropzone from "../../components/upload/Dropzone";
|
||||||
import FileList from "../../components/upload/FileList";
|
import FileList from "../../components/upload/FileList";
|
||||||
import showCompletedUploadModal from "../../components/upload/modals/showCompletedUploadModal";
|
import showCompletedUploadModal from "../../components/upload/modals/showCompletedUploadModal";
|
||||||
import showCreateUploadModal from "../../components/upload/modals/showCreateUploadModal";
|
import showCreateUploadModal from "../../components/upload/modals/showCreateUploadModal";
|
||||||
import useConfig from "../../hooks/config.hook";
|
import useConfig from "../../hooks/config.hook";
|
||||||
|
import useTranslate from "../../hooks/useTranslate.hook";
|
||||||
import useUser from "../../hooks/user.hook";
|
import useUser from "../../hooks/user.hook";
|
||||||
import shareService from "../../services/share.service";
|
import shareService from "../../services/share.service";
|
||||||
import { FileUpload } from "../../types/File.type";
|
import { FileUpload } from "../../types/File.type";
|
||||||
|
@ -29,6 +31,7 @@ const Upload = ({
|
||||||
isReverseShare: boolean;
|
isReverseShare: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
|
const t = useTranslate();
|
||||||
|
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
|
@ -126,7 +129,7 @@ const Upload = ({
|
||||||
if (fileErrorCount > 0) {
|
if (fileErrorCount > 0) {
|
||||||
if (!errorToastShown) {
|
if (!errorToastShown) {
|
||||||
toast.error(
|
toast.error(
|
||||||
`${fileErrorCount} file(s) failed to upload. Trying again.`,
|
t("upload.notify.count-failed", { count: fileErrorCount }),
|
||||||
{
|
{
|
||||||
withCloseButton: false,
|
withCloseButton: false,
|
||||||
autoClose: false,
|
autoClose: false,
|
||||||
|
@ -152,15 +155,13 @@ const Upload = ({
|
||||||
showCompletedUploadModal(modals, share, config.get("general.appUrl"));
|
showCompletedUploadModal(modals, share, config.get("general.appUrl"));
|
||||||
setFiles([]);
|
setFiles([]);
|
||||||
})
|
})
|
||||||
.catch(() =>
|
.catch(() => toast.error(t("upload.notify.generic-error")));
|
||||||
toast.error("An error occurred while finishing your share.")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}, [files]);
|
}, [files]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title="Upload" />
|
<Meta title={t("upload.title")} />
|
||||||
<Group position="right" mb={20}>
|
<Group position="right" mb={20}>
|
||||||
<Button
|
<Button
|
||||||
loading={isUploading}
|
loading={isUploading}
|
||||||
|
@ -183,7 +184,7 @@ const Upload = ({
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Share
|
<FormattedMessage id="common.button.share" />
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
<Dropzone
|
<Dropzone
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
|
||||||
export const getExpirationPreview = (
|
export const getExpirationPreview = (
|
||||||
name: string,
|
messages: {
|
||||||
|
neverExpires: string;
|
||||||
|
expiresOn: string;
|
||||||
|
},
|
||||||
form: {
|
form: {
|
||||||
values: {
|
values: {
|
||||||
never_expires?: boolean;
|
never_expires?: boolean;
|
||||||
|
@ -13,7 +16,7 @@ export const getExpirationPreview = (
|
||||||
const value = form.values.never_expires
|
const value = form.values.never_expires
|
||||||
? "never"
|
? "never"
|
||||||
: form.values.expiration_num + form.values.expiration_unit;
|
: form.values.expiration_num + form.values.expiration_unit;
|
||||||
if (value === "never") return `This ${name} will never expire.`;
|
if (value === "never") return messages.neverExpires;
|
||||||
|
|
||||||
const expirationDate = moment()
|
const expirationDate = moment()
|
||||||
.add(
|
.add(
|
||||||
|
@ -22,5 +25,8 @@ export const getExpirationPreview = (
|
||||||
)
|
)
|
||||||
.toDate();
|
.toDate();
|
||||||
|
|
||||||
return `This ${name} will expire on ${moment(expirationDate).format("LLL")}`;
|
return messages.expiresOn.replace(
|
||||||
|
"{expiration}",
|
||||||
|
moment(expirationDate).format("LLL")
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
42
frontend/src/utils/i18n.util.ts
Normal file
42
frontend/src/utils/i18n.util.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import { setCookie } from "cookies-next";
|
||||||
|
import { LOCALES } from "../i18n/locales";
|
||||||
|
|
||||||
|
const getLocaleByCode = (code: string) => {
|
||||||
|
return Object.values(LOCALES).find((l) => l.code === code) ?? LOCALES.ENGLISH;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse the Accept-Language header and return the first supported language
|
||||||
|
const getLanguageFromAcceptHeader = (acceptLanguage?: string) => {
|
||||||
|
if (!acceptLanguage) return "en";
|
||||||
|
|
||||||
|
const languages = acceptLanguage.split(",").map((l) => l.split(";")[0]);
|
||||||
|
const supportedLanguages = Object.values(LOCALES).map((l) => l.code);
|
||||||
|
|
||||||
|
for (const language of languages) {
|
||||||
|
// Try to match the full language code first, then the language code without the region
|
||||||
|
if (supportedLanguages.includes(language)) {
|
||||||
|
return language;
|
||||||
|
} else if (supportedLanguages.includes(language.split("-")[0])) {
|
||||||
|
return language.split("-")[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "en";
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLanguageSupported = (code: string) => {
|
||||||
|
return Object.values(LOCALES).some((l) => l.code === code);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setLanguageCookie = (code: string) => {
|
||||||
|
setCookie("language", code, {
|
||||||
|
sameSite: "lax",
|
||||||
|
expires: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getLocaleByCode,
|
||||||
|
getLanguageFromAcceptHeader,
|
||||||
|
isLanguageSupported,
|
||||||
|
setLanguageCookie,
|
||||||
|
};
|
|
@ -1,6 +1,5 @@
|
||||||
export const configVariableToFriendlyName = (variable: string) => {
|
export const camelToKebab = (camelCaseString: string) => {
|
||||||
const splitted = variable.split(/(?=[A-Z])/).join(" ");
|
return camelCaseString.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
||||||
return splitted.charAt(0).toUpperCase() + splitted.slice(1);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const capitalizeFirstLetter = (string: string) => {
|
export const capitalizeFirstLetter = (string: string) => {
|
||||||
|
|
|
@ -3,6 +3,10 @@ const defaultPreferences = [
|
||||||
key: "colorScheme",
|
key: "colorScheme",
|
||||||
value: "system",
|
value: "system",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "locale",
|
||||||
|
value: "system",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const get = (key: string) => {
|
const get = (key: string) => {
|
||||||
|
@ -23,8 +27,9 @@ const set = (key: string, value: string) => {
|
||||||
localStorage.setItem("preferences", JSON.stringify(preferences));
|
localStorage.setItem("preferences", JSON.stringify(preferences));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const usePreferences = () => {
|
const userPreferences = {
|
||||||
return { get, set };
|
get,
|
||||||
|
set,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default usePreferences;
|
export default userPreferences;
|
Loading…
Reference in New Issue
Block a user