Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7304b54125 | ||
|
|
ea0d5216e8 | ||
|
|
62deb6c152 | ||
|
|
9ba2b4c82c | ||
|
|
a47d080657 | ||
|
|
72a52eb33f | ||
|
|
c9a2a469c6 | ||
|
|
b534129194 | ||
|
|
0beebfd779 | ||
|
|
2ed5ecc1ea | ||
|
|
9bb05158c5 | ||
|
|
36230371fd | ||
|
|
5fd79a35cb | ||
|
|
cecaa90e15 | ||
|
|
2584bb0d48 | ||
|
|
82008aa261 | ||
|
|
a07a78a138 | ||
|
|
2618bbb897 | ||
|
|
6667c7a8d7 | ||
|
|
7f0c31c2e0 | ||
|
|
3165dcf9e6 | ||
|
|
f4c88aeb08 | ||
|
|
231a2e95b9 | ||
|
|
7827b687fa | ||
|
|
389dc87cac | ||
|
|
5816b39fc6 | ||
|
|
890588f5da | ||
|
|
e6a2014875 | ||
|
|
396363488c | ||
|
|
424331ed1a | ||
|
|
d198a132db | ||
|
|
a041a6969d | ||
|
|
be57bd3354 | ||
|
|
70b425b380 | ||
|
|
8259eb286c | ||
|
|
7071d8bd87 | ||
|
|
b2ed7b74c0 | ||
|
|
b9f6e3bd08 | ||
|
|
7c5ec8d0ea | ||
|
|
0276294f52 | ||
|
|
7574eb3191 | ||
|
|
a1ea7c0265 |
19
.github/ISSUE_TEMPLATE/language-request.yml
vendored
Normal file
19
.github/ISSUE_TEMPLATE/language-request.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: "🌐 Language request"
|
||||
description: "You want to contribute to a language that isn't on Crowdin yet?"
|
||||
title: "🌐 Language request: <language name in english>"
|
||||
labels: [language-request]
|
||||
body:
|
||||
- type: input
|
||||
id: language-name-native
|
||||
attributes:
|
||||
label: "🌐 Language name (native)"
|
||||
placeholder: "Schweizerdeutsch"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: language-code
|
||||
attributes:
|
||||
label: "🌐 Language code"
|
||||
placeholder: "de-CH"
|
||||
validations:
|
||||
required: true
|
||||
60
CHANGELOG.md
60
CHANGELOG.md
@@ -1,3 +1,63 @@
|
||||
## [0.17.4](https://github.com/stonith404/pingvin-share/compare/v0.17.3...v0.17.4) (2023-08-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* redirection to `localhost:3000` ([ea0d521](https://github.com/stonith404/pingvin-share/commit/ea0d5216e89346b8d3ef0277b76fdc6302e9de15))
|
||||
|
||||
## [0.17.3](https://github.com/stonith404/pingvin-share/compare/v0.17.2...v0.17.3) (2023-07-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* logo doesn't get loaded correctly ([9ba2b4c](https://github.com/stonith404/pingvin-share/commit/9ba2b4c82cdad9097b33f0451771818c7b972a6b))
|
||||
* share expiration never doesn't work if using another language than English ([a47d080](https://github.com/stonith404/pingvin-share/commit/a47d080657e1d08ef06ec7425d8bdafd5a26c24a))
|
||||
|
||||
## [0.17.2](https://github.com/stonith404/pingvin-share/compare/v0.17.1...v0.17.2) (2023-07-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `ECONNREFUSED` with Docker ipv6 enabled ([c9a2a46](https://github.com/stonith404/pingvin-share/commit/c9a2a469c67d3c3cd08179b44e2bf82208f05177))
|
||||
|
||||
## [0.17.1](https://github.com/stonith404/pingvin-share/compare/v0.17.0...v0.17.1) (2023-07-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* rename pt-PT.ts to pt-BR.ts ([2584bb0](https://github.com/stonith404/pingvin-share/commit/2584bb0d48c761940eafc03d5cd98d47e7a5b0ae))
|
||||
|
||||
## [0.17.0](https://github.com/stonith404/pingvin-share/compare/v0.16.1...v0.17.0) (2023-07-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* ability to define zip compression level ([7827b68](https://github.com/stonith404/pingvin-share/commit/7827b687fa022e86a2643e7a1951af8c7e80608c))
|
||||
* add note to language picker ([7f0c31c](https://github.com/stonith404/pingvin-share/commit/7f0c31c2e09b3ee9aae6c3dfb54fac2f2b1dfe23))
|
||||
* add share url alias `/s` ([231a2e9](https://github.com/stonith404/pingvin-share/commit/231a2e95b9734cf4704454e1945698753dbb378b))
|
||||
* localization ([#196](https://github.com/stonith404/pingvin-share/issues/196)) ([b9f6e3b](https://github.com/stonith404/pingvin-share/commit/b9f6e3bd08dcfc050048fba582b35958bc7b6184))
|
||||
* update default value of `maxSize` from `1073741824` to `1000000000` ([389dc87](https://github.com/stonith404/pingvin-share/commit/389dc87cac775d916d0cff9b71d3c5ff90bfe916))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* confusion between GB and GiB ([5816b39](https://github.com/stonith404/pingvin-share/commit/5816b39fc6ef6fe6b7cf8e7925aa297561f5b796))
|
||||
* mistakes in English translations ([70b425b](https://github.com/stonith404/pingvin-share/commit/70b425b3807be79a3b518cc478996c71dffcf986))
|
||||
* wrong layout if button text is too long in modals ([f4c88ae](https://github.com/stonith404/pingvin-share/commit/f4c88aeb0823c2c18535c25fcf8e16afa8b53a56))
|
||||
|
||||
### [0.16.1](https://github.com/stonith404/pingvin-share/compare/v0.16.0...v0.16.1) (2023-07-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Adding reverse share ability to copy the link ([#191](https://github.com/stonith404/pingvin-share/issues/191)) ([7574eb3](https://github.com/stonith404/pingvin-share/commit/7574eb3191f21aadd64f436e9e7c78d3e3973a07)), closes [#178](https://github.com/stonith404/pingvin-share/issues/178) [#181](https://github.com/stonith404/pingvin-share/issues/181)
|
||||
* Adding reverse shares' shares a clickable link ([#190](https://github.com/stonith404/pingvin-share/issues/190)) ([0276294](https://github.com/stonith404/pingvin-share/commit/0276294f5219a7edcc762bc52391b6720cfd741d))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* set link default value to random ([#192](https://github.com/stonith404/pingvin-share/issues/192)) ([a1ea7c0](https://github.com/stonith404/pingvin-share/commit/a1ea7c026594a54eafd52f764eecbf06e1bb4d4e)), closes [#178](https://github.com/stonith404/pingvin-share/issues/178) [#181](https://github.com/stonith404/pingvin-share/issues/181)
|
||||
|
||||
## [0.16.0](https://github.com/stonith404/pingvin-share/compare/v0.15.0...v0.16.0) (2023-07-09)
|
||||
|
||||
|
||||
|
||||
@@ -49,4 +49,5 @@ WORKDIR /opt/app
|
||||
EXPOSE 3000
|
||||
HEALTHCHECK --interval=10s --timeout=3s CMD curl -f http://localhost:3000/api/health || exit 1
|
||||
|
||||
CMD cp -rn /tmp/img /opt/app/frontend/public && node frontend/server.js & cd backend && npm run prod
|
||||
# HOSTNAME=0.0.0.0 fixes https://github.com/vercel/next.js/issues/51684. It can be removed as soon as the issue is fixed
|
||||
CMD cp -rn /tmp/img /opt/app/frontend/public && HOSTNAME=0.0.0.0 node frontend/server.js & cd backend && npm run prod
|
||||
13
README.md
13
README.md
@@ -144,4 +144,15 @@ For installation specific configuration, you can use environment variables. The
|
||||
|
||||
## 🖤 Contribute
|
||||
|
||||
You're very welcome to contribute to Pingvin Share! Follow the [contribution guide](/CONTRIBUTING.md) to get started.
|
||||
### Translations
|
||||
|
||||
You can help to translate Pingvin Share into your language.
|
||||
On [Crowdin](https://crowdin.com/project/pingvin-share) you can easily translate Pingvin Share online.
|
||||
|
||||
Is your language not on Crowdin? Feel free to [Request it](https://github.com/stonith404/pingvin-share/issues/new?assignees=&labels=language-request&projects=&template=language-request.yml&title=%F0%9F%8C%90+Language+request%3A+%3Clanguage+name+in+english%3E).
|
||||
|
||||
Any issues while translating? Feel free to participate in the [Localization discussion](https://github.com/stonith404/pingvin-share/discussions/198).
|
||||
|
||||
### Project
|
||||
|
||||
You're very welcome to contribute to Pingvin Share! Please follow the [contribution guide](/CONTRIBUTING.md) to get started.
|
||||
|
||||
4685
backend/package-lock.json
generated
4685
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,28 +1,28 @@
|
||||
{
|
||||
"name": "pingvin-share-backend",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.4",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"dev": "cross-env NODE_ENV=development nest start --watch",
|
||||
"prod": "prisma migrate deploy && prisma db seed && node dist/src/main",
|
||||
"lint": "eslint 'src/**/*.ts'",
|
||||
"format": "prettier --write 'src/**/*.ts'",
|
||||
"format": "prettier --end-of-line=auto --write 'src/**/*.ts'",
|
||||
"test:system": "prisma migrate reset -f && nest start & wait-on http://localhost:8080/api/configs && newman run ./test/newman-system-tests.json"
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "ts-node prisma/seed/config.seed.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^9.3.9",
|
||||
"@nestjs/config": "^2.3.1",
|
||||
"@nestjs/core": "^9.3.9",
|
||||
"@nestjs/jwt": "^10.0.2",
|
||||
"@nestjs/passport": "^9.0.3",
|
||||
"@nestjs/platform-express": "^9.3.9",
|
||||
"@nestjs/schedule": "^2.2.0",
|
||||
"@nestjs/swagger": "^6.2.1",
|
||||
"@nestjs/throttler": "^4.0.0",
|
||||
"@prisma/client": "^4.11.0",
|
||||
"@nestjs/common": "^10.1.2",
|
||||
"@nestjs/config": "^3.0.0",
|
||||
"@nestjs/core": "^10.1.2",
|
||||
"@nestjs/jwt": "^10.1.0",
|
||||
"@nestjs/passport": "^10.0.0",
|
||||
"@nestjs/platform-express": "^10.1.2",
|
||||
"@nestjs/schedule": "^3.0.1",
|
||||
"@nestjs/swagger": "^7.1.4",
|
||||
"@nestjs/throttler": "^4.2.1",
|
||||
"@prisma/client": "^5.0.0",
|
||||
"archiver": "^5.3.1",
|
||||
"argon2": "^0.30.3",
|
||||
"body-parser": "^1.20.2",
|
||||
@@ -33,48 +33,48 @@
|
||||
"cookie-parser": "^1.4.6",
|
||||
"mime-types": "^2.1.35",
|
||||
"moment": "^2.29.4",
|
||||
"nodemailer": "^6.9.1",
|
||||
"nodemailer": "^6.9.4",
|
||||
"otplib": "^12.0.1",
|
||||
"passport": "^0.6.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"qrcode-svg": "^1.1.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^4.4.0",
|
||||
"rxjs": "^7.8.0",
|
||||
"sharp": "^0.31.3",
|
||||
"rimraf": "^5.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"sharp": "^0.32.4",
|
||||
"ts-node": "^10.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^9.2.0",
|
||||
"@nestjs/schematics": "^9.0.4",
|
||||
"@nestjs/testing": "^9.3.9",
|
||||
"@types/archiver": "^5.3.1",
|
||||
"@nestjs/cli": "^10.1.10",
|
||||
"@nestjs/schematics": "^10.0.1",
|
||||
"@nestjs/testing": "^10.1.2",
|
||||
"@types/archiver": "^5.3.2",
|
||||
"@types/clamscan": "^2.0.4",
|
||||
"@types/cookie-parser": "^1.4.3",
|
||||
"@types/cron": "^2.0.0",
|
||||
"@types/cron": "^2.0.1",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/mime-types": "^2.1.1",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^18.15.0",
|
||||
"@types/nodemailer": "^6.4.7",
|
||||
"@types/passport-jwt": "^3.0.8",
|
||||
"@types/node": "^20.4.5",
|
||||
"@types/nodemailer": "^6.4.9",
|
||||
"@types/passport-jwt": "^3.0.9",
|
||||
"@types/qrcode-svg": "^1.1.1",
|
||||
"@types/sharp": "^0.31.1",
|
||||
"@types/supertest": "^2.0.12",
|
||||
"@typescript-eslint/eslint-plugin": "^5.54.1",
|
||||
"@typescript-eslint/parser": "^5.54.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.2.0",
|
||||
"@typescript-eslint/parser": "^6.2.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.35.0",
|
||||
"eslint-config-prettier": "^8.7.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint": "^8.46.0",
|
||||
"eslint-config-prettier": "^8.9.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"newman": "^5.3.2",
|
||||
"prettier": "^2.8.4",
|
||||
"prisma": "^4.11.0",
|
||||
"prettier": "^3.0.0",
|
||||
"prisma": "^5.0.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ts-loader": "^9.4.2",
|
||||
"tsconfig-paths": "4.1.2",
|
||||
"typescript": "^4.9.5",
|
||||
"ts-loader": "^9.4.4",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typescript": "^5.1.6",
|
||||
"wait-on": "^7.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
defaultValue String @default("")
|
||||
value String?
|
||||
description String
|
||||
obscured Boolean @default(false)
|
||||
secret Boolean @default(true)
|
||||
locked Boolean @default(false)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Prisma, PrismaClient } from "@prisma/client";
|
||||
import * as crypto from "crypto";
|
||||
|
||||
const configVariables: ConfigVariables = {
|
||||
internal: {
|
||||
jwtSecret: {
|
||||
description: "Long random string used to sign JWT tokens",
|
||||
type: "string",
|
||||
defaultValue: crypto.randomBytes(256).toString("base64"),
|
||||
locked: true,
|
||||
@@ -11,20 +11,16 @@ const configVariables: ConfigVariables = {
|
||||
},
|
||||
general: {
|
||||
appName: {
|
||||
description: "Name of the application",
|
||||
type: "string",
|
||||
defaultValue: "Pingvin Share",
|
||||
secret: false,
|
||||
},
|
||||
appUrl: {
|
||||
description: "On which URL Pingvin Share is available",
|
||||
type: "string",
|
||||
defaultValue: "http://localhost:3000",
|
||||
|
||||
secret: false,
|
||||
},
|
||||
showHomePage: {
|
||||
description: "Whether to show the home page",
|
||||
type: "boolean",
|
||||
defaultValue: "true",
|
||||
secret: false,
|
||||
@@ -32,84 +28,64 @@ const configVariables: ConfigVariables = {
|
||||
},
|
||||
share: {
|
||||
allowRegistration: {
|
||||
description: "Whether registration is allowed",
|
||||
type: "boolean",
|
||||
defaultValue: "true",
|
||||
|
||||
secret: false,
|
||||
},
|
||||
allowUnauthenticatedShares: {
|
||||
description: "Whether unauthorized users can create shares",
|
||||
type: "boolean",
|
||||
defaultValue: "false",
|
||||
|
||||
secret: false,
|
||||
},
|
||||
maxSize: {
|
||||
description: "Maximum share size in bytes",
|
||||
type: "number",
|
||||
defaultValue: "1073741824",
|
||||
|
||||
defaultValue: "1000000000",
|
||||
secret: false,
|
||||
},
|
||||
zipCompressionLevel: {
|
||||
type: "number",
|
||||
defaultValue: "9",
|
||||
},
|
||||
},
|
||||
email: {
|
||||
enableShareEmailRecipients: {
|
||||
description:
|
||||
"Whether to allow emails to share recipients. Only enable this if you have enabled SMTP.",
|
||||
type: "boolean",
|
||||
defaultValue: "false",
|
||||
|
||||
secret: false,
|
||||
},
|
||||
shareRecipientsSubject: {
|
||||
description:
|
||||
"Subject of the email which gets sent to the share recipients.",
|
||||
type: "string",
|
||||
defaultValue: "Files shared with you",
|
||||
},
|
||||
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",
|
||||
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 🐧",
|
||||
},
|
||||
reverseShareSubject: {
|
||||
description:
|
||||
"Subject of the email which gets sent when someone created a share with your reverse share link.",
|
||||
type: "string",
|
||||
defaultValue: "Reverse share link used",
|
||||
},
|
||||
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",
|
||||
defaultValue:
|
||||
"Hey!\n\nA share was just created with your reverse share link: {shareUrl}\n\nShared securely with Pingvin Share 🐧",
|
||||
},
|
||||
resetPasswordSubject: {
|
||||
description:
|
||||
"Subject of the email which gets sent when a user requests a password reset.",
|
||||
type: "string",
|
||||
defaultValue: "Pingvin Share password reset",
|
||||
},
|
||||
resetPasswordMessage: {
|
||||
description:
|
||||
"Message which gets sent when a user requests a password reset. {url} will be replaced with the reset password URL.",
|
||||
type: "text",
|
||||
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 🐧",
|
||||
},
|
||||
inviteSubject: {
|
||||
description:
|
||||
"Subject of the email which gets sent when an admin invites an user.",
|
||||
type: "string",
|
||||
defaultValue: "Pingvin Share invite",
|
||||
},
|
||||
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",
|
||||
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 🐧",
|
||||
@@ -117,34 +93,27 @@ const configVariables: ConfigVariables = {
|
||||
},
|
||||
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.",
|
||||
type: "boolean",
|
||||
defaultValue: "false",
|
||||
secret: false,
|
||||
},
|
||||
host: {
|
||||
description: "Host of the SMTP server",
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
},
|
||||
port: {
|
||||
description: "Port of the SMTP server",
|
||||
type: "number",
|
||||
defaultValue: "0",
|
||||
},
|
||||
email: {
|
||||
description: "Email address which the emails get sent from",
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
},
|
||||
username: {
|
||||
description: "Username of the SMTP server",
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
},
|
||||
password: {
|
||||
description: "Password of the SMTP server",
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
obscured: true,
|
||||
|
||||
@@ -14,9 +14,6 @@ export class AdminConfigDTO extends ConfigDTO {
|
||||
@Expose()
|
||||
updatedAt: Date;
|
||||
|
||||
@Expose()
|
||||
description: string;
|
||||
|
||||
@Expose()
|
||||
obscured: boolean;
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
export const DATA_DIRECTORY = process.env.DATA_DIRECTORY || "./data";
|
||||
export const SHARE_DIRECTORY = `${DATA_DIRECTORY}/uploads/shares`
|
||||
export const DATABASE_URL = process.env.DATABASE_URL || "file:../data/pingvin-share.db?connection_limit=1";
|
||||
export const CLAMAV_HOST = process.env.CLAMAV_HOST || (process.env.NODE_ENV == "docker" ? "clamav" : "127.0.0.1");
|
||||
export const CLAMAV_PORT = parseInt(process.env.CLAMAV_PORT) || 3310;
|
||||
export const SHARE_DIRECTORY = `${DATA_DIRECTORY}/uploads/shares`;
|
||||
export const DATABASE_URL =
|
||||
process.env.DATABASE_URL ||
|
||||
"file:../data/pingvin-share.db?connection_limit=1";
|
||||
export const CLAMAV_HOST =
|
||||
process.env.CLAMAV_HOST ||
|
||||
(process.env.NODE_ENV == "docker" ? "clamav" : "127.0.0.1");
|
||||
export const CLAMAV_PORT = parseInt(process.env.CLAMAV_PORT) || 3310;
|
||||
|
||||
@@ -54,7 +54,7 @@ export class EmailService {
|
||||
if (!this.config.get("email.enableShareEmailRecipients"))
|
||||
throw new InternalServerErrorException("Email service disabled");
|
||||
|
||||
const shareUrl = `${this.config.get("general.appUrl")}/share/${shareId}`;
|
||||
const shareUrl = `${this.config.get("general.appUrl")}/s/${shareId}`;
|
||||
|
||||
await this.sendMail(
|
||||
recipientEmail,
|
||||
@@ -75,7 +75,7 @@ export class EmailService {
|
||||
}
|
||||
|
||||
async sendMailToReverseShareCreator(recipientEmail: string, shareId: string) {
|
||||
const shareUrl = `${this.config.get("general.appUrl")}/share/${shareId}`;
|
||||
const shareUrl = `${this.config.get("general.appUrl")}/s/${shareId}`;
|
||||
|
||||
await this.sendMail(
|
||||
recipientEmail,
|
||||
|
||||
@@ -10,6 +10,9 @@ export class ReverseShareDTO {
|
||||
@Expose()
|
||||
shareExpiration: Date;
|
||||
|
||||
@Expose()
|
||||
token: string;
|
||||
|
||||
from(partial: Partial<ReverseShareDTO>) {
|
||||
return plainToClass(ReverseShareDTO, partial, {
|
||||
excludeExtraneousValues: true,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Expose, plainToClass, Type } from "class-transformer";
|
||||
import { ShareDTO } from "./share.dto";
|
||||
import {FileDTO} from "../../file/dto/file.dto";
|
||||
import {OmitType} from "@nestjs/swagger";
|
||||
import { FileDTO } from "../../file/dto/file.dto";
|
||||
import { OmitType } from "@nestjs/swagger";
|
||||
|
||||
export class MyShareDTO extends OmitType(ShareDTO, [
|
||||
"files",
|
||||
@@ -30,4 +30,4 @@ export class MyShareDTO extends OmitType(ShareDTO, [
|
||||
plainToClass(MyShareDTO, part, { excludeExtraneousValues: true })
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ export class ShareService {
|
||||
|
||||
const files = await this.prisma.file.findMany({ where: { shareId } });
|
||||
const archive = archiver("zip", {
|
||||
zlib: { level: 9 },
|
||||
zlib: { level: this.config.get("share.zipCompressionLevel") },
|
||||
});
|
||||
const writeStream = fs.createWriteStream(`${path}/archive.zip`);
|
||||
|
||||
|
||||
4
crowdin.yml
Normal file
4
crowdin.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
files:
|
||||
- source: /frontend/src/i18n/translations/en-US.ts
|
||||
translation: /%original_path%/%locale%.ts
|
||||
pull_request_title: "chore(translations): update translations via Crowdin"
|
||||
@@ -1,5 +1,10 @@
|
||||
{
|
||||
"extends": ["eslint-config-next", "eslint:recommended", "prettier"],
|
||||
"extends": [
|
||||
"next",
|
||||
"eslint-config-next",
|
||||
"eslint:recommended",
|
||||
"prettier"
|
||||
],
|
||||
"plugins": ["react"],
|
||||
"rules": {
|
||||
"quotes": ["warn", "double", { "allowTemplateLiterals": true }],
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
|
||||
const { version } = require('./package.json');
|
||||
|
||||
const withPWA = require("next-pwa")({
|
||||
|
||||
2410
frontend/package-lock.json
generated
2410
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,52 +1,53 @@
|
||||
{
|
||||
"name": "pingvin-share-frontend",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.4",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"format": "prettier --write \"src/**/*.ts*\""
|
||||
"format": "prettier --end-of-line=auto --write \"src/**/*.ts*\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/server": "^11.10.0",
|
||||
"@mantine/core": "^6.0.1",
|
||||
"@mantine/dropzone": "^6.0.1",
|
||||
"@mantine/form": "^6.0.1",
|
||||
"@mantine/hooks": "^6.0.1",
|
||||
"@mantine/modals": "^6.0.1",
|
||||
"@mantine/next": "^6.0.1",
|
||||
"@mantine/notifications": "^6.0.1",
|
||||
"axios": "^1.3.4",
|
||||
"cookies-next": "^2.1.1",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/server": "^11.11.0",
|
||||
"@mantine/core": "^6.0.17",
|
||||
"@mantine/dropzone": "^6.0.17",
|
||||
"@mantine/form": "^6.0.17",
|
||||
"@mantine/hooks": "^6.0.17",
|
||||
"@mantine/modals": "^6.0.17",
|
||||
"@mantine/next": "^6.0.17",
|
||||
"@mantine/notifications": "^6.0.17",
|
||||
"axios": "^1.4.0",
|
||||
"cookies-next": "^2.1.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"jose": "^4.13.1",
|
||||
"jose": "^4.14.4",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"mime-types": "^2.1.35",
|
||||
"moment": "^2.29.4",
|
||||
"next": "^13.2.4",
|
||||
"next": "^13.4.12",
|
||||
"next-cookies": "^2.0.3",
|
||||
"next-http-proxy-middleware": "^1.2.5",
|
||||
"next-pwa": "^5.6.0",
|
||||
"p-limit": "^4.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^4.8.0",
|
||||
"sharp": "^0.31.3",
|
||||
"yup": "^1.0.2"
|
||||
"react-icons": "^4.10.1",
|
||||
"react-intl": "^6.4.4",
|
||||
"sharp": "^0.32.4",
|
||||
"yup": "^1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mime-types": "^2.1.1",
|
||||
"@types/node": "18.15.0",
|
||||
"@types/react": "18.0.28",
|
||||
"@types/react-dom": "18.0.11",
|
||||
"axios": "^1.3.4",
|
||||
"eslint": "8.35.0",
|
||||
"eslint-config-next": "^13.2.4",
|
||||
"eslint-config-prettier": "^8.7.0",
|
||||
"prettier": "^2.8.4",
|
||||
"tar": "^6.1.13",
|
||||
"typescript": "^4.9.5"
|
||||
"@types/node": "20.4.5",
|
||||
"@types/react": "18.2.17",
|
||||
"@types/react-dom": "18.2.7",
|
||||
"axios": "^1.4.0",
|
||||
"eslint": "8.46.0",
|
||||
"eslint-config-next": "^13.4.12",
|
||||
"eslint-config-prettier": "^8.9.0",
|
||||
"prettier": "^3.0.0",
|
||||
"tar": "^6.1.15",
|
||||
"typescript": "^5.1.6"
|
||||
}
|
||||
}
|
||||
|
||||
36
frontend/src/components/account/LanguagePicker.tsx
Normal file
36
frontend/src/components/account/LanguagePicker.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Select } from "@mantine/core";
|
||||
import { getCookie, setCookie } from "cookies-next";
|
||||
import { useState } from "react";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
import { LOCALES } from "../../i18n/locales";
|
||||
|
||||
const LanguagePicker = () => {
|
||||
const t = useTranslate();
|
||||
const [selectedLanguage, setSelectedLanguage] = useState(
|
||||
getCookie("language")?.toString()
|
||||
);
|
||||
|
||||
const languages = Object.values(LOCALES).map((locale) => ({
|
||||
value: locale.code,
|
||||
label: locale.name,
|
||||
}));
|
||||
return (
|
||||
<Select
|
||||
value={selectedLanguage}
|
||||
description={t("account.card.language.description")}
|
||||
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 { useState } from "react";
|
||||
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 preferences = usePreferences();
|
||||
const [colorScheme, setColorScheme] = useState(
|
||||
preferences.get("colorScheme")
|
||||
userPreferences.get("colorScheme")
|
||||
);
|
||||
const { toggleColorScheme } = useMantineColorScheme();
|
||||
const systemColorScheme = useColorScheme();
|
||||
@@ -23,7 +23,7 @@ const ThemeSwitcher = () => {
|
||||
<SegmentedControl
|
||||
value={colorScheme}
|
||||
onChange={(value) => {
|
||||
preferences.set("colorScheme", value);
|
||||
userPreferences.set("colorScheme", value);
|
||||
setColorScheme(value);
|
||||
toggleColorScheme(
|
||||
value == "system" ? systemColorScheme : (value as ColorScheme)
|
||||
@@ -34,7 +34,9 @@ const ThemeSwitcher = () => {
|
||||
label: (
|
||||
<Center>
|
||||
<TbMoon size={16} />
|
||||
<Box ml={10}>Dark</Box>
|
||||
<Box ml={10}>
|
||||
<FormattedMessage id="account.theme.dark" />
|
||||
</Box>
|
||||
</Center>
|
||||
),
|
||||
value: "dark",
|
||||
@@ -43,7 +45,9 @@ const ThemeSwitcher = () => {
|
||||
label: (
|
||||
<Center>
|
||||
<TbSun size={16} />
|
||||
<Box ml={10}>Light</Box>
|
||||
<Box ml={10}>
|
||||
<FormattedMessage id="account.theme.light" />
|
||||
</Box>
|
||||
</Center>
|
||||
),
|
||||
value: "light",
|
||||
@@ -52,7 +56,9 @@ const ThemeSwitcher = () => {
|
||||
label: (
|
||||
<Center>
|
||||
<TbDeviceLaptop size={16} />
|
||||
<Box ml={10}>System</Box>
|
||||
<Box ml={10}>
|
||||
<FormattedMessage id="account.theme.system" />
|
||||
</Box>
|
||||
</Center>
|
||||
),
|
||||
value: "system",
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import {
|
||||
Button,
|
||||
Center,
|
||||
Col,
|
||||
Grid,
|
||||
Group,
|
||||
Image,
|
||||
Stack,
|
||||
Text,
|
||||
@@ -12,7 +11,11 @@ import {
|
||||
import { useForm, yupResolver } from "@mantine/form";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import * as yup from "yup";
|
||||
import useTranslate, {
|
||||
translateOutsideContext,
|
||||
} from "../../hooks/useTranslate.hook";
|
||||
import authService from "../../services/auth.service";
|
||||
import toast from "../../utils/toast.util";
|
||||
|
||||
@@ -25,8 +28,9 @@ const showEnableTotpModal = (
|
||||
password: string;
|
||||
}
|
||||
) => {
|
||||
const t = translateOutsideContext();
|
||||
return modals.openModal({
|
||||
title: "Enable TOTP",
|
||||
title: t("account.modal.totp.title"),
|
||||
children: (
|
||||
<CreateEnableTotpModal options={options} refreshUser={refreshUser} />
|
||||
),
|
||||
@@ -45,6 +49,7 @@ const CreateEnableTotpModal = ({
|
||||
refreshUser: () => {};
|
||||
}) => {
|
||||
const modals = useModals();
|
||||
const t = useTranslate();
|
||||
|
||||
const validationSchema = yup.object().shape({
|
||||
code: yup
|
||||
@@ -66,14 +71,19 @@ const CreateEnableTotpModal = ({
|
||||
<div>
|
||||
<Center>
|
||||
<Stack>
|
||||
<Text>Step 1: Add your authenticator</Text>
|
||||
<Text>
|
||||
<FormattedMessage id="account.modal.totp.step1" />
|
||||
</Text>
|
||||
<Image src={options.qrCode} alt="QR Code" />
|
||||
|
||||
<Center>
|
||||
<span>OR</span>
|
||||
<span>
|
||||
{" "}
|
||||
<FormattedMessage id="common.text.or" />
|
||||
</span>
|
||||
</Center>
|
||||
|
||||
<Tooltip label="Click to copy">
|
||||
<Tooltip label={t("account.modal.totp.clickToCopy")}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(options.secret);
|
||||
@@ -84,38 +94,42 @@ const CreateEnableTotpModal = ({
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Center>
|
||||
<Text fz="xs">Enter manually</Text>
|
||||
<Text fz="xs"></Text>
|
||||
</Center>
|
||||
|
||||
<Text>Step 2: Validate your code</Text>
|
||||
<Text>
|
||||
<FormattedMessage id="account.modal.totp.step2" />
|
||||
</Text>
|
||||
|
||||
<form
|
||||
onSubmit={form.onSubmit((values) => {
|
||||
authService
|
||||
.verifyTOTP(values.code, options.password)
|
||||
.then(() => {
|
||||
toast.success("Successfully enabled TOTP");
|
||||
toast.success(t("account.notify.totp.enable"));
|
||||
modals.closeAll();
|
||||
refreshUser();
|
||||
})
|
||||
.catch(toast.axiosError);
|
||||
})}
|
||||
>
|
||||
<Grid align="flex-end">
|
||||
<Col xs={9}>
|
||||
<TextInput
|
||||
variant="filled"
|
||||
label="Code"
|
||||
placeholder="******"
|
||||
{...form.getInputProps("code")}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={3}>
|
||||
<Button variant="outline" type="submit">
|
||||
Verify
|
||||
</Button>
|
||||
</Col>
|
||||
</Grid>
|
||||
<Group align="end">
|
||||
<TextInput
|
||||
style={{ flex: "1" }}
|
||||
variant="filled"
|
||||
label={t("account.modal.totp.code")}
|
||||
placeholder="******"
|
||||
{...form.getInputProps("code")}
|
||||
/>
|
||||
|
||||
<Button
|
||||
style={{ flex: "0 0 auto" }}
|
||||
variant="outline"
|
||||
type="submit"
|
||||
>
|
||||
<FormattedMessage id="account.modal.totp.verify" />
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
</Stack>
|
||||
</Center>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Stack, TextInput } from "@mantine/core";
|
||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||
import { translateOutsideContext } from "../../hooks/useTranslate.hook";
|
||||
|
||||
const showReverseShareLinkModal = (
|
||||
modals: ModalsContextProps,
|
||||
reverseShareToken: string,
|
||||
appUrl: string
|
||||
) => {
|
||||
const t = translateOutsideContext();
|
||||
const link = `${appUrl}/upload/${reverseShareToken}`;
|
||||
return modals.openModal({
|
||||
title: t("account.reverseShares.modal.reverse-share-link"),
|
||||
children: (
|
||||
<Stack align="stretch">
|
||||
<TextInput variant="filled" value={link} />
|
||||
</Stack>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
export default showReverseShareLinkModal;
|
||||
@@ -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 { MyShare } from "../../types/share.type";
|
||||
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 CopyTextField from "../upload/CopyTextField";
|
||||
import { FileMetaData } from "../../types/File.type";
|
||||
|
||||
const showShareInformationsModal = (
|
||||
modals: ModalsContextProps,
|
||||
@@ -12,7 +14,8 @@ const showShareInformationsModal = (
|
||||
appUrl: string,
|
||||
maxShareSize: number
|
||||
) => {
|
||||
const link = `${appUrl}/share/${share.id}`;
|
||||
const t = translateOutsideContext();
|
||||
const link = `${appUrl}/s/${share.id}`;
|
||||
|
||||
let shareSize: number = 0;
|
||||
for (let file of share.files as FileMetaData[])
|
||||
@@ -29,34 +32,45 @@ const showShareInformationsModal = (
|
||||
: moment(share.expiration).format("LLL");
|
||||
|
||||
return modals.openModal({
|
||||
title: "Share informations",
|
||||
title: t("account.shares.modal.share-informations"),
|
||||
|
||||
children: (
|
||||
<Stack align="stretch" spacing="md">
|
||||
<Text size="sm" color="lightgray">
|
||||
<b>ID:</b> {share.id}
|
||||
<b>
|
||||
<FormattedMessage id="account.shares.table.id" />:{" "}
|
||||
</b>
|
||||
{share.id}
|
||||
</Text>
|
||||
|
||||
<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 size="sm" color="lightgray">
|
||||
<b>Created at:</b> {formattedCreatedAt}
|
||||
<b>
|
||||
<FormattedMessage id="account.shares.table.createdAt" />:{" "}
|
||||
</b>
|
||||
{formattedCreatedAt}
|
||||
</Text>
|
||||
|
||||
<Text size="sm" color="lightgray">
|
||||
<b>Expires at:</b> {formattedExpiration}
|
||||
<b>
|
||||
<FormattedMessage id="account.shares.table.expiresAt" />:{" "}
|
||||
</b>
|
||||
{formattedExpiration}
|
||||
</Text>
|
||||
|
||||
<Divider />
|
||||
|
||||
<CopyTextField link={link} />
|
||||
|
||||
<Divider />
|
||||
|
||||
<Text size="sm" color="lightgray">
|
||||
<b>Size:</b> {formattedShareSize} / {formattedMaxShareSize} (
|
||||
<b>
|
||||
<FormattedMessage id="account.shares.table.size" />:{" "}
|
||||
</b>
|
||||
{formattedShareSize} / {formattedMaxShareSize} (
|
||||
{shareSizeProgress.toFixed(1)}%)
|
||||
</Text>
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { Stack, TextInput } from "@mantine/core";
|
||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||
import { translateOutsideContext } from "../../hooks/useTranslate.hook";
|
||||
|
||||
const showShareLinkModal = (
|
||||
modals: ModalsContextProps,
|
||||
shareId: string,
|
||||
appUrl: string
|
||||
) => {
|
||||
const link = `${appUrl}/share/${shareId}`;
|
||||
const t = translateOutsideContext();
|
||||
const link = `${appUrl}/s/${shareId}`;
|
||||
return modals.openModal({
|
||||
title: "Share link",
|
||||
title: t("account.shares.modal.share-link"),
|
||||
children: (
|
||||
<Stack align="stretch">
|
||||
<TextInput variant="filled" value={link} />
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "@mantine/core";
|
||||
import Link from "next/link";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import useConfig from "../../../hooks/config.hook";
|
||||
import Logo from "../../Logo";
|
||||
|
||||
@@ -42,7 +43,7 @@ const ConfigurationHeader = ({
|
||||
</Link>
|
||||
<MediaQuery smallerThan="sm" styles={{ display: "none" }}>
|
||||
<Button variant="light" component={Link} href="/admin">
|
||||
Go back
|
||||
<FormattedMessage id="common.button.go-back" />
|
||||
</Button>
|
||||
</MediaQuery>
|
||||
</Group>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import Link from "next/link";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { TbAt, TbMail, TbShare, TbSquare } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
const categories = [
|
||||
{ name: "General", icon: <TbSquare /> },
|
||||
@@ -53,7 +54,7 @@ const ConfigurationNavBar = ({
|
||||
>
|
||||
<Navbar.Section>
|
||||
<Text size="xs" color="dimmed" mb="sm">
|
||||
Configuration
|
||||
<FormattedMessage id="admin.config.title" />
|
||||
</Text>
|
||||
<Stack spacing="xs">
|
||||
{categories.map((category) => (
|
||||
@@ -79,7 +80,11 @@ const ConfigurationNavBar = ({
|
||||
>
|
||||
{category.icon}
|
||||
</ThemeIcon>
|
||||
<Text size="sm">{category.name}</Text>
|
||||
<Text size="sm">
|
||||
<FormattedMessage
|
||||
id={`admin.config.category.${category.name.toLowerCase()}`}
|
||||
/>
|
||||
</Text>
|
||||
</Group>
|
||||
</Box>
|
||||
))}
|
||||
@@ -87,7 +92,7 @@ const ConfigurationNavBar = ({
|
||||
</Navbar.Section>
|
||||
<MediaQuery largerThan="sm" styles={{ display: "none" }}>
|
||||
<Button mt="xl" variant="light" component={Link} href="/admin">
|
||||
Go back
|
||||
<FormattedMessage id="common.button.go-back" />
|
||||
</Button>
|
||||
</MediaQuery>
|
||||
</Navbar>
|
||||
|
||||
@@ -2,6 +2,8 @@ import { Box, FileInput, Group, Stack, Text, Title } from "@mantine/core";
|
||||
import { useMediaQuery } from "@mantine/hooks";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { TbUpload } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import useTranslate from "../../../hooks/useTranslate.hook";
|
||||
|
||||
const LogoConfigInput = ({
|
||||
logo,
|
||||
@@ -11,14 +13,16 @@ const LogoConfigInput = ({
|
||||
setLogo: Dispatch<SetStateAction<File | null>>;
|
||||
}) => {
|
||||
const isMobile = useMediaQuery("(max-width: 560px)");
|
||||
const t = useTranslate();
|
||||
|
||||
return (
|
||||
<Group position="apart">
|
||||
<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">
|
||||
Change your logo by uploading a new image. The image must be a PNG and
|
||||
should have the format 1:1.
|
||||
<FormattedMessage id="admin.config.general.logo.description" />
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack></Stack>
|
||||
@@ -29,7 +33,7 @@ const LogoConfigInput = ({
|
||||
value={logo}
|
||||
onChange={(v) => setLogo(v)}
|
||||
accept=".png"
|
||||
placeholder="Pick image"
|
||||
placeholder={t("admin.config.general.logo.placeholder")}
|
||||
/>
|
||||
</Box>
|
||||
</Group>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Button, Stack, Text, Textarea } from "@mantine/core";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import { useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import useUser from "../../../hooks/user.hook";
|
||||
import configService from "../../../services/config.service";
|
||||
import toast from "../../../utils/toast.util";
|
||||
@@ -65,7 +66,7 @@ const TestEmailButton = ({
|
||||
}
|
||||
}}
|
||||
>
|
||||
Send test email
|
||||
<FormattedMessage id="admin.config.smtp.button.test" />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useModals } from "@mantine/modals";
|
||||
import { TbCheck, TbEdit, TbTrash } from "react-icons/tb";
|
||||
import User from "../../../types/user.type";
|
||||
import showUpdateUserModal from "./showUpdateUserModal";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
const ManageUserTable = ({
|
||||
users,
|
||||
@@ -22,9 +23,15 @@ const ManageUserTable = ({
|
||||
<Table verticalSpacing="sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Email</th>
|
||||
<th>Admin</th>
|
||||
<th>
|
||||
<FormattedMessage id="admin.users.table.username" />
|
||||
</th>
|
||||
<th>
|
||||
<FormattedMessage id="admin.users.table.email" />
|
||||
</th>
|
||||
<th>
|
||||
<FormattedMessage id="admin.users.table.admin" />
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -8,7 +8,9 @@ import {
|
||||
} from "@mantine/core";
|
||||
import { useForm, yupResolver } from "@mantine/form";
|
||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import * as yup from "yup";
|
||||
import useTranslate from "../../../hooks/useTranslate.hook";
|
||||
import userService from "../../../services/user.service";
|
||||
import toast from "../../../utils/toast.util";
|
||||
|
||||
@@ -34,6 +36,7 @@ const Body = ({
|
||||
smtpEnabled: boolean;
|
||||
getUsers: () => void;
|
||||
}) => {
|
||||
const t = useTranslate();
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
username: "",
|
||||
@@ -44,9 +47,14 @@ const Body = ({
|
||||
},
|
||||
validate: yupResolver(
|
||||
yup.object().shape({
|
||||
email: yup.string().email(),
|
||||
username: yup.string().min(3),
|
||||
password: yup.string().min(8).optional(),
|
||||
email: yup.string().email(t("common.error.invalid-email")),
|
||||
username: yup
|
||||
.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>
|
||||
<TextInput label="Username" {...form.getInputProps("username")} />
|
||||
<TextInput label="Email" {...form.getInputProps("email")} />
|
||||
<TextInput
|
||||
label={t("admin.users.modal.create.username")}
|
||||
{...form.getInputProps("username")}
|
||||
/>
|
||||
<TextInput
|
||||
label={t("admin.users.modal.create.email")}
|
||||
{...form.getInputProps("email")}
|
||||
/>
|
||||
{smtpEnabled && (
|
||||
<Switch
|
||||
mt="xs"
|
||||
labelPosition="left"
|
||||
label="Set password manually"
|
||||
description="If not checked, the user will receive an email with a link to set their password."
|
||||
label={t("admin.users.modal.create.manual-password")}
|
||||
description={t(
|
||||
"admin.users.modal.create.manual-password.description"
|
||||
)}
|
||||
{...form.getInputProps("setPasswordManually", {
|
||||
type: "checkbox",
|
||||
})}
|
||||
@@ -80,7 +96,7 @@ const Body = ({
|
||||
)}
|
||||
{(form.values.setPasswordManually || !smtpEnabled) && (
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
label={t("admin.users.modal.create.password")}
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
)}
|
||||
@@ -93,12 +109,14 @@ const Body = ({
|
||||
}}
|
||||
mt="xs"
|
||||
labelPosition="left"
|
||||
label="Admin privileges"
|
||||
description="If checked, the user will be able to access the admin panel."
|
||||
label={t("admin.users.modal.create.admin")}
|
||||
description={t("admin.users.modal.create.admin.description")}
|
||||
{...form.getInputProps("isAdmin", { type: "checkbox" })}
|
||||
/>
|
||||
<Group position="right">
|
||||
<Button type="submit">Create</Button>
|
||||
<Button type="submit">
|
||||
<FormattedMessage id="common.button.create" />
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
|
||||
@@ -9,7 +9,11 @@ import {
|
||||
} from "@mantine/core";
|
||||
import { useForm, yupResolver } from "@mantine/form";
|
||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import * as yup from "yup";
|
||||
import useTranslate, {
|
||||
translateOutsideContext,
|
||||
} from "../../../hooks/useTranslate.hook";
|
||||
import userService from "../../../services/user.service";
|
||||
import User from "../../../types/user.type";
|
||||
import toast from "../../../utils/toast.util";
|
||||
@@ -19,8 +23,9 @@ const showUpdateUserModal = (
|
||||
user: User,
|
||||
getUsers: () => void
|
||||
) => {
|
||||
const t = translateOutsideContext();
|
||||
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} />,
|
||||
});
|
||||
};
|
||||
@@ -34,6 +39,8 @@ const Body = ({
|
||||
user: User;
|
||||
getUsers: () => void;
|
||||
}) => {
|
||||
const t = useTranslate();
|
||||
|
||||
const accountForm = useForm({
|
||||
initialValues: {
|
||||
username: user.username,
|
||||
@@ -42,8 +49,10 @@ const Body = ({
|
||||
},
|
||||
validate: yupResolver(
|
||||
yup.object().shape({
|
||||
email: yup.string().email(),
|
||||
username: yup.string().min(3),
|
||||
email: yup.string().email(t("common.error.invalid-email")),
|
||||
username: yup
|
||||
.string()
|
||||
.min(3, t("common.error.too-short", { length: 3 })),
|
||||
})
|
||||
),
|
||||
});
|
||||
@@ -54,7 +63,9 @@ const Body = ({
|
||||
},
|
||||
validate: yupResolver(
|
||||
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>
|
||||
<TextInput
|
||||
label="Username"
|
||||
label={t("admin.users.table.username")}
|
||||
{...accountForm.getInputProps("username")}
|
||||
/>
|
||||
<TextInput label="Email" {...accountForm.getInputProps("email")} />
|
||||
<TextInput
|
||||
label={t("admin.users.table.email")}
|
||||
{...accountForm.getInputProps("email")}
|
||||
/>
|
||||
<Switch
|
||||
mt="xs"
|
||||
labelPosition="left"
|
||||
label="Admin privileges"
|
||||
label={t("admin.users.edit.update.admin-privileges")}
|
||||
{...accountForm.getInputProps("isAdmin", { type: "checkbox" })}
|
||||
/>
|
||||
</Stack>
|
||||
</form>
|
||||
<Accordion>
|
||||
<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>
|
||||
<form
|
||||
onSubmit={passwordForm.onSubmit(async (values) => {
|
||||
@@ -97,17 +113,21 @@ const Body = ({
|
||||
.update(user.id, {
|
||||
password: values.password,
|
||||
})
|
||||
.then(() => toast.success("Password changed successfully"))
|
||||
.then(() =>
|
||||
toast.success(
|
||||
t("admin.users.edit.update.notify.password.success")
|
||||
)
|
||||
)
|
||||
.catch(toast.axiosError);
|
||||
})}
|
||||
>
|
||||
<Stack>
|
||||
<PasswordInput
|
||||
label="New password"
|
||||
label={t("admin.users.edit.update.change-password.field")}
|
||||
{...passwordForm.getInputProps("password")}
|
||||
/>
|
||||
<Button variant="light" type="submit">
|
||||
Save new password
|
||||
<FormattedMessage id="admin.users.edit.update.change-password.button" />
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
@@ -116,7 +136,7 @@ const Body = ({
|
||||
</Accordion>
|
||||
<Group position="right">
|
||||
<Button type="submit" form="accountForm">
|
||||
Save
|
||||
<FormattedMessage id="common.button.save" />
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
@@ -15,8 +15,10 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
import { TbInfoCircle } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import * as yup from "yup";
|
||||
import useConfig from "../../hooks/config.hook";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
import useUser from "../../hooks/user.hook";
|
||||
import authService from "../../services/auth.service";
|
||||
import toast from "../../utils/toast.util";
|
||||
@@ -24,14 +26,18 @@ import toast from "../../utils/toast.util";
|
||||
const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
|
||||
const config = useConfig();
|
||||
const router = useRouter();
|
||||
const t = useTranslate();
|
||||
const { refreshUser } = useUser();
|
||||
|
||||
const [showTotp, setShowTotp] = React.useState(false);
|
||||
const [loginToken, setLoginToken] = React.useState("");
|
||||
|
||||
const validationSchema = yup.object().shape({
|
||||
emailOrUsername: yup.string().required(),
|
||||
password: yup.string().min(8).required(),
|
||||
emailOrUsername: yup.string().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({
|
||||
@@ -54,8 +60,8 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
|
||||
icon: <TbInfoCircle />,
|
||||
color: "blue",
|
||||
radius: "md",
|
||||
title: "Two-factor authentication required",
|
||||
message: "Please enter your two-factor authentication code",
|
||||
title: t("signIn.notify.totp-required.title"),
|
||||
message: t("signIn.notify.totp-required.description"),
|
||||
});
|
||||
setLoginToken(response.data["loginToken"]);
|
||||
} else {
|
||||
@@ -88,13 +94,13 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
|
||||
return (
|
||||
<Container size={420} my={40}>
|
||||
<Title order={2} align="center" weight={900}>
|
||||
Welcome back
|
||||
<FormattedMessage id="signin.title" />
|
||||
</Title>
|
||||
{config.get("share.allowRegistration") && (
|
||||
<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">
|
||||
{"Sign up"}
|
||||
<FormattedMessage id="signin.button.signup" />
|
||||
</Anchor>
|
||||
</Text>
|
||||
)}
|
||||
@@ -107,20 +113,20 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
|
||||
})}
|
||||
>
|
||||
<TextInput
|
||||
label="Email or username"
|
||||
placeholder="Your email or username"
|
||||
label={t("signin.input.email-or-username")}
|
||||
placeholder={t("signin.input.email-or-username.placeholder")}
|
||||
{...form.getInputProps("emailOrUsername")}
|
||||
/>
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
placeholder="Your password"
|
||||
label={t("signin.input.password")}
|
||||
placeholder={t("signin.input.password.placeholder")}
|
||||
mt="md"
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
{showTotp && (
|
||||
<TextInput
|
||||
variant="filled"
|
||||
label="Code"
|
||||
label={t("account.modal.totp.code")}
|
||||
placeholder="******"
|
||||
mt="md"
|
||||
{...form.getInputProps("totp")}
|
||||
@@ -129,12 +135,12 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
|
||||
{config.get("smtp.enabled") && (
|
||||
<Group position="right" mt="xs">
|
||||
<Anchor component={Link} href="/auth/resetPassword" size="xs">
|
||||
Forgot password?
|
||||
<FormattedMessage id="resetPassword.title" />
|
||||
</Anchor>
|
||||
</Group>
|
||||
)}
|
||||
<Button fullWidth mt="xl" type="submit">
|
||||
Sign in
|
||||
<FormattedMessage id="signin.button.submit" />
|
||||
</Button>
|
||||
</form>
|
||||
</Paper>
|
||||
|
||||
@@ -11,8 +11,10 @@ import {
|
||||
import { useForm, yupResolver } from "@mantine/form";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import * as yup from "yup";
|
||||
import useConfig from "../../hooks/config.hook";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
import useUser from "../../hooks/user.hook";
|
||||
import authService from "../../services/auth.service";
|
||||
import toast from "../../utils/toast.util";
|
||||
@@ -20,12 +22,19 @@ import toast from "../../utils/toast.util";
|
||||
const SignUpForm = () => {
|
||||
const config = useConfig();
|
||||
const router = useRouter();
|
||||
const t = useTranslate();
|
||||
const { refreshUser } = useUser();
|
||||
|
||||
const validationSchema = yup.object().shape({
|
||||
email: yup.string().email().required(),
|
||||
username: yup.string().min(3).required(),
|
||||
password: yup.string().min(8).required(),
|
||||
email: yup.string().email(t("common.error.invalid-email")).required(),
|
||||
username: yup
|
||||
.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({
|
||||
@@ -54,13 +63,13 @@ const SignUpForm = () => {
|
||||
return (
|
||||
<Container size={420} my={40}>
|
||||
<Title order={2} align="center" weight={900}>
|
||||
Sign up
|
||||
<FormattedMessage id="signup.title" />
|
||||
</Title>
|
||||
{config.get("share.allowRegistration") && (
|
||||
<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">
|
||||
Sign in
|
||||
<FormattedMessage id="signup.button.signin" />
|
||||
</Anchor>
|
||||
</Text>
|
||||
)}
|
||||
@@ -71,24 +80,24 @@ const SignUpForm = () => {
|
||||
)}
|
||||
>
|
||||
<TextInput
|
||||
label="Username"
|
||||
placeholder="Your username"
|
||||
label={t("signup.input.username")}
|
||||
placeholder={t("signup.input.username.placeholder")}
|
||||
{...form.getInputProps("username")}
|
||||
/>
|
||||
<TextInput
|
||||
label="Email"
|
||||
placeholder="Your email"
|
||||
label={t("signup.input.email")}
|
||||
placeholder={t("signup.input.email.placeholder")}
|
||||
mt="md"
|
||||
{...form.getInputProps("email")}
|
||||
/>
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
placeholder="Your password"
|
||||
label={t("signin.input.password")}
|
||||
placeholder={t("signin.input.password.placeholder")}
|
||||
mt="md"
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
<Button fullWidth mt="xl" type="submit">
|
||||
Let's get started
|
||||
<FormattedMessage id="signup.button.submit" />
|
||||
</Button>
|
||||
</form>
|
||||
</Paper>
|
||||
|
||||
@@ -3,6 +3,7 @@ import Link from "next/link";
|
||||
import { TbDoorExit, TbSettings, TbUser } from "react-icons/tb";
|
||||
import useUser from "../../hooks/user.hook";
|
||||
import authService from "../../services/auth.service";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
const ActionAvatar = () => {
|
||||
const { user } = useUser();
|
||||
@@ -16,7 +17,7 @@ const ActionAvatar = () => {
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item component={Link} href="/account" icon={<TbUser size={14} />}>
|
||||
My account
|
||||
<FormattedMessage id="navbar.avatar.account" />
|
||||
</Menu.Item>
|
||||
{user!.isAdmin && (
|
||||
<Menu.Item
|
||||
@@ -24,7 +25,7 @@ const ActionAvatar = () => {
|
||||
href="/admin"
|
||||
icon={<TbSettings size={14} />}
|
||||
>
|
||||
Administration
|
||||
<FormattedMessage id="navbar.avatar.admin" />
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
||||
@@ -34,7 +35,7 @@ const ActionAvatar = () => {
|
||||
}}
|
||||
icon={<TbDoorExit size={14} />}
|
||||
>
|
||||
Sign out
|
||||
<FormattedMessage id="navbar.avatar.signout" />
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useRouter } from "next/router";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import useConfig from "../../hooks/config.hook";
|
||||
import useUser from "../../hooks/user.hook";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
import Logo from "../Logo";
|
||||
import ActionAvatar from "./ActionAvatar";
|
||||
import NavbarShareMenu from "./NavbarShareMenu";
|
||||
@@ -112,6 +113,7 @@ const Header = () => {
|
||||
const { user } = useUser();
|
||||
const router = useRouter();
|
||||
const config = useConfig();
|
||||
const t = useTranslate();
|
||||
|
||||
const [opened, toggleOpened] = useDisclosure(false);
|
||||
|
||||
@@ -124,7 +126,7 @@ const Header = () => {
|
||||
const authenticatedLinks: NavLink[] = [
|
||||
{
|
||||
link: "/upload",
|
||||
label: "Upload",
|
||||
label: t("navbar.upload"),
|
||||
},
|
||||
{
|
||||
component: <NavbarShareMenu />,
|
||||
@@ -137,27 +139,27 @@ const Header = () => {
|
||||
let unauthenticatedLinks: NavLink[] = [
|
||||
{
|
||||
link: "/auth/signIn",
|
||||
label: "Sign in",
|
||||
label: t("navbar.signin"),
|
||||
},
|
||||
];
|
||||
|
||||
if (config.get("share.allowUnauthenticatedShares")) {
|
||||
unauthenticatedLinks.unshift({
|
||||
link: "/upload",
|
||||
label: "Upload",
|
||||
label: t("navbar.upload"),
|
||||
});
|
||||
}
|
||||
|
||||
if (config.get("general.showHomePage"))
|
||||
unauthenticatedLinks.unshift({
|
||||
link: "/",
|
||||
label: "Home",
|
||||
label: t("navbar.home"),
|
||||
});
|
||||
|
||||
if (config.get("share.allowRegistration"))
|
||||
unauthenticatedLinks.push({
|
||||
link: "/auth/signUp",
|
||||
label: "Sign up",
|
||||
label: t("navbar.signup"),
|
||||
});
|
||||
|
||||
const { classes, cx } = useStyles();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ActionIcon, Menu } from "@mantine/core";
|
||||
import Link from "next/link";
|
||||
import { TbArrowLoopLeft, TbLink } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
const NavbarShareMneu = () => {
|
||||
return (
|
||||
@@ -12,14 +13,14 @@ const NavbarShareMneu = () => {
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item component={Link} href="/account/shares" icon={<TbLink />}>
|
||||
My shares
|
||||
<FormattedMessage id="navbar.links.shares" />
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
component={Link}
|
||||
href="/account/reverseShares"
|
||||
icon={<TbArrowLoopLeft />}
|
||||
>
|
||||
Reverse shares
|
||||
<FormattedMessage id="navbar.links.reverse" />
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { Button } from "@mantine/core";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
import shareService from "../../services/share.service";
|
||||
import toast from "../../utils/toast.util";
|
||||
|
||||
const DownloadAllButton = ({ shareId }: { shareId: string }) => {
|
||||
const [isZipReady, setIsZipReady] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const t = useTranslate();
|
||||
|
||||
const downloadAll = async () => {
|
||||
setIsLoading(true);
|
||||
await shareService
|
||||
@@ -39,13 +43,13 @@ const DownloadAllButton = ({ shareId }: { shareId: string }) => {
|
||||
loading={isLoading}
|
||||
onClick={() => {
|
||||
if (!isZipReady) {
|
||||
toast.error("The share is preparing. Try again in a few minutes.");
|
||||
toast.error(t("share.notify.download-all-preparing"));
|
||||
} else {
|
||||
downloadAll();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Download all
|
||||
<FormattedMessage id="share.button.download-all" />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -19,6 +19,8 @@ import { byteToHumanSizeString } from "../../utils/fileSize.util";
|
||||
import toast from "../../utils/toast.util";
|
||||
import TableSortIcon, { TableSort } from "../core/SortIcon";
|
||||
import showFilePreviewModal from "./modals/showFilePreviewModal";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
const FileList = ({
|
||||
files,
|
||||
@@ -34,6 +36,7 @@ const FileList = ({
|
||||
const clipboard = useClipboard();
|
||||
const config = useConfig();
|
||||
const modals = useModals();
|
||||
const t = useTranslate();
|
||||
|
||||
const [sort, setSort] = useState<TableSort>({
|
||||
property: undefined,
|
||||
@@ -68,10 +71,10 @@ const FileList = ({
|
||||
|
||||
if (window.isSecureContext) {
|
||||
clipboard.copy(link);
|
||||
toast.success("Your file link was copied to the keyboard.");
|
||||
toast.success(t("common.notify.copied"));
|
||||
} else {
|
||||
modals.openModal({
|
||||
title: "File link",
|
||||
title: t("share.modal.file-link"),
|
||||
children: (
|
||||
<Stack align="stretch">
|
||||
<TextInput variant="filled" value={link} />
|
||||
@@ -90,13 +93,13 @@ const FileList = ({
|
||||
<tr>
|
||||
<th>
|
||||
<Group spacing="xs">
|
||||
Name
|
||||
<FormattedMessage id="share.table.name" />
|
||||
<TableSortIcon sort={sort} setSort={setSort} property="name" />
|
||||
</Group>
|
||||
</th>
|
||||
<th>
|
||||
<Group spacing="xs">
|
||||
Size
|
||||
<FormattedMessage id="share.table.size" />
|
||||
<TableSortIcon sort={sort} setSort={setSort} property="size" />
|
||||
</Group>
|
||||
</th>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Button, Center, Stack, Text, Title } from "@mantine/core";
|
||||
import { modals } from "@mantine/modals";
|
||||
import Link from "next/link";
|
||||
import React, { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import api from "../../services/api.service";
|
||||
|
||||
const FilePreviewContext = React.createContext<{
|
||||
@@ -144,10 +145,11 @@ const UnSupportedFile = () => {
|
||||
return (
|
||||
<Center style={{ minHeight: 200 }}>
|
||||
<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>
|
||||
A preview for thise file type is unsupported. Please download the file
|
||||
to view it.
|
||||
<FormattedMessage id="share.modal.file-preview.error.not-supported.description" />
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Button, Stack } from "@mantine/core";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { translateOutsideContext } from "../../../hooks/useTranslate.hook";
|
||||
import CopyTextField from "../../upload/CopyTextField";
|
||||
|
||||
const showCompletedReverseShareModal = (
|
||||
@@ -8,11 +10,12 @@ const showCompletedReverseShareModal = (
|
||||
link: string,
|
||||
getReverseShares: () => void
|
||||
) => {
|
||||
const t = translateOutsideContext();
|
||||
return modals.openModal({
|
||||
closeOnClickOutside: false,
|
||||
withCloseButton: false,
|
||||
closeOnEscape: false,
|
||||
title: "Reverse share link",
|
||||
title: t("account.reverseShares.modal.reverse-share-link"),
|
||||
children: <Body link={link} getReverseShares={getReverseShares} />,
|
||||
});
|
||||
};
|
||||
@@ -36,7 +39,7 @@ const Body = ({
|
||||
getReverseShares();
|
||||
}}
|
||||
>
|
||||
Done
|
||||
<FormattedMessage id="common.button.done" />
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
import { useForm } from "@mantine/form";
|
||||
import { useModals } from "@mantine/modals";
|
||||
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 { getExpirationPreview } from "../../../utils/date.util";
|
||||
import toast from "../../../utils/toast.util";
|
||||
@@ -42,6 +44,7 @@ const Body = ({
|
||||
showSendEmailNotificationOption: boolean;
|
||||
}) => {
|
||||
const modals = useModals();
|
||||
const t = useTranslate();
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
@@ -79,7 +82,7 @@ const Body = ({
|
||||
max={99999}
|
||||
precision={0}
|
||||
variant="filled"
|
||||
label="Share expiration"
|
||||
label={t("account.reverseShares.modal.expiration.label")}
|
||||
{...form.getInputProps("expiration_num")}
|
||||
/>
|
||||
</Col>
|
||||
@@ -91,27 +94,44 @@ const Body = ({
|
||||
{
|
||||
value: "-minutes",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
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],
|
||||
})}
|
||||
>
|
||||
{getExpirationPreview("reverse share", form)}
|
||||
{getExpirationPreview(
|
||||
{
|
||||
expiresOn: t("account.reverseShare.expires-on"),
|
||||
neverExpires: t("account.reverseShare.never-expires"),
|
||||
},
|
||||
form
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
<FileSizeInput
|
||||
label="Max share size"
|
||||
label={t("account.reverseShares.modal.max-size.label")}
|
||||
value={form.values.maxShareSize}
|
||||
onChange={(number) => form.setFieldValue("maxShareSize", number)}
|
||||
/>
|
||||
@@ -138,16 +164,18 @@ const Body = ({
|
||||
max={1000}
|
||||
precision={0}
|
||||
variant="filled"
|
||||
label="Max use count"
|
||||
description="The maximum number of times this reverse share link can be used"
|
||||
label={t("account.reverseShares.modal.max-use.label")}
|
||||
description={t("account.reverseShares.modal.max-use.description")}
|
||||
{...form.getInputProps("maxUseCount")}
|
||||
/>
|
||||
{showSendEmailNotificationOption && (
|
||||
<Switch
|
||||
mt="xs"
|
||||
labelPosition="left"
|
||||
label="Send email notification"
|
||||
description="Send an email notification when a share is created with this reverse share link"
|
||||
label={t("account.reverseShares.modal.send-email")}
|
||||
description={t(
|
||||
"account.reverseShares.modal.send-email.description"
|
||||
)}
|
||||
{...form.getInputProps("sendEmailNotification", {
|
||||
type: "checkbox",
|
||||
})}
|
||||
@@ -155,7 +183,7 @@ const Body = ({
|
||||
)}
|
||||
|
||||
<Button mt="md" type="submit">
|
||||
Create
|
||||
<FormattedMessage id="common.button.create" />
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { Button, PasswordInput, Stack, Text } from "@mantine/core";
|
||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||
import { useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import useTranslate, {
|
||||
translateOutsideContext,
|
||||
} from "../../hooks/useTranslate.hook";
|
||||
|
||||
const showEnterPasswordModal = (
|
||||
modals: ModalsContextProps,
|
||||
submitCallback: (password: string) => Promise<void>
|
||||
) => {
|
||||
const t = translateOutsideContext();
|
||||
return modals.openModal({
|
||||
closeOnClickOutside: false,
|
||||
withCloseButton: false,
|
||||
closeOnEscape: false,
|
||||
title: "Password required",
|
||||
title: t("share.modal.password.title"),
|
||||
children: <Body submitCallback={submitCallback} />,
|
||||
});
|
||||
};
|
||||
@@ -22,10 +27,11 @@ const Body = ({
|
||||
}) => {
|
||||
const [password, setPassword] = useState("");
|
||||
const [passwordWrong, setPasswordWrong] = useState(false);
|
||||
const t = useTranslate();
|
||||
return (
|
||||
<Stack align="stretch">
|
||||
<Text size="sm">
|
||||
This access this share please enter the password for the share.
|
||||
<FormattedMessage id="share.modal.password.description" />
|
||||
</Text>
|
||||
|
||||
<form
|
||||
@@ -37,13 +43,15 @@ const Body = ({
|
||||
<Stack>
|
||||
<PasswordInput
|
||||
variant="filled"
|
||||
placeholder="Password"
|
||||
error={passwordWrong && "Wrong password"}
|
||||
placeholder={t("share.modal.password")}
|
||||
error={passwordWrong && t("share.modal.error.invalid-password")}
|
||||
onFocus={() => setPasswordWrong(false)}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
value={password}
|
||||
/>
|
||||
<Button type="submit">Submit</Button>
|
||||
<Button type="submit">
|
||||
<FormattedMessage id="common.button.submit" />
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Stack>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Button, Stack, Text } from "@mantine/core";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||
import { useRouter } from "next/router";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
const showErrorModal = (
|
||||
modals: ModalsContextProps,
|
||||
@@ -31,7 +32,7 @@ const Body = ({ text }: { text: string }) => {
|
||||
router.back();
|
||||
}}
|
||||
>
|
||||
Go back
|
||||
<FormattedMessage id="common.button.go-back" />
|
||||
</Button>
|
||||
</Stack>
|
||||
</>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { useRef, useState } from "react";
|
||||
import toast from "../../utils/toast.util";
|
||||
import { ActionIcon, TextInput } from "@mantine/core";
|
||||
import { TbCheck, TbCopy } from "react-icons/tb";
|
||||
import { useClipboard } from "@mantine/hooks";
|
||||
import { useRef, useState } from "react";
|
||||
import { TbCheck, TbCopy } from "react-icons/tb";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
import toast from "../../utils/toast.util";
|
||||
|
||||
function CopyTextField(props: { link: string }) {
|
||||
const clipboard = useClipboard({ timeout: 500 });
|
||||
const t = useTranslate();
|
||||
|
||||
const [checkState, setCheckState] = useState(false);
|
||||
const [textClicked, setTextClicked] = useState(false);
|
||||
const timerRef = useRef<number | ReturnType<typeof setTimeout> | undefined>(
|
||||
@@ -14,7 +17,7 @@ function CopyTextField(props: { link: string }) {
|
||||
|
||||
const copyLink = () => {
|
||||
clipboard.copy(props.link);
|
||||
toast.success("Your link was copied to the keyboard.");
|
||||
toast.success(t("common.notify.copied"));
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
timerRef.current = setTimeout(() => {
|
||||
setCheckState(false);
|
||||
@@ -25,7 +28,7 @@ function CopyTextField(props: { link: string }) {
|
||||
return (
|
||||
<TextInput
|
||||
readOnly
|
||||
label="Link"
|
||||
label={t("common.text.link")}
|
||||
variant="filled"
|
||||
value={props.link}
|
||||
onClick={() => {
|
||||
|
||||
@@ -2,7 +2,8 @@ import { Button, Center, createStyles, Group, Text } from "@mantine/core";
|
||||
import { Dropzone as MantineDropzone } from "@mantine/dropzone";
|
||||
import { Dispatch, ForwardedRef, SetStateAction, useRef } from "react";
|
||||
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 { byteToHumanSizeString } from "../../utils/fileSize.util";
|
||||
import toast from "../../utils/toast.util";
|
||||
@@ -42,7 +43,7 @@ const Dropzone = ({
|
||||
files: FileUpload[];
|
||||
setFiles: Dispatch<SetStateAction<FileUpload[]>>;
|
||||
}) => {
|
||||
const config = useConfig();
|
||||
const t = useTranslate();
|
||||
|
||||
const { classes } = useStyles();
|
||||
const openRef = useRef<() => void>();
|
||||
@@ -62,9 +63,9 @@ const Dropzone = ({
|
||||
|
||||
if (fileSizeSum > maxShareSize) {
|
||||
toast.error(
|
||||
`Your files exceed the maximum share size of ${byteToHumanSizeString(
|
||||
maxShareSize
|
||||
)}.`
|
||||
t("upload.dropzone.notify.file-too-big", {
|
||||
maxSize: byteToHumanSizeString(maxShareSize),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
newFiles = newFiles.map((newFile) => {
|
||||
@@ -82,12 +83,13 @@ const Dropzone = ({
|
||||
<TbCloudUpload size={50} />
|
||||
</Group>
|
||||
<Text align="center" weight={700} size="lg" mt="xl">
|
||||
Upload files
|
||||
<FormattedMessage id="upload.dropzone.title" />
|
||||
</Text>
|
||||
<Text align="center" size="sm" mt="xs" color="dimmed">
|
||||
Drag'n'drop files here to start your share. We can accept
|
||||
only files that are less than {byteToHumanSizeString(maxShareSize)}{" "}
|
||||
in total.
|
||||
<FormattedMessage
|
||||
id="upload.dropzone.description"
|
||||
values={{ maxSize: byteToHumanSizeString(maxShareSize) }}
|
||||
/>
|
||||
</Text>
|
||||
</div>
|
||||
</MantineDropzone>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { TbTrash } from "react-icons/tb";
|
||||
import { FileUpload } from "../../types/File.type";
|
||||
import { byteToHumanSizeString } from "../../utils/fileSize.util";
|
||||
import UploadProgressIndicator from "./UploadProgressIndicator";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
const FileList = ({
|
||||
files,
|
||||
@@ -41,8 +42,12 @@ const FileList = ({
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Size</th>
|
||||
<th>
|
||||
<FormattedMessage id="upload.filelist.name" />
|
||||
</th>
|
||||
<th>
|
||||
<FormattedMessage id="upload.filelist.size" />
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -3,6 +3,10 @@ import { useModals } from "@mantine/modals";
|
||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||
import moment from "moment";
|
||||
import { useRouter } from "next/router";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import useTranslate, {
|
||||
translateOutsideContext,
|
||||
} from "../../../hooks/useTranslate.hook";
|
||||
import { Share } from "../../../types/share.type";
|
||||
import CopyTextField from "../CopyTextField";
|
||||
|
||||
@@ -11,11 +15,12 @@ const showCompletedUploadModal = (
|
||||
share: Share,
|
||||
appUrl: string
|
||||
) => {
|
||||
const t = translateOutsideContext();
|
||||
return modals.openModal({
|
||||
closeOnClickOutside: false,
|
||||
withCloseButton: false,
|
||||
closeOnEscape: false,
|
||||
title: "Share ready",
|
||||
title: t("upload.modal.completed.share-ready"),
|
||||
children: <Body share={share} appUrl={appUrl} />,
|
||||
});
|
||||
};
|
||||
@@ -23,8 +28,9 @@ const showCompletedUploadModal = (
|
||||
const Body = ({ share, appUrl }: { share: Share; appUrl: string }) => {
|
||||
const modals = useModals();
|
||||
const router = useRouter();
|
||||
const t = useTranslate();
|
||||
|
||||
const link = `${appUrl}/share/${share.id}`;
|
||||
const link = `${appUrl}/s/${share.id}`;
|
||||
|
||||
return (
|
||||
<Stack align="stretch">
|
||||
@@ -37,10 +43,10 @@ const Body = ({ share, appUrl }: { share: Share; appUrl: string }) => {
|
||||
>
|
||||
{/* If our share.expiration is timestamp 0, show a different message */}
|
||||
{moment(share.expiration).unix() === 0
|
||||
? "This share will never expire."
|
||||
: `This share will expire on ${moment(share.expiration).format(
|
||||
"LLL"
|
||||
)}`}
|
||||
? t("upload.modal.completed.never-expires")
|
||||
: t("upload.modal.completed.expires-on", {
|
||||
expiration: moment(share.expiration).format("LLL"),
|
||||
})}
|
||||
</Text>
|
||||
|
||||
<Button
|
||||
@@ -49,7 +55,7 @@ const Body = ({ share, appUrl }: { share: Share; appUrl: string }) => {
|
||||
router.push("/upload");
|
||||
}}
|
||||
>
|
||||
Done
|
||||
<FormattedMessage id="common.button.done" />
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Checkbox,
|
||||
Col,
|
||||
Grid,
|
||||
Group,
|
||||
MultiSelect,
|
||||
NumberInput,
|
||||
PasswordInput,
|
||||
@@ -13,14 +14,17 @@ import {
|
||||
Text,
|
||||
Textarea,
|
||||
TextInput,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { useForm, yupResolver } from "@mantine/form";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||
import { useState } from "react";
|
||||
import { TbAlertCircle } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import * as yup from "yup";
|
||||
import useTranslate, {
|
||||
translateOutsideContext,
|
||||
} from "../../../hooks/useTranslate.hook";
|
||||
import shareService from "../../../services/share.service";
|
||||
import { CreateShare } from "../../../types/share.type";
|
||||
import { getExpirationPreview } from "../../../utils/date.util";
|
||||
@@ -36,8 +40,10 @@ const showCreateUploadModal = (
|
||||
},
|
||||
uploadCallback: (createShare: CreateShare) => void
|
||||
) => {
|
||||
const t = translateOutsideContext();
|
||||
|
||||
return modals.openModal({
|
||||
title: "Share",
|
||||
title: t("upload.modal.title"),
|
||||
children: (
|
||||
<CreateUploadModalBody
|
||||
options={options}
|
||||
@@ -61,24 +67,29 @@ const CreateUploadModalBody = ({
|
||||
};
|
||||
}) => {
|
||||
const modals = useModals();
|
||||
const t = useTranslate();
|
||||
|
||||
const generatedLink = Buffer.from(Math.random().toString(), "utf8")
|
||||
.toString("base64")
|
||||
.substr(10, 7);
|
||||
|
||||
const [showNotSignedInAlert, setShowNotSignedInAlert] = useState(true);
|
||||
|
||||
const validationSchema = yup.object().shape({
|
||||
link: yup
|
||||
.string()
|
||||
.required()
|
||||
.min(3)
|
||||
.max(50)
|
||||
.required(t("common.error.field-required"))
|
||||
.min(3, t("common.error.too-short", { length: 3 }))
|
||||
.max(50, t("common.error.too-long", { length: 50 }))
|
||||
.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),
|
||||
maxViews: yup.number().min(1),
|
||||
});
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
link: "",
|
||||
link: generatedLink,
|
||||
recipients: [] as string[],
|
||||
password: undefined,
|
||||
maxViews: undefined,
|
||||
@@ -96,17 +107,16 @@ const CreateUploadModalBody = ({
|
||||
withCloseButton
|
||||
onClose={() => setShowNotSignedInAlert(false)}
|
||||
icon={<TbAlertCircle size={16} />}
|
||||
title="You're not signed in"
|
||||
title={t("upload.modal.not-signed-in")}
|
||||
color="yellow"
|
||||
>
|
||||
You will be unable to delete your share manually and view the visitor
|
||||
count.
|
||||
<FormattedMessage id="upload.modal.not-signed-in-description" />
|
||||
</Alert>
|
||||
)}
|
||||
<form
|
||||
onSubmit={form.onSubmit(async (values) => {
|
||||
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 {
|
||||
const expiration = form.values.never_expires
|
||||
? "never"
|
||||
@@ -126,31 +136,29 @@ const CreateUploadModalBody = ({
|
||||
})}
|
||||
>
|
||||
<Stack align="stretch">
|
||||
<Grid align={form.errors.link ? "center" : "flex-end"}>
|
||||
<Col xs={9}>
|
||||
<TextInput
|
||||
variant="filled"
|
||||
label="Link"
|
||||
placeholder="myAwesomeShare"
|
||||
{...form.getInputProps("link")}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={3}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
form.setFieldValue(
|
||||
"link",
|
||||
Buffer.from(Math.random().toString(), "utf8")
|
||||
.toString("base64")
|
||||
.substr(10, 7)
|
||||
)
|
||||
}
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
</Col>
|
||||
</Grid>
|
||||
<Group align="end">
|
||||
<TextInput
|
||||
style={{ flex: "1" }}
|
||||
variant="filled"
|
||||
label="Link"
|
||||
placeholder="myAwesomeShare"
|
||||
{...form.getInputProps("link")}
|
||||
/>
|
||||
<Button
|
||||
style={{ flex: "0 0 auto" }}
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
form.setFieldValue(
|
||||
"link",
|
||||
Buffer.from(Math.random().toString(), "utf8")
|
||||
.toString("base64")
|
||||
.substr(10, 7)
|
||||
)
|
||||
}
|
||||
>
|
||||
<FormattedMessage id="common.button.generate" />
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<Text
|
||||
italic
|
||||
@@ -159,8 +167,7 @@ const CreateUploadModalBody = ({
|
||||
color: theme.colors.gray[6],
|
||||
})}
|
||||
>
|
||||
{options.appUrl}/share/
|
||||
{form.values.link == "" ? "myAwesomeShare" : form.values.link}
|
||||
{`${options.appUrl}/s/${form.values.link}`}
|
||||
</Text>
|
||||
{!options.isReverseShare && (
|
||||
<>
|
||||
@@ -171,8 +178,7 @@ const CreateUploadModalBody = ({
|
||||
max={99999}
|
||||
precision={0}
|
||||
variant="filled"
|
||||
label="Expiration"
|
||||
placeholder="n"
|
||||
label={t("upload.modal.expires.label")}
|
||||
disabled={form.values.never_expires}
|
||||
{...form.getInputProps("expiration_num")}
|
||||
/>
|
||||
@@ -186,41 +192,51 @@ const CreateUploadModalBody = ({
|
||||
{
|
||||
value: "-minutes",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
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:
|
||||
"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>
|
||||
</Grid>
|
||||
<Checkbox
|
||||
label="Never Expires"
|
||||
label={t("upload.modal.expires.never-long")}
|
||||
{...form.getInputProps("never_expires")}
|
||||
/>
|
||||
<Text
|
||||
@@ -230,18 +246,28 @@ const CreateUploadModalBody = ({
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
<Accordion>
|
||||
<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>
|
||||
<Stack align="stretch">
|
||||
<Textarea
|
||||
variant="filled"
|
||||
placeholder="Note for the recepients"
|
||||
placeholder={t(
|
||||
"upload.modal.accordion.description.placeholder"
|
||||
)}
|
||||
{...form.getInputProps("description")}
|
||||
/>
|
||||
</Stack>
|
||||
@@ -249,11 +275,13 @@ const CreateUploadModalBody = ({
|
||||
</Accordion.Item>
|
||||
{options.enableEmailRecepients && (
|
||||
<Accordion.Item value="recipients" sx={{ borderBottom: "none" }}>
|
||||
<Accordion.Control>Email recipients</Accordion.Control>
|
||||
<Accordion.Control>
|
||||
<FormattedMessage id="upload.modal.accordion.email.title" />
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<MultiSelect
|
||||
data={form.values.recipients}
|
||||
placeholder="Enter email recipients"
|
||||
placeholder={t("upload.modal.accordion.email.placeholder")}
|
||||
searchable
|
||||
{...form.getInputProps("recipients")}
|
||||
creatable
|
||||
@@ -262,7 +290,7 @@ const CreateUploadModalBody = ({
|
||||
if (!query.match(/^\S+@\S+\.\S+$/)) {
|
||||
form.setFieldError(
|
||||
"recipients",
|
||||
"Invalid email address"
|
||||
t("upload.modal.accordion.email.invalid-email")
|
||||
);
|
||||
} else {
|
||||
form.setFieldError("recipients", null);
|
||||
@@ -279,28 +307,36 @@ const CreateUploadModalBody = ({
|
||||
)}
|
||||
|
||||
<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>
|
||||
<Stack align="stretch">
|
||||
<PasswordInput
|
||||
variant="filled"
|
||||
placeholder="No password"
|
||||
label="Password protection"
|
||||
placeholder={t(
|
||||
"upload.modal.accordion.security.password.placeholder"
|
||||
)}
|
||||
label={t("upload.modal.accordion.security.password.label")}
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
<NumberInput
|
||||
min={1}
|
||||
type="number"
|
||||
variant="filled"
|
||||
placeholder="No limit"
|
||||
label="Maximal views"
|
||||
placeholder={t(
|
||||
"upload.modal.accordion.security.max-views.placeholder"
|
||||
)}
|
||||
label={t("upload.modal.accordion.security.max-views.label")}
|
||||
{...form.getInputProps("maxViews")}
|
||||
/>
|
||||
</Stack>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
<Button type="submit">Share</Button>
|
||||
<Button type="submit">
|
||||
<FormattedMessage id="common.button.share" />
|
||||
</Button>
|
||||
</Stack>
|
||||
</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;
|
||||
57
frontend/src/i18n/locales.ts
Normal file
57
frontend/src/i18n/locales.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import danish from "./translations/da-DK";
|
||||
import german from "./translations/de-DE";
|
||||
import english from "./translations/en-US";
|
||||
import spanish from "./translations/es-ES";
|
||||
import finnish from "./translations/fi-FI";
|
||||
import french from "./translations/fr-FR";
|
||||
import portuguese from "./translations/pt-BR";
|
||||
import russian from "./translations/ru-RU";
|
||||
import chineseSimplified from "./translations/zh-CN";
|
||||
|
||||
export const LOCALES = {
|
||||
ENGLISH: {
|
||||
name: "English",
|
||||
code: "en-US",
|
||||
messages: english,
|
||||
},
|
||||
GERMAN: {
|
||||
name: "Deutsch",
|
||||
code: "de-DE",
|
||||
messages: german,
|
||||
},
|
||||
FRENCH: {
|
||||
name: "Français",
|
||||
code: "fr-FR",
|
||||
messages: french,
|
||||
},
|
||||
PORTUGUESE_BRAZIL: {
|
||||
name: "Português (Brasil)",
|
||||
code: "pt-BR",
|
||||
messages: portuguese,
|
||||
},
|
||||
DANISH: {
|
||||
name: "Dansk",
|
||||
code: "da-DK",
|
||||
messages: danish,
|
||||
},
|
||||
SPANISH: {
|
||||
name: "Español",
|
||||
code: "es-ES",
|
||||
messages: spanish,
|
||||
},
|
||||
CHINESE_SIMPLIFIED: {
|
||||
name: "简体中文",
|
||||
code: "zh-CN",
|
||||
messages: chineseSimplified,
|
||||
},
|
||||
FINNISH: {
|
||||
name: "Suomi",
|
||||
code: "fi-FI",
|
||||
messages: finnish,
|
||||
},
|
||||
RUSSIAN: {
|
||||
name: "Русский",
|
||||
code: "ru-RU",
|
||||
messages: russian,
|
||||
},
|
||||
};
|
||||
323
frontend/src/i18n/translations/da-DK.ts
Normal file
323
frontend/src/i18n/translations/da-DK.ts
Normal file
@@ -0,0 +1,323 @@
|
||||
export default {
|
||||
// Navbar
|
||||
"navbar.upload": "Upload",
|
||||
"navbar.signin": "Log ind",
|
||||
"navbar.home": "Hjem",
|
||||
"navbar.signup": "Opret bruger",
|
||||
"navbar.links.shares": "Mine delte filer",
|
||||
"navbar.links.reverse": "Omvendt deling",
|
||||
"navbar.avatar.account": "Min bruger",
|
||||
"navbar.avatar.admin": "Administration",
|
||||
"navbar.avatar.signout": "Log ud",
|
||||
// END navbar
|
||||
// /
|
||||
"home.title": "En <h>self-hosted</h> fildelingsplatform.",
|
||||
"home.description": "Er du sikker på, at du vil overlade dine personlige filer til tredjeparter som WeTransfer?",
|
||||
"home.bullet.a.name": "Self-Hosted",
|
||||
"home.bullet.a.description": "Host Pingvin Share på din egen maskine.",
|
||||
"home.bullet.b.name": "Privatliv",
|
||||
"home.bullet.b.description": "Dine filer er dine filer og bør ikke komme i hænderne på tredjeparter.",
|
||||
"home.bullet.c.name": "Ingen irriterende grænse for filstørrelse",
|
||||
"home.bullet.c.description": "Upload så store filer, som du vil. Kun din harddisk sætter grænsen.",
|
||||
"home.button.start": "Kom i gang",
|
||||
"home.button.source": "Source code",
|
||||
// END /
|
||||
// /auth/signin
|
||||
"signin.title": "Velkommen tilbage",
|
||||
"signin.description": "Har du ikke en bruger endnu?",
|
||||
"signin.button.signup": "Opret bruger",
|
||||
"signin.input.email-or-username": "E-mail eller brugernavn",
|
||||
"signin.input.email-or-username.placeholder": "Din e-mail eller dit brugernavn",
|
||||
"signin.input.password": "Adgangskode",
|
||||
"signin.input.password.placeholder": "Din adgangskode",
|
||||
"signin.button.submit": "Log ind",
|
||||
"signIn.notify.totp-required.title": "2-faktor login påkrævet",
|
||||
"signIn.notify.totp-required.description": "Indtast den aktuelle engangskode fra din 2-faktor Authenticator",
|
||||
// END /auth/signin
|
||||
// /auth/signup
|
||||
"signup.title": "Opret en bruger",
|
||||
"signup.description": "Har du allerede en bruger?",
|
||||
"signup.button.signin": "Log ind",
|
||||
"signup.input.username": "Brugernavn",
|
||||
"signup.input.username.placeholder": "Dit brugernavn",
|
||||
"signup.input.email": "E-mail",
|
||||
"signup.input.email.placeholder": "Din e-mail",
|
||||
"signup.button.submit": "Lad os komme i gang",
|
||||
// END /auth/signup
|
||||
// /auth/reset-password
|
||||
"resetPassword.title": "Glemt din adgangskode?",
|
||||
"resetPassword.description": "Indtast din e-mail for at nulstille din adgangskode.",
|
||||
"resetPassword.notify.success": "En e-mail er blevet sendt med et link til at nulstille din adgangskode.",
|
||||
"resetPassword.button.back": "Tilbage til login",
|
||||
"resetPassword.text.resetPassword": "Nulstil adgangskode",
|
||||
"resetPassword.text.enterNewPassword": "Indtast din nye adgangskode",
|
||||
"resetPassword.input.password": "Ny adgangskode",
|
||||
"resetPassword.notify.passwordReset": "Adgangskoden er blevet nulstillet.",
|
||||
// /account
|
||||
"account.title": "Min bruger",
|
||||
"account.card.info.title": "Brugerinfo",
|
||||
"account.card.info.username": "Brugernavn",
|
||||
"account.card.info.email": "E-mail",
|
||||
"account.notify.info.success": "Brugeren blev opdateret med succes",
|
||||
"account.card.password.title": "Adgangskode",
|
||||
"account.card.password.old": "Gammel adgangskode",
|
||||
"account.card.password.new": "Ny adgangskode",
|
||||
"account.notify.password.success": "Adgangskoden er ændret",
|
||||
"account.card.security.title": "Sikkerhed",
|
||||
"account.card.security.totp.enable.description": "Indtast din nuværende adgangskode for at begynde opsætningen af 2-faktor login",
|
||||
"account.card.security.totp.disable.description": "Enter your current password to disable TOTP",
|
||||
"account.card.security.totp.button.start": "Start",
|
||||
"account.modal.totp.title": "Aktiver 2-faktor login",
|
||||
"account.modal.totp.step1": "Trin 1: Tilføj din 2-faktor Authenticator",
|
||||
"account.modal.totp.step2": "Trin 2: Valider din kode",
|
||||
"account.modal.totp.enterManually": "Indtast manuelt",
|
||||
"account.modal.totp.code": "Kode",
|
||||
"account.modal.totp.clickToCopy": "Klik for at kopiere",
|
||||
"account.modal.totp.verify": "Bekræft",
|
||||
"account.notify.totp.disable": "2-faktor blev deaktiveret",
|
||||
"account.notify.totp.enable": "2-faktor blev deaktiveret",
|
||||
"account.card.language.title": "Sprog",
|
||||
"account.card.language.description": "The project is translated by the community. Some languages might be incomplete.",
|
||||
"account.card.color.title": "Farveskema",
|
||||
// ThemeSwitcher.tsx
|
||||
"account.theme.dark": "Mørkt",
|
||||
"account.theme.light": "Lyst",
|
||||
"account.theme.system": "System",
|
||||
"account.button.delete": "Slet bruger",
|
||||
"account.modal.delete.title": "Slet bruger",
|
||||
"account.modal.delete.description": "Er du sikker på at du vil slette din bruger, herunder alle dine aktive delinger?",
|
||||
// END /account
|
||||
// /account/shares
|
||||
"account.shares.title": "Mine delte filer",
|
||||
"account.shares.title.empty": "Der er tomt her 👀",
|
||||
"account.shares.description.empty": "Du har ingen delinger.",
|
||||
"account.shares.button.create": "Opret en",
|
||||
"account.shares.info.title": "Share informations",
|
||||
"account.shares.table.id": "ID",
|
||||
"account.shares.table.name": "Navn",
|
||||
"account.shares.table.description": "Beskrivelse",
|
||||
"account.shares.table.visitors": "Besøgende",
|
||||
"account.shares.table.expiresAt": "Udløber d",
|
||||
"account.shares.table.createdAt": "Oprettet d.",
|
||||
"account.shares.table.size": "Størrelse",
|
||||
"account.shares.modal.share-informations": "Share informations",
|
||||
"account.shares.modal.share-link": "Del link",
|
||||
"account.shares.modal.delete.title": "Delete share {share}",
|
||||
"account.shares.modal.delete.description": "Ønsker du virkelig at slette denne deling?",
|
||||
// END /account/shares
|
||||
// /account/reverseShares
|
||||
"account.reverseShares.title": "Omvendt deling",
|
||||
"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": "Der er tomt her 👀",
|
||||
"account.reverseShares.description.empty": "You don't have any reverse shares.",
|
||||
// showCreateReverseShareModal.tsx
|
||||
"account.reverseShares.modal.expiration.label": "Udløb",
|
||||
"account.reverseShares.modal.expiration.minute-singular": "Minut",
|
||||
"account.reverseShares.modal.expiration.minute-plural": "Minutter",
|
||||
"account.reverseShares.modal.expiration.hour-singular": "Time",
|
||||
"account.reverseShares.modal.expiration.hour-plural": "Timer",
|
||||
"account.reverseShares.modal.expiration.day-singular": "Dag",
|
||||
"account.reverseShares.modal.expiration.day-plural": "Dage",
|
||||
"account.reverseShares.modal.expiration.week-singular": "Uge",
|
||||
"account.reverseShares.modal.expiration.week-plural": "Uger",
|
||||
"account.reverseShares.modal.expiration.month-singular": "Måned",
|
||||
"account.reverseShares.modal.expiration.month-plural": "Måneder",
|
||||
"account.reverseShares.modal.expiration.year-singular": "År",
|
||||
"account.reverseShares.modal.expiration.year-plural": "År",
|
||||
"account.reverseShares.modal.max-size.label": "Maksimal størrelse for deling",
|
||||
"account.reverseShares.modal.send-email": "Send e-mail notifikation",
|
||||
"account.reverseShares.modal.send-email.description": "Send en e-mail notifikation, når der oprettes en deling med dette omvendte delingslink.",
|
||||
"account.reverseShares.modal.max-use.label": "Maksimal anvendelser",
|
||||
"account.reverseShares.modal.max-use.description": "Det maksimale antal gange, denne URL kan bruges til at oprette en deling.",
|
||||
"account.reverseShare.never-expires": "Denne omvendte deling udløber aldrig.",
|
||||
"account.reverseShare.expires-on": "Denne omvendte deling udløber den {expiration}.",
|
||||
"account.reverseShares.table.no-shares": "Der er ikke oprettet nogle delinger endnu",
|
||||
"account.reverseShares.table.count.singular": "del",
|
||||
"account.reverseShares.table.count.plural": "delinger",
|
||||
"account.reverseShares.table.shares": "Delinger",
|
||||
"account.reverseShares.table.remaining": "Resterende anvendelser",
|
||||
"account.reverseShares.table.max-size": "Maksimal størrelse for deling",
|
||||
"account.reverseShares.table.expires": "Udløber d",
|
||||
"account.reverseShares.modal.reverse-share-link": "Omvendt deling",
|
||||
"account.reverseShares.modal.delete.title": "Delete reverse share",
|
||||
"account.reverseShares.modal.delete.description": "Ønsker du virkelig at slette denne omvendte deling? Hvis du gør det, vil de tilknyttede delinger også blive slettet.",
|
||||
// END /account/reverseShares
|
||||
// /admin
|
||||
"admin.title": "Administration",
|
||||
"admin.button.users": "Brugeradministration",
|
||||
"admin.button.config": "Konfiguration",
|
||||
"admin.version": "Version",
|
||||
// END /admin
|
||||
// /admin/users
|
||||
"admin.users.title": "Brugeradministration",
|
||||
"admin.users.table.username": "Brugernavn",
|
||||
"admin.users.table.email": "E-mail",
|
||||
"admin.users.table.admin": "Admin",
|
||||
"admin.users.edit.update.title": "Opdater bruger {username}",
|
||||
"admin.users.edit.update.admin-privileges": "Admin rettigheder",
|
||||
"admin.users.edit.update.change-password.title": "Skift adgangskode",
|
||||
"admin.users.edit.update.change-password.field": "Ny adgangskode",
|
||||
"admin.users.edit.update.change-password.button": "Gem ny adgangskode",
|
||||
"admin.users.edit.update.notify.password.success": "Adgangskoden er ændret",
|
||||
"admin.users.edit.delete.title": "Slet bruger {username}",
|
||||
"admin.users.edit.delete.description": "Er du sikker på du vil slette denne bruger og tilhørende delinger?",
|
||||
// showCreateUserModal.tsx
|
||||
"admin.users.modal.create.title": "Opret bruger",
|
||||
"admin.users.modal.create.username": "Brugernavn",
|
||||
"admin.users.modal.create.email": "E-mail",
|
||||
"admin.users.modal.create.password": "Adgangskode",
|
||||
"admin.users.modal.create.manual-password": "Angiv adgangskode manuelt",
|
||||
"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": "Der opstod en fejl under afslutningen af din deling.",
|
||||
"upload.notify.count-failed": "{count} files failed to upload. Trying again.",
|
||||
// Dropzone.tsx
|
||||
"upload.dropzone.title": "Upload filer",
|
||||
"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": "Navn",
|
||||
"upload.filelist.size": "Størrelse",
|
||||
// showCreateUploadModal.tsx
|
||||
"upload.modal.title": "Opret Deling",
|
||||
"upload.modal.link.error.invalid": "Can only contain letters, numbers, underscores, and hyphens",
|
||||
"upload.modal.link.error.taken": "Det valgte link er allerede i brug",
|
||||
"upload.modal.not-signed-in": "Du er ikke logget ind",
|
||||
"upload.modal.not-signed-in-description": "Du vil ikke være i stand til at slette din deling manuelt og se antallet af besøgende.",
|
||||
"upload.modal.expires.never": "aldrig",
|
||||
"upload.modal.expires.never-long": "Udløber aldrig",
|
||||
"upload.modal.link.label": "Link",
|
||||
"upload.modal.expires.label": "Udløb",
|
||||
"upload.modal.expires.minute-singular": "Minut",
|
||||
"upload.modal.expires.minute-plural": "Minutter",
|
||||
"upload.modal.expires.hour-singular": "Time",
|
||||
"upload.modal.expires.hour-plural": "Timer",
|
||||
"upload.modal.expires.day-singular": "Dag",
|
||||
"upload.modal.expires.day-plural": "Dage",
|
||||
"upload.modal.expires.week-singular": "Uge",
|
||||
"upload.modal.expires.week-plural": "Uger",
|
||||
"upload.modal.expires.month-singular": "Måned",
|
||||
"upload.modal.expires.month-plural": "Måneder",
|
||||
"upload.modal.expires.year-singular": "År",
|
||||
"upload.modal.expires.year-plural": "År",
|
||||
"upload.modal.accordion.description.title": "Beskrivelse",
|
||||
"upload.modal.accordion.description.placeholder": "Note for the recipients of this share",
|
||||
"upload.modal.accordion.email.title": "E-mail modtagere",
|
||||
"upload.modal.accordion.email.placeholder": "Indtast e-mail modtagere",
|
||||
"upload.modal.accordion.email.invalid-email": "Ugyldig e-mailadresse",
|
||||
"upload.modal.accordion.security.title": "Sikkerhedsindstillinger",
|
||||
"upload.modal.accordion.security.password.label": "Adgangskodebeskyttelse",
|
||||
"upload.modal.accordion.security.password.placeholder": "Ingen adgangskode",
|
||||
"upload.modal.accordion.security.max-views.label": "Max antal visninger",
|
||||
"upload.modal.accordion.security.max-views.placeholder": "Ingen begrænsning",
|
||||
// showCompletedUploadModal.tsx
|
||||
"upload.modal.completed.never-expires": "Denne deling vil aldrig udløbe.",
|
||||
"upload.modal.completed.expires-on": "Denne omvendte deling udløber den {expiration}.",
|
||||
"upload.modal.completed.share-ready": "Delingen er klar",
|
||||
// END /upload
|
||||
// /share/[id]
|
||||
"share.title": "Del {shareId}",
|
||||
"share.description": "Se hvad jeg har delt med dig!",
|
||||
"share.error.visitor-limit-exceeded.title": "Grænsen for besøgende overskredet",
|
||||
"share.error.visitor-limit-exceeded.description": "Besøgsgrænsen for denne deling er blevet overskredet.",
|
||||
"share.error.removed.title": "Deling fjernet",
|
||||
"share.error.not-found.title": "Delingen blev ikke fundet",
|
||||
"share.error.not-found.description": "Den deling, du leder efter, eksisterer ikke.",
|
||||
"share.modal.password.title": "Adgangskode påkrævet",
|
||||
"share.modal.password.description": "For at få adgang til denne deling, indtast venligst adgangskoden til delingen.",
|
||||
"share.modal.password": "Adgangskode",
|
||||
"share.modal.error.invalid-password": "Ugyldig adgangskode",
|
||||
"share.button.download-all": "Download alle",
|
||||
"share.notify.download-all-preparing": "Delingen forberedes. Prøv igen om et par minutter.",
|
||||
"share.modal.file-link": "Fil link",
|
||||
"share.table.name": "Navn",
|
||||
"share.table.size": "Størrelse",
|
||||
"share.modal.file-preview.error.not-supported.title": "Forhåndsvisning ikke understøttet",
|
||||
"share.modal.file-preview.error.not-supported.description": "En forhåndsvisning for thise filtype er ikke understøttet. Download venligst filen for at se den.",
|
||||
// END /share/[id]
|
||||
// /admin/config
|
||||
"admin.config.title": "Konfiguration",
|
||||
"admin.config.category.general": "Generelt",
|
||||
"admin.config.category.share": "Del",
|
||||
"admin.config.category.email": "E-mail",
|
||||
"admin.config.category.smtp": "SMTP",
|
||||
"admin.config.general.app-name": "App-navn",
|
||||
"admin.config.general.app-name.description": "Navnet på applikationen",
|
||||
"admin.config.general.app-url": "App URL",
|
||||
"admin.config.general.app-url.description": "På hvilken URL Pingvin Share er tilgængelig",
|
||||
"admin.config.general.show-home-page": "Vis forside",
|
||||
"admin.config.general.show-home-page.description": "Om forsiden skal vises",
|
||||
"admin.config.general.logo": "Logo",
|
||||
"admin.config.general.logo.description": "Skift dit logo ved at uploade et nyt billede. Billedet skal være PNG og skal have formatet 1:1.",
|
||||
"admin.config.general.logo.placeholder": "Vælg billede",
|
||||
"admin.config.email.enable-share-email-recipients": "Aktiver deling til e-mail modtagere",
|
||||
"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 value.",
|
||||
"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": "Nulstil adgangskode besked",
|
||||
"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": "Invitations emne",
|
||||
"admin.config.email.invite-subject.description": "Emne for den e-mail, der sendes, når en administrator inviterer en ny bruger.",
|
||||
"admin.config.email.invite-message": "Invitations besked",
|
||||
"admin.config.email.invite-message.description": "Besked som bliver sendt, når en administrator inviterer en bruger. {url} vil blive erstattet med invitations-URL'en og {password} med adgangskoden.",
|
||||
"admin.config.share.allow-registration": "Tillad oprettelser",
|
||||
"admin.config.share.allow-registration.description": "Om alle skal kunne oprette en bruger",
|
||||
"admin.config.share.allow-unauthenticated-shares": "Tillad uautoriserede delinger",
|
||||
"admin.config.share.allow-unauthenticated-shares.description": "Whether unauthenticated users can create shares",
|
||||
"admin.config.share.max-size": "Maks. størrelse",
|
||||
"admin.config.share.max-size.description": "Maksimal filstørrelse i bytes",
|
||||
"admin.config.share.zip-compression-level": "Zip compression level",
|
||||
"admin.config.share.zip-compression-level.description": "Adjust the level to balance between file size and compression speed. Valid values range from 0 to 9, with 0 being no compression and 9 being maximum compression. ",
|
||||
"admin.config.smtp.enabled": "Aktiveret",
|
||||
"admin.config.smtp.enabled.description": "Om SMTP er aktiveret. Aktiver kun SMTP, hvis du har indtastet SMTP-server vært, port, e-mail, bruger og adgangskode.",
|
||||
"admin.config.smtp.host": "Vært",
|
||||
"admin.config.smtp.host.description": "Vært for SMTP serveren",
|
||||
"admin.config.smtp.port": "Port",
|
||||
"admin.config.smtp.port.description": "Porten til SMTP serveren",
|
||||
"admin.config.smtp.email": "E-mail",
|
||||
"admin.config.smtp.email.description": "E-mail adressen som der skal afsendes fra",
|
||||
"admin.config.smtp.username": "Brugernavn",
|
||||
"admin.config.smtp.username.description": "Brugernavnet til SMTP serveren",
|
||||
"admin.config.smtp.password": "Adgangskode",
|
||||
"admin.config.smtp.password.description": "Adgangskoden til SMTP serveren",
|
||||
"admin.config.smtp.button.test": "Send test e-mail",
|
||||
// 404
|
||||
"404.description": "Ups! Denne side findes ikke.",
|
||||
"404.button.home": "Bring me back home",
|
||||
// Common translations
|
||||
"common.button.save": "Gem",
|
||||
"common.button.create": "Opret",
|
||||
"common.button.submit": "Submit",
|
||||
"common.button.delete": "Slet",
|
||||
"common.button.cancel": "Annuller",
|
||||
"common.button.confirm": "Bekræft",
|
||||
"common.button.disable": "Deaktiver",
|
||||
"common.button.share": "Del",
|
||||
"common.button.generate": "Generer",
|
||||
"common.button.done": "Færdig",
|
||||
"common.text.link": "Link",
|
||||
"common.text.or": "eller",
|
||||
"common.button.go-back": "Gå tilbage",
|
||||
"common.notify.copied": "Linket blev kopieret til udklipsholderen",
|
||||
"common.success": "Success",
|
||||
"common.error": "Fejl",
|
||||
"common.error.unknown": "En ukendt fejl opstod",
|
||||
"common.error.invalid-email": "Ugyldig e-mail",
|
||||
"common.error.too-short": "Skal være på mindst {length} tegn",
|
||||
"common.error.too-long": "Må højst være {length} tegn",
|
||||
"common.error.exact-length": "Skal være præcis {length} tegn",
|
||||
"common.error.invalid-number": "Skal være et tal",
|
||||
"common.error.field-required": "Dette felt er påkrævet"
|
||||
};
|
||||
323
frontend/src/i18n/translations/de-DE.ts
Normal file
323
frontend/src/i18n/translations/de-DE.ts
Normal file
@@ -0,0 +1,323 @@
|
||||
export default {
|
||||
// Navbar
|
||||
"navbar.upload": "Hochladen",
|
||||
"navbar.signin": "Anmelden",
|
||||
"navbar.home": "Startseite",
|
||||
"navbar.signup": "Registrieren",
|
||||
"navbar.links.shares": "Meine Freigaben",
|
||||
"navbar.links.reverse": "Externe Freigaben",
|
||||
"navbar.avatar.account": "Mein Konto",
|
||||
"navbar.avatar.admin": "Verwaltung",
|
||||
"navbar.avatar.signout": "Abmelden",
|
||||
// END navbar
|
||||
// /
|
||||
"home.title": "Eine <h>selbst gehostete</h> Dateifreigabe-Plattform.",
|
||||
"home.description": "Möchtest du wirklich deine persönlichen Dateien an Drittanbieter wie WeTransfer weitergeben?",
|
||||
"home.bullet.a.name": "Selbst gehostet",
|
||||
"home.bullet.a.description": "Betreibe Pingvin Share auf deinem eigenen Server.",
|
||||
"home.bullet.b.name": "Privatsphäre",
|
||||
"home.bullet.b.description": "Deine Dateien gehören dir und sollten niemals in die Hände Dritter gelangen.",
|
||||
"home.bullet.c.name": "Keine lästige Dateigrößenbegrenzung",
|
||||
"home.bullet.c.description": "Lade Dateien beliebiger Größe hoch. Nur dein Festplattenspeicher stellt die Grenze dar.",
|
||||
"home.button.start": "Lege los",
|
||||
"home.button.source": "Quellcode",
|
||||
// END /
|
||||
// /auth/signin
|
||||
"signin.title": "Willkommen zurück",
|
||||
"signin.description": "Du hast noch kein Konto?",
|
||||
"signin.button.signup": "Registrieren",
|
||||
"signin.input.email-or-username": "Email oder Benutzername",
|
||||
"signin.input.email-or-username.placeholder": "Deine Email Adresse oder Benutzername",
|
||||
"signin.input.password": "Passwort",
|
||||
"signin.input.password.placeholder": "Dein Passwort",
|
||||
"signin.button.submit": "Anmelden",
|
||||
"signIn.notify.totp-required.title": "Zwei-Faktor-Authentifizierung benötigt",
|
||||
"signIn.notify.totp-required.description": "Bitte füge deinen Zwei-Faktor-Authentifizierungscode ein",
|
||||
// END /auth/signin
|
||||
// /auth/signup
|
||||
"signup.title": "Erstelle ein Konto",
|
||||
"signup.description": "Besitzt du bereits ein Konto?",
|
||||
"signup.button.signin": "Anmelden",
|
||||
"signup.input.username": "Benutzername",
|
||||
"signup.input.username.placeholder": "Dein Benutzername",
|
||||
"signup.input.email": "Email",
|
||||
"signup.input.email.placeholder": "Deine Emailadresse",
|
||||
"signup.button.submit": "Lass uns loslegen",
|
||||
// END /auth/signup
|
||||
// /auth/reset-password
|
||||
"resetPassword.title": "Passwort vergessen?",
|
||||
"resetPassword.description": "Gib deine Email Adresse ein, um dein Passwort zurückzusetzen.",
|
||||
"resetPassword.notify.success": "Ein Link zum Rücksetzen des Passwortes wurde an deine Emailadresse versandt.",
|
||||
"resetPassword.button.back": "Zurück zur Anmeldeseite",
|
||||
"resetPassword.text.resetPassword": "Passwort zurücksetzen",
|
||||
"resetPassword.text.enterNewPassword": "Gib dein neues Passwort ein",
|
||||
"resetPassword.input.password": "Neues Passwort",
|
||||
"resetPassword.notify.passwordReset": "Dein Passwort wurde erfolgreich zurückgesetzt.",
|
||||
// /account
|
||||
"account.title": "Mein Konto",
|
||||
"account.card.info.title": "Kontoinformationen",
|
||||
"account.card.info.username": "Benutzername",
|
||||
"account.card.info.email": "Email",
|
||||
"account.notify.info.success": "Konto erfolgreich aktualisiert",
|
||||
"account.card.password.title": "Passwort",
|
||||
"account.card.password.old": "Altes Passwort",
|
||||
"account.card.password.new": "Neues Passwort",
|
||||
"account.notify.password.success": "Passwort erfolgreich geändert",
|
||||
"account.card.security.title": "Sicherheit",
|
||||
"account.card.security.totp.enable.description": "Geben dein aktuelles Passwort ein, um TOTP zu aktivieren",
|
||||
"account.card.security.totp.disable.description": "Geben dein aktuelles Passwort ein, um TOTP zu deaktivieren",
|
||||
"account.card.security.totp.button.start": "Starten",
|
||||
"account.modal.totp.title": "TOTP aktivieren",
|
||||
"account.modal.totp.step1": "Schritt 1: Füge deinen Authenticator hinzu",
|
||||
"account.modal.totp.step2": "Schritt 2: Bestätige deinen Code",
|
||||
"account.modal.totp.enterManually": "Manuell eingeben",
|
||||
"account.modal.totp.code": "Code",
|
||||
"account.modal.totp.clickToCopy": "Klicken zum Kopieren",
|
||||
"account.modal.totp.verify": "Überprüfen",
|
||||
"account.notify.totp.disable": "TOTP erfolgreich deaktiviert",
|
||||
"account.notify.totp.enable": "TOTP erfolgreich aktiviert",
|
||||
"account.card.language.title": "Sprache",
|
||||
"account.card.language.description": "Das Projekt wird von der Community übersetzt. Einige Sprachen könnten unvollständig sein.",
|
||||
"account.card.color.title": "Farbschema",
|
||||
// ThemeSwitcher.tsx
|
||||
"account.theme.dark": "Dunkel",
|
||||
"account.theme.light": "Hell",
|
||||
"account.theme.system": "System",
|
||||
"account.button.delete": "Konto löschen",
|
||||
"account.modal.delete.title": "Konto löschen",
|
||||
"account.modal.delete.description": "Möchtest du wirklich dein Konto inklusive aller aktiven Freigaben löschen?",
|
||||
// END /account
|
||||
// /account/shares
|
||||
"account.shares.title": "Meine Freigaben",
|
||||
"account.shares.title.empty": "Es ist so leer hier 👀",
|
||||
"account.shares.description.empty": "Du hast keine Freigaben eingerichtet.",
|
||||
"account.shares.button.create": "Erstelle eine",
|
||||
"account.shares.info.title": "Teile deine Information",
|
||||
"account.shares.table.id": "ID",
|
||||
"account.shares.table.name": "Name",
|
||||
"account.shares.table.description": "Beschreibung",
|
||||
"account.shares.table.visitors": "Besucher",
|
||||
"account.shares.table.expiresAt": "Läuft ab am",
|
||||
"account.shares.table.createdAt": "Erstellt am",
|
||||
"account.shares.table.size": "Größe",
|
||||
"account.shares.modal.share-informations": "Teile deine Information",
|
||||
"account.shares.modal.share-link": "Freigabe teilen",
|
||||
"account.shares.modal.delete.title": "Lösche Freigabe {share}",
|
||||
"account.shares.modal.delete.description": "Möchtest du wirklich diese Freigabe löschen?",
|
||||
// END /account/shares
|
||||
// /account/reverseShares
|
||||
"account.reverseShares.title": "Externe Freigaben",
|
||||
"account.reverseShares.description": "Eine externe Freigabe erlaubt dir eine einzigartige URL zu erstellen, die externen Benutzern erlaubt Dateien hochzuladen.",
|
||||
"account.reverseShares.title.empty": "Es ist leer hier 👀",
|
||||
"account.reverseShares.description.empty": "Du hast keine externen Freigaben erstellt.",
|
||||
// showCreateReverseShareModal.tsx
|
||||
"account.reverseShares.modal.expiration.label": "Gültig bis",
|
||||
"account.reverseShares.modal.expiration.minute-singular": "Minute",
|
||||
"account.reverseShares.modal.expiration.minute-plural": "Minuten",
|
||||
"account.reverseShares.modal.expiration.hour-singular": "Stunde",
|
||||
"account.reverseShares.modal.expiration.hour-plural": "Stunden",
|
||||
"account.reverseShares.modal.expiration.day-singular": "Tag",
|
||||
"account.reverseShares.modal.expiration.day-plural": "Tage",
|
||||
"account.reverseShares.modal.expiration.week-singular": "Woche",
|
||||
"account.reverseShares.modal.expiration.week-plural": "Wochen",
|
||||
"account.reverseShares.modal.expiration.month-singular": "Monat",
|
||||
"account.reverseShares.modal.expiration.month-plural": "Monate",
|
||||
"account.reverseShares.modal.expiration.year-singular": "Jahr",
|
||||
"account.reverseShares.modal.expiration.year-plural": "Jahre",
|
||||
"account.reverseShares.modal.max-size.label": "Max. Freigabengröße",
|
||||
"account.reverseShares.modal.send-email": "Email Benachrichtigung senden",
|
||||
"account.reverseShares.modal.send-email.description": "Sendet eine Email Benachrichtigung, wenn eine Datei auf einer externen Freigabe hochgeladen wurde.",
|
||||
"account.reverseShares.modal.max-use.label": "Maximale Nutzungen",
|
||||
"account.reverseShares.modal.max-use.description": "Die maximale Anzahl von Verwendungen der URL, um Dateien hochzuladen.",
|
||||
"account.reverseShare.never-expires": "Diese externe Freigabe wird nicht ablaufen.",
|
||||
"account.reverseShare.expires-on": "Diese externe Freigabe wird am {expiration} ablaufen.",
|
||||
"account.reverseShares.table.no-shares": "Noch keine Freigaben erstellt",
|
||||
"account.reverseShares.table.count.singular": "Freigabe",
|
||||
"account.reverseShares.table.count.plural": "Freigaben",
|
||||
"account.reverseShares.table.shares": "Freigaben",
|
||||
"account.reverseShares.table.remaining": "Verbleibende Verwendungen",
|
||||
"account.reverseShares.table.max-size": "Maximale Freigabegröße",
|
||||
"account.reverseShares.table.expires": "Läuft ab am",
|
||||
"account.reverseShares.modal.reverse-share-link": "Link zu externer Freigabe",
|
||||
"account.reverseShares.modal.delete.title": "Lösche externe Freigabe",
|
||||
"account.reverseShares.modal.delete.description": "Möchtest du wirklich diese externe Freigabe löschen? In diesem Falle werden auch hiermit verbundene Freigaben gelöscht.",
|
||||
// END /account/reverseShares
|
||||
// /admin
|
||||
"admin.title": "Verwaltung",
|
||||
"admin.button.users": "Benutzerverwaltung",
|
||||
"admin.button.config": "Konfiguration",
|
||||
"admin.version": "Version",
|
||||
// END /admin
|
||||
// /admin/users
|
||||
"admin.users.title": "Benutzerverwaltung",
|
||||
"admin.users.table.username": "Benutzername",
|
||||
"admin.users.table.email": "Email",
|
||||
"admin.users.table.admin": "Administrator",
|
||||
"admin.users.edit.update.title": "Benutzer {username} aktualisieren",
|
||||
"admin.users.edit.update.admin-privileges": "Administratorrechte",
|
||||
"admin.users.edit.update.change-password.title": "Passwort ändern",
|
||||
"admin.users.edit.update.change-password.field": "Neues Passwort",
|
||||
"admin.users.edit.update.change-password.button": "Neues Passwort speichern",
|
||||
"admin.users.edit.update.notify.password.success": "Passwort erfolgreich geändert",
|
||||
"admin.users.edit.delete.title": "Löschen des Nutzers {username}",
|
||||
"admin.users.edit.delete.description": "Möchtest du wirklich diesen Benutzer und all seine Freigaben löschen?",
|
||||
// showCreateUserModal.tsx
|
||||
"admin.users.modal.create.title": "Benutzer erstellen",
|
||||
"admin.users.modal.create.username": "Benutzername",
|
||||
"admin.users.modal.create.email": "Email",
|
||||
"admin.users.modal.create.password": "Passwort",
|
||||
"admin.users.modal.create.manual-password": "Passwort manuell festlegen",
|
||||
"admin.users.modal.create.manual-password.description": "Wenn nicht aktiviert, erhält der Benutzer eine Email mit einem Link, um sein Passwort festzulegen.",
|
||||
"admin.users.modal.create.admin": "Administratorrechte",
|
||||
"admin.users.modal.create.admin.description": "Wenn aktiviert, kann der Benutzer auf das Administrator-Panel zugreifen.",
|
||||
// END /admin/users
|
||||
// /upload
|
||||
"upload.title": "Upload",
|
||||
"upload.notify.generic-error": "Während der Erstellung der Freigabe ist ein Fehler aufgetreten.",
|
||||
"upload.notify.count-failed": "{count} Dateien konnten nicht hochgeladen werden. Wird erneut versucht.",
|
||||
// Dropzone.tsx
|
||||
"upload.dropzone.title": "Dateien hochladen",
|
||||
"upload.dropzone.description": "Ziehen Sie Dateien hierher, um Ihre Freigabe zu starten. Wir können nur Dateien akzeptieren, die insgesamt weniger als {maxSize} groß sind.",
|
||||
"upload.dropzone.notify.file-too-big": "Ihre Dateien überschreiten die maximale Freigabegröße von {maxSize}.",
|
||||
// FileList.tsx
|
||||
"upload.filelist.name": "Name",
|
||||
"upload.filelist.size": "Größe",
|
||||
// showCreateUploadModal.tsx
|
||||
"upload.modal.title": "Erstelle Freigabe",
|
||||
"upload.modal.link.error.invalid": "Darf nur Buchstaben, Zahlen, Unterstriche und Bindestriche enthalten",
|
||||
"upload.modal.link.error.taken": "Dieser Link wird bereits verwendet",
|
||||
"upload.modal.not-signed-in": "Du bist nicht angemeldet",
|
||||
"upload.modal.not-signed-in-description": "Du wirst deine Freigabe nicht löschen können oder die Besucheranzahl sehen.",
|
||||
"upload.modal.expires.never": "niemals",
|
||||
"upload.modal.expires.never-long": "Läuft nicht ab",
|
||||
"upload.modal.link.label": "Link",
|
||||
"upload.modal.expires.label": "Gültig bis",
|
||||
"upload.modal.expires.minute-singular": "Minute",
|
||||
"upload.modal.expires.minute-plural": "Minuten",
|
||||
"upload.modal.expires.hour-singular": "Stunde",
|
||||
"upload.modal.expires.hour-plural": "Stunden",
|
||||
"upload.modal.expires.day-singular": "Tag",
|
||||
"upload.modal.expires.day-plural": "Tage",
|
||||
"upload.modal.expires.week-singular": "Woche",
|
||||
"upload.modal.expires.week-plural": "Wochen",
|
||||
"upload.modal.expires.month-singular": "Monat",
|
||||
"upload.modal.expires.month-plural": "Monate",
|
||||
"upload.modal.expires.year-singular": "Jahr",
|
||||
"upload.modal.expires.year-plural": "Year",
|
||||
"upload.modal.accordion.description.title": "Beschreibung",
|
||||
"upload.modal.accordion.description.placeholder": "Hinweis für die Empfänger dieser Freigabe",
|
||||
"upload.modal.accordion.email.title": "Email Empfänger",
|
||||
"upload.modal.accordion.email.placeholder": "Email der Empfänger eingeben",
|
||||
"upload.modal.accordion.email.invalid-email": "Ungültige Emailadresse",
|
||||
"upload.modal.accordion.security.title": "Sicherheitseinstellungen",
|
||||
"upload.modal.accordion.security.password.label": "Passwortschutz",
|
||||
"upload.modal.accordion.security.password.placeholder": "Kein Passwort",
|
||||
"upload.modal.accordion.security.max-views.label": "Maximale Aufrufe",
|
||||
"upload.modal.accordion.security.max-views.placeholder": "Unbegrenzt",
|
||||
// showCompletedUploadModal.tsx
|
||||
"upload.modal.completed.never-expires": "Diese Freigabe läuft niemals ab.",
|
||||
"upload.modal.completed.expires-on": "Diese Freigabe wird am {expiration} ablaufen.",
|
||||
"upload.modal.completed.share-ready": "Freigabe bereit",
|
||||
// END /upload
|
||||
// /share/[id]
|
||||
"share.title": "Freigabe {shareId}",
|
||||
"share.description": "Schau, was ich mit dir geteilt habe!",
|
||||
"share.error.visitor-limit-exceeded.title": "Besucher Limit erreicht",
|
||||
"share.error.visitor-limit-exceeded.description": "Die maximale Besucheranzahl für diese Freigabe ist überschritten.",
|
||||
"share.error.removed.title": "Freigabe entfernt",
|
||||
"share.error.not-found.title": "Freigabe nicht gefunden",
|
||||
"share.error.not-found.description": "Die gesuchte Freigabe existiert nicht.",
|
||||
"share.modal.password.title": "Passwort erforderlich",
|
||||
"share.modal.password.description": "Um auf diese Freigabe zuzugreifen, geben Sie bitte das Passwort für die Freigabe ein.",
|
||||
"share.modal.password": "Passwort",
|
||||
"share.modal.error.invalid-password": "Ungültiges Passwort",
|
||||
"share.button.download-all": "Alles herunterladen",
|
||||
"share.notify.download-all-preparing": "Die Freigabe wird vorbereitet. Versuche es in ein paar Minuten erneut.",
|
||||
"share.modal.file-link": "Dateilink",
|
||||
"share.table.name": "Name",
|
||||
"share.table.size": "Größe",
|
||||
"share.modal.file-preview.error.not-supported.title": "Vorschau wird nicht unterstützt",
|
||||
"share.modal.file-preview.error.not-supported.description": "Eine Vorschau für diesen Dateityp wird nicht unterstützt. Bitte laden Sie die Datei herunter, um sie anzuzeigen.",
|
||||
// END /share/[id]
|
||||
// /admin/config
|
||||
"admin.config.title": "Einstellungen",
|
||||
"admin.config.category.general": "Allgemein",
|
||||
"admin.config.category.share": "Freigabe",
|
||||
"admin.config.category.email": "E-Mail",
|
||||
"admin.config.category.smtp": "SMTP",
|
||||
"admin.config.general.app-name": "App-Name",
|
||||
"admin.config.general.app-name.description": "Name der Applikation",
|
||||
"admin.config.general.app-url": "App-URL",
|
||||
"admin.config.general.app-url.description": "Auf welcher URL Pingvin Share verfügbar ist",
|
||||
"admin.config.general.show-home-page": "Startseite anzeigen",
|
||||
"admin.config.general.show-home-page.description": "Ob die Startseite angezeigt werden soll",
|
||||
"admin.config.general.logo": "Logo",
|
||||
"admin.config.general.logo.description": "Ändere dein Logo durch Hochladen eines Bildes. Das Bild muss im PNG-Format vorliegen und sollte mit Seitenverhältnis 1:1 sein.",
|
||||
"admin.config.general.logo.placeholder": "Bild auswählen",
|
||||
"admin.config.email.enable-share-email-recipients": "Erlaube das Teilen der Freigabe via Email",
|
||||
"admin.config.email.enable-share-email-recipients.description": "Gibt an, ob Emails an Freigabe-Empfänger ermöglicht werden sollen. Aktiviere dies nur, wenn Du SMTP aktivierst hast.",
|
||||
"admin.config.email.share-recipients-subject": "Betreff für Freigabe-Empfänger",
|
||||
"admin.config.email.share-recipients-subject.description": "Betreff der E-Mail, die an die Freigabe-Empfänger gesendet wird.",
|
||||
"admin.config.email.share-recipients-message": "Nachricht für Freigabe-Empfänger",
|
||||
"admin.config.email.share-recipients-message.description": "Nachricht, die an die Freigabe-Empfänger gesendet wird. Verfügbare Variablen:\n- {creator} - Der Benutzername des Erstellers der Freigabe\n- {shareUrl} - Die URL der Freigabe\n- {desc} - Die Beschreibung der Freigabe\n- {expires} - Das Ablaufdatum der Freigabe\nVariablen werden durch die tatsächlichen Werte ersetzt.",
|
||||
"admin.config.email.reverse-share-subject": "Name der externen Freigabe",
|
||||
"admin.config.email.reverse-share-subject.description": "Betreff der Email, die gesendet wird, wenn jemand eine Datei mit deinem externen Freigabe-Link hochlädt.",
|
||||
"admin.config.email.reverse-share-message": "Nachricht für externe Freigabe",
|
||||
"admin.config.email.reverse-share-message.description": "Nachricht, die gesendet wird, wenn jemand eine Freigabe mit deinem externen Freigabe-Link erstellt. {shareUrl} wird durch den Namen des Erstellers und die Freigabe-URL ersetzt.",
|
||||
"admin.config.email.reset-password-subject": "Betreff für Passwortzurücksetzung",
|
||||
"admin.config.email.reset-password-subject.description": "Betreff der E-Mail, die gesendet wird, wenn ein Benutzer eine Passwortzurücksetzung anfordert.",
|
||||
"admin.config.email.reset-password-message": "Nachricht für Passwortzurücksetzung",
|
||||
"admin.config.email.reset-password-message.description": "Nachricht, die gesendet wird, wenn ein Benutzer eine Passwortzurücksetzung anfordert. {url} wird durch die URL für das Zurücksetzen des Passworts ersetzt.",
|
||||
"admin.config.email.invite-subject": "Betreff für Einladung",
|
||||
"admin.config.email.invite-subject.description": "Betreff der E-Mail, die gesendet wird, wenn ein Administrator einen Benutzer einlädt.",
|
||||
"admin.config.email.invite-message": "Nachricht für Einladung",
|
||||
"admin.config.email.invite-message.description": "Nachricht, die gesendet wird, wenn ein Administrator einen Benutzer einlädt. {url} wird durch die Einladungs-URL und {password} durch das Passwort ersetzt.",
|
||||
"admin.config.share.allow-registration": "Registrierung erlauben",
|
||||
"admin.config.share.allow-registration.description": "Gibt an, ob eine Registrierung erlaubt ist",
|
||||
"admin.config.share.allow-unauthenticated-shares": "Nicht authentifizierte Freigaben erlauben",
|
||||
"admin.config.share.allow-unauthenticated-shares.description": "Gibt an, ob nicht authentifizierte Benutzer Freigaben erstellen können",
|
||||
"admin.config.share.max-size": "Maximale Größe",
|
||||
"admin.config.share.max-size.description": "Maximale Größe einer Freigabe in Bytes",
|
||||
"admin.config.share.zip-compression-level": "Zip Komprimierungsstufe",
|
||||
"admin.config.share.zip-compression-level.description": "Adjust the level to balance between file size and compression speed. Valid values range from 0 to 9, with 0 being no compression and 9 being maximum compression. ",
|
||||
"admin.config.smtp.enabled": "Aktiviert",
|
||||
"admin.config.smtp.enabled.description": "Gibt an, ob SMTP aktiviert ist. Aktiviere dies nur, wenn Du den Host, den Port, die Email, den Benutzernamen und das Passwort deines SMTP-Servers eingegeben hast.",
|
||||
"admin.config.smtp.host": "Host",
|
||||
"admin.config.smtp.host.description": "Host des SMTP-Servers",
|
||||
"admin.config.smtp.port": "Port",
|
||||
"admin.config.smtp.port.description": "Port des SMTP-Servers",
|
||||
"admin.config.smtp.email": "E-Mail",
|
||||
"admin.config.smtp.email.description": "E-Mail-Adresse, von der die E-Mails gesendet werden",
|
||||
"admin.config.smtp.username": "Benutzername",
|
||||
"admin.config.smtp.username.description": "Benutzername des SMTP-Servers",
|
||||
"admin.config.smtp.password": "Passwort",
|
||||
"admin.config.smtp.password.description": "Passwort des SMTP-Servers",
|
||||
"admin.config.smtp.button.test": "Test-E-Mail senden",
|
||||
// 404
|
||||
"404.description": "Ups, diese Seite existiert nicht.",
|
||||
"404.button.home": "Zurück zur Startseite",
|
||||
// Common translations
|
||||
"common.button.save": "Speichern",
|
||||
"common.button.create": "Erstellen",
|
||||
"common.button.submit": "Bestätigen",
|
||||
"common.button.delete": "Löschen",
|
||||
"common.button.cancel": "Abbrechen",
|
||||
"common.button.confirm": "Bestätigen",
|
||||
"common.button.disable": "Deaktivieren",
|
||||
"common.button.share": "Teilen",
|
||||
"common.button.generate": "Generieren",
|
||||
"common.button.done": "Fertig",
|
||||
"common.text.link": "Link",
|
||||
"common.text.or": "oder",
|
||||
"common.button.go-back": "Zurück",
|
||||
"common.notify.copied": "Dein Link wurde in die Zwischenablage kopiert",
|
||||
"common.success": "Erfolg",
|
||||
"common.error": "Fehler",
|
||||
"common.error.unknown": "Ein unbekannter Fehler ist aufgetreten",
|
||||
"common.error.invalid-email": "Ungültige E-Mail-Adresse",
|
||||
"common.error.too-short": "Muss mindestens {length} Zeichen enthalten",
|
||||
"common.error.too-long": "Muss maximal {length} Zeichen enthalten",
|
||||
"common.error.exact-length": "Muss genau {length} Zeichen lang sein",
|
||||
"common.error.invalid-number": "Muss eine Zahl sein",
|
||||
"common.error.field-required": "Dieses Feld ist erforderlich"
|
||||
};
|
||||
438
frontend/src/i18n/translations/en-US.ts
Normal file
438
frontend/src/i18n/translations/en-US.ts
Normal file
@@ -0,0 +1,438 @@
|
||||
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 disable 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.language.description":
|
||||
"The project is translated by the community. Some languages might be incomplete.",
|
||||
"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 informations",
|
||||
"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-informations": "Share informations",
|
||||
"account.shares.modal.share-link": "Share link",
|
||||
|
||||
"account.shares.modal.delete.title": "Delete share {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": "Delete reverse share",
|
||||
"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} files 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.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 value.",
|
||||
"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 unauthenticated users can create shares",
|
||||
"admin.config.share.max-size": "Max size",
|
||||
"admin.config.share.max-size.description": "Maximum share size in bytes",
|
||||
"admin.config.share.zip-compression-level": "Zip compression level",
|
||||
"admin.config.share.zip-compression-level.description":
|
||||
"Adjust the level to balance between file size and compression speed. Valid values range from 0 to 9, with 0 being no compression and 9 being maximum compression. ",
|
||||
|
||||
"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.description": "Oops this page doesn't exist.",
|
||||
"404.button.home": "Bring 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.success": "Success",
|
||||
|
||||
"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",
|
||||
};
|
||||
323
frontend/src/i18n/translations/es-ES.ts
Normal file
323
frontend/src/i18n/translations/es-ES.ts
Normal file
@@ -0,0 +1,323 @@
|
||||
export default {
|
||||
// Navbar
|
||||
"navbar.upload": "Subir",
|
||||
"navbar.signin": "Iniciar sesión",
|
||||
"navbar.home": "Inicio",
|
||||
"navbar.signup": "Registrarse",
|
||||
"navbar.links.shares": "Mis compartidos",
|
||||
"navbar.links.reverse": "Reverse Shares",
|
||||
"navbar.avatar.account": "Mi cuenta",
|
||||
"navbar.avatar.admin": "Administración",
|
||||
"navbar.avatar.signout": "Cerrar sesión",
|
||||
// END navbar
|
||||
// /
|
||||
"home.title": "Una plataforma <h>autoalojada</h> para compartir archivos.",
|
||||
"home.description": "¿En realidad quieres dejar tus archivos personales en manos de terceros como WeTransfer?",
|
||||
"home.bullet.a.name": "Autoalojada",
|
||||
"home.bullet.a.description": "Aloja Pingvin Share en tu propio equipo.",
|
||||
"home.bullet.b.name": "Privacidad",
|
||||
"home.bullet.b.description": "Tus archivos son tus archivos y nunca deberían terminar en manos de terceros.",
|
||||
"home.bullet.c.name": "Sin molestos límites de tamaño de archivo",
|
||||
"home.bullet.c.description": "Sube archivos tan grandes como quieras. El único límite es la capacidad de tu disco duro.",
|
||||
"home.button.start": "Comenzar",
|
||||
"home.button.source": "Código fuente",
|
||||
// END /
|
||||
// /auth/signin
|
||||
"signin.title": "Bienvenido nuevamente",
|
||||
"signin.description": "¿Todavía no tienes una cuenta?",
|
||||
"signin.button.signup": "Registrarse",
|
||||
"signin.input.email-or-username": "Correo o nombre de usuario",
|
||||
"signin.input.email-or-username.placeholder": "Tu correo o nombre de usuario",
|
||||
"signin.input.password": "Contraseña",
|
||||
"signin.input.password.placeholder": "Tu contraseña",
|
||||
"signin.button.submit": "Iniciar sesión",
|
||||
"signIn.notify.totp-required.title": "Se requiere autenticación de dos factores",
|
||||
"signIn.notify.totp-required.description": "Por favor ingrese su código de autenticación de dos factores",
|
||||
// END /auth/signin
|
||||
// /auth/signup
|
||||
"signup.title": "Crear una cuenta",
|
||||
"signup.description": "¿Ya tienes una cuenta?",
|
||||
"signup.button.signin": "Iniciar sesión",
|
||||
"signup.input.username": "Nombre de usuario",
|
||||
"signup.input.username.placeholder": "Tu nombre de usuario",
|
||||
"signup.input.email": "Correo",
|
||||
"signup.input.email.placeholder": "Tu correo",
|
||||
"signup.button.submit": "Comencemos",
|
||||
// END /auth/signup
|
||||
// /auth/reset-password
|
||||
"resetPassword.title": "¿Olvidaste tu contraseña?",
|
||||
"resetPassword.description": "Ingresa tu correo para restablecer tu contraseña.",
|
||||
"resetPassword.notify.success": "Se ha enviado un correo con el enlace para restablecer tu contraseña.",
|
||||
"resetPassword.button.back": "Volver al inicio de sesión",
|
||||
"resetPassword.text.resetPassword": "Restablecer contraseña",
|
||||
"resetPassword.text.enterNewPassword": "Ingresa tu nueva contraseña",
|
||||
"resetPassword.input.password": "Nueva contraseña",
|
||||
"resetPassword.notify.passwordReset": "Tu contraseña se ha restablecido correctamente.",
|
||||
// /account
|
||||
"account.title": "Mi cuenta",
|
||||
"account.card.info.title": "Información de cuenta",
|
||||
"account.card.info.username": "Nombre de usuario",
|
||||
"account.card.info.email": "Correo",
|
||||
"account.notify.info.success": "Cuenta actualizada correctamente",
|
||||
"account.card.password.title": "Contraseña",
|
||||
"account.card.password.old": "Anterior contraseña",
|
||||
"account.card.password.new": "Nueva contraseña",
|
||||
"account.notify.password.success": "Contraseña cambiada correctamente",
|
||||
"account.card.security.title": "Seguridad",
|
||||
"account.card.security.totp.enable.description": "Ingrese su contraseña actual para habilitar TOTP",
|
||||
"account.card.security.totp.disable.description": "Ingrese su contraseña actual para deshabilitar TOTP",
|
||||
"account.card.security.totp.button.start": "Iniciar",
|
||||
"account.modal.totp.title": "Habilitar TOTP",
|
||||
"account.modal.totp.step1": "Paso 1: Añadir tu autentificador",
|
||||
"account.modal.totp.step2": "Paso 2: Validar tu código",
|
||||
"account.modal.totp.enterManually": "Ingresar manualmente",
|
||||
"account.modal.totp.code": "Código",
|
||||
"account.modal.totp.clickToCopy": "Clic para copiar",
|
||||
"account.modal.totp.verify": "Verificar",
|
||||
"account.notify.totp.disable": "TOTP deshabilitado correctamente",
|
||||
"account.notify.totp.enable": "TOTP habilitado correctamente",
|
||||
"account.card.language.title": "Idioma",
|
||||
"account.card.language.description": "El proyecto ha sido traducido por la comunidad. Algunos idiomas pueden estar incompletos.",
|
||||
"account.card.color.title": "Esquema de colores",
|
||||
// ThemeSwitcher.tsx
|
||||
"account.theme.dark": "Oscuro",
|
||||
"account.theme.light": "Claro",
|
||||
"account.theme.system": "Sistema",
|
||||
"account.button.delete": "Eliminar Cuenta",
|
||||
"account.modal.delete.title": "Eliminar Cuenta",
|
||||
"account.modal.delete.description": "¿Realmente quieres eliminar tu cuenta con todos los archivos que estás compartiendo actualmente?",
|
||||
// END /account
|
||||
// /account/shares
|
||||
"account.shares.title": "Mis compartidos",
|
||||
"account.shares.title.empty": "Aquí está vacío 👀",
|
||||
"account.shares.description.empty": "No tienes nada compartido.",
|
||||
"account.shares.button.create": "Crear uno",
|
||||
"account.shares.info.title": "Información del compartido",
|
||||
"account.shares.table.id": "ID",
|
||||
"account.shares.table.name": "Nombre",
|
||||
"account.shares.table.description": "Descripción",
|
||||
"account.shares.table.visitors": "Visitas",
|
||||
"account.shares.table.expiresAt": "Expira en",
|
||||
"account.shares.table.createdAt": "Creado en",
|
||||
"account.shares.table.size": "Tamaño",
|
||||
"account.shares.modal.share-informations": "Información del compartido",
|
||||
"account.shares.modal.share-link": "Enlace",
|
||||
"account.shares.modal.delete.title": "Eliminar compartido {share}",
|
||||
"account.shares.modal.delete.description": "¿Seguro que quieres eliminar este compartido?",
|
||||
// END /account/shares
|
||||
// /account/reverseShares
|
||||
"account.reverseShares.title": "Reverse Shares",
|
||||
"account.reverseShares.description": "Un Reverse Share te permite generar una URL única con la que usuarios externos pueden compartir archivos.",
|
||||
"account.reverseShares.title.empty": "Aquí está vacío 👀",
|
||||
"account.reverseShares.description.empty": "No tienes ningún Reverse Share.",
|
||||
// showCreateReverseShareModal.tsx
|
||||
"account.reverseShares.modal.expiration.label": "Expiración",
|
||||
"account.reverseShares.modal.expiration.minute-singular": "Minuto",
|
||||
"account.reverseShares.modal.expiration.minute-plural": "Minutos",
|
||||
"account.reverseShares.modal.expiration.hour-singular": "Hora",
|
||||
"account.reverseShares.modal.expiration.hour-plural": "Horas",
|
||||
"account.reverseShares.modal.expiration.day-singular": "Día",
|
||||
"account.reverseShares.modal.expiration.day-plural": "Días",
|
||||
"account.reverseShares.modal.expiration.week-singular": "Semana",
|
||||
"account.reverseShares.modal.expiration.week-plural": "Semanas",
|
||||
"account.reverseShares.modal.expiration.month-singular": "Mes",
|
||||
"account.reverseShares.modal.expiration.month-plural": "Meses",
|
||||
"account.reverseShares.modal.expiration.year-singular": "Año",
|
||||
"account.reverseShares.modal.expiration.year-plural": "Años",
|
||||
"account.reverseShares.modal.max-size.label": "Tamaño máximo del compartido",
|
||||
"account.reverseShares.modal.send-email": "Enviar notificación por correo",
|
||||
"account.reverseShares.modal.send-email.description": "Enviar una notificación por correo cuando se comparta algo con este enlace de Reverse Share.",
|
||||
"account.reverseShares.modal.max-use.label": "Máximo de usos",
|
||||
"account.reverseShares.modal.max-use.description": "Cantidad máxima de veces que esta URL se puede usar para crear un compartido.",
|
||||
"account.reverseShare.never-expires": "Esta Reverse Share nunca expirará.",
|
||||
"account.reverseShare.expires-on": "Esta Reverse Share expirará en {expiration}.",
|
||||
"account.reverseShares.table.no-shares": "Todavía no se han creado compartidos",
|
||||
"account.reverseShares.table.count.singular": "compartido",
|
||||
"account.reverseShares.table.count.plural": "compartidos",
|
||||
"account.reverseShares.table.shares": "Compartidos",
|
||||
"account.reverseShares.table.remaining": "Usos restantes",
|
||||
"account.reverseShares.table.max-size": "Tamaño máximo del compartido",
|
||||
"account.reverseShares.table.expires": "Expira en",
|
||||
"account.reverseShares.modal.reverse-share-link": "Enlace de Reverse Share",
|
||||
"account.reverseShares.modal.delete.title": "Eliminar Reverse Share",
|
||||
"account.reverseShares.modal.delete.description": "¿Seguro que quieres eliminar esta Reverse Share? Si lo haces, todos los archivos asociados también serán eliminados.",
|
||||
// END /account/reverseShares
|
||||
// /admin
|
||||
"admin.title": "Administración",
|
||||
"admin.button.users": "Gestión de usuarios",
|
||||
"admin.button.config": "Configuración",
|
||||
"admin.version": "Versión",
|
||||
// END /admin
|
||||
// /admin/users
|
||||
"admin.users.title": "Gestión de usuarios",
|
||||
"admin.users.table.username": "Nombre de usuario",
|
||||
"admin.users.table.email": "Correo",
|
||||
"admin.users.table.admin": "Administrador",
|
||||
"admin.users.edit.update.title": "Actualizar usuario {username}",
|
||||
"admin.users.edit.update.admin-privileges": "Privilegios de administrador",
|
||||
"admin.users.edit.update.change-password.title": "Cambiar contraseña",
|
||||
"admin.users.edit.update.change-password.field": "Nueva contraseña",
|
||||
"admin.users.edit.update.change-password.button": "Guardar nueva contraseña",
|
||||
"admin.users.edit.update.notify.password.success": "Contraseña cambiada correctamente",
|
||||
"admin.users.edit.delete.title": "Eliminar usuario {username}",
|
||||
"admin.users.edit.delete.description": "¿Realmente quiere eliminar este usuario y todos sus archivos compartidos?",
|
||||
// showCreateUserModal.tsx
|
||||
"admin.users.modal.create.title": "Crear usuario",
|
||||
"admin.users.modal.create.username": "Nombre de usuario",
|
||||
"admin.users.modal.create.email": "Correo",
|
||||
"admin.users.modal.create.password": "Contraseña",
|
||||
"admin.users.modal.create.manual-password": "Establecer contraseña manualmente",
|
||||
"admin.users.modal.create.manual-password.description": "Si no se marca, el usuario recibirá un correo con un enlace para configurar su contraseña.",
|
||||
"admin.users.modal.create.admin": "Privilegios de administrador",
|
||||
"admin.users.modal.create.admin.description": "Si se marca, el usuario podrá acceder al panel de administrador.",
|
||||
// END /admin/users
|
||||
// /upload
|
||||
"upload.title": "Subir",
|
||||
"upload.notify.generic-error": "Ha ocurrido un error mientras se compartía tu archivo.",
|
||||
"upload.notify.count-failed": "No se pudo cargar {count} archivos. Intentando nuevamente.",
|
||||
// Dropzone.tsx
|
||||
"upload.dropzone.title": "Subir archivos",
|
||||
"upload.dropzone.description": "Arrastra archivos aquí para comenzar a compartir. Aceptamos archivos de un tamaño menor a {maxSize} en total.",
|
||||
"upload.dropzone.notify.file-too-big": "Tus archivos exceden el tamaño máximo de {maxSize}.",
|
||||
// FileList.tsx
|
||||
"upload.filelist.name": "Nombre",
|
||||
"upload.filelist.size": "Tamaño",
|
||||
// showCreateUploadModal.tsx
|
||||
"upload.modal.title": "Crear compartido",
|
||||
"upload.modal.link.error.invalid": "Solo puede contener letras, números, guiones y guiones bajos",
|
||||
"upload.modal.link.error.taken": "Este enlace ya está en uso",
|
||||
"upload.modal.not-signed-in": "No has iniciado sesión",
|
||||
"upload.modal.not-signed-in-description": "No podrás eliminar tus compartidos manualmente ni ver el número de visitas.",
|
||||
"upload.modal.expires.never": "nunca",
|
||||
"upload.modal.expires.never-long": "Nunca Expira",
|
||||
"upload.modal.link.label": "Enlace",
|
||||
"upload.modal.expires.label": "Expiración",
|
||||
"upload.modal.expires.minute-singular": "Minuto",
|
||||
"upload.modal.expires.minute-plural": "Minutos",
|
||||
"upload.modal.expires.hour-singular": "Hora",
|
||||
"upload.modal.expires.hour-plural": "Horas",
|
||||
"upload.modal.expires.day-singular": "Día",
|
||||
"upload.modal.expires.day-plural": "Días",
|
||||
"upload.modal.expires.week-singular": "Semana",
|
||||
"upload.modal.expires.week-plural": "Semanas",
|
||||
"upload.modal.expires.month-singular": "Mes",
|
||||
"upload.modal.expires.month-plural": "Meses",
|
||||
"upload.modal.expires.year-singular": "Año",
|
||||
"upload.modal.expires.year-plural": "Años",
|
||||
"upload.modal.accordion.description.title": "Descripción",
|
||||
"upload.modal.accordion.description.placeholder": "Nota para los destinatarios de este compartido",
|
||||
"upload.modal.accordion.email.title": "Correo de los destinatarios",
|
||||
"upload.modal.accordion.email.placeholder": "Ingresa los correos de los destinatarios",
|
||||
"upload.modal.accordion.email.invalid-email": "Dirección de correo inválida",
|
||||
"upload.modal.accordion.security.title": "Opciones de seguridad",
|
||||
"upload.modal.accordion.security.password.label": "Protección por contraseña",
|
||||
"upload.modal.accordion.security.password.placeholder": "Sin contraseña",
|
||||
"upload.modal.accordion.security.max-views.label": "Máximo de vistas",
|
||||
"upload.modal.accordion.security.max-views.placeholder": "Sin límite",
|
||||
// showCompletedUploadModal.tsx
|
||||
"upload.modal.completed.never-expires": "Este compartido nunca expirará.",
|
||||
"upload.modal.completed.expires-on": "Este compartido expira en {expiration}.",
|
||||
"upload.modal.completed.share-ready": "Compartido listo",
|
||||
// END /upload
|
||||
// /share/[id]
|
||||
"share.title": "Compartido {shareId}",
|
||||
"share.description": "¡Mira lo que he compartido contigo!",
|
||||
"share.error.visitor-limit-exceeded.title": "Se excedió el límite de visitas",
|
||||
"share.error.visitor-limit-exceeded.description": "Se ha excedido el límite de visitas para este compartido.",
|
||||
"share.error.removed.title": "Compartido eliminado",
|
||||
"share.error.not-found.title": "Compartido no encontrado",
|
||||
"share.error.not-found.description": "El compartido que estás buscando no existe.",
|
||||
"share.modal.password.title": "Se requiere contraseña",
|
||||
"share.modal.password.description": "Por favor ingrese la contraseña para poder acceder a este compartido.",
|
||||
"share.modal.password": "Contraseña",
|
||||
"share.modal.error.invalid-password": "Contraseña inválida",
|
||||
"share.button.download-all": "Descargar todo",
|
||||
"share.notify.download-all-preparing": "Se está preparando el compartido. Intente de nuevo en unos minutos.",
|
||||
"share.modal.file-link": "Enlace del archivo",
|
||||
"share.table.name": "Nombre",
|
||||
"share.table.size": "Tamaño",
|
||||
"share.modal.file-preview.error.not-supported.title": "Vista previa no disponible",
|
||||
"share.modal.file-preview.error.not-supported.description": "La vista previa para este tipo de archivo no está disponible. Por favor descargue el archivo para verlo.",
|
||||
// END /share/[id]
|
||||
// /admin/config
|
||||
"admin.config.title": "Configuración",
|
||||
"admin.config.category.general": "General",
|
||||
"admin.config.category.share": "Compartido",
|
||||
"admin.config.category.email": "Correo",
|
||||
"admin.config.category.smtp": "SMTP",
|
||||
"admin.config.general.app-name": "Nombre de la App",
|
||||
"admin.config.general.app-name.description": "Nombre de la aplicación",
|
||||
"admin.config.general.app-url": "App URL",
|
||||
"admin.config.general.app-url.description": "En cuál URL está disponible Pingvin Share",
|
||||
"admin.config.general.show-home-page": "Mostrar página de inicio",
|
||||
"admin.config.general.show-home-page.description": "Mostrar o no la página de inicio",
|
||||
"admin.config.general.logo": "Logo",
|
||||
"admin.config.general.logo.description": "Cambia tu logo subiendo una nueva imagen. La imagen debe ser un PNG y debe estar en formato 1:1.",
|
||||
"admin.config.general.logo.placeholder": "Elegir imagen",
|
||||
"admin.config.email.enable-share-email-recipients": "Activar destinatarios por correo",
|
||||
"admin.config.email.enable-share-email-recipients.description": "Si desea permitir a los destinatarios compartir por correo. Activa esto solo si tienes habilitado SMTP.",
|
||||
"admin.config.email.share-recipients-subject": "Asunto destinatario",
|
||||
"admin.config.email.share-recipients-subject.description": "Asunto del correo el cual es enviado al destinatario del compartido.",
|
||||
"admin.config.email.share-recipients-message": "Mensaje destinatario",
|
||||
"admin.config.email.share-recipients-message.description": "Mensaje el cual es enviado al destinatario del compartido. Variables disponibles:\n{creator} - Nombre del creador del compartido\n {shareUrl} - URL del compartido\n {desc} - Descripción del compartido\n {expires} - Fecha de expiración del compartido\nLas variables serán remplazadas con los valores reales.",
|
||||
"admin.config.email.reverse-share-subject": "Asunto del Reverse Share",
|
||||
"admin.config.email.reverse-share-subject.description": "Asunto del correo el cual se envía cuando alguien comparte algo con tu enlace de Reverse Share.",
|
||||
"admin.config.email.reverse-share-message": "Mensaje del Reverse Share",
|
||||
"admin.config.email.reverse-share-message.description": "Mensaje que se envía cuando alguien comparte algo con tu enlace de Reverse Share. {shareUrl} Se remplazará con el nombre del creador y la URL del compartido.",
|
||||
"admin.config.email.reset-password-subject": "Asunto restablecer contraseña",
|
||||
"admin.config.email.reset-password-subject.description": "Asunto del correo que se envía cuando un usuario solicita restablecer la contraseña.",
|
||||
"admin.config.email.reset-password-message": "Mensaje restablecer contraseña",
|
||||
"admin.config.email.reset-password-message.description": "Mensaje que se envía cuando un usuario solicita restablecer la contraseña. {url} se remplazará con la URL para restablecer la contraseña.",
|
||||
"admin.config.email.invite-subject": "Asunto de la invitación",
|
||||
"admin.config.email.invite-subject.description": "Asunto del correo que se envía cuando un administrador invita a un usuario.",
|
||||
"admin.config.email.invite-message": "Mensaje de invitación",
|
||||
"admin.config.email.invite-message.description": "Mensaje que se envía cuando un administrador invita a un usuario. {url} Se remplazará con la URL de invitación y {password} con la contraseña.",
|
||||
"admin.config.share.allow-registration": "Permitir registro",
|
||||
"admin.config.share.allow-registration.description": "Si se permite el registro",
|
||||
"admin.config.share.allow-unauthenticated-shares": "Permitir compartir sin iniciar sesión",
|
||||
"admin.config.share.allow-unauthenticated-shares.description": "Si los usuarios que no han iniciado sesión pueden compartir",
|
||||
"admin.config.share.max-size": "Tamaño máximo",
|
||||
"admin.config.share.max-size.description": "Tamaño máximo de los archivos, en bytes",
|
||||
"admin.config.share.zip-compression-level": "Nivel de compresión del Zip",
|
||||
"admin.config.share.zip-compression-level.description": "Ajustar el nivel para equilibrar entre el tamaño del archivo y la velocidad de compresión. Los valores válidos van del 0 al 9, siendo 0 sin compresión y 9 el nivel máximo de compresión. ",
|
||||
"admin.config.smtp.enabled": "Habilitado",
|
||||
"admin.config.smtp.enabled.description": "Si SMTP está habilitado. Active solo si ha introducido el host, el puerto, el correo, el usuario y la contraseña de su servidor SMTP.",
|
||||
"admin.config.smtp.host": "Host",
|
||||
"admin.config.smtp.host.description": "Host del servidor SMTP",
|
||||
"admin.config.smtp.port": "Puerto",
|
||||
"admin.config.smtp.port.description": "Puerto del servidor SMTP",
|
||||
"admin.config.smtp.email": "Correo",
|
||||
"admin.config.smtp.email.description": "Dirección de correo desde la cual se envían los correos",
|
||||
"admin.config.smtp.username": "Usuario",
|
||||
"admin.config.smtp.username.description": "Usuario del servidor SMTP",
|
||||
"admin.config.smtp.password": "Contraseña",
|
||||
"admin.config.smtp.password.description": "Contraseña del servidor SMTP",
|
||||
"admin.config.smtp.button.test": "Enviar correo de prueba",
|
||||
// 404
|
||||
"404.description": "Oops esta página no existe.",
|
||||
"404.button.home": "Regrésame al inicio",
|
||||
// Common translations
|
||||
"common.button.save": "Guardar",
|
||||
"common.button.create": "Crear",
|
||||
"common.button.submit": "Enviar",
|
||||
"common.button.delete": "Eliminar",
|
||||
"common.button.cancel": "Cancelar",
|
||||
"common.button.confirm": "Confirmar",
|
||||
"common.button.disable": "Deshabilitar",
|
||||
"common.button.share": "Compartir",
|
||||
"common.button.generate": "Generar",
|
||||
"common.button.done": "Listo",
|
||||
"common.text.link": "Enlace",
|
||||
"common.text.or": "o",
|
||||
"common.button.go-back": "Volver",
|
||||
"common.notify.copied": "Tu enlace se ha copiado al portapapeles",
|
||||
"common.success": "Éxito",
|
||||
"common.error": "Error",
|
||||
"common.error.unknown": "Ocurrió un error desconocido",
|
||||
"common.error.invalid-email": "Correo electrónico no válido",
|
||||
"common.error.too-short": "Debe tener al menos {length} caracteres",
|
||||
"common.error.too-long": "Debe tener como máximo {length} caracteres",
|
||||
"common.error.exact-length": "Debe tener exactamente {length} caracteres",
|
||||
"common.error.invalid-number": "Debe ser un número",
|
||||
"common.error.field-required": "Este campo es requerido"
|
||||
};
|
||||
323
frontend/src/i18n/translations/fi-FI.ts
Normal file
323
frontend/src/i18n/translations/fi-FI.ts
Normal file
@@ -0,0 +1,323 @@
|
||||
export default {
|
||||
// Navbar
|
||||
"navbar.upload": "Pilvetä",
|
||||
"navbar.signin": "Kirjaudu Sisään",
|
||||
"navbar.home": "Koti",
|
||||
"navbar.signup": "Rekisteröidy",
|
||||
"navbar.links.shares": "Minun jakaukset",
|
||||
"navbar.links.reverse": "Takaperin jaetut",
|
||||
"navbar.avatar.account": "Oma tIli",
|
||||
"navbar.avatar.admin": "Ylläpito",
|
||||
"navbar.avatar.signout": "Kirjaudu ulos",
|
||||
// END navbar
|
||||
// /
|
||||
"home.title": "<h>\"Itse isännöitty\"</h> tiedostojen jakamisen alusta.",
|
||||
"home.description": "Haluatko oikeasti jakaa yksityisiä tiedostojasi kolmannen osapuolen yhtiöille niin kuin WeTransfer?",
|
||||
"home.bullet.a.name": "Itse-Isännöitty",
|
||||
"home.bullet.a.description": "Isännöi \"Pingvin Share\" omalla palvelimellasi.",
|
||||
"home.bullet.b.name": "Yksityisyys",
|
||||
"home.bullet.b.description": "Sinun tiedostosi ovat sinun ja niiden ei ikinä pidä päättyä kolmannen osapuolen käsiin.",
|
||||
"home.bullet.c.name": "Ei ärsyttävää tiedoston kokorajoitusta",
|
||||
"home.bullet.c.description": "Lataa niin paljon isoja tiedostoja kuin tykkäät. Vain kovalevysi on rajana.",
|
||||
"home.button.start": "Aloita",
|
||||
"home.button.source": "Lähdekoodi",
|
||||
// END /
|
||||
// /auth/signin
|
||||
"signin.title": "Tervetuloa takaisin",
|
||||
"signin.description": "Eikö sinulla ole vielä tiliä?",
|
||||
"signin.button.signup": "Rekisteröidy",
|
||||
"signin.input.email-or-username": "Sähköposti tai käyttäjänimi",
|
||||
"signin.input.email-or-username.placeholder": "Sähköpostisi tai käyttäjänimesi",
|
||||
"signin.input.password": "Salasana",
|
||||
"signin.input.password.placeholder": "Salasana",
|
||||
"signin.button.submit": "Kirjaudu sisään",
|
||||
"signIn.notify.totp-required.title": "Kaksivaiheinen tunnistautuminen vaadittu",
|
||||
"signIn.notify.totp-required.description": "Syötä kaksivaiheisen tunnistautumisen koodi tähän",
|
||||
// END /auth/signin
|
||||
// /auth/signup
|
||||
"signup.title": "Rekisteröidy",
|
||||
"signup.description": "Onko sinulla jo tili?",
|
||||
"signup.button.signin": "Kirjaudu sisään",
|
||||
"signup.input.username": "Käyttäjätunnus",
|
||||
"signup.input.username.placeholder": "Käyttäjätunnus",
|
||||
"signup.input.email": "Sähköposti",
|
||||
"signup.input.email.placeholder": "Sähköpostisi",
|
||||
"signup.button.submit": "Aloitetaan",
|
||||
// END /auth/signup
|
||||
// /auth/reset-password
|
||||
"resetPassword.title": "Unohditko salasanan?",
|
||||
"resetPassword.description": "Kirjoita sähköpostiosoitteesi palauttaaksesi salasanasi.",
|
||||
"resetPassword.notify.success": "Sähköpostiosoite on lähetetty linkillä, jolla voit nollata salasanasi.",
|
||||
"resetPassword.button.back": "Takaisin kirjautumiseen",
|
||||
"resetPassword.text.resetPassword": "Nollaa salasana",
|
||||
"resetPassword.text.enterNewPassword": "Anna uusi salasana",
|
||||
"resetPassword.input.password": "Uusi salasana",
|
||||
"resetPassword.notify.passwordReset": "Salasanan nollaus onnistui.",
|
||||
// /account
|
||||
"account.title": "Oma tIli",
|
||||
"account.card.info.title": "Tilin tiedot",
|
||||
"account.card.info.username": "Käyttäjätunnus",
|
||||
"account.card.info.email": "Sähköposti",
|
||||
"account.notify.info.success": "Tili päivitetty onnistuneesti",
|
||||
"account.card.password.title": "Salasana",
|
||||
"account.card.password.old": "Vanha salasana",
|
||||
"account.card.password.new": "Uusi salasana",
|
||||
"account.notify.password.success": "Salasana vaihdettu",
|
||||
"account.card.security.title": "Turvallisuus",
|
||||
"account.card.security.totp.enable.description": "Anna nykyinen salasanasi aloittaaksesi TOTP käytön",
|
||||
"account.card.security.totp.disable.description": "Syötä nykyinen salasanasi poistaaksesi TOTP käytöstä",
|
||||
"account.card.security.totp.button.start": "Aloita",
|
||||
"account.modal.totp.title": "Ota Käyttöön TOTP",
|
||||
"account.modal.totp.step1": "Vaihe 1: Lisää todentaja",
|
||||
"account.modal.totp.step2": "Vaihe 2: Vahvista koodisi",
|
||||
"account.modal.totp.enterManually": "Syötä manuaalisesti",
|
||||
"account.modal.totp.code": "Koodi",
|
||||
"account.modal.totp.clickToCopy": "Klikkaa kopioidaksesi",
|
||||
"account.modal.totp.verify": "Vahvista",
|
||||
"account.notify.totp.disable": "TOTP poistettu käytöstä",
|
||||
"account.notify.totp.enable": "TOTP otettu käyttöön onnistuneesti",
|
||||
"account.card.language.title": "Kieli",
|
||||
"account.card.language.description": "Projekti on yhteisön kääntämä. Jotkut kielet saattavat olla puutteellisia.",
|
||||
"account.card.color.title": "Väriteema",
|
||||
// ThemeSwitcher.tsx
|
||||
"account.theme.dark": "Tumma",
|
||||
"account.theme.light": "Vaalea",
|
||||
"account.theme.system": "Järjestelmä",
|
||||
"account.button.delete": "Poista tili",
|
||||
"account.modal.delete.title": "Poista tili",
|
||||
"account.modal.delete.description": "Haluatko varmasti poistaa tilisi mukaan lukien kaikki aktiiviset jaetut tiedostot?",
|
||||
// END /account
|
||||
// /account/shares
|
||||
"account.shares.title": "Minun jakaukset",
|
||||
"account.shares.title.empty": "Täällä on tyhjää 👀",
|
||||
"account.shares.description.empty": "Sinulla ei ole jaettuja tiedostoja.",
|
||||
"account.shares.button.create": "Luo yksi",
|
||||
"account.shares.info.title": "Jaetun tiedot",
|
||||
"account.shares.table.id": "ID",
|
||||
"account.shares.table.name": "Nimi",
|
||||
"account.shares.table.description": "Kuvaus",
|
||||
"account.shares.table.visitors": "Vierailijat",
|
||||
"account.shares.table.expiresAt": "Vanhenee",
|
||||
"account.shares.table.createdAt": "Luotu",
|
||||
"account.shares.table.size": "Koko",
|
||||
"account.shares.modal.share-informations": "Jaetun tiedot",
|
||||
"account.shares.modal.share-link": "Jaa linkki",
|
||||
"account.shares.modal.delete.title": "Poista jaettu {share}",
|
||||
"account.shares.modal.delete.description": "Haluatko todella poistaa tämän jaetun tiedoston/ot?",
|
||||
// END /account/shares
|
||||
// /account/reverseShares
|
||||
"account.reverseShares.title": "Takaperin jaetut",
|
||||
"account.reverseShares.description": "Käänteisen jaon avulla voit luoda ainutlaatuisen URL-osoitteen, jonka avulla ulkoiset käyttäjät voivat luoda jaon.",
|
||||
"account.reverseShares.title.empty": "Täällä on tyhjää 👀",
|
||||
"account.reverseShares.description.empty": "Sinulla ei ole käänteisiä jakoja.",
|
||||
// showCreateReverseShareModal.tsx
|
||||
"account.reverseShares.modal.expiration.label": "Vanhentuminen",
|
||||
"account.reverseShares.modal.expiration.minute-singular": "Minuutti",
|
||||
"account.reverseShares.modal.expiration.minute-plural": "Minuuttia",
|
||||
"account.reverseShares.modal.expiration.hour-singular": "Tunti",
|
||||
"account.reverseShares.modal.expiration.hour-plural": "Tuntia",
|
||||
"account.reverseShares.modal.expiration.day-singular": "Päivä",
|
||||
"account.reverseShares.modal.expiration.day-plural": "Päivää",
|
||||
"account.reverseShares.modal.expiration.week-singular": "Viikko",
|
||||
"account.reverseShares.modal.expiration.week-plural": "Viikkoa",
|
||||
"account.reverseShares.modal.expiration.month-singular": "Kuukausi",
|
||||
"account.reverseShares.modal.expiration.month-plural": "Kuukautta",
|
||||
"account.reverseShares.modal.expiration.year-singular": "Vuosi",
|
||||
"account.reverseShares.modal.expiration.year-plural": "Vuotta",
|
||||
"account.reverseShares.modal.max-size.label": "Suurin tiedostonkoko",
|
||||
"account.reverseShares.modal.send-email": "Lähetä sähköposti-ilmoitus",
|
||||
"account.reverseShares.modal.send-email.description": "Lähetä sähköpostiilmoitus kun jako on luotu tällä käänteisellä jakolinkillä.",
|
||||
"account.reverseShares.modal.max-use.label": "Käyttökertoja enintään",
|
||||
"account.reverseShares.modal.max-use.description": "Enimmäismäärä kertoja, joilla tämä URL-osoite voidaan käyttää joita luomiseen.",
|
||||
"account.reverseShare.never-expires": "Tämä käänteinen jako ei koskaan vanhene.",
|
||||
"account.reverseShare.expires-on": "Tämä käänteinen jako vanhenee kun on {expiration}.",
|
||||
"account.reverseShares.table.no-shares": "Ei vielä luotuja jakoja",
|
||||
"account.reverseShares.table.count.singular": "jaa",
|
||||
"account.reverseShares.table.count.plural": "jaot",
|
||||
"account.reverseShares.table.shares": "Jaot",
|
||||
"account.reverseShares.table.remaining": "Jäljellä olevat käyttökerrat",
|
||||
"account.reverseShares.table.max-size": "Suurin tiedostonkoko",
|
||||
"account.reverseShares.table.expires": "Vanhenee",
|
||||
"account.reverseShares.modal.reverse-share-link": "Takaperin jaetun jaon linkki",
|
||||
"account.reverseShares.modal.delete.title": "Poista käänteinen jako",
|
||||
"account.reverseShares.modal.delete.description": "Haluatko varmasti poistaa tämän käänteisen jaon? Jos kyllä, myös siihen liittyvät jaot poistetaan.",
|
||||
// END /account/reverseShares
|
||||
// /admin
|
||||
"admin.title": "Ylläpito",
|
||||
"admin.button.users": "Käyttäjien Hallinta",
|
||||
"admin.button.config": "Asetukset",
|
||||
"admin.version": "Versio",
|
||||
// END /admin
|
||||
// /admin/users
|
||||
"admin.users.title": "Käyttäjien Hallinta",
|
||||
"admin.users.table.username": "Käyttäjätunnus",
|
||||
"admin.users.table.email": "Sähköposti",
|
||||
"admin.users.table.admin": "Ylläpitäjä",
|
||||
"admin.users.edit.update.title": "Päivitä käyttäjä {username}",
|
||||
"admin.users.edit.update.admin-privileges": "Ylläpitäjän oikeudet",
|
||||
"admin.users.edit.update.change-password.title": "Vaihda salasana",
|
||||
"admin.users.edit.update.change-password.field": "Uusi salasana",
|
||||
"admin.users.edit.update.change-password.button": "Tallenna uusi salasana",
|
||||
"admin.users.edit.update.notify.password.success": "Salasana vaihdettu",
|
||||
"admin.users.edit.delete.title": "Poista käyttäjä {username}",
|
||||
"admin.users.edit.delete.description": "Haluatko varmasti poistaa tämän käyttäjän ja kaikki hänen jaot?",
|
||||
// showCreateUserModal.tsx
|
||||
"admin.users.modal.create.title": "Luo käyttäjä",
|
||||
"admin.users.modal.create.username": "Käyttäjätunnus",
|
||||
"admin.users.modal.create.email": "Sähköposti",
|
||||
"admin.users.modal.create.password": "Salasana",
|
||||
"admin.users.modal.create.manual-password": "Aseta salasana manuaalisesti",
|
||||
"admin.users.modal.create.manual-password.description": "Jos ei ole valittuna, käyttäjä saa sähköpostiviestin, jossa on linkki, joka määrittää heidän salasanansa.",
|
||||
"admin.users.modal.create.admin": "Ylläpitäjän oikeudet",
|
||||
"admin.users.modal.create.admin.description": "Jos valittu, käyttäjä voi käyttää hallintapaneelia.",
|
||||
// END /admin/users
|
||||
// /upload
|
||||
"upload.title": "Pilvetä",
|
||||
"upload.notify.generic-error": "Kohdattiin odottamaton virhe jaon luomisessa.",
|
||||
"upload.notify.count-failed": "{count} tiedostoa ei voitu ladata. Yritetään uudelleen.",
|
||||
// Dropzone.tsx
|
||||
"upload.dropzone.title": "Pilvetä tiedostoja",
|
||||
"upload.dropzone.description": "Vedä ja pudota tiedostot tähän aloittaaksesi jakamisen. Voimme hyväksyä vain tiedostot, jotka ovat yhteensä alle {maxSize}.",
|
||||
"upload.dropzone.notify.file-too-big": "Tiedostojen enimmäiskoko ylittää {maxSize} -arvon enimmäismäärän.",
|
||||
// FileList.tsx
|
||||
"upload.filelist.name": "Nimi",
|
||||
"upload.filelist.size": "Koko",
|
||||
// showCreateUploadModal.tsx
|
||||
"upload.modal.title": "Luo Jako",
|
||||
"upload.modal.link.error.invalid": "Voi sisältää vain kirjaimia, numeroita, alaviivoja ja väliviivoja",
|
||||
"upload.modal.link.error.taken": "Tämä linkki on jo käytössä",
|
||||
"upload.modal.not-signed-in": "Et ole kirjautunut sisään",
|
||||
"upload.modal.not-signed-in-description": "Et voi poistaa jakoasi manuaalisesti ja tarkastella kävijöiden määrää.",
|
||||
"upload.modal.expires.never": "ei koskaan",
|
||||
"upload.modal.expires.never-long": "Ei vanhene koskaan",
|
||||
"upload.modal.link.label": "Linkki",
|
||||
"upload.modal.expires.label": "Vanhentuminen",
|
||||
"upload.modal.expires.minute-singular": "Minuutti",
|
||||
"upload.modal.expires.minute-plural": "Minuuttia",
|
||||
"upload.modal.expires.hour-singular": "Tunti",
|
||||
"upload.modal.expires.hour-plural": "Tuntia",
|
||||
"upload.modal.expires.day-singular": "Päivä",
|
||||
"upload.modal.expires.day-plural": "Päivää",
|
||||
"upload.modal.expires.week-singular": "Viikko",
|
||||
"upload.modal.expires.week-plural": "Viikkoa",
|
||||
"upload.modal.expires.month-singular": "Kuukausi",
|
||||
"upload.modal.expires.month-plural": "Kuukautta",
|
||||
"upload.modal.expires.year-singular": "Vuosi",
|
||||
"upload.modal.expires.year-plural": "Vuotta",
|
||||
"upload.modal.accordion.description.title": "Kuvaus",
|
||||
"upload.modal.accordion.description.placeholder": "Huomautus tämän jaon vastaanottajille",
|
||||
"upload.modal.accordion.email.title": "Sähköpostin vastaanottajat",
|
||||
"upload.modal.accordion.email.placeholder": "Syötä sähköpostin vastaanottajat",
|
||||
"upload.modal.accordion.email.invalid-email": "Virheellinen sähköpostiosoite",
|
||||
"upload.modal.accordion.security.title": "Turvallisuusasetukset",
|
||||
"upload.modal.accordion.security.password.label": "Salasanasuojaus",
|
||||
"upload.modal.accordion.security.password.placeholder": "Ei salasanaa",
|
||||
"upload.modal.accordion.security.max-views.label": "Näkymien enimmäismäärä",
|
||||
"upload.modal.accordion.security.max-views.placeholder": "Ei rajoitusta",
|
||||
// showCompletedUploadModal.tsx
|
||||
"upload.modal.completed.never-expires": "Tämä käänteinen jako ei koskaan vanhene.",
|
||||
"upload.modal.completed.expires-on": "Tämä käänteinen jako vanhenee kun on {expiration}.",
|
||||
"upload.modal.completed.share-ready": "Jako valmiina",
|
||||
// END /upload
|
||||
// /share/[id]
|
||||
"share.title": "Jaa {shareId}",
|
||||
"share.description": "Katso, mitä olen jakanut kanssasi!",
|
||||
"share.error.visitor-limit-exceeded.title": "Vierailijoiden raja ylitetty",
|
||||
"share.error.visitor-limit-exceeded.description": "Tämän jaon kävijäraja on ylittynyt.",
|
||||
"share.error.removed.title": "Jako poistettu",
|
||||
"share.error.not-found.title": "Jakoa ei löydetty",
|
||||
"share.error.not-found.description": "Etsimääsi sivua ei ole olemassa.",
|
||||
"share.modal.password.title": "Salasana vaaditaan",
|
||||
"share.modal.password.description": "Päästäksesi käsiksi tähän jakoon anna jaon salasana.",
|
||||
"share.modal.password": "Salasana",
|
||||
"share.modal.error.invalid-password": "Virheellinen salasana",
|
||||
"share.button.download-all": "Lataa kaikki",
|
||||
"share.notify.download-all-preparing": "Jako on valmistumassa. Yritä uudelleen muutaman minuutin kuluttua.",
|
||||
"share.modal.file-link": "Tiedoston linkki",
|
||||
"share.table.name": "Nimi",
|
||||
"share.table.size": "Koko",
|
||||
"share.modal.file-preview.error.not-supported.title": "Esikatselua ei tuettu",
|
||||
"share.modal.file-preview.error.not-supported.description": "Esikatselua thise tiedostotyypille ei tueta. Ole hyvä ja lataa tiedosto nähdäksesi sen.",
|
||||
// END /share/[id]
|
||||
// /admin/config
|
||||
"admin.config.title": "Asetukset",
|
||||
"admin.config.category.general": "Yleiset",
|
||||
"admin.config.category.share": "Jako",
|
||||
"admin.config.category.email": "Sähköposti",
|
||||
"admin.config.category.smtp": "SMTP",
|
||||
"admin.config.general.app-name": "Sovelluksen nimi",
|
||||
"admin.config.general.app-name.description": "Sovelluksen nimi",
|
||||
"admin.config.general.app-url": "Sovelluksen URL",
|
||||
"admin.config.general.app-url.description": "Millä URL-osoitteella Pingvin Share on saatavilla",
|
||||
"admin.config.general.show-home-page": "Näytä kotisivu",
|
||||
"admin.config.general.show-home-page.description": "Näytetäänkö kotisivu vai ei",
|
||||
"admin.config.general.logo": "Logo",
|
||||
"admin.config.general.logo.description": "Muuta logoa lataamalla uusi kuva. Kuvan on oltava PNG ja sen on oltava formaatti 1:1.",
|
||||
"admin.config.general.logo.placeholder": "Valitse kuva",
|
||||
"admin.config.email.enable-share-email-recipients": "Salli sähköpostin vastaanottajien jakaminen",
|
||||
"admin.config.email.enable-share-email-recipients.description": "Salli sähköpostien jakaminen vastaanottajille. Ota tämä käyttöön vain, jos olet ottanut SMTP:n käyttöön.",
|
||||
"admin.config.email.share-recipients-subject": "Sähköpostijaon otsikko",
|
||||
"admin.config.email.share-recipients-subject.description": "Sähköpostin aihe, joka saa lähetetään jaon vastaanottajille.",
|
||||
"admin.config.email.share-recipients-message": "Sähköpostijaon otsikko",
|
||||
"admin.config.email.share-recipients-message.description": "Viesti joka lähetetään jaon vastaanottajille. Saatavilla olevat muuttujat:\n {creator} - Jaon luojan käyttäjänimi\n {shareUrl} - Jaon URL\n {desc} - Jaon kuvaus\n {expires} - Jaon päättymispäivä\n Muuttujat korvataan todellisella arvolla.",
|
||||
"admin.config.email.reverse-share-subject": "Käänteisen jaon aihe",
|
||||
"admin.config.email.reverse-share-subject.description": "Aihe sähköpostin joka lähetetään kun joku loi jaon käänteisen jakolinkin kanssa.",
|
||||
"admin.config.email.reverse-share-message": "Käänteisen jakoviestin viesti",
|
||||
"admin.config.email.reverse-share-message.description": "Viesti joka lähetetään kun joku loi jaon käänteisen jakolinkin kanssa. {shareUrl} korvataan luojan nimellä ja jaon URL:lla.",
|
||||
"admin.config.email.reset-password-subject": "Nollaa salasanan aihe",
|
||||
"admin.config.email.reset-password-subject.description": "Sähköpostin aihe, joka lähetetään kun käyttäjä pyytää salasanan palauttamista.",
|
||||
"admin.config.email.reset-password-message": "Nollaa salasanan viesti",
|
||||
"admin.config.email.reset-password-message.description": "Viesti joka lähetetään kun käyttäjä pyytää salasanan nollausta. {url} korvataan nollaussalasanan URL-osoitteella.",
|
||||
"admin.config.email.invite-subject": "Kutsun aihe",
|
||||
"admin.config.email.invite-subject.description": "Sähköpostin aihe, mikä lähetetään kun ylläpitäjä kutsuu käyttäjää.",
|
||||
"admin.config.email.invite-message": "Kutsun viesti",
|
||||
"admin.config.email.invite-message.description": "Viesti mikä lähetetään kuin yp invaa käyttäjän. {url} korvataan kutsuosoitteella ja {password} salasanalla.",
|
||||
"admin.config.share.allow-registration": "Salli rekisteröinti",
|
||||
"admin.config.share.allow-registration.description": "Onko rekisteröinti sallittu",
|
||||
"admin.config.share.allow-unauthenticated-shares": "Salli anonyymit jaot",
|
||||
"admin.config.share.allow-unauthenticated-shares.description": "Voiko tunnistamattomat käyttäjät luoda jakoja",
|
||||
"admin.config.share.max-size": "Maksimikoko",
|
||||
"admin.config.share.max-size.description": "Jaon enimmäiskoko tavuissa (bytes)",
|
||||
"admin.config.share.zip-compression-level": "Zip puristustaso",
|
||||
"admin.config.share.zip-compression-level.description": "Säädä tasoa tiedoston koon ja pakkausnopeuden välillä. Kelvolliset arvot vaihtelevat 0–9, 0 ei puristusta ja 9 on suurin puristusvoima. ",
|
||||
"admin.config.smtp.enabled": "Käytössä",
|
||||
"admin.config.smtp.enabled.description": "Onko SMTP käytössä. Aseta tämä todeksi vain, jos olet syöttänyt SMTP-palvelimen isäntän, portin, sähköpostin, käyttäjän ja salasanan.",
|
||||
"admin.config.smtp.host": "Isäntä",
|
||||
"admin.config.smtp.host.description": "SMTP palvelimen isäntä",
|
||||
"admin.config.smtp.port": "Portti",
|
||||
"admin.config.smtp.port.description": "SMTP palvelimen portti",
|
||||
"admin.config.smtp.email": "Sähköposti",
|
||||
"admin.config.smtp.email.description": "Sähköpostiosoite, josta sähköpostit on lähetetty",
|
||||
"admin.config.smtp.username": "Käyttäjätunnus",
|
||||
"admin.config.smtp.username.description": "SMTP palvelimen käyttäjänimi",
|
||||
"admin.config.smtp.password": "Salasana",
|
||||
"admin.config.smtp.password.description": "SMTP palvelimen salasana",
|
||||
"admin.config.smtp.button.test": "Lähetä testisähköposti",
|
||||
// 404
|
||||
"404.description": "Hups tätä sivua ei ole olemassa.",
|
||||
"404.button.home": "Tuo minut takaisin kotiin",
|
||||
// Common translations
|
||||
"common.button.save": "Tallenna",
|
||||
"common.button.create": "Luo",
|
||||
"common.button.submit": "Lähetä",
|
||||
"common.button.delete": "Poista",
|
||||
"common.button.cancel": "Peruuta",
|
||||
"common.button.confirm": "Vahvista",
|
||||
"common.button.disable": "Poista käytöstä",
|
||||
"common.button.share": "Jako",
|
||||
"common.button.generate": "Luo",
|
||||
"common.button.done": "Valmis",
|
||||
"common.text.link": "Linkki",
|
||||
"common.text.or": "tai",
|
||||
"common.button.go-back": "Takaisin",
|
||||
"common.notify.copied": "Linkki kopioitiin leikepöydälle",
|
||||
"common.success": "Suoritettu",
|
||||
"common.error": "Virhe",
|
||||
"common.error.unknown": "Tapahtui tuntematon virhe",
|
||||
"common.error.invalid-email": "Virheellinen sähköpostiosoite",
|
||||
"common.error.too-short": "Täytyy olla vähintään {length} merkkiä",
|
||||
"common.error.too-long": "Täytyy olla enintään {length} merkkiä",
|
||||
"common.error.exact-length": "On oltava tarkasti {length} merkkiä pitkä",
|
||||
"common.error.invalid-number": "Pitää olla luku",
|
||||
"common.error.field-required": "Tämä kenttä on pakollinen"
|
||||
};
|
||||
323
frontend/src/i18n/translations/fr-FR.ts
Normal file
323
frontend/src/i18n/translations/fr-FR.ts
Normal file
@@ -0,0 +1,323 @@
|
||||
export default {
|
||||
// Navbar
|
||||
"navbar.upload": "Envoyer",
|
||||
"navbar.signin": "Se connecter",
|
||||
"navbar.home": "Accueil",
|
||||
"navbar.signup": "S'inscrire",
|
||||
"navbar.links.shares": "Mes partages",
|
||||
"navbar.links.reverse": "Partages inversés",
|
||||
"navbar.avatar.account": "Mon compte",
|
||||
"navbar.avatar.admin": "Administration",
|
||||
"navbar.avatar.signout": "Se déconnecter",
|
||||
// END navbar
|
||||
// /
|
||||
"home.title": "Une plateforme de partage de fichier <h>auto-hébergée</h>.",
|
||||
"home.description": "Voulez-vous vraiment remettre vos fichiers personnels dans les mains de tiers comme WeTransfer ?",
|
||||
"home.bullet.a.name": "Auto-hébergé",
|
||||
"home.bullet.a.description": "Hébergez Pingvin Share sur votre propre machine.",
|
||||
"home.bullet.b.name": "Confidentialité",
|
||||
"home.bullet.b.description": "Vos fichiers sont vos fichiers et ne devraient jamais être mis entre les mains de tiers.",
|
||||
"home.bullet.c.name": "Aucune rébarbative limite de taille",
|
||||
"home.bullet.c.description": "Téléchargez des fichiers volumineux que vous le souhaitez. Seul votre disque dur est la limite.",
|
||||
"home.button.start": "Commencer",
|
||||
"home.button.source": "Code source",
|
||||
// END /
|
||||
// /auth/signin
|
||||
"signin.title": "Content de vous revoir",
|
||||
"signin.description": "Pas encore de compte ?",
|
||||
"signin.button.signup": "S'inscrire",
|
||||
"signin.input.email-or-username": "Courriel ou pseudo",
|
||||
"signin.input.email-or-username.placeholder": "Votre courriel ou pseudo",
|
||||
"signin.input.password": "Mot de passe",
|
||||
"signin.input.password.placeholder": "Votre mot de passe",
|
||||
"signin.button.submit": "Se connecter",
|
||||
"signIn.notify.totp-required.title": "Une authentification à deux facteurs est requise",
|
||||
"signIn.notify.totp-required.description": "Veuillez entrer votre code d'authentification à deux facteurs",
|
||||
// END /auth/signin
|
||||
// /auth/signup
|
||||
"signup.title": "Créer un compte",
|
||||
"signup.description": "Vous avez déjà un compte ?",
|
||||
"signup.button.signin": "Se connecter",
|
||||
"signup.input.username": "Pseudo",
|
||||
"signup.input.username.placeholder": "Votre pseudo",
|
||||
"signup.input.email": "Adresse email",
|
||||
"signup.input.email.placeholder": "Votre adresse email",
|
||||
"signup.button.submit": "Commençons",
|
||||
// END /auth/signup
|
||||
// /auth/reset-password
|
||||
"resetPassword.title": "Mot de passe oublié ?",
|
||||
"resetPassword.description": "Saisissez votre email pour réinitialiser votre mot de passe.",
|
||||
"resetPassword.notify.success": "Un email a été envoyé avec un lien pour réinitialiser votre mot de passe.",
|
||||
"resetPassword.button.back": "Retour à la page de connexion",
|
||||
"resetPassword.text.resetPassword": "Réinitialiser le mot de passe",
|
||||
"resetPassword.text.enterNewPassword": "Saisissez votre nouveau mot de passe",
|
||||
"resetPassword.input.password": "Nouveau mot de passe",
|
||||
"resetPassword.notify.passwordReset": "Votre mot de passe a bien été réinitialisé.",
|
||||
// /account
|
||||
"account.title": "Mon compte",
|
||||
"account.card.info.title": "Détails du compte",
|
||||
"account.card.info.username": "Pseudo",
|
||||
"account.card.info.email": "Adresse email",
|
||||
"account.notify.info.success": "Compte mis à jour avec succès",
|
||||
"account.card.password.title": "Mot de passe",
|
||||
"account.card.password.old": "Ancien mot de passe",
|
||||
"account.card.password.new": "Nouveau mot de passe",
|
||||
"account.notify.password.success": "Le mot de passe a été modifié avec succès",
|
||||
"account.card.security.title": "Sécurité",
|
||||
"account.card.security.totp.enable.description": "Entrez votre mot de passe actuel pour commencer à activer TOTP",
|
||||
"account.card.security.totp.disable.description": "Entrez votre mot de passe pour désactiver TOTP",
|
||||
"account.card.security.totp.button.start": "Commencer",
|
||||
"account.modal.totp.title": "Activer TOTP",
|
||||
"account.modal.totp.step1": "Étape 1 : Ajouter votre authentificateur",
|
||||
"account.modal.totp.step2": "Étape 2 : Valider votre code",
|
||||
"account.modal.totp.enterManually": "Saisir manuellement",
|
||||
"account.modal.totp.code": "Code",
|
||||
"account.modal.totp.clickToCopy": "Cliquez pour copier",
|
||||
"account.modal.totp.verify": "Vérifier",
|
||||
"account.notify.totp.disable": "TOTP désactivé",
|
||||
"account.notify.totp.enable": "TOTP activé",
|
||||
"account.card.language.title": "Langue",
|
||||
"account.card.language.description": "Le projet est traduit par la communauté. Certaines traductions peuvent être incomplètes.",
|
||||
"account.card.color.title": "Thème de couleurs",
|
||||
// ThemeSwitcher.tsx
|
||||
"account.theme.dark": "Sombre",
|
||||
"account.theme.light": "Clair",
|
||||
"account.theme.system": "Système",
|
||||
"account.button.delete": "Supprimer le compte",
|
||||
"account.modal.delete.title": "Supprimer le compte",
|
||||
"account.modal.delete.description": "Voulez-vous vraiment supprimer votre compte, y compris tous vos partages actifs ?",
|
||||
// END /account
|
||||
// /account/shares
|
||||
"account.shares.title": "Mes partages",
|
||||
"account.shares.title.empty": "Il n'y a rien ici 👀",
|
||||
"account.shares.description.empty": "Vous n’avez aucun partage.",
|
||||
"account.shares.button.create": "Créez-en un",
|
||||
"account.shares.info.title": "Détails du partage",
|
||||
"account.shares.table.id": "ID",
|
||||
"account.shares.table.name": "Nom",
|
||||
"account.shares.table.description": "Description",
|
||||
"account.shares.table.visitors": "Visiteurs",
|
||||
"account.shares.table.expiresAt": "Expire le",
|
||||
"account.shares.table.createdAt": "Créé le",
|
||||
"account.shares.table.size": "Taille",
|
||||
"account.shares.modal.share-informations": "Détails du partage",
|
||||
"account.shares.modal.share-link": "Lien de partage",
|
||||
"account.shares.modal.delete.title": "Supprimer le partage {share}",
|
||||
"account.shares.modal.delete.description": "Voulez-vous vraiment supprimer ce partage ?",
|
||||
// END /account/shares
|
||||
// /account/reverseShares
|
||||
"account.reverseShares.title": "Partages inversés",
|
||||
"account.reverseShares.description": "Un partage inversé vous permet de générer une URL unique qui permet à des utilisateurs externes de créer un partage.",
|
||||
"account.reverseShares.title.empty": "Il n'y a rien ici 👀",
|
||||
"account.reverseShares.description.empty": "Vous n'avez aucun partage inversé.",
|
||||
// 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": "Heure",
|
||||
"account.reverseShares.modal.expiration.hour-plural": "Heures",
|
||||
"account.reverseShares.modal.expiration.day-singular": "Jour",
|
||||
"account.reverseShares.modal.expiration.day-plural": "Jours",
|
||||
"account.reverseShares.modal.expiration.week-singular": "Semaine",
|
||||
"account.reverseShares.modal.expiration.week-plural": "Semaines",
|
||||
"account.reverseShares.modal.expiration.month-singular": "Mois",
|
||||
"account.reverseShares.modal.expiration.month-plural": "Mois",
|
||||
"account.reverseShares.modal.expiration.year-singular": "An",
|
||||
"account.reverseShares.modal.expiration.year-plural": "Ans",
|
||||
"account.reverseShares.modal.max-size.label": "Taille maximale du partage",
|
||||
"account.reverseShares.modal.send-email": "Envoyer un email de notification",
|
||||
"account.reverseShares.modal.send-email.description": "Envoyer une notification par email lorsqu'un partage est créé depuis ce partage inversé.",
|
||||
"account.reverseShares.modal.max-use.label": "Nombre d'utilisation max",
|
||||
"account.reverseShares.modal.max-use.description": "Le nombre maximal de fois que cette URL peut être utilisée pour créer un partage.",
|
||||
"account.reverseShare.never-expires": "Ce partage inversé n'expirera jamais.",
|
||||
"account.reverseShare.expires-on": "Ce partage inversé expirera le {expiration}.",
|
||||
"account.reverseShares.table.no-shares": "Aucun partage créé pour le moment",
|
||||
"account.reverseShares.table.count.singular": "partage",
|
||||
"account.reverseShares.table.count.plural": "partages",
|
||||
"account.reverseShares.table.shares": "Partages",
|
||||
"account.reverseShares.table.remaining": "Utilisations restantes",
|
||||
"account.reverseShares.table.max-size": "Taille maximale du partage",
|
||||
"account.reverseShares.table.expires": "Expire dans",
|
||||
"account.reverseShares.modal.reverse-share-link": "Lien du partage inversé",
|
||||
"account.reverseShares.modal.delete.title": "Supprimer le partage inversé",
|
||||
"account.reverseShares.modal.delete.description": "Voulez-vous vraiment supprimer ce partage inversé ? Si vous le faites, les partages qu'il contient seront également supprimés.",
|
||||
// END /account/reverseShares
|
||||
// /admin
|
||||
"admin.title": "Administration",
|
||||
"admin.button.users": "Gestion des utilisateurs",
|
||||
"admin.button.config": "Paramètres",
|
||||
"admin.version": "Version",
|
||||
// END /admin
|
||||
// /admin/users
|
||||
"admin.users.title": "Gestion des utilisateurs",
|
||||
"admin.users.table.username": "Pseudo",
|
||||
"admin.users.table.email": "Adresse email",
|
||||
"admin.users.table.admin": "Admin",
|
||||
"admin.users.edit.update.title": "Modifier l'utilisateur {username}",
|
||||
"admin.users.edit.update.admin-privileges": "Privilèges admin",
|
||||
"admin.users.edit.update.change-password.title": "Changer le mot de passe",
|
||||
"admin.users.edit.update.change-password.field": "Nouveau mot de passe",
|
||||
"admin.users.edit.update.change-password.button": "Enregistrer le nouveau mot de passe",
|
||||
"admin.users.edit.update.notify.password.success": "Le mot de passe a été modifié",
|
||||
"admin.users.edit.delete.title": "Supprimer l'utilisateur {username}",
|
||||
"admin.users.edit.delete.description": "Voulez-vous vraiment supprimer cet utilisateur et toutes ses partages ?",
|
||||
// showCreateUserModal.tsx
|
||||
"admin.users.modal.create.title": "Créer un utilisateur",
|
||||
"admin.users.modal.create.username": "Surnom",
|
||||
"admin.users.modal.create.email": "Email",
|
||||
"admin.users.modal.create.password": "Mot de passe",
|
||||
"admin.users.modal.create.manual-password": "Définir le mot de passe manuellement",
|
||||
"admin.users.modal.create.manual-password.description": "S'il n'est pas coché, l'utilisateur recevra un email avec un lien pour définir son mot de passe.",
|
||||
"admin.users.modal.create.admin": "Privilèges admin",
|
||||
"admin.users.modal.create.admin.description": "Si coché, l'utilisateur pourra accéder au panneau d'administration.",
|
||||
// END /admin/users
|
||||
// /upload
|
||||
"upload.title": "Envoyer",
|
||||
"upload.notify.generic-error": "Une erreur est survenue durant le traitement de votre partage.",
|
||||
"upload.notify.count-failed": "{count} fichier(s) n'a(ont) pas pu être envoyé(s). Veuillez réessayer.",
|
||||
// Dropzone.tsx
|
||||
"upload.dropzone.title": "Téléverser des fichiers",
|
||||
"upload.dropzone.description": "Glissez-déposez des fichiers ici pour commencer votre partage. Ils ne peuvent avoir une taille supérieur à {maxSize} au total.",
|
||||
"upload.dropzone.notify.file-too-big": "Vos fichiers dépassent la taille maximale de {maxSize}.",
|
||||
// FileList.tsx
|
||||
"upload.filelist.name": "Nom",
|
||||
"upload.filelist.size": "Taille",
|
||||
// showCreateUploadModal.tsx
|
||||
"upload.modal.title": "Créer un partage",
|
||||
"upload.modal.link.error.invalid": "Ne peut contenir que des lettres, des chiffres, des tirets bas et des traits d'union",
|
||||
"upload.modal.link.error.taken": "Ce lien est déjà utilisé",
|
||||
"upload.modal.not-signed-in": "Vous n'êtes pas connecté",
|
||||
"upload.modal.not-signed-in-description": "Vous ne pourrez pas supprimer votre partage manuellement et afficher le nombre de visiteurs.",
|
||||
"upload.modal.expires.never": "jamais",
|
||||
"upload.modal.expires.never-long": "N'expire jamais",
|
||||
"upload.modal.link.label": "Lien",
|
||||
"upload.modal.expires.label": "Expiration",
|
||||
"upload.modal.expires.minute-singular": "Minute",
|
||||
"upload.modal.expires.minute-plural": "Minutes",
|
||||
"upload.modal.expires.hour-singular": "Heure",
|
||||
"upload.modal.expires.hour-plural": "Heures",
|
||||
"upload.modal.expires.day-singular": "Jour",
|
||||
"upload.modal.expires.day-plural": "Jours",
|
||||
"upload.modal.expires.week-singular": "Semaine",
|
||||
"upload.modal.expires.week-plural": "Semaines",
|
||||
"upload.modal.expires.month-singular": "Mois",
|
||||
"upload.modal.expires.month-plural": "Mois",
|
||||
"upload.modal.expires.year-singular": "An",
|
||||
"upload.modal.expires.year-plural": "Ans",
|
||||
"upload.modal.accordion.description.title": "Description",
|
||||
"upload.modal.accordion.description.placeholder": "Note pour les destinataires de ce partage",
|
||||
"upload.modal.accordion.email.title": "Adresse courriel des destinataires",
|
||||
"upload.modal.accordion.email.placeholder": "Saisir les destinataires de ce partage",
|
||||
"upload.modal.accordion.email.invalid-email": "Adresse email invalide",
|
||||
"upload.modal.accordion.security.title": "Options de sécurité",
|
||||
"upload.modal.accordion.security.password.label": "Protection par mot de passe",
|
||||
"upload.modal.accordion.security.password.placeholder": "Aucun mot de passe",
|
||||
"upload.modal.accordion.security.max-views.label": "Nombre de vues maximum",
|
||||
"upload.modal.accordion.security.max-views.placeholder": "Aucune limite",
|
||||
// showCompletedUploadModal.tsx
|
||||
"upload.modal.completed.never-expires": "Ce partage n'expirera jamais.",
|
||||
"upload.modal.completed.expires-on": "Ce partage expirera le {expiration}.",
|
||||
"upload.modal.completed.share-ready": "Partage prêt",
|
||||
// END /upload
|
||||
// /share/[id]
|
||||
"share.title": "Partage {shareId}",
|
||||
"share.description": "Regardez ce que j'ai partagé !",
|
||||
"share.error.visitor-limit-exceeded.title": "Limite de visiteurs dépassée",
|
||||
"share.error.visitor-limit-exceeded.description": "La limite de visiteurs de ce partage a été dépassée.",
|
||||
"share.error.removed.title": "Partage supprimé",
|
||||
"share.error.not-found.title": "Partage introuvable",
|
||||
"share.error.not-found.description": "Le partage que vous cherchez n'existe pas.",
|
||||
"share.modal.password.title": "Mot de passe requis",
|
||||
"share.modal.password.description": "Pour accéder à ce partage, veuillez entrer le mot de passe du partage.",
|
||||
"share.modal.password": "Mot de passe",
|
||||
"share.modal.error.invalid-password": "Mot de passe incorrect",
|
||||
"share.button.download-all": "Télécharger tout",
|
||||
"share.notify.download-all-preparing": "Le partage est en préparation. Réessayez dans quelques minutes.",
|
||||
"share.modal.file-link": "Lien du fichier",
|
||||
"share.table.name": "Nom",
|
||||
"share.table.size": "Taille",
|
||||
"share.modal.file-preview.error.not-supported.title": "Aperçu non supporté",
|
||||
"share.modal.file-preview.error.not-supported.description": "Un aperçu pour ce type de fichier n'est pas pris en charge. Veuillez télécharger le fichier pour le voir.",
|
||||
// END /share/[id]
|
||||
// /admin/config
|
||||
"admin.config.title": "Paramètres",
|
||||
"admin.config.category.general": "Général",
|
||||
"admin.config.category.share": "Partage",
|
||||
"admin.config.category.email": "Courriel",
|
||||
"admin.config.category.smtp": "SMTP",
|
||||
"admin.config.general.app-name": "Nom de l'appli",
|
||||
"admin.config.general.app-name.description": "Le nom de l'application",
|
||||
"admin.config.general.app-url": "URL de l’appli",
|
||||
"admin.config.general.app-url.description": "Depuis quel URL le partage Pingvin est disponible",
|
||||
"admin.config.general.show-home-page": "Afficher la page d'accueil",
|
||||
"admin.config.general.show-home-page.description": "Afficher ou non la page d'accueil",
|
||||
"admin.config.general.logo": "Logo",
|
||||
"admin.config.general.logo.description": "Changez de logo en envoyant une nouvelle image. L'image doit être au format PNG et doit avoir un ratio 1:1.",
|
||||
"admin.config.general.logo.placeholder": "Sélectionner une image",
|
||||
"admin.config.email.enable-share-email-recipients": "Autoriser le partage par courriel",
|
||||
"admin.config.email.enable-share-email-recipients.description": "Permet d'envoyer le lien du partage par courriel. N'activez cette option que si vous avez activé SMTP.",
|
||||
"admin.config.email.share-recipients-subject": "Sujet d'un partage",
|
||||
"admin.config.email.share-recipients-subject.description": "Intitulé du courriel envoyé aux destinataires d'un partage.",
|
||||
"admin.config.email.share-recipients-message": "Message d'un partage",
|
||||
"admin.config.email.share-recipients-message.description": "Contenu du courriel qui est envoyé aux destinataires du partage. Variables possibles :\n• {creator} : Le pseudo de l'auteur du partage\n• {shareUrl} : L'URL du partage\n• {desc} : La description du partage\n• {expires} : La date d'expiration du partage\nLes variables seront remplacées par leur valeur réelle.",
|
||||
"admin.config.email.reverse-share-subject": "Sujet d'un partage inversé",
|
||||
"admin.config.email.reverse-share-subject.description": "Intitulé du courriel envoyé lorsque quelqu'un a partagé des fichiers depuis votre partage inversé.",
|
||||
"admin.config.email.reverse-share-message": "Message du partage inversé",
|
||||
"admin.config.email.reverse-share-message.description": "Contenu du courriel envoyé lorsque quelqu'un partage des fichiers depuis votre partage inversé. {shareUrl} sera remplacé par le nom du créateur et l'URL de partage.",
|
||||
"admin.config.email.reset-password-subject": "Sujet d'une réinitialisation du mot de passe",
|
||||
"admin.config.email.reset-password-subject.description": "Intitulé du courriel envoyé lorsqu'un utilisateur demande une réinitialisation de son mot de passe.",
|
||||
"admin.config.email.reset-password-message": "Message de réinitialisation du mot de passe",
|
||||
"admin.config.email.reset-password-message.description": "Contenu du courriel envoyé lorsqu'un utilisateur demande à réinitialiser son mot de passe. {url} sera remplacé par l'URL de réinitialisation du mot de passe.",
|
||||
"admin.config.email.invite-subject": "Sujet d'une invitation",
|
||||
"admin.config.email.invite-subject.description": "Intitulé du courriel envoyé lorsqu'un administrateur invite un utilisateur.",
|
||||
"admin.config.email.invite-message": "Message de l'invitation",
|
||||
"admin.config.email.invite-message.description": "Contenu du courriel envoyé lorsqu'un administrateur invite un utilisateur. {url} sera remplacé par l'URL d'invitation et {password} par le mot de passe.",
|
||||
"admin.config.share.allow-registration": "Autoriser les inscriptions",
|
||||
"admin.config.share.allow-registration.description": "Permet aux visiteurs de créer un compte.",
|
||||
"admin.config.share.allow-unauthenticated-shares": "Autoriser les partages anonymes",
|
||||
"admin.config.share.allow-unauthenticated-shares.description": "Autorise des utilisateurs non authentifiés à créer des partages",
|
||||
"admin.config.share.max-size": "Taille max",
|
||||
"admin.config.share.max-size.description": "Taille maximale du partage en octets",
|
||||
"admin.config.share.zip-compression-level": "Niveau de compression",
|
||||
"admin.config.share.zip-compression-level.description": "Ajustez le niveau pour trouver l'équilibre entre la taille du fichier et la vitesse de compression. Les valeurs valides vont de 0 à 9, 0 étant sans compression et 9 étant la compression maximale. ",
|
||||
"admin.config.smtp.enabled": "Activer",
|
||||
"admin.config.smtp.enabled.description": "Active SMTP. Activez ceci uniquement si vous avez saisi l'hôte, le port, le courriel, l'utilisateur et le mot de passe de votre serveur SMTP.",
|
||||
"admin.config.smtp.host": "Hôte",
|
||||
"admin.config.smtp.host.description": "Nom du serveur SMTP",
|
||||
"admin.config.smtp.port": "Port",
|
||||
"admin.config.smtp.port.description": "Port du serveur SMTP",
|
||||
"admin.config.smtp.email": "Adresse email",
|
||||
"admin.config.smtp.email.description": "Adresse à partir de laquelle les emails sont envoyés",
|
||||
"admin.config.smtp.username": "Nom d'utilisateur",
|
||||
"admin.config.smtp.username.description": "Nom d'utilisateur du serveur SMTP",
|
||||
"admin.config.smtp.password": "Mot de passe",
|
||||
"admin.config.smtp.password.description": "Mot de passe du serveur SMTP",
|
||||
"admin.config.smtp.button.test": "Envoyer un email de test",
|
||||
// 404
|
||||
"404.description": "Désolé, mais cette page n’existe pas.",
|
||||
"404.button.home": "Retour à l'accueil",
|
||||
// Common translations
|
||||
"common.button.save": "Sauvegarder",
|
||||
"common.button.create": "Créer",
|
||||
"common.button.submit": "Envoyer",
|
||||
"common.button.delete": "Supprimer",
|
||||
"common.button.cancel": "Annuler",
|
||||
"common.button.confirm": "Confirmer",
|
||||
"common.button.disable": "Désactiver",
|
||||
"common.button.share": "Partager",
|
||||
"common.button.generate": "Générer",
|
||||
"common.button.done": "Terminer",
|
||||
"common.text.link": "Lien",
|
||||
"common.text.or": "ou",
|
||||
"common.button.go-back": "Précédent",
|
||||
"common.notify.copied": "Votre lien a été copié dans le presse-papiers",
|
||||
"common.success": "Opération réussie",
|
||||
"common.error": "Erreur",
|
||||
"common.error.unknown": "Une erreur inconnue est survenue",
|
||||
"common.error.invalid-email": "Adresse courriel invalide",
|
||||
"common.error.too-short": "Doit comporter au moins {length} caractères",
|
||||
"common.error.too-long": "Doit comporter au plus {length} caractères",
|
||||
"common.error.exact-length": "Doit comporter exactement {length} caractères",
|
||||
"common.error.invalid-number": "Doit être un nombre",
|
||||
"common.error.field-required": "Ce champ est obligatoire"
|
||||
};
|
||||
323
frontend/src/i18n/translations/pt-BR.ts
Normal file
323
frontend/src/i18n/translations/pt-BR.ts
Normal file
@@ -0,0 +1,323 @@
|
||||
export default {
|
||||
// Navbar
|
||||
"navbar.upload": "Carregar",
|
||||
"navbar.signin": "Iniciar sessão",
|
||||
"navbar.home": "Início",
|
||||
"navbar.signup": "Registar-se",
|
||||
"navbar.links.shares": "Meus compartilhamentos",
|
||||
"navbar.links.reverse": "Compartilhamentos reversos",
|
||||
"navbar.avatar.account": "Minha conta",
|
||||
"navbar.avatar.admin": "Administração",
|
||||
"navbar.avatar.signout": "Terminar sessão",
|
||||
// END navbar
|
||||
// /
|
||||
"home.title": "Uma plataforma de compartilhamento de arquivos <h>auto-hospedada</h>.",
|
||||
"home.description": "Deseja realmente dar os seus arquivos pessoais na mão de terceiros como o WeTransfer?",
|
||||
"home.bullet.a.name": "Auto-Hospedado",
|
||||
"home.bullet.a.description": "Hospede o Pingvin Share em sua própria máquina.",
|
||||
"home.bullet.b.name": "Privacidade",
|
||||
"home.bullet.b.description": "Seus arquivos são seus arquivos e nunca devem cair nas mãos de terceiros.",
|
||||
"home.bullet.c.name": "Sem limite de tamanho de arquivo irritante",
|
||||
"home.bullet.c.description": "Carregue os arquivos grandes que desejar. Apenas o seu disco rígido será o seu limite.",
|
||||
"home.button.start": "Começar",
|
||||
"home.button.source": "Código-fonte",
|
||||
// END /
|
||||
// /auth/signin
|
||||
"signin.title": "Bem-vindo de volta",
|
||||
"signin.description": "Ainda não tem uma conta?",
|
||||
"signin.button.signup": "Registo",
|
||||
"signin.input.email-or-username": "Correio ou Nome de utilizador",
|
||||
"signin.input.email-or-username.placeholder": "Seu e-mail ou nome de usuário",
|
||||
"signin.input.password": "Senha",
|
||||
"signin.input.password.placeholder": "A sua senha",
|
||||
"signin.button.submit": "Iniciar sessão",
|
||||
"signIn.notify.totp-required.title": "Autenticação de dois fatores necessária",
|
||||
"signIn.notify.totp-required.description": "Insira seu código de autenticação de dois fatores",
|
||||
// END /auth/signin
|
||||
// /auth/signup
|
||||
"signup.title": "Criar uma conta",
|
||||
"signup.description": "Já tem uma conta?",
|
||||
"signup.button.signin": "Iniciar sessão",
|
||||
"signup.input.username": "Utilizador",
|
||||
"signup.input.username.placeholder": "Seu nome de usuário",
|
||||
"signup.input.email": "E-mail",
|
||||
"signup.input.email.placeholder": "Seu e-mail",
|
||||
"signup.button.submit": "Vamos começar",
|
||||
// END /auth/signup
|
||||
// /auth/reset-password
|
||||
"resetPassword.title": "Esqueceu a sua senha?",
|
||||
"resetPassword.description": "Insira o seu e-mail para redefinir a sua senha.",
|
||||
"resetPassword.notify.success": "Um e-mail foi enviado com um link para redefinir a sua senha.",
|
||||
"resetPassword.button.back": "Voltar para a página inicial",
|
||||
"resetPassword.text.resetPassword": "Redefinir senha",
|
||||
"resetPassword.text.enterNewPassword": "Digite uma nova senha",
|
||||
"resetPassword.input.password": "Nova senha",
|
||||
"resetPassword.notify.passwordReset": "A sua senha foi redefinida com sucesso.",
|
||||
// /account
|
||||
"account.title": "A minha conta",
|
||||
"account.card.info.title": "Informação sobre a conta",
|
||||
"account.card.info.username": "Nome do usuário",
|
||||
"account.card.info.email": "E-mail",
|
||||
"account.notify.info.success": "A conta foi atualizada com sucesso",
|
||||
"account.card.password.title": "Senha",
|
||||
"account.card.password.old": "Senha antiga",
|
||||
"account.card.password.new": "Nova senha",
|
||||
"account.notify.password.success": "Senha alterada com sucesso",
|
||||
"account.card.security.title": "Segurança",
|
||||
"account.card.security.totp.enable.description": "Digite a sua senha atual para começar a habilitar o TOTP",
|
||||
"account.card.security.totp.disable.description": "Digite a sua senha atual para desabilitar o TOTP",
|
||||
"account.card.security.totp.button.start": "Iniciar",
|
||||
"account.modal.totp.title": "Habilitar TOTP",
|
||||
"account.modal.totp.step1": "Passo 1: Adicionar o seu autenticador",
|
||||
"account.modal.totp.step2": "Passo 2: Valide o seu código",
|
||||
"account.modal.totp.enterManually": "Inserir manualmente",
|
||||
"account.modal.totp.code": "Código",
|
||||
"account.modal.totp.clickToCopy": "Clique para copiar",
|
||||
"account.modal.totp.verify": "Verificar",
|
||||
"account.notify.totp.disable": "TOTP desabilitado com sucesso",
|
||||
"account.notify.totp.enable": "TOTP habilitado com sucesso",
|
||||
"account.card.language.title": "Idioma",
|
||||
"account.card.language.description": "O projeto é traduzido pela comunidade. Alguns idiomas podem estar incompletos.",
|
||||
"account.card.color.title": "Esquema de cores",
|
||||
// ThemeSwitcher.tsx
|
||||
"account.theme.dark": "Escuro",
|
||||
"account.theme.light": "Claro",
|
||||
"account.theme.system": "Sistema",
|
||||
"account.button.delete": "Excluir conta",
|
||||
"account.modal.delete.title": "Excluir conta",
|
||||
"account.modal.delete.description": "Você realmente deseja excluir a sua conta, incluindo todos os seus compartilhamentos ativos?",
|
||||
// END /account
|
||||
// /account/shares
|
||||
"account.shares.title": "Meus compartilhamentos",
|
||||
"account.shares.title.empty": "Está vazio aqui 👀",
|
||||
"account.shares.description.empty": "Você não tem nenhum compartilhamento.",
|
||||
"account.shares.button.create": "Crie um",
|
||||
"account.shares.info.title": "Informações do compartilhamento",
|
||||
"account.shares.table.id": "ID",
|
||||
"account.shares.table.name": "Nome",
|
||||
"account.shares.table.description": "Descrição",
|
||||
"account.shares.table.visitors": "Visitantes",
|
||||
"account.shares.table.expiresAt": "Expira em",
|
||||
"account.shares.table.createdAt": "Criado em",
|
||||
"account.shares.table.size": "Tamanho",
|
||||
"account.shares.modal.share-informations": "Informações do compartilhamento",
|
||||
"account.shares.modal.share-link": "Link do compartilhamento",
|
||||
"account.shares.modal.delete.title": "Excluir o compartilhamento {share}",
|
||||
"account.shares.modal.delete.description": "Tem certeza que deseja excluir este compartilhamento?",
|
||||
// END /account/shares
|
||||
// /account/reverseShares
|
||||
"account.reverseShares.title": "Compartilhamentos reversos",
|
||||
"account.reverseShares.description": "Um compartilhamento reverso permite gerar uma URL única que autoriza usuários externos criarem um compartilhamento.",
|
||||
"account.reverseShares.title.empty": "Está vazio aqui 👀",
|
||||
"account.reverseShares.description.empty": "Você não tem nenhum compartilhamento reverso.",
|
||||
// showCreateReverseShareModal.tsx
|
||||
"account.reverseShares.modal.expiration.label": "Expiração",
|
||||
"account.reverseShares.modal.expiration.minute-singular": "Minuto",
|
||||
"account.reverseShares.modal.expiration.minute-plural": "Minutos",
|
||||
"account.reverseShares.modal.expiration.hour-singular": "Hora",
|
||||
"account.reverseShares.modal.expiration.hour-plural": "Horas",
|
||||
"account.reverseShares.modal.expiration.day-singular": "Dia",
|
||||
"account.reverseShares.modal.expiration.day-plural": "Dias",
|
||||
"account.reverseShares.modal.expiration.week-singular": "Semana",
|
||||
"account.reverseShares.modal.expiration.week-plural": "Semanas",
|
||||
"account.reverseShares.modal.expiration.month-singular": "Mês",
|
||||
"account.reverseShares.modal.expiration.month-plural": "Meses",
|
||||
"account.reverseShares.modal.expiration.year-singular": "Ano",
|
||||
"account.reverseShares.modal.expiration.year-plural": "Anos",
|
||||
"account.reverseShares.modal.max-size.label": "Tamanho máximo do compartilhamento",
|
||||
"account.reverseShares.modal.send-email": "Enviar notificação por e-mail",
|
||||
"account.reverseShares.modal.send-email.description": "Enviar uma notificação por e-mail quando um compartilhamento for criado com este link reverso.",
|
||||
"account.reverseShares.modal.max-use.label": "Limite de uso",
|
||||
"account.reverseShares.modal.max-use.description": "A quantidade máxima de vezes que esta URL pode ser usada para criar um compartilhamento.",
|
||||
"account.reverseShare.never-expires": "Este compartilhamento reverso nunca irá expirar.",
|
||||
"account.reverseShare.expires-on": "Este compartilhamento reverso irá expirar em {expiration}.",
|
||||
"account.reverseShares.table.no-shares": "Nenhum compartilhamento criado ainda",
|
||||
"account.reverseShares.table.count.singular": "compartilhar",
|
||||
"account.reverseShares.table.count.plural": "compartilhamentos",
|
||||
"account.reverseShares.table.shares": "Compartilhamentos",
|
||||
"account.reverseShares.table.remaining": "Usos restantes",
|
||||
"account.reverseShares.table.max-size": "Tamanho máximo do compartilhamento",
|
||||
"account.reverseShares.table.expires": "Expira em",
|
||||
"account.reverseShares.modal.reverse-share-link": "Link do compartilhamento reverso",
|
||||
"account.reverseShares.modal.delete.title": "Excluir o compartilhamento reverso",
|
||||
"account.reverseShares.modal.delete.description": "Você realmente deseja excluir este compartilhamento reverso? Se você o fizer, os compartilhamentos associados também serão excluídos.",
|
||||
// END /account/reverseShares
|
||||
// /admin
|
||||
"admin.title": "Administração",
|
||||
"admin.button.users": "Gerenciamento de usuários",
|
||||
"admin.button.config": "Configuração",
|
||||
"admin.version": "Versão",
|
||||
// END /admin
|
||||
// /admin/users
|
||||
"admin.users.title": "Gerenciamento de usuários",
|
||||
"admin.users.table.username": "Nome do usuário",
|
||||
"admin.users.table.email": "E-mail",
|
||||
"admin.users.table.admin": "Admin",
|
||||
"admin.users.edit.update.title": "Atualizar usuário {username}",
|
||||
"admin.users.edit.update.admin-privileges": "Privilégios de administrador",
|
||||
"admin.users.edit.update.change-password.title": "Alterar senha",
|
||||
"admin.users.edit.update.change-password.field": "Nova senha",
|
||||
"admin.users.edit.update.change-password.button": "Salvar nova senha",
|
||||
"admin.users.edit.update.notify.password.success": "Senha alterada com sucesso",
|
||||
"admin.users.edit.delete.title": "Excluir usuário {username}",
|
||||
"admin.users.edit.delete.description": "Você realmente quer excluir este usuário e todos os seus compartilhamentos?",
|
||||
// showCreateUserModal.tsx
|
||||
"admin.users.modal.create.title": "Criar usuário",
|
||||
"admin.users.modal.create.username": "Nome do usuário",
|
||||
"admin.users.modal.create.email": "E-mail",
|
||||
"admin.users.modal.create.password": "Senha",
|
||||
"admin.users.modal.create.manual-password": "Definir senha manualmente",
|
||||
"admin.users.modal.create.manual-password.description": "Se não estiver marcado, o usuário receberá um e-mail com um link para definir sua senha.",
|
||||
"admin.users.modal.create.admin": "Privilégios de administrador",
|
||||
"admin.users.modal.create.admin.description": "Se marcado, o usuário poderá acessar o painel de administração.",
|
||||
// END /admin/users
|
||||
// /upload
|
||||
"upload.title": "Carregar",
|
||||
"upload.notify.generic-error": "Ocorreu um erro ao terminar seu compartilhamento.",
|
||||
"upload.notify.count-failed": "Falha ao enviar {count} arquivos. Tentando novamente.",
|
||||
// Dropzone.tsx
|
||||
"upload.dropzone.title": "Carregar arquivos",
|
||||
"upload.dropzone.description": "Arraste os arquivos aqui para iniciar o seu compartilhamento. Podemos aceitar apenas arquivos que são menores que {maxSize} no total.",
|
||||
"upload.dropzone.notify.file-too-big": "Seus arquivos excedem o tamanho máximo do compartilhamento {maxSize}.",
|
||||
// FileList.tsx
|
||||
"upload.filelist.name": "Nome",
|
||||
"upload.filelist.size": "Tamanho",
|
||||
// showCreateUploadModal.tsx
|
||||
"upload.modal.title": "Criar Compartilhamento",
|
||||
"upload.modal.link.error.invalid": "Pode conter apenas letras, números, sublinhados e hífens",
|
||||
"upload.modal.link.error.taken": "Este link já está em uso",
|
||||
"upload.modal.not-signed-in": "Você não está conectado",
|
||||
"upload.modal.not-signed-in-description": "Você não poderá excluir seu compartilhamento manualmente e visualizar a contagem de visitantes.",
|
||||
"upload.modal.expires.never": "nunca",
|
||||
"upload.modal.expires.never-long": "Nunca expira",
|
||||
"upload.modal.link.label": "Link",
|
||||
"upload.modal.expires.label": "Expiração",
|
||||
"upload.modal.expires.minute-singular": "Minuto",
|
||||
"upload.modal.expires.minute-plural": "Minutos",
|
||||
"upload.modal.expires.hour-singular": "Hora",
|
||||
"upload.modal.expires.hour-plural": "Horas",
|
||||
"upload.modal.expires.day-singular": "Dia",
|
||||
"upload.modal.expires.day-plural": "Dias",
|
||||
"upload.modal.expires.week-singular": "Semana",
|
||||
"upload.modal.expires.week-plural": "Semanas",
|
||||
"upload.modal.expires.month-singular": "Mês",
|
||||
"upload.modal.expires.month-plural": "Meses",
|
||||
"upload.modal.expires.year-singular": "Ano",
|
||||
"upload.modal.expires.year-plural": "Anos",
|
||||
"upload.modal.accordion.description.title": "Descrição",
|
||||
"upload.modal.accordion.description.placeholder": "Nota para os destinatários deste compartilhamento",
|
||||
"upload.modal.accordion.email.title": "Destinatários de e-mail",
|
||||
"upload.modal.accordion.email.placeholder": "Insira os destinatários do e-mail",
|
||||
"upload.modal.accordion.email.invalid-email": "Endereço de e-mail inválido",
|
||||
"upload.modal.accordion.security.title": "Opções de segurança",
|
||||
"upload.modal.accordion.security.password.label": "Protecção por senha",
|
||||
"upload.modal.accordion.security.password.placeholder": "Sem senha",
|
||||
"upload.modal.accordion.security.max-views.label": "Máximo de visualizações",
|
||||
"upload.modal.accordion.security.max-views.placeholder": "Sem limite",
|
||||
// showCompletedUploadModal.tsx
|
||||
"upload.modal.completed.never-expires": "Este compartilhamento reverso nunca irá expirar.",
|
||||
"upload.modal.completed.expires-on": "Este compartilhamento reverso irá expirar em {expiration}.",
|
||||
"upload.modal.completed.share-ready": "Compartilhamento pronto",
|
||||
// END /upload
|
||||
// /share/[id]
|
||||
"share.title": "Compartilhar {shareId}",
|
||||
"share.description": "Veja o que eu compartilhei com você!",
|
||||
"share.error.visitor-limit-exceeded.title": "Limite de visitantes excedido",
|
||||
"share.error.visitor-limit-exceeded.description": "O limite de visitantes deste compartilhamento foi excedido.",
|
||||
"share.error.removed.title": "Compartilhamento removido",
|
||||
"share.error.not-found.title": "Compartilhamento não encontrado",
|
||||
"share.error.not-found.description": "O compartilhamento que você procura não existe.",
|
||||
"share.modal.password.title": "Senha necessária",
|
||||
"share.modal.password.description": "Para acessar este compartilhamento, por favor digite a senha para o compartilhamento.",
|
||||
"share.modal.password": "Senha",
|
||||
"share.modal.error.invalid-password": "Senha inválida",
|
||||
"share.button.download-all": "Transferir tudo",
|
||||
"share.notify.download-all-preparing": "O compartilhamento está sendo preparado. Tente novamente em alguns minutos.",
|
||||
"share.modal.file-link": "Link do arquivo",
|
||||
"share.table.name": "Nome",
|
||||
"share.table.size": "Tamanho",
|
||||
"share.modal.file-preview.error.not-supported.title": "Visualização não suportada",
|
||||
"share.modal.file-preview.error.not-supported.description": "Uma visualização para este tipo de arquivo não é suportada. Faça o download do arquivo para visualizá-lo.",
|
||||
// END /share/[id]
|
||||
// /admin/config
|
||||
"admin.config.title": "Configuração",
|
||||
"admin.config.category.general": "Geral",
|
||||
"admin.config.category.share": "Compartilhamento",
|
||||
"admin.config.category.email": "E-mail",
|
||||
"admin.config.category.smtp": "SMTP",
|
||||
"admin.config.general.app-name": "Nome da aplicação",
|
||||
"admin.config.general.app-name.description": "Nome da aplicação",
|
||||
"admin.config.general.app-url": "URL do Aplicativo",
|
||||
"admin.config.general.app-url.description": "Em qual URL o Pingvin Share está disponível",
|
||||
"admin.config.general.show-home-page": "Mostrar a página inicial",
|
||||
"admin.config.general.show-home-page.description": "Mostrar ou não a página inicial",
|
||||
"admin.config.general.logo": "Logo",
|
||||
"admin.config.general.logo.description": "Alterar o seu logo carregando uma nova imagem. A imagem deve ser PNG e deve ter o formato 1:1.",
|
||||
"admin.config.general.logo.placeholder": "Escolhe uma imagem",
|
||||
"admin.config.email.enable-share-email-recipients": "Ativar compartilhamento de e-mails destinatários",
|
||||
"admin.config.email.enable-share-email-recipients.description": "Se quiser permitir que e-mails compartilhem destinatários. Apenas habilite isso se você tiver ativado o SMTP.",
|
||||
"admin.config.email.share-recipients-subject": "Assunto dos destinatários do compartilhamento",
|
||||
"admin.config.email.share-recipients-subject.description": "Assunto do e-mail enviado para os destinatários do compartilhamento.",
|
||||
"admin.config.email.share-recipients-message": "Assunto dos destinatários do compartilhamento",
|
||||
"admin.config.email.share-recipients-message.description": "Mensagem que é enviada aos destinatários do compartilhamento. Variáveis disponíveis:\n {creator} - O nome de usuário do criador do compartilhamento\n {shareUrl} - O URL do compartilhamento\n {desc} - A descrição do compartilhamento\n {expires} - A data de expiração do compartilhamento\n As variáveis serão substituídas pelo valor real.",
|
||||
"admin.config.email.reverse-share-subject": "Assunto do compartilhamento reverso",
|
||||
"admin.config.email.reverse-share-subject.description": "Assunto do e-mail enviado quando alguém criou um compartilhamento com o seu link reverso.",
|
||||
"admin.config.email.reverse-share-message": "Mensagem do compartilhamento reverso",
|
||||
"admin.config.email.reverse-share-message.description": "Mensagem enviada quando alguém criou um compartilhamento com o link reverso. {shareUrl} será substituído pelo nome do criador e pela URL de compartilhamento.",
|
||||
"admin.config.email.reset-password-subject": "Redefinir assunto da senha",
|
||||
"admin.config.email.reset-password-subject.description": "Assunto do e-mail enviado quando um usuário solicita uma redefinição de senha.",
|
||||
"admin.config.email.reset-password-message": "Mensagem de redefinição de senha",
|
||||
"admin.config.email.reset-password-message.description": "Mensagem enviada quando um usuário solicita uma redefinição de senha. {url} será substituído pela URL de redefinição de senha.",
|
||||
"admin.config.email.invite-subject": "Assunto do convite",
|
||||
"admin.config.email.invite-subject.description": "Assunto do e-mail enviado quando um administrador convida um usuário.",
|
||||
"admin.config.email.invite-message": "Mensagem de convite",
|
||||
"admin.config.email.invite-message.description": "Mensagem enviada quando um administrador convida um usuário. {url} será substituído pelo URL de convite e {password} pela senha.",
|
||||
"admin.config.share.allow-registration": "Permitir novos registos",
|
||||
"admin.config.share.allow-registration.description": "Se o registro é permitido",
|
||||
"admin.config.share.allow-unauthenticated-shares": "Permitir compartilhamentos sem autenticação",
|
||||
"admin.config.share.allow-unauthenticated-shares.description": "Se usuários não autenticados podem criar compartilhamentos",
|
||||
"admin.config.share.max-size": "Tamanho máximo",
|
||||
"admin.config.share.max-size.description": "Tamanho máximo do compartilhamento em bytes",
|
||||
"admin.config.share.zip-compression-level": "Nível de compressão",
|
||||
"admin.config.share.zip-compression-level.description": "Ajuste o nível para equilibrar entre o tamanho do arquivo e a velocidade de compressão. Valores válidos vão de 0 a 9, com 0 sendo sem compressão e 9 sendo compressão máxima. ",
|
||||
"admin.config.smtp.enabled": "Ativado",
|
||||
"admin.config.smtp.enabled.description": "Se o SMTP está habilitado. Apenas defina como verdadeiro se você digitou o host, porta, e-mail, usuário e senha do seu servidor SMTP.",
|
||||
"admin.config.smtp.host": "Servidor",
|
||||
"admin.config.smtp.host.description": "Nome do Servidor SMTP",
|
||||
"admin.config.smtp.port": "Porta",
|
||||
"admin.config.smtp.port.description": "Porta do Servidor SMTP",
|
||||
"admin.config.smtp.email": "E-mail",
|
||||
"admin.config.smtp.email.description": "Endereço de e-mail do qual os e-mails são enviados",
|
||||
"admin.config.smtp.username": "Nome do usuário",
|
||||
"admin.config.smtp.username.description": "Nome de usuário do servidor SMTP",
|
||||
"admin.config.smtp.password": "Senha",
|
||||
"admin.config.smtp.password.description": "Senha do servidor SMTP",
|
||||
"admin.config.smtp.button.test": "Enviar email de teste",
|
||||
// 404
|
||||
"404.description": "Ops, esta página não existe.",
|
||||
"404.button.home": "Me traga de volta para casa",
|
||||
// Common translations
|
||||
"common.button.save": "Salvar",
|
||||
"common.button.create": "Criar",
|
||||
"common.button.submit": "Submeter",
|
||||
"common.button.delete": "Excluir",
|
||||
"common.button.cancel": "Cancelar",
|
||||
"common.button.confirm": "Confirmar",
|
||||
"common.button.disable": "Desativar",
|
||||
"common.button.share": "Compartilhamento",
|
||||
"common.button.generate": "Gerar",
|
||||
"common.button.done": "Concluído",
|
||||
"common.text.link": "Link",
|
||||
"common.text.or": "ou",
|
||||
"common.button.go-back": "Voltar",
|
||||
"common.notify.copied": "O seu link foi copiado para a área de transferência",
|
||||
"common.success": "Sucesso",
|
||||
"common.error": "Erro",
|
||||
"common.error.unknown": "Ocorreu um erro desconhecido",
|
||||
"common.error.invalid-email": "Endereço de e-mail inválido",
|
||||
"common.error.too-short": "Deve ter no mínimo {length} caracteres",
|
||||
"common.error.too-long": "Deve ter no máximo {length} caracteres",
|
||||
"common.error.exact-length": "Deve ter exatamente {length} caracteres",
|
||||
"common.error.invalid-number": "Tem que ser um número",
|
||||
"common.error.field-required": "Este campo é obrigatório"
|
||||
};
|
||||
438
frontend/src/i18n/translations/ru-RU.ts
Normal file
438
frontend/src/i18n/translations/ru-RU.ts
Normal file
@@ -0,0 +1,438 @@
|
||||
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 disable 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.language.description":
|
||||
"The project is translated by the community. Some languages might be incomplete.",
|
||||
"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 informations",
|
||||
"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-informations": "Share informations",
|
||||
"account.shares.modal.share-link": "Share link",
|
||||
|
||||
"account.shares.modal.delete.title": "Delete share {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": "Delete reverse share",
|
||||
"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} files 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.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 value.",
|
||||
"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 unauthenticated users can create shares",
|
||||
"admin.config.share.max-size": "Max size",
|
||||
"admin.config.share.max-size.description": "Maximum share size in bytes",
|
||||
"admin.config.share.zip-compression-level": "Zip compression level",
|
||||
"admin.config.share.zip-compression-level.description":
|
||||
"Adjust the level to balance between file size and compression speed. Valid values range from 0 to 9, with 0 being no compression and 9 being maximum compression. ",
|
||||
|
||||
"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.description": "Oops this page doesn't exist.",
|
||||
"404.button.home": "Bring 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.success": "Success",
|
||||
|
||||
"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",
|
||||
};
|
||||
323
frontend/src/i18n/translations/zh-CN.ts
Normal file
323
frontend/src/i18n/translations/zh-CN.ts
Normal file
@@ -0,0 +1,323 @@
|
||||
export default {
|
||||
// Navbar
|
||||
"navbar.upload": "上传",
|
||||
"navbar.signin": "登录",
|
||||
"navbar.home": "首页",
|
||||
"navbar.signup": "注册",
|
||||
"navbar.links.shares": "我的共享",
|
||||
"navbar.links.reverse": "预留共享",
|
||||
"navbar.avatar.account": "我的账户",
|
||||
"navbar.avatar.admin": "管理",
|
||||
"navbar.avatar.signout": "登出",
|
||||
// END navbar
|
||||
// /
|
||||
"home.title": "<h>自建</h> 文件共享平台!",
|
||||
"home.description": "你真的放心把文件交到第三方文件平台手中吗?",
|
||||
"home.bullet.a.name": "完全自建",
|
||||
"home.bullet.a.description": "轻松使用私有服务器搭建文件共享平台",
|
||||
"home.bullet.b.name": "完全隐私",
|
||||
"home.bullet.b.description": "你的文件只属于你!不要将它放到第三方文件平台",
|
||||
"home.bullet.c.name": "完全无限",
|
||||
"home.bullet.c.description": "想上传多大都可以,更需要担心的是你的存储卷容量",
|
||||
"home.button.start": "开始使用",
|
||||
"home.button.source": "源代码",
|
||||
// END /
|
||||
// /auth/signin
|
||||
"signin.title": "欢迎回来",
|
||||
"signin.description": "还没有账户吗?请",
|
||||
"signin.button.signup": "注册",
|
||||
"signin.input.email-or-username": "电子邮件或用户名",
|
||||
"signin.input.email-or-username.placeholder": "请输入电子邮件或用户名",
|
||||
"signin.input.password": "密码",
|
||||
"signin.input.password.placeholder": "请输入密码",
|
||||
"signin.button.submit": "登录",
|
||||
"signIn.notify.totp-required.title": "请继续两步验证",
|
||||
"signIn.notify.totp-required.description": "请输入一次性验证码",
|
||||
// END /auth/signin
|
||||
// /auth/signup
|
||||
"signup.title": "创建账户",
|
||||
"signup.description": "已经有账户了?请",
|
||||
"signup.button.signin": "登录",
|
||||
"signup.input.username": "用户名",
|
||||
"signup.input.username.placeholder": "请输入用户名",
|
||||
"signup.input.email": "电子邮件",
|
||||
"signup.input.email.placeholder": "请输入电子邮件",
|
||||
"signup.button.submit": "注册",
|
||||
// END /auth/signup
|
||||
// /auth/reset-password
|
||||
"resetPassword.title": "忘记密码?",
|
||||
"resetPassword.description": "请输入电子邮件接受重置密码邮件",
|
||||
"resetPassword.notify.success": "一封包含密码重置地址的邮件已发送到你的邮箱中",
|
||||
"resetPassword.button.back": "返回登录页面",
|
||||
"resetPassword.text.resetPassword": "重置密码",
|
||||
"resetPassword.text.enterNewPassword": "请输入新密码",
|
||||
"resetPassword.input.password": "新密码",
|
||||
"resetPassword.notify.passwordReset": "密码重置成功!",
|
||||
// /account
|
||||
"account.title": "我的账户",
|
||||
"account.card.info.title": "账户信息",
|
||||
"account.card.info.username": "用户名",
|
||||
"account.card.info.email": "电子邮件",
|
||||
"account.notify.info.success": "账户信息更新成功!",
|
||||
"account.card.password.title": "密码",
|
||||
"account.card.password.old": "旧密码",
|
||||
"account.card.password.new": "新密码",
|
||||
"account.notify.password.success": "密码更改成功!",
|
||||
"account.card.security.title": "安全",
|
||||
"account.card.security.totp.enable.description": "请输入当前密码开启两步验证",
|
||||
"account.card.security.totp.disable.description": "请输入当前密码关闭两步验证",
|
||||
"account.card.security.totp.button.start": "开启",
|
||||
"account.modal.totp.title": "开启两步验证",
|
||||
"account.modal.totp.step1": "第一步:添加验证器 Authenticator",
|
||||
"account.modal.totp.step2": "第二步:输入一次性验证码",
|
||||
"account.modal.totp.enterManually": "手动输入",
|
||||
"account.modal.totp.code": "验证码",
|
||||
"account.modal.totp.clickToCopy": "点击复制",
|
||||
"account.modal.totp.verify": "确定",
|
||||
"account.notify.totp.disable": "成功关闭两步验证!",
|
||||
"account.notify.totp.enable": "成功开启两步验证!",
|
||||
"account.card.language.title": "语言",
|
||||
"account.card.language.description": "The project is translated by the community. Some languages might be incomplete.",
|
||||
"account.card.color.title": "颜色外观",
|
||||
// ThemeSwitcher.tsx
|
||||
"account.theme.dark": "暗黑模式",
|
||||
"account.theme.light": "明亮模式",
|
||||
"account.theme.system": "跟随系统",
|
||||
"account.button.delete": "删除账户",
|
||||
"account.modal.delete.title": "删除账户",
|
||||
"account.modal.delete.description": "你真的想删除你的账户,并删除所有的共享吗?",
|
||||
// END /account
|
||||
// /account/shares
|
||||
"account.shares.title": "我的共享",
|
||||
"account.shares.title.empty": "这里空空如也 👀",
|
||||
"account.shares.description.empty": "你没有创建任何共享",
|
||||
"account.shares.button.create": "创建共享",
|
||||
"account.shares.info.title": "共享信息",
|
||||
"account.shares.table.id": "ID",
|
||||
"account.shares.table.name": "共享后缀",
|
||||
"account.shares.table.description": "描述",
|
||||
"account.shares.table.visitors": "已访问",
|
||||
"account.shares.table.expiresAt": "过期时间",
|
||||
"account.shares.table.createdAt": "创建时间",
|
||||
"account.shares.table.size": "文件大小",
|
||||
"account.shares.modal.share-informations": "共享信息",
|
||||
"account.shares.modal.share-link": "共享链接",
|
||||
"account.shares.modal.delete.title": "删除 {share}",
|
||||
"account.shares.modal.delete.description": "你真的想删除这个共享吗?",
|
||||
// END /account/shares
|
||||
// /account/reverseShares
|
||||
"account.reverseShares.title": "预留共享",
|
||||
"account.reverseShares.description": "预留共享允许你创建一个特定共享链接,以便外部用户以此创建共享",
|
||||
"account.reverseShares.title.empty": "这里空空如也 👀",
|
||||
"account.reverseShares.description.empty": "你没有创建任何预留共享",
|
||||
// showCreateReverseShareModal.tsx
|
||||
"account.reverseShares.modal.expiration.label": "过期时间",
|
||||
"account.reverseShares.modal.expiration.minute-singular": "1 分钟",
|
||||
"account.reverseShares.modal.expiration.minute-plural": "分钟",
|
||||
"account.reverseShares.modal.expiration.hour-singular": "1 小时",
|
||||
"account.reverseShares.modal.expiration.hour-plural": "小时",
|
||||
"account.reverseShares.modal.expiration.day-singular": "1 天",
|
||||
"account.reverseShares.modal.expiration.day-plural": "天",
|
||||
"account.reverseShares.modal.expiration.week-singular": "1 周",
|
||||
"account.reverseShares.modal.expiration.week-plural": "周",
|
||||
"account.reverseShares.modal.expiration.month-singular": "1 月",
|
||||
"account.reverseShares.modal.expiration.month-plural": "月",
|
||||
"account.reverseShares.modal.expiration.year-singular": "1 年",
|
||||
"account.reverseShares.modal.expiration.year-plural": "年",
|
||||
"account.reverseShares.modal.max-size.label": "共享文件上限",
|
||||
"account.reverseShares.modal.send-email": "发送邮件提醒",
|
||||
"account.reverseShares.modal.send-email.description": "当这个预留共享链接被用于共享时,发送邮件提醒",
|
||||
"account.reverseShares.modal.max-use.label": "最大使用次数",
|
||||
"account.reverseShares.modal.max-use.description": "这个预留共享链接可被用于创建共享的最大使用次数",
|
||||
"account.reverseShare.never-expires": "这个预留共享永不过期",
|
||||
"account.reverseShare.expires-on": "这个预留共享将过期于 {expiration}",
|
||||
"account.reverseShares.table.no-shares": "当前没有创建任何共享",
|
||||
"account.reverseShares.table.count.singular": "共享",
|
||||
"account.reverseShares.table.count.plural": "共享",
|
||||
"account.reverseShares.table.shares": "共享",
|
||||
"account.reverseShares.table.remaining": "剩余使用次数",
|
||||
"account.reverseShares.table.max-size": "共享文件上限",
|
||||
"account.reverseShares.table.expires": "过期时间",
|
||||
"account.reverseShares.modal.reverse-share-link": "预留共享链接",
|
||||
"account.reverseShares.modal.delete.title": "删除预留共享链接",
|
||||
"account.reverseShares.modal.delete.description": "你真的想删除此预留共享链接吗?链接下所有关联的共享都将被删除",
|
||||
// END /account/reverseShares
|
||||
// /admin
|
||||
"admin.title": "管理",
|
||||
"admin.button.users": "用户管理",
|
||||
"admin.button.config": "配置管理",
|
||||
"admin.version": "版本",
|
||||
// END /admin
|
||||
// /admin/users
|
||||
"admin.users.title": "用户管理",
|
||||
"admin.users.table.username": "用户名",
|
||||
"admin.users.table.email": "电子邮件",
|
||||
"admin.users.table.admin": "管理员",
|
||||
"admin.users.edit.update.title": "更新用户 {username}",
|
||||
"admin.users.edit.update.admin-privileges": "管理员",
|
||||
"admin.users.edit.update.change-password.title": "更改密码",
|
||||
"admin.users.edit.update.change-password.field": "新密码",
|
||||
"admin.users.edit.update.change-password.button": "保存新密码",
|
||||
"admin.users.edit.update.notify.password.success": "密码更新成功!",
|
||||
"admin.users.edit.delete.title": "删除用户 {username}",
|
||||
"admin.users.edit.delete.description": "你真的想删除这个账户,并删除该用户所有的共享吗?",
|
||||
// showCreateUserModal.tsx
|
||||
"admin.users.modal.create.title": "创建用户",
|
||||
"admin.users.modal.create.username": "用户名",
|
||||
"admin.users.modal.create.email": "电子邮件",
|
||||
"admin.users.modal.create.password": "密码",
|
||||
"admin.users.modal.create.manual-password": "手动设置密码",
|
||||
"admin.users.modal.create.manual-password.description": "如果不勾选,用户将会收到一封电子邮件来设置他们的密码",
|
||||
"admin.users.modal.create.admin": "管理员",
|
||||
"admin.users.modal.create.admin.description": "如果勾选,用户将能访问管理员面板",
|
||||
// END /admin/users
|
||||
// /upload
|
||||
"upload.title": "上传",
|
||||
"upload.notify.generic-error": "创建共享的过程中发生了错误",
|
||||
"upload.notify.count-failed": "{count} 文件上传失败,请重试",
|
||||
// Dropzone.tsx
|
||||
"upload.dropzone.title": "上传文件",
|
||||
"upload.dropzone.description": "拖放至此以上传文件,文件大小不能超过上限 {maxSize}",
|
||||
"upload.dropzone.notify.file-too-big": "你的文件超过了最大上传限制 {maxSize}",
|
||||
// FileList.tsx
|
||||
"upload.filelist.name": "文件名",
|
||||
"upload.filelist.size": "文件大小",
|
||||
// showCreateUploadModal.tsx
|
||||
"upload.modal.title": "创建共享",
|
||||
"upload.modal.link.error.invalid": "只能包括字母,数字,下划线(_),和横线(-)",
|
||||
"upload.modal.link.error.taken": "这个链接已经存在了",
|
||||
"upload.modal.not-signed-in": "当前没有登录",
|
||||
"upload.modal.not-signed-in-description": "你将不能删除你的共享或查看访问次数",
|
||||
"upload.modal.expires.never": "永不",
|
||||
"upload.modal.expires.never-long": "永不过期",
|
||||
"upload.modal.link.label": "共享链接",
|
||||
"upload.modal.expires.label": "过期时间",
|
||||
"upload.modal.expires.minute-singular": "1 分钟",
|
||||
"upload.modal.expires.minute-plural": "分钟",
|
||||
"upload.modal.expires.hour-singular": "1 小时",
|
||||
"upload.modal.expires.hour-plural": "小时",
|
||||
"upload.modal.expires.day-singular": "1 天",
|
||||
"upload.modal.expires.day-plural": "天",
|
||||
"upload.modal.expires.week-singular": "1 周",
|
||||
"upload.modal.expires.week-plural": "周",
|
||||
"upload.modal.expires.month-singular": "1 月",
|
||||
"upload.modal.expires.month-plural": "月",
|
||||
"upload.modal.expires.year-singular": "1 年",
|
||||
"upload.modal.expires.year-plural": "年",
|
||||
"upload.modal.accordion.description.title": "描述",
|
||||
"upload.modal.accordion.description.placeholder": "共享文件备注信息",
|
||||
"upload.modal.accordion.email.title": "邮件提醒",
|
||||
"upload.modal.accordion.email.placeholder": "收件人电子邮件地址",
|
||||
"upload.modal.accordion.email.invalid-email": "邮件地址不可用",
|
||||
"upload.modal.accordion.security.title": "安全选项",
|
||||
"upload.modal.accordion.security.password.label": "密码保护",
|
||||
"upload.modal.accordion.security.password.placeholder": "无密码",
|
||||
"upload.modal.accordion.security.max-views.label": "最大访问次数",
|
||||
"upload.modal.accordion.security.max-views.placeholder": "无限",
|
||||
// showCompletedUploadModal.tsx
|
||||
"upload.modal.completed.never-expires": "这个共享永不过期",
|
||||
"upload.modal.completed.expires-on": "这个共享将过期于 {expiration}.",
|
||||
"upload.modal.completed.share-ready": "共享创建完毕",
|
||||
// END /upload
|
||||
// /share/[id]
|
||||
"share.title": "共享 {shareId}",
|
||||
"share.description": "瞧瞧我给你共享了些什么!",
|
||||
"share.error.visitor-limit-exceeded.title": "访问次数达到上限",
|
||||
"share.error.visitor-limit-exceeded.description": "访问次数达到上限",
|
||||
"share.error.removed.title": "共享已删除",
|
||||
"share.error.not-found.title": "共享未找到",
|
||||
"share.error.not-found.description": "共享文件走丢了",
|
||||
"share.modal.password.title": "需要密码",
|
||||
"share.modal.password.description": "请输入密码来访问此共享",
|
||||
"share.modal.password": "密码",
|
||||
"share.modal.error.invalid-password": "密码错误",
|
||||
"share.button.download-all": "全部下载",
|
||||
"share.notify.download-all-preparing": "该共享正在处理中,请稍等片刻",
|
||||
"share.modal.file-link": "文件链接",
|
||||
"share.table.name": "文件名",
|
||||
"share.table.size": "文件大小",
|
||||
"share.modal.file-preview.error.not-supported.title": "该文件类型不支持预览",
|
||||
"share.modal.file-preview.error.not-supported.description": "该文件类型不支持预览,请下载后打开查看",
|
||||
// END /share/[id]
|
||||
// /admin/config
|
||||
"admin.config.title": "配置管理",
|
||||
"admin.config.category.general": "通用",
|
||||
"admin.config.category.share": "共享",
|
||||
"admin.config.category.email": "电子邮件",
|
||||
"admin.config.category.smtp": "SMTP",
|
||||
"admin.config.general.app-name": "App 名称",
|
||||
"admin.config.general.app-name.description": "这个 App 的名称",
|
||||
"admin.config.general.app-url": "App 的地址",
|
||||
"admin.config.general.app-url.description": "Pingvin Share 的 URL 地址",
|
||||
"admin.config.general.show-home-page": "显示首页",
|
||||
"admin.config.general.show-home-page.description": "是否显示首页",
|
||||
"admin.config.general.logo": "Logo",
|
||||
"admin.config.general.logo.description": "上传个性化 Logo,图片必须是长宽比 1:1 的 PNG 格式",
|
||||
"admin.config.general.logo.placeholder": "选择图片",
|
||||
"admin.config.email.enable-share-email-recipients": "开启共享邮件通知",
|
||||
"admin.config.email.enable-share-email-recipients.description": "是否允许通过邮件通知发送共享信息,只有设置了 SMTP 后该选项才有效",
|
||||
"admin.config.email.share-recipients-subject": "共享邮件通知主题",
|
||||
"admin.config.email.share-recipients-subject.description": "发送共享邮件通知的邮件主题",
|
||||
"admin.config.email.share-recipients-message": "共享邮件通知内容",
|
||||
"admin.config.email.share-recipients-message.description": "发送到接收者的共享邮件通知具体内容。可选的变量有:\n {creator} - 共享创建者\n {shareUrl} - 共享链接\n {desc} - 共享描述\n {expires} - 共享过期时间\n 这些变量会被实际的值所替代",
|
||||
"admin.config.email.reverse-share-subject": "预留共享邮件通知主题",
|
||||
"admin.config.email.reverse-share-subject.description": "当有人使用了你的预留共享链接时,发送的预留共享邮件通知主题",
|
||||
"admin.config.email.reverse-share-message": "预留共享邮件通知内容",
|
||||
"admin.config.email.reverse-share-message.description": "当有人使用了你的预留共享链接时,发送的通知内容。{shareUrl} 会被创建者的用户名和共享链接代替",
|
||||
"admin.config.email.reset-password-subject": "重置密码邮件通知主题",
|
||||
"admin.config.email.reset-password-subject.description": "当用户发起重置密码时,发送的重置密码邮件通知主题",
|
||||
"admin.config.email.reset-password-message": "重置密码邮件通知内容",
|
||||
"admin.config.email.reset-password-message.description": "当用户发起重置密码时,重置密码邮件通知内容。{url} 会被重置密码链接代替",
|
||||
"admin.config.email.invite-subject": "邀请邮件通知主题",
|
||||
"admin.config.email.invite-subject.description": "当管理员邀请用户时,发送的邀请邮件通知主题",
|
||||
"admin.config.email.invite-message": "邀请邮件通知内容",
|
||||
"admin.config.email.invite-message.description": "当管理员邀请用户时,发送的邀请邮件通知内容。{url} 会被邀请链接代替,{password} 会被密码代替",
|
||||
"admin.config.share.allow-registration": "允许注册",
|
||||
"admin.config.share.allow-registration.description": "是否允许注册",
|
||||
"admin.config.share.allow-unauthenticated-shares": "是否允许未验证的共享",
|
||||
"admin.config.share.allow-unauthenticated-shares.description": "是否允许未验证的用户创建共享",
|
||||
"admin.config.share.max-size": "最大文件上限",
|
||||
"admin.config.share.max-size.description": "最大文件上限,单位 bytes (1GB=1024MB=1048576KB=1073741824bytes)",
|
||||
"admin.config.share.zip-compression-level": "Zip compression level",
|
||||
"admin.config.share.zip-compression-level.description": "Adjust the level to balance between file size and compression speed. Valid values range from 0 to 9, with 0 being no compression and 9 being maximum compression. ",
|
||||
"admin.config.smtp.enabled": "启用",
|
||||
"admin.config.smtp.enabled.description": "是否开启 SMTP,仅当输入主机名、端口、发送邮箱、用户名和密码后开启",
|
||||
"admin.config.smtp.host": "主机名",
|
||||
"admin.config.smtp.host.description": "SMTP 主机名",
|
||||
"admin.config.smtp.port": "端口",
|
||||
"admin.config.smtp.port.description": "SMTP 主机端口",
|
||||
"admin.config.smtp.email": "发送邮箱",
|
||||
"admin.config.smtp.email.description": "发送邮箱地址",
|
||||
"admin.config.smtp.username": "用户名",
|
||||
"admin.config.smtp.username.description": "SMTP 主机用户名",
|
||||
"admin.config.smtp.password": "密码",
|
||||
"admin.config.smtp.password.description": "SMTP 主机密码",
|
||||
"admin.config.smtp.button.test": "发送测试邮件",
|
||||
// 404
|
||||
"404.description": "当前的页面走丢啦",
|
||||
"404.button.home": "返回主页",
|
||||
// Common translations
|
||||
"common.button.save": "保存",
|
||||
"common.button.create": "创建",
|
||||
"common.button.submit": "提交",
|
||||
"common.button.delete": "删除",
|
||||
"common.button.cancel": "取消",
|
||||
"common.button.confirm": "确认",
|
||||
"common.button.disable": "关闭",
|
||||
"common.button.share": "共享",
|
||||
"common.button.generate": "生成",
|
||||
"common.button.done": "完成",
|
||||
"common.text.link": "链接",
|
||||
"common.text.or": "或",
|
||||
"common.button.go-back": "返回",
|
||||
"common.notify.copied": "已复制到剪贴板",
|
||||
"common.success": "Success",
|
||||
"common.error": "错误",
|
||||
"common.error.unknown": "发生未知错误",
|
||||
"common.error.invalid-email": "邮件地址不可用",
|
||||
"common.error.too-short": "必须不少于 {length} 个字符",
|
||||
"common.error.too-long": "必须不超过 {length} 个字符",
|
||||
"common.error.exact-length": "必须为 {length} 个字符",
|
||||
"common.error.invalid-number": "必须为数字",
|
||||
"common.error.field-required": "必填项"
|
||||
};
|
||||
@@ -14,7 +14,7 @@ export const config = {
|
||||
export async function middleware(request: NextRequest) {
|
||||
const routes = {
|
||||
unauthenticated: new Routes(["/auth/*", "/"]),
|
||||
public: new Routes(["/share/*", "/upload/*"]),
|
||||
public: new Routes(["/share/*", "/s/*", "/upload/*"]),
|
||||
admin: new Routes(["/admin/*"]),
|
||||
account: new Routes(["/account*"]),
|
||||
disabled: new Routes([]),
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "@mantine/core";
|
||||
import Link from "next/link";
|
||||
import Meta from "../components/Meta";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
root: {
|
||||
@@ -42,9 +43,11 @@ const ErrorNotFound = () => {
|
||||
<>
|
||||
<Meta title="Not found" />
|
||||
<Container className={classes.root}>
|
||||
<div className={classes.label}>404</div>
|
||||
<div className={classes.label}>
|
||||
404
|
||||
</div>
|
||||
<Title align="center" order={3}>
|
||||
Oops this page doesn't exist.
|
||||
<FormattedMessage id="404.description" />
|
||||
</Title>
|
||||
<Text
|
||||
color="dimmed"
|
||||
@@ -53,7 +56,7 @@ const ErrorNotFound = () => {
|
||||
></Text>
|
||||
<Group position="center">
|
||||
<Button component={Link} href="/" variant="light">
|
||||
Bring me back
|
||||
<FormattedMessage id="404.button.home" />
|
||||
</Button>
|
||||
</Group>
|
||||
</Container>
|
||||
|
||||
@@ -13,11 +13,12 @@ import { GetServerSidePropsContext } from "next";
|
||||
import type { AppProps } from "next/app";
|
||||
import getConfig from "next/config";
|
||||
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 { ConfigContext } from "../hooks/config.hook";
|
||||
import usePreferences from "../hooks/usePreferences";
|
||||
import { UserContext } from "../hooks/user.hook";
|
||||
import { LOCALES } from "../i18n/locales";
|
||||
import authService from "../services/auth.service";
|
||||
import configService from "../services/config.service";
|
||||
import userService from "../services/user.service";
|
||||
@@ -25,6 +26,8 @@ import GlobalStyle from "../styles/global.style";
|
||||
import globalStyle from "../styles/mantine.style";
|
||||
import Config from "../types/config.type";
|
||||
import { CurrentUser } from "../types/user.type";
|
||||
import i18nUtil from "../utils/i18n.util";
|
||||
import userPreferences from "../utils/userPreferences.util";
|
||||
|
||||
const excludeDefaultLayoutRoutes = ["/admin/config/[category]"];
|
||||
|
||||
@@ -33,7 +36,6 @@ function App({ Component, pageProps }: AppProps) {
|
||||
const router = useRouter();
|
||||
|
||||
const [colorScheme, setColorScheme] = useState<ColorScheme>(systemTheme);
|
||||
const preferences = usePreferences();
|
||||
|
||||
const [user, setUser] = useState<CurrentUser | null>(pageProps.user);
|
||||
const [route, setRoute] = useState<string>(pageProps.route);
|
||||
@@ -50,11 +52,20 @@ function App({ Component, pageProps }: AppProps) {
|
||||
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(() => {
|
||||
const colorScheme =
|
||||
preferences.get("colorScheme") == "system"
|
||||
userPreferences.get("colorScheme") == "system"
|
||||
? systemTheme
|
||||
: preferences.get("colorScheme");
|
||||
: userPreferences.get("colorScheme");
|
||||
|
||||
toggleColorScheme(colorScheme);
|
||||
}, [systemTheme]);
|
||||
@@ -66,52 +77,60 @@ function App({ Component, pageProps }: AppProps) {
|
||||
});
|
||||
};
|
||||
|
||||
const language = useRef(pageProps.language);
|
||||
|
||||
return (
|
||||
<MantineProvider
|
||||
withGlobalStyles
|
||||
withNormalizeCSS
|
||||
theme={{ colorScheme, ...globalStyle }}
|
||||
<IntlProvider
|
||||
messages={i18nUtil.getLocaleByCode(language.current)?.messages}
|
||||
locale={language.current}
|
||||
defaultLocale={LOCALES.ENGLISH.code}
|
||||
>
|
||||
<ColorSchemeProvider
|
||||
colorScheme={colorScheme}
|
||||
toggleColorScheme={toggleColorScheme}
|
||||
<MantineProvider
|
||||
withGlobalStyles
|
||||
withNormalizeCSS
|
||||
theme={{ colorScheme, ...globalStyle }}
|
||||
>
|
||||
<GlobalStyle />
|
||||
<Notifications />
|
||||
<ModalsProvider>
|
||||
<ConfigContext.Provider
|
||||
value={{
|
||||
configVariables,
|
||||
refresh: async () => {
|
||||
setConfigVariables(await configService.list());
|
||||
},
|
||||
}}
|
||||
>
|
||||
<UserContext.Provider
|
||||
<ColorSchemeProvider
|
||||
colorScheme={colorScheme}
|
||||
toggleColorScheme={toggleColorScheme}
|
||||
>
|
||||
<GlobalStyle />
|
||||
<Notifications />
|
||||
<ModalsProvider>
|
||||
<ConfigContext.Provider
|
||||
value={{
|
||||
user,
|
||||
refreshUser: async () => {
|
||||
const user = await userService.getCurrentUser();
|
||||
setUser(user);
|
||||
return user;
|
||||
configVariables,
|
||||
refresh: async () => {
|
||||
setConfigVariables(await configService.list());
|
||||
},
|
||||
}}
|
||||
>
|
||||
{excludeDefaultLayoutRoutes.includes(route) ? (
|
||||
<Component {...pageProps} />
|
||||
) : (
|
||||
<>
|
||||
<Header />
|
||||
<Container>
|
||||
<Component {...pageProps} />
|
||||
</Container>
|
||||
</>
|
||||
)}
|
||||
</UserContext.Provider>
|
||||
</ConfigContext.Provider>
|
||||
</ModalsProvider>
|
||||
</ColorSchemeProvider>
|
||||
</MantineProvider>
|
||||
<UserContext.Provider
|
||||
value={{
|
||||
user,
|
||||
refreshUser: async () => {
|
||||
const user = await userService.getCurrentUser();
|
||||
setUser(user);
|
||||
return user;
|
||||
},
|
||||
}}
|
||||
>
|
||||
{excludeDefaultLayoutRoutes.includes(route) ? (
|
||||
<Component {...pageProps} />
|
||||
) : (
|
||||
<>
|
||||
<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[];
|
||||
route?: string;
|
||||
colorScheme: ColorScheme;
|
||||
language?: string;
|
||||
} = {
|
||||
route: ctx.resolvedUrl,
|
||||
colorScheme:
|
||||
(getCookie("mantine-color-scheme", ctx) as ColorScheme) ?? "light",
|
||||
};
|
||||
|
||||
if (ctx.req) {
|
||||
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.route = ctx.req.url;
|
||||
}
|
||||
|
||||
const requestLanguage = i18nUtil.getLanguageFromAcceptHeader(
|
||||
ctx.req.headers["accept-language"]
|
||||
);
|
||||
|
||||
pageProps.language = ctx.req.cookies["language"] ?? requestLanguage;
|
||||
}
|
||||
return { pageProps };
|
||||
};
|
||||
|
||||
|
||||
@@ -14,10 +14,13 @@ import {
|
||||
import { useForm, yupResolver } from "@mantine/form";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import { Tb2Fa } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import * as yup from "yup";
|
||||
import showEnableTotpModal from "../../components/account/showEnableTotpModal";
|
||||
import ThemeSwitcher from "../../components/account/ThemeSwitcher";
|
||||
import Meta from "../../components/Meta";
|
||||
import LanguagePicker from "../../components/account/LanguagePicker";
|
||||
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 authService from "../../services/auth.service";
|
||||
import userService from "../../services/user.service";
|
||||
@@ -26,6 +29,7 @@ import toast from "../../utils/toast.util";
|
||||
const Account = () => {
|
||||
const { user, refreshUser } = useUser();
|
||||
const modals = useModals();
|
||||
const t = useTranslate();
|
||||
|
||||
const accountForm = useForm({
|
||||
initialValues: {
|
||||
@@ -34,8 +38,10 @@ const Account = () => {
|
||||
},
|
||||
validate: yupResolver(
|
||||
yup.object().shape({
|
||||
email: yup.string().email(),
|
||||
username: yup.string().min(3),
|
||||
email: yup.string().email(t("common.error.invalid-email")),
|
||||
username: yup
|
||||
.string()
|
||||
.min(3, t("common.error.too-short", { length: 3 })),
|
||||
})
|
||||
),
|
||||
});
|
||||
@@ -47,8 +53,14 @@ const Account = () => {
|
||||
},
|
||||
validate: yupResolver(
|
||||
yup.object().shape({
|
||||
oldPassword: yup.string().min(8),
|
||||
password: yup.string().min(8),
|
||||
oldPassword: yup
|
||||
.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(
|
||||
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),
|
||||
code: yup
|
||||
.string()
|
||||
.min(6)
|
||||
.max(6)
|
||||
.matches(/^[0-9]+$/, { message: "Code must be a number" }),
|
||||
.min(6, t("common.error.exact-length", { length: 6 }))
|
||||
.max(6, t("common.error.exact-length", { length: 6 }))
|
||||
.matches(/^[0-9]+$/, { message: t("common.error.invalid-number") }),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title="My account" />
|
||||
<Meta title={t("account.title")} />
|
||||
<Container size="sm">
|
||||
<Title order={3} mb="xs">
|
||||
My account
|
||||
<FormattedMessage id="account.title" />
|
||||
</Title>
|
||||
<Paper withBorder p="xl">
|
||||
<Title order={5} mb="xs">
|
||||
Account Info
|
||||
<FormattedMessage id="account.card.info.title" />
|
||||
</Title>
|
||||
<form
|
||||
onSubmit={accountForm.onSubmit((values) =>
|
||||
@@ -99,35 +114,37 @@ const Account = () => {
|
||||
username: values.username,
|
||||
email: values.email,
|
||||
})
|
||||
.then(() => toast.success("User updated successfully"))
|
||||
.then(() => toast.success(t("account.notify.info.success")))
|
||||
.catch(toast.axiosError)
|
||||
)}
|
||||
>
|
||||
<Stack>
|
||||
<TextInput
|
||||
label="Username"
|
||||
label={t("account.card.info.username")}
|
||||
{...accountForm.getInputProps("username")}
|
||||
/>
|
||||
<TextInput
|
||||
label="Email"
|
||||
label={t("account.card.info.email")}
|
||||
{...accountForm.getInputProps("email")}
|
||||
/>
|
||||
<Group position="right">
|
||||
<Button type="submit">Save</Button>
|
||||
<Button type="submit">
|
||||
<FormattedMessage id="common.button.save" />
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
</Paper>
|
||||
<Paper withBorder p="xl" mt="lg">
|
||||
<Title order={5} mb="xs">
|
||||
Password
|
||||
<FormattedMessage id="account.card.password.title" />
|
||||
</Title>
|
||||
<form
|
||||
onSubmit={passwordForm.onSubmit((values) =>
|
||||
authService
|
||||
.updatePassword(values.oldPassword, values.password)
|
||||
.then(() => {
|
||||
toast.success("Password updated successfully");
|
||||
toast.success(t("account.notify.password.success"));
|
||||
passwordForm.reset();
|
||||
})
|
||||
.catch(toast.axiosError)
|
||||
@@ -135,15 +152,17 @@ const Account = () => {
|
||||
>
|
||||
<Stack>
|
||||
<PasswordInput
|
||||
label="Old password"
|
||||
label={t("account.card.password.old")}
|
||||
{...passwordForm.getInputProps("oldPassword")}
|
||||
/>
|
||||
<PasswordInput
|
||||
label="New password"
|
||||
label={t("account.card.password.new")}
|
||||
{...passwordForm.getInputProps("password")}
|
||||
/>
|
||||
<Group position="right">
|
||||
<Button type="submit">Save</Button>
|
||||
<Button type="submit">
|
||||
<FormattedMessage id="common.button.save" />
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
@@ -151,7 +170,7 @@ const Account = () => {
|
||||
|
||||
<Paper withBorder p="xl" mt="lg">
|
||||
<Title order={5} mb="xs">
|
||||
Security
|
||||
<FormattedMessage id="account.card.security.title" />
|
||||
</Title>
|
||||
|
||||
<Tabs defaultValue="totp">
|
||||
@@ -162,14 +181,14 @@ const Account = () => {
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Panel value="totp" pt="xs">
|
||||
{user!.totpVerified ? (
|
||||
{user?.totpVerified ? (
|
||||
<>
|
||||
<form
|
||||
onSubmit={disableTotpForm.onSubmit((values) => {
|
||||
authService
|
||||
.disableTOTP(values.code, values.password)
|
||||
.then(() => {
|
||||
toast.success("Successfully disabled TOTP");
|
||||
toast.success(t("account.notify.totp.disable"));
|
||||
values.password = "";
|
||||
values.code = "";
|
||||
refreshUser();
|
||||
@@ -179,21 +198,23 @@ const Account = () => {
|
||||
>
|
||||
<Stack>
|
||||
<PasswordInput
|
||||
description="Enter your current password to disable TOTP"
|
||||
label="Password"
|
||||
description={t(
|
||||
"account.card.security.totp.disable.description"
|
||||
)}
|
||||
label={t("account.card.password.title")}
|
||||
{...disableTotpForm.getInputProps("password")}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
variant="filled"
|
||||
label="Code"
|
||||
label={t("account.modal.totp.code")}
|
||||
placeholder="******"
|
||||
{...disableTotpForm.getInputProps("code")}
|
||||
/>
|
||||
|
||||
<Group position="right">
|
||||
<Button color="red" type="submit">
|
||||
Disable
|
||||
<FormattedMessage id="common.button.disable" />
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
@@ -218,12 +239,16 @@ const Account = () => {
|
||||
>
|
||||
<Stack>
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
description="Enter your current password to start enabling TOTP"
|
||||
label={t("account.card.password.title")}
|
||||
description={t(
|
||||
"account.card.security.totp.enable.description"
|
||||
)}
|
||||
{...enableTotpForm.getInputProps("password")}
|
||||
/>
|
||||
<Group position="right">
|
||||
<Button type="submit">Start</Button>
|
||||
<Button type="submit">
|
||||
<FormattedMessage id="account.card.security.totp.button.start" />
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
@@ -234,7 +259,13 @@ const Account = () => {
|
||||
</Paper>
|
||||
<Paper withBorder p="xl" mt="lg">
|
||||
<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>
|
||||
<ThemeSwitcher />
|
||||
</Paper>
|
||||
@@ -245,15 +276,17 @@ const Account = () => {
|
||||
color="red"
|
||||
onClick={() =>
|
||||
modals.openConfirmModal({
|
||||
title: "Account deletion",
|
||||
title: t("account.modal.delete.title"),
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Do you really want to delete your account including all
|
||||
your active shares?
|
||||
<FormattedMessage id="account.modal.delete.description" />
|
||||
</Text>
|
||||
),
|
||||
|
||||
labels: { confirm: "Delete", cancel: "Cancel" },
|
||||
labels: {
|
||||
confirm: t("common.button.delete"),
|
||||
cancel: t("common.button.cancel"),
|
||||
},
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: async () => {
|
||||
await userService.removeCurrentUser();
|
||||
@@ -262,7 +295,7 @@ const Account = () => {
|
||||
})
|
||||
}
|
||||
>
|
||||
Delete Account
|
||||
<FormattedMessage id="account.button.delete" />
|
||||
</Button>
|
||||
</Stack>
|
||||
</Center>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
Accordion,
|
||||
ActionIcon,
|
||||
Anchor,
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
@@ -16,11 +17,14 @@ import { useModals } from "@mantine/modals";
|
||||
import moment from "moment";
|
||||
import { useEffect, useState } from "react";
|
||||
import { TbInfoCircle, TbLink, TbPlus, TbTrash } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import Meta from "../../components/Meta";
|
||||
import showReverseShareLinkModal from "../../components/account/showReverseShareLinkModal";
|
||||
import showShareLinkModal from "../../components/account/showShareLinkModal";
|
||||
import CenterLoader from "../../components/core/CenterLoader";
|
||||
import Meta from "../../components/Meta";
|
||||
import showCreateReverseShareModal from "../../components/share/modals/showCreateReverseShareModal";
|
||||
import useConfig from "../../hooks/config.hook";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
import shareService from "../../services/share.service";
|
||||
import { MyReverseShare } from "../../types/share.type";
|
||||
import { byteToHumanSizeString } from "../../utils/fileSize.util";
|
||||
@@ -29,9 +33,12 @@ import toast from "../../utils/toast.util";
|
||||
const MyShares = () => {
|
||||
const modals = useModals();
|
||||
const clipboard = useClipboard();
|
||||
const t = useTranslate();
|
||||
|
||||
const config = useConfig();
|
||||
|
||||
const appUrl = config.get("general.appUrl");
|
||||
|
||||
const [reverseShares, setReverseShares] = useState<MyReverseShare[]>();
|
||||
|
||||
const getReverseShares = () => {
|
||||
@@ -47,15 +54,17 @@ const MyShares = () => {
|
||||
if (!reverseShares) return <CenterLoader />;
|
||||
return (
|
||||
<>
|
||||
<Meta title="My shares" />
|
||||
<Meta title={t("account.reverseShares.title")} />
|
||||
<Group position="apart" align="baseline" mb={20}>
|
||||
<Group align="center" spacing={3} mb={30}>
|
||||
<Title order={3}>My reverse shares</Title>
|
||||
<Title order={3}>
|
||||
<FormattedMessage id="account.reverseShares.title" />
|
||||
</Title>
|
||||
<Tooltip
|
||||
position="bottom"
|
||||
multiline
|
||||
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 }}
|
||||
>
|
||||
<ActionIcon>
|
||||
@@ -73,14 +82,18 @@ const MyShares = () => {
|
||||
}
|
||||
leftIcon={<TbPlus size={20} />}
|
||||
>
|
||||
Create
|
||||
<FormattedMessage id="common.button.create" />
|
||||
</Button>
|
||||
</Group>
|
||||
{reverseShares.length == 0 ? (
|
||||
<Center style={{ height: "70vh" }}>
|
||||
<Stack align="center" spacing={10}>
|
||||
<Title order={3}>It's empty here 👀</Title>
|
||||
<Text>You don't have any reverse shares.</Text>
|
||||
<Title order={3}>
|
||||
<FormattedMessage id="account.reverseShares.title.empty" />
|
||||
</Title>
|
||||
<Text>
|
||||
<FormattedMessage id="account.reverseShares.description.empty" />
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
) : (
|
||||
@@ -88,10 +101,18 @@ const MyShares = () => {
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Shares</th>
|
||||
<th>Remaining uses</th>
|
||||
<th>Max share size</th>
|
||||
<th>Expires at</th>
|
||||
<th>
|
||||
<FormattedMessage id="account.reverseShares.table.shares" />
|
||||
</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>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -101,7 +122,7 @@ const MyShares = () => {
|
||||
<td style={{ width: 220 }}>
|
||||
{reverseShare.shares.length == 0 ? (
|
||||
<Text color="dimmed" size="sm">
|
||||
No shares created yet
|
||||
<FormattedMessage id="account.reverseShares.table.no-shares" />
|
||||
</Text>
|
||||
) : (
|
||||
<Accordion>
|
||||
@@ -111,17 +132,26 @@ const MyShares = () => {
|
||||
>
|
||||
<Accordion.Control p={0}>
|
||||
<Text size="sm">
|
||||
{`${reverseShare.shares.length} share${
|
||||
reverseShare.shares.length > 1 ? "s" : ""
|
||||
}`}
|
||||
{reverseShare.shares.length == 1
|
||||
? `1 ${t(
|
||||
"account.reverseShares.table.count.singular"
|
||||
)}`
|
||||
: `${reverseShare.shares.length} ${t(
|
||||
"account.reverseShares.table.count.plural"
|
||||
)}`}
|
||||
</Text>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
{reverseShare.shares.map((share) => (
|
||||
<Group key={share.id} mb={4}>
|
||||
<Text maw={120} truncate>
|
||||
{share.id}
|
||||
</Text>
|
||||
<Anchor
|
||||
href={`${appUrl}/share/${share.id}`}
|
||||
target="_blank"
|
||||
>
|
||||
<Text maw={120} truncate>
|
||||
{share.id}
|
||||
</Text>
|
||||
</Anchor>
|
||||
<ActionIcon
|
||||
color="victoria"
|
||||
variant="light"
|
||||
@@ -129,13 +159,9 @@ const MyShares = () => {
|
||||
onClick={() => {
|
||||
if (window.isSecureContext) {
|
||||
clipboard.copy(
|
||||
`${config.get(
|
||||
"general.appUrl"
|
||||
)}/share/${share.id}`
|
||||
);
|
||||
toast.success(
|
||||
"The share link was copied to the keyboard."
|
||||
`${appUrl}/s/${share.id}`
|
||||
);
|
||||
toast.success(t("common.notify.copied"));
|
||||
} else {
|
||||
showShareLinkModal(
|
||||
modals,
|
||||
@@ -165,24 +191,50 @@ const MyShares = () => {
|
||||
</td>
|
||||
<td>
|
||||
<Group position="right">
|
||||
<ActionIcon
|
||||
color="victoria"
|
||||
variant="light"
|
||||
size={25}
|
||||
onClick={() => {
|
||||
if (window.isSecureContext) {
|
||||
clipboard.copy(
|
||||
`${config.get("general.appUrl")}/upload/${
|
||||
reverseShare.token
|
||||
}`
|
||||
);
|
||||
toast.success(t("common.notify.copied"));
|
||||
} else {
|
||||
showReverseShareLinkModal(
|
||||
modals,
|
||||
reverseShare.token,
|
||||
config.get("general.appUrl")
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TbLink />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
color="red"
|
||||
variant="light"
|
||||
size={25}
|
||||
onClick={() => {
|
||||
modals.openConfirmModal({
|
||||
title: `Delete reverse share`,
|
||||
title: t(
|
||||
"account.reverseShares.modal.delete.title"
|
||||
),
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Do you really want to delete this reverse share?
|
||||
If you do, the associated shares will be deleted
|
||||
as well.
|
||||
<FormattedMessage id="account.reverseShares.modal.delete.description" />
|
||||
</Text>
|
||||
),
|
||||
confirmProps: {
|
||||
color: "red",
|
||||
},
|
||||
labels: { confirm: "Delete", cancel: "Cancel" },
|
||||
labels: {
|
||||
confirm: t("common.button.delete"),
|
||||
cancel: t("common.button.cancel"),
|
||||
},
|
||||
onConfirm: () => {
|
||||
shareService.removeReverseShare(reverseShare.id);
|
||||
setReverseShares(
|
||||
|
||||
@@ -16,20 +16,23 @@ import { useModals } from "@mantine/modals";
|
||||
import moment from "moment";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { TbLink, TbTrash, TbInfoCircle } from "react-icons/tb";
|
||||
import { TbInfoCircle, TbLink, TbTrash } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import Meta from "../../components/Meta";
|
||||
import showShareInformationsModal from "../../components/account/showShareInformationsModal";
|
||||
import showShareLinkModal from "../../components/account/showShareLinkModal";
|
||||
import CenterLoader from "../../components/core/CenterLoader";
|
||||
import Meta from "../../components/Meta";
|
||||
import useConfig from "../../hooks/config.hook";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
import shareService from "../../services/share.service";
|
||||
import { MyShare } from "../../types/share.type";
|
||||
import toast from "../../utils/toast.util";
|
||||
import showShareInformationsModal from "../../components/account/showShareInformationsModal";
|
||||
|
||||
const MyShares = () => {
|
||||
const modals = useModals();
|
||||
const clipboard = useClipboard();
|
||||
const config = useConfig();
|
||||
const t = useTranslate();
|
||||
|
||||
const [shares, setShares] = useState<MyShare[]>();
|
||||
|
||||
@@ -41,18 +44,22 @@ const MyShares = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title="My shares" />
|
||||
<Meta title={t("account.shares.title")} />
|
||||
<Title mb={30} order={3}>
|
||||
My shares
|
||||
<FormattedMessage id="account.shares.title" />
|
||||
</Title>
|
||||
{shares.length == 0 ? (
|
||||
<Center style={{ height: "70vh" }}>
|
||||
<Stack align="center" spacing={10}>
|
||||
<Title order={3}>It's empty here 👀</Title>
|
||||
<Text>You don't have any shares.</Text>
|
||||
<Title order={3}>
|
||||
<FormattedMessage id="account.shares.title.empty" />
|
||||
</Title>
|
||||
<Text>
|
||||
<FormattedMessage id="account.shares.description.empty" />
|
||||
</Text>
|
||||
<Space h={5} />
|
||||
<Button component={Link} href="/upload" variant="light">
|
||||
Create one
|
||||
<FormattedMessage id="account.shares.button.create" />
|
||||
</Button>
|
||||
</Stack>
|
||||
</Center>
|
||||
@@ -61,13 +68,21 @@ const MyShares = () => {
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>
|
||||
<FormattedMessage id="account.shares.table.name" />
|
||||
</th>
|
||||
<MediaQuery smallerThan="md" styles={{ display: "none" }}>
|
||||
<th>Description</th>
|
||||
<th>
|
||||
<FormattedMessage id="account.shares.table.description" />
|
||||
</th>
|
||||
</MediaQuery>
|
||||
|
||||
<th>Visitors</th>
|
||||
<th>Expires at</th>
|
||||
<th>
|
||||
<FormattedMessage id="account.shares.table.visitors" />
|
||||
</th>
|
||||
<th>
|
||||
<FormattedMessage id="account.shares.table.expiresAt" />
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -117,13 +132,11 @@ const MyShares = () => {
|
||||
onClick={() => {
|
||||
if (window.isSecureContext) {
|
||||
clipboard.copy(
|
||||
`${config.get("general.appUrl")}/share/${
|
||||
`${config.get("general.appUrl")}/s/${
|
||||
share.id
|
||||
}`
|
||||
);
|
||||
toast.success(
|
||||
"Your link was copied to the keyboard."
|
||||
);
|
||||
toast.success(t("common.notify.copied"));
|
||||
} else {
|
||||
showShareLinkModal(
|
||||
modals,
|
||||
@@ -141,16 +154,21 @@ const MyShares = () => {
|
||||
size={25}
|
||||
onClick={() => {
|
||||
modals.openConfirmModal({
|
||||
title: `Delete share ${share.id}`,
|
||||
title: t("account.shares.modal.delete.title", {
|
||||
share: share.id,
|
||||
}),
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Do you really want to delete this share?
|
||||
<FormattedMessage id="account.shares.modal.delete.description" />
|
||||
</Text>
|
||||
),
|
||||
confirmProps: {
|
||||
color: "red",
|
||||
},
|
||||
labels: { confirm: "Confirm", cancel: "Cancel" },
|
||||
labels: {
|
||||
confirm: t("common.button.delete"),
|
||||
cancel: t("common.button.cancel"),
|
||||
},
|
||||
onConfirm: () => {
|
||||
shareService.remove(share.id);
|
||||
setShares(
|
||||
|
||||
@@ -13,25 +13,28 @@ import { useMediaQuery } from "@mantine/hooks";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import Meta from "../../../components/Meta";
|
||||
import AdminConfigInput from "../../../components/admin/configuration/AdminConfigInput";
|
||||
import ConfigurationHeader from "../../../components/admin/configuration/ConfigurationHeader";
|
||||
import ConfigurationNavBar from "../../../components/admin/configuration/ConfigurationNavBar";
|
||||
import LogoConfigInput from "../../../components/admin/configuration/LogoConfigInput";
|
||||
import TestEmailButton from "../../../components/admin/configuration/TestEmailButton";
|
||||
import CenterLoader from "../../../components/core/CenterLoader";
|
||||
import Meta from "../../../components/Meta";
|
||||
import useConfig from "../../../hooks/config.hook";
|
||||
import configService from "../../../services/config.service";
|
||||
import { AdminConfig, UpdateConfig } from "../../../types/config.type";
|
||||
import {
|
||||
camelToKebab,
|
||||
capitalizeFirstLetter,
|
||||
configVariableToFriendlyName,
|
||||
} from "../../../utils/string.util";
|
||||
import toast from "../../../utils/toast.util";
|
||||
import useTranslate from "../../../hooks/useTranslate.hook";
|
||||
|
||||
export default function AppShellDemo() {
|
||||
const theme = useMantineTheme();
|
||||
const router = useRouter();
|
||||
const t = useTranslate();
|
||||
|
||||
const [isMobileNavBarOpened, setIsMobileNavBarOpened] = useState(false);
|
||||
const isMobile = useMediaQuery("(max-width: 560px)");
|
||||
@@ -94,7 +97,7 @@ export default function AppShellDemo() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title="Configuration" />
|
||||
<Meta title={t("admin.config.title")} />
|
||||
<AppShell
|
||||
styles={{
|
||||
main: {
|
||||
@@ -134,26 +137,28 @@ export default function AppShellDemo() {
|
||||
spacing={0}
|
||||
>
|
||||
<Title order={6}>
|
||||
{configVariableToFriendlyName(configVariable.name)}
|
||||
<FormattedMessage
|
||||
id={`admin.config.${camelToKebab(
|
||||
configVariable.key
|
||||
)}`}
|
||||
/>
|
||||
</Title>
|
||||
{configVariable.description.split("\n").length == 1 ? (
|
||||
<Text color="dimmed" size="sm" mb="xs">
|
||||
{configVariable.description}
|
||||
</Text>
|
||||
) : (
|
||||
configVariable.description.split("\n").map((line) => (
|
||||
<Text
|
||||
key={line}
|
||||
color="dimmed"
|
||||
size="sm"
|
||||
style={{
|
||||
marginBottom: line === "" ? "1rem" : "0",
|
||||
}}
|
||||
>
|
||||
{line}
|
||||
</Text>
|
||||
))
|
||||
)}
|
||||
|
||||
<Text
|
||||
sx={{
|
||||
whiteSpace: "pre-line",
|
||||
}}
|
||||
color="dimmed"
|
||||
size="sm"
|
||||
mb="xs"
|
||||
>
|
||||
<FormattedMessage
|
||||
id={`admin.config.${camelToKebab(
|
||||
configVariable.key
|
||||
)}.description`}
|
||||
values={{ br: <br /> }}
|
||||
/>
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack></Stack>
|
||||
<Box style={{ width: isMobile ? "100%" : "50%" }}>
|
||||
@@ -176,7 +181,9 @@ export default function AppShellDemo() {
|
||||
saveConfigVariables={saveConfigVariables}
|
||||
/>
|
||||
)}
|
||||
<Button onClick={saveConfigVariables}>Save</Button>
|
||||
<Button onClick={saveConfigVariables}>
|
||||
<FormattedMessage id="common.button.save" />
|
||||
</Button>
|
||||
</Group>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -11,7 +11,9 @@ import {
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { TbRefresh, TbSettings, TbUsers } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import Meta from "../../components/Meta";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
import configService from "../../services/config.service";
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
@@ -31,15 +33,16 @@ const useStyles = createStyles((theme) => ({
|
||||
|
||||
const Admin = () => {
|
||||
const { classes, theme } = useStyles();
|
||||
const t = useTranslate();
|
||||
|
||||
const [managementOptions, setManagementOptions] = useState([
|
||||
{
|
||||
title: "User management",
|
||||
title: t("admin.button.users"),
|
||||
icon: TbUsers,
|
||||
route: "/admin/users",
|
||||
},
|
||||
{
|
||||
title: "Configuration",
|
||||
title: t("admin.button.config"),
|
||||
icon: TbSettings,
|
||||
route: "/admin/config/general",
|
||||
},
|
||||
@@ -63,9 +66,9 @@ const Admin = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title="Administration" />
|
||||
<Meta title={t("admin.title")} />
|
||||
<Title mb={30} order={3}>
|
||||
Administration
|
||||
<FormattedMessage id="admin.title" />
|
||||
</Title>
|
||||
<Stack justify="space-between" style={{ height: "calc(100vh - 180px)" }}>
|
||||
<Paper withBorder p={40}>
|
||||
@@ -91,7 +94,7 @@ const Admin = () => {
|
||||
|
||||
<Center>
|
||||
<Text size="xs" color="dimmed">
|
||||
Version {process.env.VERSION}
|
||||
<FormattedMessage id="admin.version" /> {process.env.VERSION}
|
||||
</Text>
|
||||
</Center>
|
||||
</Stack>
|
||||
|
||||
@@ -2,10 +2,12 @@ import { Button, Group, Space, Text, Title } from "@mantine/core";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import { useEffect, useState } from "react";
|
||||
import { TbPlus } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import Meta from "../../components/Meta";
|
||||
import ManageUserTable from "../../components/admin/users/ManageUserTable";
|
||||
import showCreateUserModal from "../../components/admin/users/showCreateUserModal";
|
||||
import Meta from "../../components/Meta";
|
||||
import useConfig from "../../hooks/config.hook";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
import userService from "../../services/user.service";
|
||||
import User from "../../types/user.type";
|
||||
import toast from "../../utils/toast.util";
|
||||
@@ -16,6 +18,7 @@ const Users = () => {
|
||||
|
||||
const config = useConfig();
|
||||
const modals = useModals();
|
||||
const t = useTranslate();
|
||||
|
||||
const getUsers = () => {
|
||||
setIsLoading(true);
|
||||
@@ -27,14 +30,18 @@ const Users = () => {
|
||||
|
||||
const deleteUser = (user: User) => {
|
||||
modals.openConfirmModal({
|
||||
title: `Delete ${user.username}?`,
|
||||
title: t("admin.users.edit.delete.title", {
|
||||
username: user.username,
|
||||
}),
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Do you really want to delete <b>{user.username}</b> and all his
|
||||
shares?
|
||||
<FormattedMessage id="admin.users.edit.delete.description" />
|
||||
</Text>
|
||||
),
|
||||
labels: { confirm: "Delete", cancel: "Cancel" },
|
||||
labels: {
|
||||
confirm: t("common.button.delete"),
|
||||
cancel: t("common.button.cancel"),
|
||||
},
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: async () => {
|
||||
userService
|
||||
@@ -51,10 +58,10 @@ const Users = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title="User management" />
|
||||
<Meta title={t("admin.users.title")} />
|
||||
<Group position="apart" align="baseline" mb={20}>
|
||||
<Title mb={30} order={3}>
|
||||
User management
|
||||
<FormattedMessage id="admin.users.title" />
|
||||
</Title>
|
||||
<Button
|
||||
onClick={() =>
|
||||
@@ -62,7 +69,7 @@ const Users = () => {
|
||||
}
|
||||
leftIcon={<TbPlus size={20} />}
|
||||
>
|
||||
Create
|
||||
<FormattedMessage id="common.button.create" />
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
|
||||
@@ -10,7 +10,9 @@ import {
|
||||
} from "@mantine/core";
|
||||
import { useForm, yupResolver } from "@mantine/form";
|
||||
import { useRouter } from "next/router";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import * as yup from "yup";
|
||||
import useTranslate from "../../../hooks/useTranslate.hook";
|
||||
import authService from "../../../services/auth.service";
|
||||
import toast from "../../../utils/toast.util";
|
||||
|
||||
@@ -25,6 +27,7 @@ const useStyles = createStyles((theme) => ({
|
||||
const ResetPassword = () => {
|
||||
const { classes } = useStyles();
|
||||
const router = useRouter();
|
||||
const t = useTranslate();
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
@@ -32,7 +35,10 @@ const ResetPassword = () => {
|
||||
},
|
||||
validate: yupResolver(
|
||||
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 (
|
||||
<Container size={460} my={30}>
|
||||
<Title order={2} weight={900} align="center">
|
||||
Reset password
|
||||
<FormattedMessage id="resetPassword.text.resetPassword" />
|
||||
</Title>
|
||||
<Text color="dimmed" size="sm" align="center">
|
||||
Enter your new password
|
||||
<FormattedMessage id="resetPassword.text.enterNewPassword" />
|
||||
</Text>
|
||||
|
||||
<Paper withBorder shadow="md" p={30} radius="md" mt="xl">
|
||||
@@ -54,7 +60,7 @@ const ResetPassword = () => {
|
||||
authService
|
||||
.resetPassword(resetPasswordToken, values.password)
|
||||
.then(() => {
|
||||
toast.success("Your password has been reset successfully.");
|
||||
toast.success(t("resetPassword.notify.passwordReset"));
|
||||
|
||||
router.push("/auth/signIn");
|
||||
})
|
||||
@@ -62,13 +68,13 @@ const ResetPassword = () => {
|
||||
})}
|
||||
>
|
||||
<PasswordInput
|
||||
label="New password"
|
||||
label={t("resetPassword.text.password")}
|
||||
placeholder="••••••••••"
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
<Group position="right" mt="lg">
|
||||
<Button type="submit" className={classes.control}>
|
||||
Reset password
|
||||
<FormattedMessage id="resetPassword.button.resetPassword" />
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
|
||||
@@ -15,7 +15,9 @@ import { useForm, yupResolver } from "@mantine/form";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { TbArrowLeft } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import * as yup from "yup";
|
||||
import useTranslate from "../../../hooks/useTranslate.hook";
|
||||
import authService from "../../../services/auth.service";
|
||||
import toast from "../../../utils/toast.util";
|
||||
|
||||
@@ -43,6 +45,7 @@ const useStyles = createStyles((theme) => ({
|
||||
const ResetPassword = () => {
|
||||
const { classes } = useStyles();
|
||||
const router = useRouter();
|
||||
const t = useTranslate();
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
@@ -50,7 +53,10 @@ const ResetPassword = () => {
|
||||
},
|
||||
validate: yupResolver(
|
||||
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 (
|
||||
<Container size={460} my={30}>
|
||||
<Title order={2} weight={900} align="center">
|
||||
Forgot your password?
|
||||
<FormattedMessage id="resetPassword.title" />
|
||||
</Title>
|
||||
<Text color="dimmed" size="sm" align="center">
|
||||
Enter your email to get a reset link
|
||||
<FormattedMessage id="resetPassword.description" />
|
||||
</Text>
|
||||
|
||||
<Paper withBorder shadow="md" p={30} radius="md" mt="xl">
|
||||
@@ -70,15 +76,15 @@ const ResetPassword = () => {
|
||||
authService
|
||||
.requestResetPassword(values.email)
|
||||
.then(() => {
|
||||
toast.success("The email has been sent.");
|
||||
toast.success(t("resetPassword.notify.success"));
|
||||
router.push("/auth/signIn");
|
||||
})
|
||||
.catch(toast.axiosError)
|
||||
)}
|
||||
>
|
||||
<TextInput
|
||||
label="Your email"
|
||||
placeholder="Your email"
|
||||
label={t("signup.input.email")}
|
||||
placeholder={t("signup.input.email.placeholder")}
|
||||
{...form.getInputProps("email")}
|
||||
/>
|
||||
<Group position="apart" mt="lg" className={classes.controls}>
|
||||
@@ -91,11 +97,13 @@ const ResetPassword = () => {
|
||||
>
|
||||
<Center inline>
|
||||
<TbArrowLeft size={12} />
|
||||
<Box ml={5}>Back to login page</Box>
|
||||
<Box ml={5}>
|
||||
<FormattedMessage id="resetPassword.button.back" />
|
||||
</Box>
|
||||
</Center>
|
||||
</Anchor>
|
||||
<Button type="submit" className={classes.control}>
|
||||
Reset password
|
||||
<FormattedMessage id="resetPassword.text.resetPassword" />
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useEffect, useState } from "react";
|
||||
import SignInForm from "../../components/auth/SignInForm";
|
||||
import Meta from "../../components/Meta";
|
||||
import useUser from "../../hooks/user.hook";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
|
||||
export function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
return {
|
||||
@@ -15,6 +16,7 @@ export function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const SignIn = ({ redirectPath }: { redirectPath?: string }) => {
|
||||
const { refreshUser } = useUser();
|
||||
const router = useRouter();
|
||||
const t = useTranslate();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(redirectPath ? true : false);
|
||||
|
||||
@@ -34,7 +36,7 @@ const SignIn = ({ redirectPath }: { redirectPath?: string }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title="Sign In" />
|
||||
<Meta title={t("signin.title")} />
|
||||
<SignInForm redirectPath={redirectPath ?? "/upload"} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import SignUpForm from "../../components/auth/SignUpForm";
|
||||
import Meta from "../../components/Meta";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
|
||||
const SignUp = () => {
|
||||
const t = useTranslate();
|
||||
return (
|
||||
<>
|
||||
<Meta title="Sign Up" />
|
||||
<Meta title={t("signup.title")} />
|
||||
<SignUpForm />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
import { TbCheck } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import Logo from "../components/Logo";
|
||||
import Meta from "../components/Meta";
|
||||
import useUser from "../hooks/user.hook";
|
||||
@@ -89,12 +90,17 @@ export default function Home() {
|
||||
<div className={classes.inner}>
|
||||
<div className={classes.content}>
|
||||
<Title className={classes.title}>
|
||||
A <span className={classes.highlight}>self-hosted</span> <br />{" "}
|
||||
file sharing platform.
|
||||
<FormattedMessage
|
||||
id="home.title"
|
||||
values={{
|
||||
h: (chunks) => (
|
||||
<span className={classes.highlight}>{chunks} </span>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Title>
|
||||
<Text color="dimmed" mt="md">
|
||||
Do you really want to give your personal files in the hand of
|
||||
third parties like WeTransfer?
|
||||
<FormattedMessage id="home.description" />
|
||||
</Text>
|
||||
|
||||
<List
|
||||
@@ -109,19 +115,26 @@ export default function Home() {
|
||||
>
|
||||
<List.Item>
|
||||
<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>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<div>
|
||||
<b>Privacy</b> - Your files are your files and should never
|
||||
get into the hands of third parties.
|
||||
<b>
|
||||
<FormattedMessage id="home.bullet.b.name" />
|
||||
</b>{" "}
|
||||
- <FormattedMessage id="home.bullet.b.description" />
|
||||
</div>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<div>
|
||||
<b>No annoying file size limit</b> - Upload as big files as
|
||||
you want. Only your hard drive will be your limit.
|
||||
<b>
|
||||
<FormattedMessage id="home.bullet.c.name" />
|
||||
</b>{" "}
|
||||
- <FormattedMessage id="home.bullet.c.description" />
|
||||
</div>
|
||||
</List.Item>
|
||||
</List>
|
||||
@@ -134,7 +147,7 @@ export default function Home() {
|
||||
size="md"
|
||||
className={classes.control}
|
||||
>
|
||||
Get started
|
||||
<FormattedMessage id="home.button.start" />
|
||||
</Button>
|
||||
<Button
|
||||
component={Link}
|
||||
@@ -145,7 +158,7 @@ export default function Home() {
|
||||
size="md"
|
||||
className={classes.control}
|
||||
>
|
||||
Source code
|
||||
<FormattedMessage id="home.button.source" />
|
||||
</Button>
|
||||
</Group>
|
||||
</div>
|
||||
|
||||
18
frontend/src/pages/s/[shareId].ts
Normal file
18
frontend/src/pages/s/[shareId].ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
|
||||
// Redirect to the share page
|
||||
export function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const { shareId } = context.params!;
|
||||
|
||||
return {
|
||||
props: {},
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/share/" + shareId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function ShareAlias() {
|
||||
return null;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import DownloadAllButton from "../../../components/share/DownloadAllButton";
|
||||
import FileList from "../../../components/share/FileList";
|
||||
import showEnterPasswordModal from "../../../components/share/showEnterPasswordModal";
|
||||
import showErrorModal from "../../../components/share/showErrorModal";
|
||||
import useTranslate from "../../../hooks/useTranslate.hook";
|
||||
import shareService from "../../../services/share.service";
|
||||
import { Share as ShareType } from "../../../types/share.type";
|
||||
import toast from "../../../utils/toast.util";
|
||||
@@ -20,6 +21,7 @@ export function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const Share = ({ shareId }: { shareId: string }) => {
|
||||
const modals = useModals();
|
||||
const [share, setShare] = useState<ShareType>();
|
||||
const t = useTranslate();
|
||||
|
||||
const getShareToken = async (password?: string) => {
|
||||
await shareService
|
||||
@@ -33,8 +35,8 @@ const Share = ({ shareId }: { shareId: string }) => {
|
||||
if (error == "share_max_views_exceeded") {
|
||||
showErrorModal(
|
||||
modals,
|
||||
"Visitor limit exceeded",
|
||||
"The visitor limit from this share has been exceeded."
|
||||
t("share.error.visitor-limit-exceeded.title"),
|
||||
t("share.error.visitor-limit-exceeded.description")
|
||||
);
|
||||
} else {
|
||||
toast.axiosError(e);
|
||||
@@ -52,12 +54,16 @@ const Share = ({ shareId }: { shareId: string }) => {
|
||||
const { error } = e.response.data;
|
||||
if (e.response.status == 404) {
|
||||
if (error == "share_removed") {
|
||||
showErrorModal(modals, "Share removed", e.response.data.message);
|
||||
showErrorModal(
|
||||
modals,
|
||||
t("share.error.removed.title"),
|
||||
e.response.data.message
|
||||
);
|
||||
} else {
|
||||
showErrorModal(
|
||||
modals,
|
||||
"Not found",
|
||||
"This share can't be found. Please check your link."
|
||||
t("share.error.not-found.title"),
|
||||
t("share.error.not-found.description")
|
||||
);
|
||||
}
|
||||
} else if (error == "share_password_required") {
|
||||
@@ -65,7 +71,7 @@ const Share = ({ shareId }: { shareId: string }) => {
|
||||
} else if (error == "share_token_required") {
|
||||
getShareToken();
|
||||
} 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 (
|
||||
<>
|
||||
<Meta
|
||||
title={`Share ${shareId}`}
|
||||
description="Look what I've shared with you."
|
||||
title={t("share.title", { shareId })}
|
||||
description={t("share.description")}
|
||||
/>
|
||||
|
||||
<Group position="apart" mb="lg">
|
||||
|
||||
@@ -4,12 +4,14 @@ import { cleanNotifications } from "@mantine/notifications";
|
||||
import { AxiosError } from "axios";
|
||||
import pLimit from "p-limit";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import Meta from "../../components/Meta";
|
||||
import Dropzone from "../../components/upload/Dropzone";
|
||||
import FileList from "../../components/upload/FileList";
|
||||
import showCompletedUploadModal from "../../components/upload/modals/showCompletedUploadModal";
|
||||
import showCreateUploadModal from "../../components/upload/modals/showCreateUploadModal";
|
||||
import useConfig from "../../hooks/config.hook";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
import useUser from "../../hooks/user.hook";
|
||||
import shareService from "../../services/share.service";
|
||||
import { FileUpload } from "../../types/File.type";
|
||||
@@ -29,6 +31,7 @@ const Upload = ({
|
||||
isReverseShare: boolean;
|
||||
}) => {
|
||||
const modals = useModals();
|
||||
const t = useTranslate();
|
||||
|
||||
const { user } = useUser();
|
||||
const config = useConfig();
|
||||
@@ -126,7 +129,7 @@ const Upload = ({
|
||||
if (fileErrorCount > 0) {
|
||||
if (!errorToastShown) {
|
||||
toast.error(
|
||||
`${fileErrorCount} file(s) failed to upload. Trying again.`,
|
||||
t("upload.notify.count-failed", { count: fileErrorCount }),
|
||||
{
|
||||
withCloseButton: false,
|
||||
autoClose: false,
|
||||
@@ -152,15 +155,13 @@ const Upload = ({
|
||||
showCompletedUploadModal(modals, share, config.get("general.appUrl"));
|
||||
setFiles([]);
|
||||
})
|
||||
.catch(() =>
|
||||
toast.error("An error occurred while finishing your share.")
|
||||
);
|
||||
.catch(() => toast.error(t("upload.notify.generic-error")));
|
||||
}
|
||||
}, [files]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title="Upload" />
|
||||
<Meta title={t("upload.title")} />
|
||||
<Group position="right" mb={20}>
|
||||
<Button
|
||||
loading={isUploading}
|
||||
@@ -183,7 +184,7 @@ const Upload = ({
|
||||
);
|
||||
}}
|
||||
>
|
||||
Share
|
||||
<FormattedMessage id="common.button.share" />
|
||||
</Button>
|
||||
</Group>
|
||||
<Dropzone
|
||||
|
||||
@@ -32,6 +32,7 @@ export type MyReverseShare = {
|
||||
maxShareSize: string;
|
||||
shareExpiration: Date;
|
||||
remainingUses: number;
|
||||
token: string;
|
||||
shares: MyShare[];
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import moment from "moment";
|
||||
|
||||
export const getExpirationPreview = (
|
||||
name: string,
|
||||
messages: {
|
||||
neverExpires: string;
|
||||
expiresOn: string;
|
||||
},
|
||||
form: {
|
||||
values: {
|
||||
never_expires?: boolean;
|
||||
@@ -13,7 +16,7 @@ export const getExpirationPreview = (
|
||||
const value = form.values.never_expires
|
||||
? "never"
|
||||
: 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()
|
||||
.add(
|
||||
@@ -22,5 +25,8 @@ export const getExpirationPreview = (
|
||||
)
|
||||
.toDate();
|
||||
|
||||
return `This ${name} will expire on ${moment(expirationDate).format("LLL")}`;
|
||||
return messages.expiresOn.replace(
|
||||
"{expiration}",
|
||||
moment(expirationDate).format("LLL")
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
export function byteToHumanSizeString(bytes: number) {
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
||||
if (bytes == 0) return "0 Byte";
|
||||
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)).toString());
|
||||
return (bytes / Math.pow(1024, i)).toFixed(1).toString() + " " + sizes[i];
|
||||
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1000)).toString());
|
||||
return (bytes / Math.pow(1000, i)).toFixed(1).toString() + " " + sizes[i];
|
||||
}
|
||||
|
||||
export function byteToUnitAndSize(bytes: number) {
|
||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||
if (bytes == 0) return { unit: "B", size: 0 };
|
||||
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)).toString());
|
||||
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1000)).toString());
|
||||
|
||||
return {
|
||||
size: parseFloat((bytes / Math.pow(1024, i)).toFixed(1)),
|
||||
size: parseFloat((bytes / Math.pow(1000, i)).toFixed(1)),
|
||||
unit: units[i],
|
||||
};
|
||||
}
|
||||
@@ -19,5 +19,5 @@ export function byteToUnitAndSize(bytes: number) {
|
||||
export function unitAndSizeToByte(unit: string, size: number) {
|
||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||
const i = units.indexOf(unit);
|
||||
return Math.pow(1024, i) * size;
|
||||
return Math.pow(1000, i) * size;
|
||||
}
|
||||
|
||||
50
frontend/src/utils/i18n.util.ts
Normal file
50
frontend/src/utils/i18n.util.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
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);
|
||||
const supportedLanguagesWithoutRegion = supportedLanguages.map(
|
||||
(l) => l.split("-")[0]
|
||||
);
|
||||
|
||||
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 (
|
||||
supportedLanguagesWithoutRegion.includes(language.split("-")[0])
|
||||
) {
|
||||
const similarLanguage = supportedLanguages.find((l) =>
|
||||
l.startsWith(language.split("-")[0])
|
||||
);
|
||||
return similarLanguage;
|
||||
}
|
||||
}
|
||||
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) => {
|
||||
const splitted = variable.split(/(?=[A-Z])/).join(" ");
|
||||
return splitted.charAt(0).toUpperCase() + splitted.slice(1);
|
||||
export const camelToKebab = (camelCaseString: string) => {
|
||||
return camelCaseString.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
||||
};
|
||||
|
||||
export const capitalizeFirstLetter = (string: string) => {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { NotificationProps, showNotification } from "@mantine/notifications";
|
||||
import { TbCheck, TbX } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
const error = (message: string, config?: Omit<NotificationProps, "message">) =>
|
||||
showNotification({
|
||||
icon: <TbX />,
|
||||
color: "red",
|
||||
radius: "md",
|
||||
title: "Error",
|
||||
title: <FormattedMessage id="common.error" />,
|
||||
message: message,
|
||||
|
||||
autoClose: true,
|
||||
@@ -24,7 +26,7 @@ const success = (
|
||||
icon: <TbCheck />,
|
||||
color: "green",
|
||||
radius: "md",
|
||||
title: "Success",
|
||||
title: <FormattedMessage id="common.success" />,
|
||||
message: message,
|
||||
autoClose: true,
|
||||
...config,
|
||||
|
||||
@@ -3,6 +3,10 @@ const defaultPreferences = [
|
||||
key: "colorScheme",
|
||||
value: "system",
|
||||
},
|
||||
{
|
||||
key: "locale",
|
||||
value: "system",
|
||||
},
|
||||
];
|
||||
|
||||
const get = (key: string) => {
|
||||
@@ -23,8 +27,9 @@ const set = (key: string, value: string) => {
|
||||
localStorage.setItem("preferences", JSON.stringify(preferences));
|
||||
}
|
||||
};
|
||||
const usePreferences = () => {
|
||||
return { get, set };
|
||||
const userPreferences = {
|
||||
get,
|
||||
set,
|
||||
};
|
||||
|
||||
export default usePreferences;
|
||||
export default userPreferences;
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pingvin-share",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.4",
|
||||
"scripts": {
|
||||
"format": "cd frontend && npm run format && cd ../backend && npm run format",
|
||||
"lint": "cd frontend && npm run lint && cd ../backend && npm run lint",
|
||||
@@ -8,5 +8,8 @@
|
||||
"release:patch": "cd backend && npm version patch --commit-hooks false && cd ../frontend && npm version patch --commit-hooks false && cd .. && git add . && npm version patch --force -m 'release: %s' && git push && git push --tags",
|
||||
"release:minor": "cd backend && npm version minor --commit-hooks false && cd ../frontend && npm version minor --commit-hooks false && cd .. && git add . && npm version minor --force -m 'release: %s' && git push && git push --tags",
|
||||
"deploy:dev": "docker buildx build --push --tag stonith404/pingvin-share:development --platform linux/amd64,linux/arm64 ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"conventional-changelog-cli": "^3.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user