Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
adf0f8d57e | ||
|
|
447c86f1c9 | ||
|
|
1466240461 | ||
|
|
348852cfa4 | ||
|
|
932496a121 | ||
|
|
0c7b2a8e70 | ||
|
|
1df5c7123e | ||
|
|
2dc0fc9332 | ||
|
|
98c0de78e8 | ||
|
|
5132d177b8 | ||
|
|
e5071cba12 | ||
|
|
b33c1d7f4b | ||
|
|
39a74510c1 | ||
|
|
b7db9b9b40 | ||
|
|
2ca0092b71 | ||
|
|
b4bf43910e | ||
|
|
90aa919694 | ||
|
|
f2e4019190 | ||
|
|
ffd4e43f11 | ||
|
|
0e5c673270 | ||
|
|
beece56327 | ||
|
|
a0d1d98e24 | ||
|
|
ca73ccf629 | ||
|
|
9f2097e788 | ||
|
|
2158df4228 | ||
|
|
37e765ddc7 | ||
|
|
a91c531642 | ||
|
|
5a7f7ca2f6 | ||
|
|
813ee4de2c | ||
|
|
b25c30d1ed | ||
|
|
c807d208d8 | ||
|
|
f82099f36e | ||
|
|
6345e21db9 | ||
|
|
f55aa80516 | ||
|
|
0ce8b528e1 | ||
|
|
8ff417a013 | ||
|
|
cb1a0d4090 | ||
|
|
753dbe83b7 |
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
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,6 +23,7 @@ yarn-error.log*
|
||||
|
||||
# env file
|
||||
.env
|
||||
!/backend/prisma/.env
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
||||
/backend/src/constants.ts
|
||||
112
CHANGELOG.md
112
CHANGELOG.md
@@ -1,3 +1,115 @@
|
||||
## [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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Adding more informations on My Shares page (table and modal) ([#174](https://github.com/stonith404/pingvin-share/issues/174)) ([1466240](https://github.com/stonith404/pingvin-share/commit/14662404614f15bc25384d924d8cb0458ab06cd8))
|
||||
* Adding the possibility of copying the link by clicking text and icons ([#171](https://github.com/stonith404/pingvin-share/issues/171)) ([348852c](https://github.com/stonith404/pingvin-share/commit/348852cfa4275f5c642669b43697f83c35333044))
|
||||
|
||||
## [0.15.0](https://github.com/stonith404/pingvin-share/compare/v0.14.1...v0.15.0) (2023-05-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add env variables for port, database url and data dir ([98c0de7](https://github.com/stonith404/pingvin-share/commit/98c0de78e8a73e3e5bf0928226cfb8a024b566a1))
|
||||
* add healthcheck endpoint ([5132d17](https://github.com/stonith404/pingvin-share/commit/5132d177b8ab4e00a7e701e9956222fa2352d42c))
|
||||
* allow to configure clamav with environment variables ([1df5c71](https://github.com/stonith404/pingvin-share/commit/1df5c7123e4ca8695f4f1b7d49f46cdf147fb920))
|
||||
* configure ports, db url and api url with env variables ([e5071cb](https://github.com/stonith404/pingvin-share/commit/e5071cba1204093197b72e18d024b484e72e360a))
|
||||
|
||||
### [0.14.1](https://github.com/stonith404/pingvin-share/compare/v0.14.0...v0.14.1) (2023-04-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* boolean config variables can't be set to false ([39a7451](https://github.com/stonith404/pingvin-share/commit/39a74510c1f00466acaead39f7bee003b3db60d7))
|
||||
|
||||
## [0.14.0](https://github.com/stonith404/pingvin-share/compare/v0.13.1...v0.14.0) (2023-04-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **share, config:** more variables, placeholder and reset default ([#132](https://github.com/stonith404/pingvin-share/issues/132)) ([beece56](https://github.com/stonith404/pingvin-share/commit/beece56327da141c222fd9f5259697df6db9347a))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bool config variable can't be changed ([0e5c673](https://github.com/stonith404/pingvin-share/commit/0e5c67327092e4751208e559a2b0d5ee2b91b6e3))
|
||||
|
||||
### [0.13.1](https://github.com/stonith404/pingvin-share/compare/v0.13.0...v0.13.1) (2023-03-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* empty file can't be uploaded in chrome ([9f2097e](https://github.com/stonith404/pingvin-share/commit/9f2097e788dfb79c2f95085025934c3134a3eb38))
|
||||
|
||||
## [0.13.0](https://github.com/stonith404/pingvin-share/compare/v0.12.1...v0.13.0) (2023-03-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add preview modal ([c807d20](https://github.com/stonith404/pingvin-share/commit/c807d208d8f0518f6390f9f0f3d0eb00c12d213b))
|
||||
* sort shared files ([b25c30d](https://github.com/stonith404/pingvin-share/commit/b25c30d1ed57230096b17afaf8545c7b0ef2e4b1))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* replace "pingvin share" with dynamic app name ([f55aa80](https://github.com/stonith404/pingvin-share/commit/f55aa805167f31864cb07e269a47533927cb533c))
|
||||
* set password manually input not shown ([8ff417a](https://github.com/stonith404/pingvin-share/commit/8ff417a013a45a777308f71c4f0d1817bfeed6be))
|
||||
* show line breaks in txt preview ([37e765d](https://github.com/stonith404/pingvin-share/commit/37e765ddc7b19554bc6fb50eb969984b58bf3cc5))
|
||||
* upload file if it is 0 bytes ([f82099f](https://github.com/stonith404/pingvin-share/commit/f82099f36eb4699385fc16dfb0e0c02e5d55b1e3))
|
||||
|
||||
### [0.12.1](https://github.com/stonith404/pingvin-share/compare/v0.12.0...v0.12.1) (2023-03-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 48px icon does not update ([753dbe8](https://github.com/stonith404/pingvin-share/commit/753dbe83b770814115a2576c7a50e1bac9dc8ce1))
|
||||
|
||||
## [0.12.0](https://github.com/stonith404/pingvin-share/compare/v0.11.1...v0.12.0) (2023-03-10)
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
_Read this in another language: [Spanish](/docs/CONTRIBUTING.es.md), [English](/CONTRIBUTING.md), [Simplified Chinese](/docs/CONTRIBUTING.zh-cn.md)_
|
||||
|
||||
---
|
||||
|
||||
# Contributing
|
||||
|
||||
We would ❤️ for you to contribute to Pingvin Share and help make it better! All contributions are welcome, including issues, suggestions, pull requests and more.
|
||||
|
||||
17
Dockerfile
17
Dockerfile
@@ -1,26 +1,26 @@
|
||||
# Using node slim because prisma ORM needs libc for ARM builds
|
||||
|
||||
# Stage 1: on frontend dependency change
|
||||
FROM node:18-slim AS frontend-dependencies
|
||||
FROM node:19-slim AS frontend-dependencies
|
||||
WORKDIR /opt/app
|
||||
COPY frontend/package.json frontend/package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Stage 2: on frontend change
|
||||
FROM node:18-slim AS frontend-builder
|
||||
FROM node:19-slim AS frontend-builder
|
||||
WORKDIR /opt/app
|
||||
COPY ./frontend .
|
||||
COPY --from=frontend-dependencies /opt/app/node_modules ./node_modules
|
||||
RUN npm run build
|
||||
|
||||
# Stage 3: on backend dependency change
|
||||
FROM node:18-slim AS backend-dependencies
|
||||
FROM node:19-slim AS backend-dependencies
|
||||
WORKDIR /opt/app
|
||||
COPY backend/package.json backend/package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Stage 4:on backend change
|
||||
FROM node:18-slim AS backend-builder
|
||||
FROM node:19-slim AS backend-builder
|
||||
RUN apt-get update && apt-get install -y openssl
|
||||
WORKDIR /opt/app
|
||||
COPY ./backend .
|
||||
@@ -29,9 +29,9 @@ RUN npx prisma generate
|
||||
RUN npm run build && npm prune --production
|
||||
|
||||
# Stage 5: Final image
|
||||
FROM node:18-slim AS runner
|
||||
FROM node:19-slim AS runner
|
||||
ENV NODE_ENV=docker
|
||||
RUN apt-get update && apt-get install -y openssl
|
||||
RUN apt-get update && apt-get install -y curl openssl
|
||||
|
||||
WORKDIR /opt/app/frontend
|
||||
COPY --from=frontend-builder /opt/app/public ./public
|
||||
@@ -47,4 +47,7 @@ COPY --from=backend-builder /opt/app/package.json ./
|
||||
|
||||
WORKDIR /opt/app
|
||||
EXPOSE 3000
|
||||
CMD cp -rn /tmp/img /opt/app/frontend/public && node frontend/server.js & cd backend && npm run prod
|
||||
HEALTHCHECK --interval=10s --timeout=3s CMD curl -f http://localhost:3000/api/health || exit 1
|
||||
|
||||
# HOSTNAME=127.0.0.1 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=127.0.0.1 node frontend/server.js & cd backend && npm run prod
|
||||
71
README.md
71
README.md
@@ -1,5 +1,11 @@
|
||||
# <div align="center"><img src="https://user-images.githubusercontent.com/58886915/166198400-c2134044-1198-4647-a8b6-da9c4a204c68.svg" width="40"/> </br>Pingvin Share</div>
|
||||
|
||||
---
|
||||
|
||||
_Read this in another language: [Spanish](/docs/README.es.md), [English](/README.md), [Simplified Chinese](/docs/README.zh-cn.md)_
|
||||
|
||||
---
|
||||
|
||||
Pingvin Share is self-hosted file sharing platform and an alternative for WeTransfer.
|
||||
|
||||
## ✨ Features
|
||||
@@ -16,7 +22,7 @@ Pingvin Share is self-hosted file sharing platform and an alternative for WeTran
|
||||
- [Demo](https://pingvin-share.dev.eliasschneider.com)
|
||||
- [Review by DB Tech](https://www.youtube.com/watch?v=rWwNeZCOPJA)
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/58886915/167101708-b85032ad-f5b1-480a-b8d7-ec0096ea2a43.png" width="700"/>
|
||||
<img src="https://user-images.githubusercontent.com/58886915/225038319-b2ef742c-3a74-4eb6-9689-4207a36842a4.png" width="700"/>
|
||||
|
||||
## ⌨️ Setup
|
||||
|
||||
@@ -33,7 +39,7 @@ The website is now listening on `http://localhost:3000`, have fun with Pingvin S
|
||||
|
||||
Required tools:
|
||||
|
||||
- [Node.js](https://nodejs.org/en/download/) >= 14
|
||||
- [Node.js](https://nodejs.org/en/download/) >= 16
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [pm2](https://pm2.keymetrics.io/) for running Pingvin Share in the background
|
||||
|
||||
@@ -88,16 +94,65 @@ docker compose up -d
|
||||
|
||||
#### Stand-alone
|
||||
|
||||
1. Remove the running app
|
||||
```
|
||||
pm2 delete pingvin-share-backend pingvin-share-frontend
|
||||
1. Stop the running app
|
||||
```bash
|
||||
pm2 stop pingvin-share-backend pingvin-share-frontend
|
||||
```
|
||||
2. Repeat the steps from the [installation guide](#stand-alone-installation) except the `git clone` step.
|
||||
|
||||
### Custom branding
|
||||
```bash
|
||||
cd pingvin-share
|
||||
|
||||
You can change the name and the logo of the app by visiting the admin configuration page.
|
||||
# Checkout the latest version
|
||||
git fetch --tags && git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
|
||||
|
||||
# Start the backend
|
||||
cd backend
|
||||
npm run build
|
||||
pm2 restart pingvin-share-backend
|
||||
|
||||
# Start the frontend
|
||||
cd ../frontend
|
||||
npm run build
|
||||
pm2 restart pingvin-share-frontend
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
You can customize Pingvin Share by going to the configuration page in your admin dashboard.
|
||||
|
||||
#### Environment variables
|
||||
|
||||
For installation specific configuration, you can use environment variables. The following variables are available:
|
||||
|
||||
##### Backend
|
||||
|
||||
| Variable | Default Value | Description |
|
||||
| ---------------- | -------------------------------------------------- | -------------------------------------- |
|
||||
| `PORT` | `8080` | The port on which the backend listens. |
|
||||
| `DATABASE_URL` | `file:../data/pingvin-share.db?connection_limit=1` | The URL of the SQLite database. |
|
||||
| `DATA_DIRECTORY` | `./data` | The directory where data is stored. |
|
||||
| `CLAMAV_HOST` | `127.0.0.1` | The IP address of the ClamAV server. |
|
||||
| `CLAMAV_PORT` | `3310` | The port number of the ClamAV server. |
|
||||
|
||||
##### Frontend
|
||||
|
||||
| Variable | Default Value | Description |
|
||||
| --------- | ----------------------- | ---------------------------------------- |
|
||||
| `PORT` | `3000` | The port on which the frontend listens. |
|
||||
| `API_URL` | `http://localhost:8080` | The URL of the backend for the frontend. |
|
||||
|
||||
## 🖤 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.
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
FROM node:18 AS deps
|
||||
WORKDIR /opt/app
|
||||
COPY package.json package-lock.json ./
|
||||
COPY prisma ./prisma
|
||||
RUN npm ci
|
||||
RUN npx prisma generate
|
||||
|
||||
|
||||
FROM node:18 As build
|
||||
WORKDIR /opt/app
|
||||
COPY . .
|
||||
COPY --from=deps /opt/app/node_modules ./node_modules
|
||||
RUN npm run build
|
||||
|
||||
|
||||
FROM node:18 As runner
|
||||
WORKDIR /opt/app
|
||||
COPY --from=build /opt/app/node_modules ./node_modules
|
||||
COPY --from=build /opt/app/dist ./dist
|
||||
COPY --from=build /opt/app/prisma ./prisma
|
||||
COPY --from=deps /opt/app/package.json ./
|
||||
CMD npm run prod
|
||||
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.12.0",
|
||||
"version": "0.17.2",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
2
backend/prisma/.env
Normal file
2
backend/prisma/.env
Normal file
@@ -0,0 +1,2 @@
|
||||
#This file is only used to set a default value for the database url
|
||||
DATABASE_URL="file:../data/pingvin-share.db"
|
||||
@@ -0,0 +1,23 @@
|
||||
-- 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,
|
||||
"value" TEXT,
|
||||
"defaultValue" TEXT NOT NULL DEFAULT '',
|
||||
"description" TEXT NOT NULL,
|
||||
"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", "description", "locked", "name", "obscured", "order", "secret", "type", "updatedAt", "value") SELECT "category", "description", "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;
|
||||
@@ -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;
|
||||
@@ -4,7 +4,7 @@ generator client {
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = "file:../data/pingvin-share.db"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model User {
|
||||
@@ -131,15 +131,15 @@ model ShareSecurity {
|
||||
model Config {
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
name String
|
||||
category String
|
||||
type String
|
||||
value String
|
||||
description String
|
||||
obscured Boolean @default(false)
|
||||
secret Boolean @default(true)
|
||||
locked Boolean @default(false)
|
||||
order Int
|
||||
name String
|
||||
category String
|
||||
type String
|
||||
defaultValue String @default("")
|
||||
value String?
|
||||
obscured Boolean @default(false)
|
||||
secret Boolean @default(true)
|
||||
locked Boolean @default(false)
|
||||
order Int
|
||||
|
||||
@@id([name, category])
|
||||
}
|
||||
|
||||
@@ -4,150 +4,118 @@ import * as crypto from "crypto";
|
||||
const configVariables: ConfigVariables = {
|
||||
internal: {
|
||||
jwtSecret: {
|
||||
description: "Long random string used to sign JWT tokens",
|
||||
type: "string",
|
||||
value: crypto.randomBytes(256).toString("base64"),
|
||||
defaultValue: crypto.randomBytes(256).toString("base64"),
|
||||
locked: true,
|
||||
},
|
||||
},
|
||||
general: {
|
||||
appName: {
|
||||
description: "Name of the application",
|
||||
type: "string",
|
||||
value: "Pingvin Share",
|
||||
defaultValue: "Pingvin Share",
|
||||
secret: false,
|
||||
},
|
||||
appUrl: {
|
||||
description: "On which URL Pingvin Share is available",
|
||||
type: "string",
|
||||
value: "http://localhost:3000",
|
||||
|
||||
defaultValue: "http://localhost:3000",
|
||||
secret: false,
|
||||
},
|
||||
showHomePage: {
|
||||
description: "Whether to show the home page",
|
||||
type: "boolean",
|
||||
value: "true",
|
||||
defaultValue: "true",
|
||||
secret: false,
|
||||
},
|
||||
},
|
||||
share: {
|
||||
allowRegistration: {
|
||||
description: "Whether registration is allowed",
|
||||
type: "boolean",
|
||||
value: "true",
|
||||
|
||||
defaultValue: "true",
|
||||
secret: false,
|
||||
},
|
||||
allowUnauthenticatedShares: {
|
||||
description: "Whether unauthorized users can create shares",
|
||||
type: "boolean",
|
||||
value: "false",
|
||||
|
||||
defaultValue: "false",
|
||||
secret: false,
|
||||
},
|
||||
maxSize: {
|
||||
description: "Maximum share size in bytes",
|
||||
type: "number",
|
||||
value: "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",
|
||||
value: "false",
|
||||
defaultValue: "false",
|
||||
|
||||
secret: false,
|
||||
},
|
||||
shareRecipientsSubject: {
|
||||
description:
|
||||
"Subject of the email which gets sent to the share recipients.",
|
||||
type: "string",
|
||||
value: "Files shared with you",
|
||||
defaultValue: "Files shared with you",
|
||||
},
|
||||
shareRecipientsMessage: {
|
||||
description:
|
||||
"Message which gets sent to the share recipients. {creator} and {shareUrl} will be replaced with the creator's name and the share URL.",
|
||||
type: "text",
|
||||
value:
|
||||
"Hey!\n{creator} shared some files with you. View or download the files with this link: {shareUrl}\nShared securely with Pingvin Share 🐧",
|
||||
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",
|
||||
value: "Reverse share link used",
|
||||
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",
|
||||
value:
|
||||
"Hey!\nA share was just created with your reverse share link: {shareUrl}\nShared securely with Pingvin Share 🐧",
|
||||
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",
|
||||
value: "Pingvin Share password reset",
|
||||
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",
|
||||
value:
|
||||
"Hey!\nYou requested a password reset. Click this link to reset your password: {url}\nThe link expires in a hour.\nPingvin Share 🐧",
|
||||
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",
|
||||
value: "Pingvin Share invite",
|
||||
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",
|
||||
value:
|
||||
"Hey!\nYou were invited to Pingvin Share. Click this link to accept the invite: {url}\nYour password is: {password}\nPingvin Share 🐧",
|
||||
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 🐧",
|
||||
},
|
||||
},
|
||||
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",
|
||||
value: "false",
|
||||
defaultValue: "false",
|
||||
secret: false,
|
||||
},
|
||||
host: {
|
||||
description: "Host of the SMTP server",
|
||||
type: "string",
|
||||
value: "",
|
||||
defaultValue: "",
|
||||
},
|
||||
port: {
|
||||
description: "Port of the SMTP server",
|
||||
type: "number",
|
||||
value: "0",
|
||||
defaultValue: "0",
|
||||
},
|
||||
email: {
|
||||
description: "Email address which the emails get sent from",
|
||||
type: "string",
|
||||
value: "",
|
||||
defaultValue: "",
|
||||
},
|
||||
username: {
|
||||
description: "Username of the SMTP server",
|
||||
type: "string",
|
||||
value: "",
|
||||
defaultValue: "",
|
||||
},
|
||||
password: {
|
||||
description: "Password of the SMTP server",
|
||||
type: "string",
|
||||
value: "",
|
||||
defaultValue: "",
|
||||
obscured: true,
|
||||
},
|
||||
},
|
||||
@@ -162,7 +130,15 @@ type ConfigVariables = {
|
||||
};
|
||||
};
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
const prisma = new PrismaClient({
|
||||
datasources: {
|
||||
db: {
|
||||
url:
|
||||
process.env.DATABASE_URL ||
|
||||
"file:../data/pingvin-share.db?connection_limit=1",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
async function seedConfigVariables() {
|
||||
for (const [category, configVariablesOfCategory] of Object.entries(
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from "@nestjs/common";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
import { User } from "@prisma/client";
|
||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
||||
import * as argon from "argon2";
|
||||
import * as moment from "moment";
|
||||
import { ConfigService } from "src/config/config.service";
|
||||
|
||||
@@ -8,6 +8,7 @@ import { User } from "@prisma/client";
|
||||
import * as argon from "argon2";
|
||||
import { authenticator, totp } from "otplib";
|
||||
import * as qrcode from "qrcode-svg";
|
||||
import { ConfigService } from "src/config/config.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { AuthSignInTotpDTO } from "./dto/authSignInTotp.dto";
|
||||
@@ -16,7 +17,8 @@ import { AuthSignInTotpDTO } from "./dto/authSignInTotp.dto";
|
||||
export class AuthTotpService {
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private authService: AuthService
|
||||
private authService: AuthService,
|
||||
private config: ConfigService
|
||||
) {}
|
||||
|
||||
async signInTotp(dto: AuthSignInTotpDTO) {
|
||||
@@ -95,7 +97,7 @@ export class AuthTotpService {
|
||||
|
||||
const otpURL = totp.keyuri(
|
||||
user.username || user.email,
|
||||
"pingvin-share",
|
||||
this.config.get("general.appName"),
|
||||
secret
|
||||
);
|
||||
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import * as NodeClam from "clamscan";
|
||||
import * as fs from "fs";
|
||||
import { FileService } from "src/file/file.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { CLAMAV_HOST, CLAMAV_PORT, SHARE_DIRECTORY } from "../constants";
|
||||
|
||||
const clamscanConfig = {
|
||||
clamdscan: {
|
||||
host: process.env.NODE_ENV == "docker" ? "clamav" : "127.0.0.1",
|
||||
port: 3310,
|
||||
host: CLAMAV_HOST,
|
||||
port: CLAMAV_PORT,
|
||||
localFallback: false,
|
||||
},
|
||||
preference: "clamdscan",
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class ClamScanService {
|
||||
private readonly logger = new Logger(ClamScanService.name);
|
||||
|
||||
constructor(
|
||||
private fileService: FileService,
|
||||
private prisma: PrismaService
|
||||
@@ -23,11 +25,11 @@ export class ClamScanService {
|
||||
private ClamScan: Promise<NodeClam | null> = new NodeClam()
|
||||
.init(clamscanConfig)
|
||||
.then((res) => {
|
||||
console.log("ClamAV is active");
|
||||
this.logger.log("ClamAV is active");
|
||||
return res;
|
||||
})
|
||||
.catch(() => {
|
||||
console.log("ClamAV is not active");
|
||||
this.logger.log("ClamAV is not active");
|
||||
return null;
|
||||
});
|
||||
|
||||
@@ -39,14 +41,14 @@ export class ClamScanService {
|
||||
const infectedFiles = [];
|
||||
|
||||
const files = fs
|
||||
.readdirSync(`./data/uploads/shares/${shareId}`)
|
||||
.readdirSync(`${SHARE_DIRECTORY}/${shareId}`)
|
||||
.filter((file) => file != "archive.zip");
|
||||
|
||||
for (const fileId of files) {
|
||||
const { isInfected } = await clamScan
|
||||
.isInfected(`./data/uploads/shares/${shareId}/${fileId}`)
|
||||
.isInfected(`${SHARE_DIRECTORY}/${shareId}/${fileId}`)
|
||||
.catch(() => {
|
||||
console.log("ClamAV is not active");
|
||||
this.logger.log("ClamAV is not active");
|
||||
return { isInfected: false };
|
||||
});
|
||||
|
||||
@@ -78,7 +80,7 @@ export class ClamScanService {
|
||||
},
|
||||
});
|
||||
|
||||
console.log(
|
||||
this.logger.warn(
|
||||
`Share ${shareId} deleted because it contained ${infectedFiles.length} malicious file(s)`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,10 +21,12 @@ export class ConfigService {
|
||||
|
||||
if (!configVariable) throw new Error(`Config variable ${key} not found`);
|
||||
|
||||
if (configVariable.type == "number") return parseInt(configVariable.value);
|
||||
if (configVariable.type == "boolean") return configVariable.value == "true";
|
||||
const value = configVariable.value ?? configVariable.defaultValue;
|
||||
|
||||
if (configVariable.type == "number") return parseInt(value);
|
||||
if (configVariable.type == "boolean") return value == "true";
|
||||
if (configVariable.type == "string" || configVariable.type == "text")
|
||||
return configVariable.value;
|
||||
return value;
|
||||
}
|
||||
|
||||
async getByCategory(category: string) {
|
||||
@@ -35,8 +37,9 @@ export class ConfigService {
|
||||
|
||||
return configVariables.map((variable) => {
|
||||
return {
|
||||
key: `${variable.category}.${variable.name}`,
|
||||
...variable,
|
||||
key: `${variable.category}.${variable.name}`,
|
||||
value: variable.value ?? variable.defaultValue,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -48,8 +51,9 @@ export class ConfigService {
|
||||
|
||||
return configVariables.map((variable) => {
|
||||
return {
|
||||
key: `${variable.category}.${variable.name}`,
|
||||
...variable,
|
||||
key: `${variable.category}.${variable.name}`,
|
||||
value: variable.value ?? variable.defaultValue,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -77,7 +81,9 @@ export class ConfigService {
|
||||
if (!configVariable || configVariable.locked)
|
||||
throw new NotFoundException("Config variable not found");
|
||||
|
||||
if (
|
||||
if (value === "") {
|
||||
value = null;
|
||||
} else if (
|
||||
typeof value != configVariable.type &&
|
||||
typeof value == "string" &&
|
||||
configVariable.type != "text"
|
||||
@@ -94,7 +100,7 @@ export class ConfigService {
|
||||
name: key.split(".")[1],
|
||||
},
|
||||
},
|
||||
data: { value: value.toString() },
|
||||
data: { value: value === null ? null : value.toString() },
|
||||
});
|
||||
|
||||
this.configVariables = await this.prisma.config.findMany();
|
||||
|
||||
@@ -9,10 +9,10 @@ export class AdminConfigDTO extends ConfigDTO {
|
||||
secret: boolean;
|
||||
|
||||
@Expose()
|
||||
updatedAt: Date;
|
||||
defaultValue: string;
|
||||
|
||||
@Expose()
|
||||
description: string;
|
||||
updatedAt: Date;
|
||||
|
||||
@Expose()
|
||||
obscured: boolean;
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { IsNotEmpty, IsString, ValidateIf } from "class-validator";
|
||||
import { IsNotEmpty, IsString } from "class-validator";
|
||||
|
||||
class UpdateConfigDTO {
|
||||
@IsString()
|
||||
key: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@ValidateIf((dto) => dto.value !== "")
|
||||
value: string | number | boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ export class LogoService {
|
||||
}
|
||||
|
||||
async createPWAIcons(file: Buffer) {
|
||||
const sizes = [72, 96, 128, 144, 152, 192, 384, 512];
|
||||
const sizes = [48, 72, 96, 128, 144, 152, 192, 384, 512];
|
||||
|
||||
for (const size of sizes) {
|
||||
const resized = await sharp(file).resize(size).toBuffer();
|
||||
|
||||
9
backend/src/constants.ts
Normal file
9
backend/src/constants.ts
Normal file
@@ -0,0 +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;
|
||||
@@ -1,11 +1,17 @@
|
||||
import { Injectable, InternalServerErrorException } from "@nestjs/common";
|
||||
import {
|
||||
Injectable,
|
||||
InternalServerErrorException,
|
||||
Logger,
|
||||
} from "@nestjs/common";
|
||||
import { User } from "@prisma/client";
|
||||
import * as moment from "moment";
|
||||
import * as nodemailer from "nodemailer";
|
||||
import { ConfigService } from "src/config/config.service";
|
||||
|
||||
@Injectable()
|
||||
export class EmailService {
|
||||
constructor(private config: ConfigService) {}
|
||||
private readonly logger = new Logger(EmailService.name);
|
||||
|
||||
getTransporter() {
|
||||
if (!this.config.get("smtp.enabled"))
|
||||
@@ -22,44 +28,63 @@ export class EmailService {
|
||||
});
|
||||
}
|
||||
|
||||
async sendMailToShareRecepients(
|
||||
private async sendMail(email: string, subject: string, text: string) {
|
||||
await this.getTransporter()
|
||||
.sendMail({
|
||||
from: `"${this.config.get("general.appName")}" <${this.config.get(
|
||||
"smtp.email"
|
||||
)}>`,
|
||||
to: email,
|
||||
subject,
|
||||
text,
|
||||
})
|
||||
.catch((e) => {
|
||||
this.logger.error(e);
|
||||
throw new InternalServerErrorException("Failed to send email");
|
||||
});
|
||||
}
|
||||
|
||||
async sendMailToShareRecipients(
|
||||
recipientEmail: string,
|
||||
shareId: string,
|
||||
creator?: User
|
||||
creator?: User,
|
||||
description?: string,
|
||||
expiration?: Date
|
||||
) {
|
||||
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.getTransporter().sendMail({
|
||||
from: `"${this.config.get("general.appName")}" <${this.config.get(
|
||||
"smtp.email"
|
||||
)}>`,
|
||||
to: recipientEmail,
|
||||
subject: this.config.get("email.shareRecipientsSubject"),
|
||||
text: this.config
|
||||
await this.sendMail(
|
||||
recipientEmail,
|
||||
this.config.get("email.shareRecipientsSubject"),
|
||||
this.config
|
||||
.get("email.shareRecipientsMessage")
|
||||
.replaceAll("\\n", "\n")
|
||||
.replaceAll("{creator}", creator?.username ?? "Someone")
|
||||
.replaceAll("{shareUrl}", shareUrl),
|
||||
});
|
||||
.replaceAll("{shareUrl}", shareUrl)
|
||||
.replaceAll("{desc}", description ?? "No description")
|
||||
.replaceAll(
|
||||
"{expires}",
|
||||
moment(expiration).unix() != 0
|
||||
? moment(expiration).fromNow()
|
||||
: "in: never"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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.getTransporter().sendMail({
|
||||
from: `"${this.config.get("general.appName")}" <${this.config.get(
|
||||
"smtp.email"
|
||||
)}>`,
|
||||
to: recipientEmail,
|
||||
subject: this.config.get("email.reverseShareSubject"),
|
||||
text: this.config
|
||||
await this.sendMail(
|
||||
recipientEmail,
|
||||
this.config.get("email.reverseShareSubject"),
|
||||
this.config
|
||||
.get("email.reverseShareMessage")
|
||||
.replaceAll("\\n", "\n")
|
||||
.replaceAll("{shareUrl}", shareUrl),
|
||||
});
|
||||
.replaceAll("{shareUrl}", shareUrl)
|
||||
);
|
||||
}
|
||||
|
||||
async sendResetPasswordEmail(recipientEmail: string, token: string) {
|
||||
@@ -67,47 +92,42 @@ export class EmailService {
|
||||
"general.appUrl"
|
||||
)}/auth/resetPassword/${token}`;
|
||||
|
||||
await this.getTransporter().sendMail({
|
||||
from: `"${this.config.get("general.appName")}" <${this.config.get(
|
||||
"smtp.email"
|
||||
)}>`,
|
||||
to: recipientEmail,
|
||||
subject: this.config.get("email.resetPasswordSubject"),
|
||||
text: this.config
|
||||
await this.sendMail(
|
||||
recipientEmail,
|
||||
this.config.get("email.resetPasswordSubject"),
|
||||
this.config
|
||||
.get("email.resetPasswordMessage")
|
||||
.replaceAll("{url}", resetPasswordUrl),
|
||||
});
|
||||
.replaceAll("\\n", "\n")
|
||||
.replaceAll("{url}", resetPasswordUrl)
|
||||
);
|
||||
}
|
||||
|
||||
async sendInviteEmail(recipientEmail: string, password: string) {
|
||||
const loginUrl = `${this.config.get("general.appUrl")}/auth/signIn`;
|
||||
|
||||
await this.getTransporter().sendMail({
|
||||
from: `"${this.config.get("general.appName")}" <${this.config.get(
|
||||
"smtp.email"
|
||||
)}>`,
|
||||
to: recipientEmail,
|
||||
subject: this.config.get("email.inviteSubject"),
|
||||
text: this.config
|
||||
await this.sendMail(
|
||||
recipientEmail,
|
||||
this.config.get("email.inviteSubject"),
|
||||
this.config
|
||||
.get("email.inviteMessage")
|
||||
.replaceAll("{url}", loginUrl)
|
||||
.replaceAll("{password}", password),
|
||||
});
|
||||
.replaceAll("{password}", password)
|
||||
);
|
||||
}
|
||||
|
||||
async sendTestMail(recipientEmail: string) {
|
||||
try {
|
||||
await this.getTransporter().sendMail({
|
||||
await this.getTransporter()
|
||||
.sendMail({
|
||||
from: `"${this.config.get("general.appName")}" <${this.config.get(
|
||||
"smtp.email"
|
||||
)}>`,
|
||||
to: recipientEmail,
|
||||
subject: "Test email",
|
||||
text: "This is a test email",
|
||||
})
|
||||
.catch((e) => {
|
||||
this.logger.error(e);
|
||||
throw new InternalServerErrorException(e.message);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
throw new InternalServerErrorException(e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@ export class FileController {
|
||||
) {
|
||||
const { id, name, chunkIndex, totalChunks } = query;
|
||||
|
||||
const data = body.toString().split(",")[1];
|
||||
// Data can be empty if the file is empty
|
||||
const data = body.toString().split(",")[1] ?? "";
|
||||
|
||||
return await this.fileService.create(
|
||||
data,
|
||||
@@ -51,7 +52,7 @@ export class FileController {
|
||||
const zip = this.fileService.getZip(shareId);
|
||||
res.set({
|
||||
"Content-Type": "application/zip",
|
||||
"Content-Disposition": contentDisposition(`pingvin-share-${shareId}.zip`),
|
||||
"Content-Disposition": contentDisposition(`${shareId}.zip`),
|
||||
});
|
||||
|
||||
return new StreamableFile(zip);
|
||||
|
||||
@@ -11,6 +11,7 @@ import * as fs from "fs";
|
||||
import * as mime from "mime-types";
|
||||
import { ConfigService } from "src/config/config.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { SHARE_DIRECTORY } from "../constants";
|
||||
|
||||
@Injectable()
|
||||
export class FileService {
|
||||
@@ -39,7 +40,7 @@ export class FileService {
|
||||
let diskFileSize: number;
|
||||
try {
|
||||
diskFileSize = fs.statSync(
|
||||
`./data/uploads/shares/${shareId}/${file.id}.tmp-chunk`
|
||||
`${SHARE_DIRECTORY}/${shareId}/${file.id}.tmp-chunk`
|
||||
).size;
|
||||
} catch {
|
||||
diskFileSize = 0;
|
||||
@@ -78,18 +79,18 @@ export class FileService {
|
||||
}
|
||||
|
||||
fs.appendFileSync(
|
||||
`./data/uploads/shares/${shareId}/${file.id}.tmp-chunk`,
|
||||
`${SHARE_DIRECTORY}/${shareId}/${file.id}.tmp-chunk`,
|
||||
buffer
|
||||
);
|
||||
|
||||
const isLastChunk = chunk.index == chunk.total - 1;
|
||||
if (isLastChunk) {
|
||||
fs.renameSync(
|
||||
`./data/uploads/shares/${shareId}/${file.id}.tmp-chunk`,
|
||||
`./data/uploads/shares/${shareId}/${file.id}`
|
||||
`${SHARE_DIRECTORY}/${shareId}/${file.id}.tmp-chunk`,
|
||||
`${SHARE_DIRECTORY}/${shareId}/${file.id}`
|
||||
);
|
||||
const fileSize = fs.statSync(
|
||||
`./data/uploads/shares/${shareId}/${file.id}`
|
||||
`${SHARE_DIRECTORY}/${shareId}/${file.id}`
|
||||
).size;
|
||||
await this.prisma.file.create({
|
||||
data: {
|
||||
@@ -111,9 +112,7 @@ export class FileService {
|
||||
|
||||
if (!fileMetaData) throw new NotFoundException("File not found");
|
||||
|
||||
const file = fs.createReadStream(
|
||||
`./data/uploads/shares/${shareId}/${fileId}`
|
||||
);
|
||||
const file = fs.createReadStream(`${SHARE_DIRECTORY}/${shareId}/${fileId}`);
|
||||
|
||||
return {
|
||||
metaData: {
|
||||
@@ -126,13 +125,13 @@ export class FileService {
|
||||
}
|
||||
|
||||
async deleteAllFiles(shareId: string) {
|
||||
await fs.promises.rm(`./data/uploads/shares/${shareId}`, {
|
||||
await fs.promises.rm(`${SHARE_DIRECTORY}/${shareId}`, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
|
||||
getZip(shareId: string) {
|
||||
return fs.createReadStream(`./data/uploads/shares/${shareId}/archive.zip`);
|
||||
return fs.createReadStream(`${SHARE_DIRECTORY}/${shareId}/archive.zip`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { Cron } from "@nestjs/schedule";
|
||||
import * as fs from "fs";
|
||||
import * as moment from "moment";
|
||||
import { FileService } from "src/file/file.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { ReverseShareService } from "src/reverseShare/reverseShare.service";
|
||||
import { SHARE_DIRECTORY } from "../constants";
|
||||
|
||||
@Injectable()
|
||||
export class JobsService {
|
||||
private readonly logger = new Logger(JobsService.name);
|
||||
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private reverseShareService: ReverseShareService,
|
||||
@@ -34,8 +37,9 @@ export class JobsService {
|
||||
await this.fileService.deleteAllFiles(expiredShare.id);
|
||||
}
|
||||
|
||||
if (expiredShares.length > 0)
|
||||
console.log(`job: deleted ${expiredShares.length} expired shares`);
|
||||
if (expiredShares.length > 0) {
|
||||
this.logger.log(`Deleted ${expiredShares.length} expired shares`);
|
||||
}
|
||||
}
|
||||
|
||||
@Cron("0 * * * *")
|
||||
@@ -50,10 +54,11 @@ export class JobsService {
|
||||
await this.reverseShareService.remove(expiredReverseShare.id);
|
||||
}
|
||||
|
||||
if (expiredReverseShares.length > 0)
|
||||
console.log(
|
||||
`job: deleted ${expiredReverseShares.length} expired reverse shares`
|
||||
if (expiredReverseShares.length > 0) {
|
||||
this.logger.log(
|
||||
`Deleted ${expiredReverseShares.length} expired reverse shares`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Cron("0 0 * * *")
|
||||
@@ -61,31 +66,31 @@ export class JobsService {
|
||||
let filesDeleted = 0;
|
||||
|
||||
const shareDirectories = fs
|
||||
.readdirSync("./data/uploads/shares", { withFileTypes: true })
|
||||
.readdirSync(SHARE_DIRECTORY, { withFileTypes: true })
|
||||
.filter((dirent) => dirent.isDirectory())
|
||||
.map((dirent) => dirent.name);
|
||||
|
||||
for (const shareDirectory of shareDirectories) {
|
||||
const temporaryFiles = fs
|
||||
.readdirSync(`./data/uploads/shares/${shareDirectory}`)
|
||||
.readdirSync(`${SHARE_DIRECTORY}/${shareDirectory}`)
|
||||
.filter((file) => file.endsWith(".tmp-chunk"));
|
||||
|
||||
for (const file of temporaryFiles) {
|
||||
const stats = fs.statSync(
|
||||
`./data/uploads/shares/${shareDirectory}/${file}`
|
||||
`${SHARE_DIRECTORY}/${shareDirectory}/${file}`
|
||||
);
|
||||
const isOlderThanOneDay = moment(stats.mtime)
|
||||
.add(1, "day")
|
||||
.isBefore(moment());
|
||||
|
||||
if (isOlderThanOneDay) {
|
||||
fs.rmSync(`./data/uploads/shares/${shareDirectory}/${file}`);
|
||||
fs.rmSync(`${SHARE_DIRECTORY}/${shareDirectory}/${file}`);
|
||||
filesDeleted++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`job: deleted ${filesDeleted} temporary files`);
|
||||
this.logger.log(`Deleted ${filesDeleted} temporary files`);
|
||||
}
|
||||
|
||||
@Cron("0 * * * *")
|
||||
@@ -107,7 +112,8 @@ export class JobsService {
|
||||
const deletedTokensCount =
|
||||
refreshTokenCount + loginTokenCount + resetPasswordTokenCount;
|
||||
|
||||
if (deletedTokensCount > 0)
|
||||
console.log(`job: deleted ${deletedTokensCount} expired refresh tokens`);
|
||||
if (deletedTokensCount > 0) {
|
||||
this.logger.log(`Deleted ${deletedTokensCount} expired refresh tokens`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import * as bodyParser from "body-parser";
|
||||
import * as cookieParser from "cookie-parser";
|
||||
import * as fs from "fs";
|
||||
import { AppModule } from "./app.module";
|
||||
import { DATA_DIRECTORY } from "./constants";
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
||||
@@ -16,7 +17,9 @@ async function bootstrap() {
|
||||
app.use(cookieParser());
|
||||
app.set("trust proxy", true);
|
||||
|
||||
await fs.promises.mkdir("./data/uploads/_temp", { recursive: true });
|
||||
await fs.promises.mkdir(`${DATA_DIRECTORY}/uploads/_temp`, {
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
app.setGlobalPrefix("api");
|
||||
|
||||
@@ -30,6 +33,6 @@ async function bootstrap() {
|
||||
SwaggerModule.setup("api/swagger", app, document);
|
||||
}
|
||||
|
||||
await app.listen(8080);
|
||||
await app.listen(parseInt(process.env.PORT) || 8080);
|
||||
}
|
||||
bootstrap();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { DATABASE_URL } from "../constants";
|
||||
|
||||
@Injectable()
|
||||
export class PrismaService extends PrismaClient {
|
||||
@@ -7,7 +8,7 @@ export class PrismaService extends PrismaClient {
|
||||
super({
|
||||
datasources: {
|
||||
db: {
|
||||
url: "file:../data/pingvin-share.db?connection_limit=1",
|
||||
url: DATABASE_URL,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,13 @@
|
||||
import { Expose, plainToClass } from "class-transformer";
|
||||
import { Expose, plainToClass, Type } from "class-transformer";
|
||||
import { ShareDTO } from "./share.dto";
|
||||
import { FileDTO } from "../../file/dto/file.dto";
|
||||
import { OmitType } from "@nestjs/swagger";
|
||||
|
||||
export class MyShareDTO extends ShareDTO {
|
||||
export class MyShareDTO extends OmitType(ShareDTO, [
|
||||
"files",
|
||||
"from",
|
||||
"fromList",
|
||||
] as const) {
|
||||
@Expose()
|
||||
views: number;
|
||||
|
||||
@@ -11,6 +17,10 @@ export class MyShareDTO extends ShareDTO {
|
||||
@Expose()
|
||||
recipients: string[];
|
||||
|
||||
@Expose()
|
||||
@Type(() => OmitType(FileDTO, ["share", "from"] as const))
|
||||
files: Omit<FileDTO, "share" | "from">[];
|
||||
|
||||
from(partial: Partial<MyShareDTO>) {
|
||||
return plainToClass(MyShareDTO, partial, { excludeExtraneousValues: true });
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { EmailService } from "src/email/email.service";
|
||||
import { FileService } from "src/file/file.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { ReverseShareService } from "src/reverseShare/reverseShare.service";
|
||||
import { SHARE_DIRECTORY } from "../constants";
|
||||
import { CreateShareDTO } from "./dto/createShare.dto";
|
||||
|
||||
@Injectable()
|
||||
@@ -65,7 +66,7 @@ export class ShareService {
|
||||
}
|
||||
}
|
||||
|
||||
fs.mkdirSync(`./data/uploads/shares/${share.id}`, {
|
||||
fs.mkdirSync(`${SHARE_DIRECTORY}/${share.id}`, {
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
@@ -99,11 +100,11 @@ export class ShareService {
|
||||
}
|
||||
|
||||
async createZip(shareId: string) {
|
||||
const path = `./data/uploads/shares/${shareId}`;
|
||||
const path = `${SHARE_DIRECTORY}/${shareId}`;
|
||||
|
||||
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`);
|
||||
|
||||
@@ -142,12 +143,14 @@ export class ShareService {
|
||||
this.prisma.share.update({ where: { id }, data: { isZipReady: true } })
|
||||
);
|
||||
|
||||
// Send email for each recepient
|
||||
for (const recepient of share.recipients) {
|
||||
await this.emailService.sendMailToShareRecepients(
|
||||
recepient.email,
|
||||
// Send email for each recipient
|
||||
for (const recipient of share.recipients) {
|
||||
await this.emailService.sendMailToShareRecipients(
|
||||
recipient.email,
|
||||
share.id,
|
||||
share.creator
|
||||
share.creator,
|
||||
share.description,
|
||||
share.expiration
|
||||
);
|
||||
}
|
||||
|
||||
@@ -163,7 +166,7 @@ export class ShareService {
|
||||
}
|
||||
|
||||
// Check if any file is malicious with ClamAV
|
||||
this.clamScanService.checkAndRemove(share.id);
|
||||
void this.clamScanService.checkAndRemove(share.id);
|
||||
|
||||
if (share.reverseShare) {
|
||||
await this.prisma.reverseShare.update({
|
||||
@@ -172,7 +175,7 @@ export class ShareService {
|
||||
});
|
||||
}
|
||||
|
||||
return await this.prisma.share.update({
|
||||
return this.prisma.share.update({
|
||||
where: { id },
|
||||
data: { uploadLocked: true },
|
||||
});
|
||||
@@ -192,17 +195,15 @@ export class ShareService {
|
||||
orderBy: {
|
||||
expiration: "desc",
|
||||
},
|
||||
include: { recipients: true },
|
||||
include: { recipients: true, files: true },
|
||||
});
|
||||
|
||||
const sharesWithEmailRecipients = shares.map((share) => {
|
||||
return shares.map((share) => {
|
||||
return {
|
||||
...share,
|
||||
recipients: share.recipients.map((recipients) => recipients.email),
|
||||
};
|
||||
});
|
||||
|
||||
return sharesWithEmailRecipients;
|
||||
}
|
||||
|
||||
async get(id: string): Promise<any> {
|
||||
@@ -222,7 +223,7 @@ export class ShareService {
|
||||
throw new NotFoundException("Share not found");
|
||||
return {
|
||||
...share,
|
||||
hasPassword: share.security?.password ? true : false,
|
||||
hasPassword: !!share.security?.password,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -278,7 +279,7 @@ export class ShareService {
|
||||
share?.security?.password &&
|
||||
!(await argon.verify(share.security.password, password))
|
||||
) {
|
||||
throw new ForbiddenException("Wrong password");
|
||||
throw new ForbiddenException("Wrong password", "wrong_password");
|
||||
}
|
||||
|
||||
if (share.security?.maxViews && share.security.maxViews <= share.views) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
||||
import * as argon from "argon2";
|
||||
import * as crypto from "crypto";
|
||||
import { EmailService } from "src/email/email.service";
|
||||
@@ -29,7 +29,7 @@ export class UserSevice {
|
||||
if (!dto.password) {
|
||||
const randomPassword = crypto.randomUUID();
|
||||
hash = await argon.hash(randomPassword);
|
||||
this.emailService.sendInviteEmail(dto.email, randomPassword);
|
||||
await this.emailService.sendInviteEmail(dto.email, randomPassword);
|
||||
} else {
|
||||
hash = await argon.hash(dto.password);
|
||||
}
|
||||
|
||||
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"
|
||||
95
docs/CONTRIBUTING.es.md
Normal file
95
docs/CONTRIBUTING.es.md
Normal file
@@ -0,0 +1,95 @@
|
||||
_Leer esto en otro idioma: [Inglés](/CONTRIBUTING.md), [Español](/docs/CONTRIBUTING.es.md), [Chino Simplificado](/docs/CONTRIBUTING.zh-cn.md)_
|
||||
|
||||
---
|
||||
|
||||
# Contribuyendo
|
||||
|
||||
¡Nos ❤️ encantaría que contribuyas a Pingvin Share y nos ayudes a hacerlo mejor! Todas las contribuciones son bienvenidas, incluyendo problemas, sugerencias, _pull requests_ y más.
|
||||
|
||||
## Para comenzar
|
||||
|
||||
Si encontraste un error, tienes una sugerencia o algo más, simplemente crea un problema (issue) en GitHub y nos pondremos en contacto contigo 😊.
|
||||
|
||||
## Para hacer una Pull Request
|
||||
|
||||
Antes de enviar la pull request para su revisión, asegúrate de que:
|
||||
|
||||
- El nombre de la pull request sigue las [especificaciones de Commits Convencionales](https://www.conventionalcommits.org/):
|
||||
|
||||
`<tipo>[ámbito opcional]: <descripción>`
|
||||
|
||||
ejemplo:
|
||||
|
||||
```
|
||||
feat(share): agregar protección con contraseña
|
||||
```
|
||||
|
||||
Donde `tipo` puede ser:
|
||||
|
||||
- **feat** - es una nueva función
|
||||
- **doc** - cambios solo en la documentación
|
||||
- **fix** - una corrección de error
|
||||
- **refactor** - cambios en el código que no solucionan un error ni agregan una función
|
||||
|
||||
- Tu pull requests tiene una descripción detallada.
|
||||
|
||||
- Ejecutaste `npm run format` para formatear el código.
|
||||
|
||||
<details>
|
||||
<summary>¿No sabes como crear una pull request? Aprende cómo crear una pull request</summary>
|
||||
|
||||
1. Crea un fork del repositorio haciendo clic en el botón `Fork` en el repositorio de Pingvin Share.
|
||||
|
||||
2. Clona tu fork en tu máquina con `git clone`.
|
||||
|
||||
```
|
||||
$ git clone https://github.com/[your_username]/pingvin-share
|
||||
```
|
||||
|
||||
3. Trabajar - hacer commit - repetir
|
||||
|
||||
4. Haz un `push` de tus cambios a GitHub.
|
||||
|
||||
```
|
||||
$ git push origin [nombre_de_tu_nueva_rama]
|
||||
```
|
||||
|
||||
5. Envía tus cambios para su revisión. Si vas a tu repositorio en GitHub, verás un botón `Comparar y crear pull requests`. Haz clic en ese botón.
|
||||
6. Inicia una Pull Request
|
||||
7. Ahora envía la pull requests y haz clic en `Crear pull requests`
|
||||
8. Espera a que alguien revise tu solicitud y apruebe o rechace tus cambios. Puedes ver los comentarios en la página de la solicitud en GitHub.
|
||||
|
||||
</details>
|
||||
|
||||
## Instalación del proyecto
|
||||
|
||||
Pingvin Share consiste de un frontend y un backend.
|
||||
|
||||
### Backend
|
||||
|
||||
El backend está hecho con [Nest.js](https://nestjs.com) y usa Typescript.
|
||||
|
||||
#### Instalación
|
||||
|
||||
1. Abrimos la carpeta `backend`
|
||||
2. Instalamos las dependencias con `npm install`
|
||||
3. Haz un `push` del esquema de la base de datos a la base de datos ejecutando `npx prisma db push`
|
||||
4. Rellena la base de datos ejecutando `npx prisma db seed`
|
||||
5. Inicia el backend con `npm run dev`
|
||||
|
||||
### Frontend
|
||||
|
||||
El frontend está hecho con [Next.js](https://nextjs.org) y usa Typescript.
|
||||
|
||||
#### Instalación
|
||||
|
||||
1. Primero inicia el backend
|
||||
2. Abre la carpeta `frontend`
|
||||
3. Instala las dependencias con `npm install`
|
||||
4. Inicia el frontend con `npm run dev`
|
||||
|
||||
¡Ya está todo listo!
|
||||
|
||||
### Testing
|
||||
|
||||
Por el momento, solo tenemos pruebas para el backend. Para ejecutar estas pruebas, debes ejecutar el comando `npm run test:system` en la carpeta del backend.
|
||||
97
docs/CONTRIBUTING.zh-cn.md
Normal file
97
docs/CONTRIBUTING.zh-cn.md
Normal file
@@ -0,0 +1,97 @@
|
||||
_选择合适的语言阅读: [西班牙语](/docs/CONTRIBUTING.es.md), [英语](/CONTRIBUTING.md), [简体中文](/docs/CONTRIBUTING.zh-cn.md)_
|
||||
|
||||
---
|
||||
|
||||
# 提交贡献
|
||||
|
||||
我们非常感谢你 ❤️ 为 Pingvin Share 提交贡献使其变得更棒! 欢迎任何形式的贡献,包括 issues, 建议, PRs 和其他形式
|
||||
|
||||
## 小小的开始
|
||||
|
||||
你找到了一个 bug,有新特性建议或者其他提议,请在 GitHub 建立一个 issue 以便我和你联络 😊
|
||||
|
||||
## 提交一个 Pull Request
|
||||
|
||||
在你提交 PR 前请确保
|
||||
|
||||
- PR 的名字遵守 [Conventional Commits specification](https://www.conventionalcommits.org):
|
||||
|
||||
`<type>[optional scope]: <description>`
|
||||
|
||||
例如:
|
||||
|
||||
```
|
||||
feat(share): add password protection
|
||||
```
|
||||
|
||||
`TYPE` 可以是:
|
||||
|
||||
- **feat** - 这是一个新特性 feature
|
||||
- **doc** - 仅仅改变了文档部分 documentation
|
||||
- **fix** - 修复了一个 bug
|
||||
- **refactor** - 更新了代码,但是并非出于增加新特性 feature 或修复 bug 的目的
|
||||
|
||||
- 请在 PR 中附详细的解释说明
|
||||
- 使用 `npm run format` 格式化你的代码
|
||||
|
||||
<details>
|
||||
<summary>不知道怎么发起一个 PR? 点开了解怎么发起一个 PR </summary>
|
||||
|
||||
1. 点击 Pingvin Share 仓库的 `Fork` 按钮,复制一份你的仓库
|
||||
|
||||
2. 通过 `git clone` 将你的仓库克隆到本地
|
||||
|
||||
```
|
||||
$ git clone https://github.com/[你的用户名]/pingvin-share
|
||||
```
|
||||
|
||||
3. 进行你的修改 - 提交 commit 你的修改 - 重复直到完成
|
||||
|
||||
4. 将你的修改提交到 GitHub
|
||||
|
||||
```
|
||||
$ git push origin [你的新分支的名字]
|
||||
```
|
||||
|
||||
5. 提交你的代码以便代码审查
|
||||
|
||||
如果你进入你 fork 的 Github 仓库,你会看到一个 `Compare & pull request` 按钮,点击该按钮
|
||||
|
||||
6. 发起一个 PR
|
||||
7. 点击 `Create pull request` 来提交你的 PR
|
||||
8. 等待代码审查,通过或以某些原因拒绝
|
||||
|
||||
</details>
|
||||
|
||||
## 配置开发项目
|
||||
|
||||
Pingvin Share 包括前端和后端部分
|
||||
|
||||
### 后端
|
||||
|
||||
后端使用 [Nest.js](https://nestjs.com) 建立,使用 Typescript
|
||||
|
||||
#### 搭建
|
||||
|
||||
1. 打开 `backend` 文件夹
|
||||
2. 使用 `npm install` 安装依赖
|
||||
3. 通过 `npx prisma db push` 配置数据库结构
|
||||
4. 通过 `npx prisma db seed` 初始化数据库数据
|
||||
5. 通过 `npm run dev` 启动后端
|
||||
|
||||
### 前端
|
||||
|
||||
后端使用 [Next.js](https://nextjs.org) 建立,使用 Typescript
|
||||
|
||||
#### 搭建
|
||||
|
||||
1. 首先启动后端
|
||||
2. 打开 `frontend` 文件夹
|
||||
3. 通过 `npm install` 安装依赖
|
||||
4. 通过 `npm run dev` 启动前端
|
||||
|
||||
开发项目配置完成
|
||||
|
||||
### 测试
|
||||
|
||||
目前阶段我们只有后端的系统测试,在 `backend` 文件夹运行 `npm run test:system` 来执行系统测试
|
||||
128
docs/README.es.md
Normal file
128
docs/README.es.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# <div align="center"><img src="https://user-images.githubusercontent.com/58886915/166198400-c2134044-1198-4647-a8b6-da9c4a204c68.svg" width="40"/> </br>Pingvin Share</div>
|
||||
|
||||
---
|
||||
|
||||
_Leer esto en otro idioma: [Inglés](/README.md), [Español](/docs/README.es.md), [Chino Simplificado](/docs/README.zh-cn.md)_
|
||||
|
||||
---
|
||||
|
||||
Pingvin Share es una plataforma de intercambio de archivos autoalojada y una alternativa a WeTransfer.
|
||||
|
||||
## ✨ Características
|
||||
|
||||
- Compartir archivos utilizando un enlace
|
||||
- Tamaño de archivo ilimitado (unicamente restringido por el espacio en disco)
|
||||
- Establecer una fecha de caducidad para los recursos compartidos
|
||||
- Uso compartido seguro con límites de visitantes y contraseñas
|
||||
- Destinatarios de correo electrónico
|
||||
- Integración con ClamAV para escaneos de seguridad
|
||||
|
||||
## 🐧 Conoce Pingvin Share
|
||||
|
||||
- [Demo](https://pingvin-share.dev.eliasschneider.com)
|
||||
- [Reseña por DB Tech](https://www.youtube.com/watch?v=rWwNeZCOPJA)
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/58886915/225038319-b2ef742c-3a74-4eb6-9689-4207a36842a4.png" width="700"/>
|
||||
|
||||
## ⌨️ Instalación
|
||||
|
||||
> Nota: Pingvin Share está en sus primeras etapas y puede contener errores.
|
||||
|
||||
### Instalación con Docker (recomendada)
|
||||
|
||||
1. Descarge el archivo `docker-compose.yml`
|
||||
2. Ejecute `docker-compose up -d`
|
||||
|
||||
El sitio web ahora está esperando conexiones en `http://localhost:3000`, ¡diviértase usando Pingvin Share 🐧!
|
||||
|
||||
### Instalación autónoma
|
||||
|
||||
Herramientas requeridas:
|
||||
|
||||
- [Node.js](https://nodejs.org/en/download/) >= 16
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [pm2](https://pm2.keymetrics.io/) para ejecutar Pingvin Share en segundo plano
|
||||
|
||||
```bash
|
||||
git clone https://github.com/stonith404/pingvin-share
|
||||
cd pingvin-share
|
||||
|
||||
# Consultar la última versión
|
||||
git fetch --tags && git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
|
||||
|
||||
# Iniciar el backend
|
||||
cd backend
|
||||
npm install
|
||||
npm run build
|
||||
pm2 start --name="pingvin-share-backend" npm -- run prod
|
||||
|
||||
# Iniciar el frontend
|
||||
cd ../frontend
|
||||
npm install
|
||||
npm run build
|
||||
pm2 start --name="pingvin-share-frontend" npm -- run start
|
||||
```
|
||||
|
||||
El sitio web ahora está esperando conexiones en `http://localhost:3000`, ¡diviértase usando Pingvin Share 🐧!
|
||||
|
||||
### Integraciones
|
||||
|
||||
#### ClamAV (Unicamente con Docker)
|
||||
|
||||
ClamAV se utiliza para escanear los recursos compartidos en busca de archivos maliciosos y eliminarlos si los encuentra.
|
||||
|
||||
1. Añade el contenedor ClamAV al stack de Docker Compose (ver `docker-compose.yml`) e inicie el contenedor.
|
||||
2. Docker esperará a que ClamAV se inicie antes de iniciar Pingvin Share. Esto puede tardar uno o dos minutos.
|
||||
3. Los registros de Pingvin Share ahora deberían decir "ClamAV está activo".
|
||||
|
||||
Por favor, ten en cuenta que ClamAV necesita muchos [recursos](https://docs.clamav.net/manual/Installing/Docker.html#memory-ram-requirements).
|
||||
|
||||
### Recursos adicionales
|
||||
|
||||
- [Instalación en Synology NAS (Inglés)](https://mariushosting.com/how-to-install-pingvin-share-on-your-synology-nas/)
|
||||
|
||||
### Actualizar a una nueva versión
|
||||
|
||||
Dado que Pingvin Share se encuentra en una fase inicial, consulte las notas de la versión para conocer los cambios de última hora antes de actualizar.
|
||||
|
||||
#### Docker
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
#### Instalación autónoma
|
||||
|
||||
1. Deten la aplicación en ejecución
|
||||
|
||||
```bash
|
||||
pm2 stop pingvin-share-backend pingvin-share-frontend
|
||||
```
|
||||
|
||||
2. Repite los pasos de la [guía de instalación](#instalación-autonoma) excepto el paso de `git clone`.
|
||||
|
||||
```bash
|
||||
cd pingvin-share
|
||||
|
||||
# Consultar la última versión
|
||||
git fetch --tags && git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
|
||||
|
||||
# Iniciar el backend
|
||||
cd backend
|
||||
npm run build
|
||||
pm2 restart pingvin-share-backend
|
||||
|
||||
# Iniciar frontend
|
||||
cd ../frontend
|
||||
npm run build
|
||||
pm2 restart pingvin-share-frontend
|
||||
```
|
||||
|
||||
### Marca personalizada
|
||||
|
||||
Puedes cambiar el nombre y el logotipo de la aplicación visitando la página de configuración de administrador.
|
||||
|
||||
## 🖤 Contribuye
|
||||
|
||||
¡Eres bienvenido a contribuir a Pingvin Share! Sige la [guía de contribución](/CONTRIBUTING.md) para empezar.
|
||||
126
docs/README.zh-cn.md
Normal file
126
docs/README.zh-cn.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# <div align="center"><img src="https://user-images.githubusercontent.com/58886915/166198400-c2134044-1198-4647-a8b6-da9c4a204c68.svg" width="40"/> </br>Pingvin Share</div>
|
||||
|
||||
---
|
||||
|
||||
_选择合适的语言阅读: [西班牙语](/docs/README.es.md), [英语](/README.md), [简体中文](/docs/README.zh-cn.md)_
|
||||
|
||||
---
|
||||
|
||||
Pingvin Share 是一个可自建的文件分享平台,是 WeTransfer 的一个替代品
|
||||
|
||||
## ✨ 特性
|
||||
|
||||
- 通过可自定义后缀的链接分享文件
|
||||
- 可自定义任意大小的文件上传限制 (受制于托管所在的硬盘大小)
|
||||
- 对共享链接设置有效期限
|
||||
- 对共享链接设置访问次数和访问密码
|
||||
- 通过邮件自动发送共享链接
|
||||
- 整合 ClamAV 进行反病毒检查
|
||||
|
||||
## 🐧 了解一下 Pingvin Share
|
||||
|
||||
- [示例网站](https://pingvin-share.dev.eliasschneider.com)
|
||||
- [DB Tech 推荐视频](https://www.youtube.com/watch?v=rWwNeZCOPJA)
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/58886915/225038319-b2ef742c-3a74-4eb6-9689-4207a36842a4.png" width="700"/>
|
||||
|
||||
## ⌨️ 自建指南
|
||||
|
||||
> 注意:Pingvin Share 仍处于开发阶段并且可能存在 bugs
|
||||
|
||||
### Docker 部署 (推荐)
|
||||
|
||||
1. 下载 `docker-compose.yml`
|
||||
2. 运行命令 `docker-compose up -d`
|
||||
|
||||
现在网站运行在 `http://localhost:3000`,尝试一下你本地的 Pingvin Share 🐧!
|
||||
|
||||
### Stand-alone 部署
|
||||
|
||||
必须的依赖:
|
||||
|
||||
- [Node.js](https://nodejs.org/en/download/) >= 16
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [pm2](https://pm2.keymetrics.io/) 用于后台运行 Pingvin Share
|
||||
|
||||
```bash
|
||||
git clone https://github.com/stonith404/pingvin-share
|
||||
cd pingvin-share
|
||||
|
||||
# 获取最新的版本
|
||||
git fetch --tags && git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
|
||||
|
||||
# 启动后端 backend
|
||||
cd backend
|
||||
npm install
|
||||
npm run build
|
||||
pm2 start --name="pingvin-share-backend" npm -- run prod
|
||||
|
||||
# 启动前端 frontend
|
||||
cd ../frontend
|
||||
npm install
|
||||
npm run build
|
||||
pm2 start --name="pingvin-share-frontend" npm -- run start
|
||||
```
|
||||
|
||||
现在网站运行在 `http://localhost:3000`,尝试一下你本地的 Pingvin Share 🐧!
|
||||
|
||||
### 整合组件
|
||||
|
||||
#### ClamAV (仅限 Docker 部署)
|
||||
|
||||
扫描上传文件中是否存在可疑文件,如果存在 ClamAV 会自动移除
|
||||
|
||||
1. 在 docker-compose 配置中添加 ClamAV 容器 (见 `docker-compose.yml` 注释部分) 并启动容器
|
||||
2. Docker 会在启动 Pingvin Share 前启动 ClamAV,也许会花费 1-2 分钟
|
||||
3. Pingvin Share 日志中应该有 "ClamAV is active"
|
||||
|
||||
请注意 ClamAV 会消耗很多 [系统资源(特别是内存)](https://docs.clamav.net/manual/Installing/Docker.html#memory-ram-requirements)
|
||||
|
||||
### 更多资源
|
||||
|
||||
- [群晖 NAS 配置](https://mariushosting.com/how-to-install-pingvin-share-on-your-synology-nas/)
|
||||
|
||||
### 升级
|
||||
|
||||
因为 Pingvin Share 仍处在开发阶段,在升级前请务必阅读 release notes 避免不可逆的改变
|
||||
|
||||
#### Docker 升级
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
#### Stand-alone 升级
|
||||
|
||||
1. 停止正在运行的 app
|
||||
```bash
|
||||
pm2 stop pingvin-share-backend pingvin-share-frontend
|
||||
```
|
||||
2. 重复 [installation guide](#stand-alone-installation) 中的步骤,除了 `git clone` 这一步
|
||||
|
||||
```bash
|
||||
cd pingvin-share
|
||||
|
||||
# 获取最新的版本
|
||||
git fetch --tags && git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
|
||||
|
||||
# 启动后端 backend
|
||||
cd backend
|
||||
npm run build
|
||||
pm2 restart pingvin-share-backend
|
||||
|
||||
# 启动前端 frontend
|
||||
cd ../frontend
|
||||
npm run build
|
||||
pm2 restart pingvin-share-frontend
|
||||
```
|
||||
|
||||
### 自定义品牌
|
||||
|
||||
你可以在管理员配置页面改变网站的名字和 logo
|
||||
|
||||
## 🖤 提交贡献
|
||||
|
||||
非常欢迎向 Pingvin Share 提交贡献! 请阅读 [contribution guide](/CONTRIBUTING.md) 来提交你的贡献
|
||||
@@ -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")({
|
||||
@@ -19,4 +18,7 @@ module.exports = withPWA({
|
||||
output: "standalone", env: {
|
||||
VERSION: version,
|
||||
},
|
||||
serverRuntimeConfig: {
|
||||
apiURL: process.env.API_URL ?? 'http://localhost:8080',
|
||||
},
|
||||
});
|
||||
|
||||
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.12.0",
|
||||
"version": "0.17.2",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"start": "node .next/standalone/server.js",
|
||||
"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,19 +1,21 @@
|
||||
import {
|
||||
Button,
|
||||
Center,
|
||||
Col,
|
||||
Grid,
|
||||
Group,
|
||||
Image,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
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";
|
||||
|
||||
@@ -26,8 +28,9 @@ const showEnableTotpModal = (
|
||||
password: string;
|
||||
}
|
||||
) => {
|
||||
const t = translateOutsideContext();
|
||||
return modals.openModal({
|
||||
title: <Title order={4}>Enable TOTP</Title>,
|
||||
title: t("account.modal.totp.title"),
|
||||
children: (
|
||||
<CreateEnableTotpModal options={options} refreshUser={refreshUser} />
|
||||
),
|
||||
@@ -46,6 +49,7 @@ const CreateEnableTotpModal = ({
|
||||
refreshUser: () => {};
|
||||
}) => {
|
||||
const modals = useModals();
|
||||
const t = useTranslate();
|
||||
|
||||
const validationSchema = yup.object().shape({
|
||||
code: yup
|
||||
@@ -67,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);
|
||||
@@ -85,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;
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Divider, Flex, Progress, Stack, Text } from "@mantine/core";
|
||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||
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";
|
||||
|
||||
const showShareInformationsModal = (
|
||||
modals: ModalsContextProps,
|
||||
share: MyShare,
|
||||
appUrl: string,
|
||||
maxShareSize: number
|
||||
) => {
|
||||
const t = translateOutsideContext();
|
||||
const link = `${appUrl}/s/${share.id}`;
|
||||
|
||||
let shareSize: number = 0;
|
||||
for (let file of share.files as FileMetaData[])
|
||||
shareSize += parseInt(file.size);
|
||||
|
||||
const formattedShareSize = byteToHumanSizeString(shareSize);
|
||||
const formattedMaxShareSize = byteToHumanSizeString(maxShareSize);
|
||||
const shareSizeProgress = (shareSize / maxShareSize) * 100;
|
||||
|
||||
const formattedCreatedAt = moment(share.createdAt).format("LLL");
|
||||
const formattedExpiration =
|
||||
moment(share.expiration).unix() === 0
|
||||
? "Never"
|
||||
: moment(share.expiration).format("LLL");
|
||||
|
||||
return modals.openModal({
|
||||
title: t("account.shares.modal.share-informations"),
|
||||
|
||||
children: (
|
||||
<Stack align="stretch" spacing="md">
|
||||
<Text size="sm" color="lightgray">
|
||||
<b>
|
||||
<FormattedMessage id="account.shares.table.id" />:{" "}
|
||||
</b>
|
||||
{share.id}
|
||||
</Text>
|
||||
|
||||
<Text size="sm" color="lightgray">
|
||||
<b>
|
||||
<FormattedMessage id="account.shares.table.description" />:{" "}
|
||||
</b>
|
||||
{share.description || "No description"}
|
||||
</Text>
|
||||
|
||||
<Text size="sm" color="lightgray">
|
||||
<b>
|
||||
<FormattedMessage id="account.shares.table.createdAt" />:{" "}
|
||||
</b>
|
||||
{formattedCreatedAt}
|
||||
</Text>
|
||||
|
||||
<Text size="sm" color="lightgray">
|
||||
<b>
|
||||
<FormattedMessage id="account.shares.table.expiresAt" />:{" "}
|
||||
</b>
|
||||
{formattedExpiration}
|
||||
</Text>
|
||||
<Divider />
|
||||
<CopyTextField link={link} />
|
||||
<Divider />
|
||||
<Text size="sm" color="lightgray">
|
||||
<b>
|
||||
<FormattedMessage id="account.shares.table.size" />:{" "}
|
||||
</b>
|
||||
{formattedShareSize} / {formattedMaxShareSize} (
|
||||
{shareSizeProgress.toFixed(1)}%)
|
||||
</Text>
|
||||
|
||||
<Flex align="center" justify="center">
|
||||
{shareSize / maxShareSize < 0.1 && (
|
||||
<Text size="xs" color="lightgray" style={{ marginRight: "4px" }}>
|
||||
{formattedShareSize}
|
||||
</Text>
|
||||
)}
|
||||
<Progress
|
||||
value={shareSizeProgress}
|
||||
label={shareSize / maxShareSize >= 0.1 ? formattedShareSize : ""}
|
||||
style={{ width: shareSize / maxShareSize < 0.1 ? "70%" : "80%" }}
|
||||
size="xl"
|
||||
radius="xl"
|
||||
/>
|
||||
<Text size="xs" color="lightgray" style={{ marginLeft: "4px" }}>
|
||||
{formattedMaxShareSize}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Stack>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
export default showShareInformationsModal;
|
||||
@@ -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} />
|
||||
|
||||
@@ -18,10 +18,13 @@ const AdminConfigInput = ({
|
||||
}) => {
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
stringValue: configVariable.value,
|
||||
textValue: configVariable.value,
|
||||
numberValue: parseInt(configVariable.value),
|
||||
booleanValue: configVariable.value == "true",
|
||||
stringValue: configVariable.value ?? configVariable.defaultValue,
|
||||
textValue: configVariable.value ?? configVariable.defaultValue,
|
||||
numberValue: parseInt(
|
||||
configVariable.value ?? configVariable.defaultValue
|
||||
),
|
||||
booleanValue:
|
||||
(configVariable.value ?? configVariable.defaultValue) == "true",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -35,29 +38,38 @@ const AdminConfigInput = ({
|
||||
{configVariable.type == "string" &&
|
||||
(configVariable.obscured ? (
|
||||
<PasswordInput
|
||||
style={{ width: "100%" }}
|
||||
style={{
|
||||
width: "100%",
|
||||
}}
|
||||
{...form.getInputProps("stringValue")}
|
||||
onChange={(e) => onValueChange(configVariable, e.target.value)}
|
||||
/>
|
||||
) : (
|
||||
<TextInput
|
||||
style={{ width: "100%" }}
|
||||
style={{
|
||||
width: "100%",
|
||||
}}
|
||||
{...form.getInputProps("stringValue")}
|
||||
placeholder={configVariable.defaultValue}
|
||||
onChange={(e) => onValueChange(configVariable, e.target.value)}
|
||||
/>
|
||||
))}
|
||||
|
||||
{configVariable.type == "text" && (
|
||||
<Textarea
|
||||
style={{ width: "100%" }}
|
||||
style={{
|
||||
width: "100%",
|
||||
}}
|
||||
autosize
|
||||
{...form.getInputProps("textValue")}
|
||||
placeholder={configVariable.defaultValue}
|
||||
onChange={(e) => onValueChange(configVariable, e.target.value)}
|
||||
/>
|
||||
)}
|
||||
{configVariable.type == "number" && (
|
||||
<NumberInput
|
||||
{...form.getInputProps("numberValue")}
|
||||
placeholder={configVariable.defaultValue}
|
||||
onChange={(number) => onValueChange(configVariable, number)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -5,11 +5,12 @@ import {
|
||||
Stack,
|
||||
Switch,
|
||||
TextInput,
|
||||
Title,
|
||||
} 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";
|
||||
|
||||
@@ -19,7 +20,7 @@ const showCreateUserModal = (
|
||||
getUsers: () => void
|
||||
) => {
|
||||
return modals.openModal({
|
||||
title: <Title order={5}>Create user</Title>,
|
||||
title: "Create user",
|
||||
children: (
|
||||
<Body modals={modals} smtpEnabled={smtpEnabled} getUsers={getUsers} />
|
||||
),
|
||||
@@ -35,6 +36,7 @@ const Body = ({
|
||||
smtpEnabled: boolean;
|
||||
getUsers: () => void;
|
||||
}) => {
|
||||
const t = useTranslate();
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
username: "",
|
||||
@@ -45,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(),
|
||||
})
|
||||
),
|
||||
});
|
||||
@@ -66,26 +73,33 @@ 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",
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{form.values.setPasswordManually ||
|
||||
(!smtpEnabled && (
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
))}
|
||||
{(form.values.setPasswordManually || !smtpEnabled) && (
|
||||
<PasswordInput
|
||||
label={t("admin.users.modal.create.password")}
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
)}
|
||||
<Switch
|
||||
styles={{
|
||||
body: {
|
||||
@@ -95,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>
|
||||
|
||||
@@ -6,11 +6,14 @@ import {
|
||||
Stack,
|
||||
Switch,
|
||||
TextInput,
|
||||
Title,
|
||||
} 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";
|
||||
@@ -20,8 +23,9 @@ const showUpdateUserModal = (
|
||||
user: User,
|
||||
getUsers: () => void
|
||||
) => {
|
||||
const t = translateOutsideContext();
|
||||
return modals.openModal({
|
||||
title: <Title order={5}>Update {user.username}</Title>,
|
||||
title: t("admin.users.edit.update.title", { username: user.username }),
|
||||
children: <Body user={user} modals={modals} getUsers={getUsers} />,
|
||||
});
|
||||
};
|
||||
@@ -35,6 +39,8 @@ const Body = ({
|
||||
user: User;
|
||||
getUsers: () => void;
|
||||
}) => {
|
||||
const t = useTranslate();
|
||||
|
||||
const accountForm = useForm({
|
||||
initialValues: {
|
||||
username: user.username,
|
||||
@@ -43,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 })),
|
||||
})
|
||||
),
|
||||
});
|
||||
@@ -55,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 })),
|
||||
})
|
||||
),
|
||||
});
|
||||
@@ -76,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) => {
|
||||
@@ -98,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>
|
||||
@@ -117,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>
|
||||
|
||||
41
frontend/src/components/core/SortIcon.tsx
Normal file
41
frontend/src/components/core/SortIcon.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { ActionIcon } from "@mantine/core";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { TbChevronDown, TbChevronUp, TbSelector } from "react-icons/tb";
|
||||
|
||||
export type TableSort = {
|
||||
property?: string;
|
||||
direction: "asc" | "desc";
|
||||
};
|
||||
|
||||
const TableSortIcon = ({
|
||||
sort,
|
||||
setSort,
|
||||
property,
|
||||
}: {
|
||||
sort: TableSort;
|
||||
setSort: Dispatch<SetStateAction<TableSort>>;
|
||||
property: string;
|
||||
}) => {
|
||||
if (sort.property === property) {
|
||||
return (
|
||||
<ActionIcon
|
||||
onClick={() =>
|
||||
setSort({
|
||||
property,
|
||||
direction: sort.direction === "asc" ? "desc" : "asc",
|
||||
})
|
||||
}
|
||||
>
|
||||
{sort.direction === "asc" ? <TbChevronDown /> : <TbChevronUp />}
|
||||
</ActionIcon>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<ActionIcon onClick={() => setSort({ property, direction: "asc" })}>
|
||||
<TbSelector />
|
||||
</ActionIcon>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default TableSortIcon;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
Group,
|
||||
Skeleton,
|
||||
Stack,
|
||||
@@ -8,9 +9,7 @@ import {
|
||||
} from "@mantine/core";
|
||||
import { useClipboard } from "@mantine/hooks";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import mime from "mime-types";
|
||||
|
||||
import Link from "next/link";
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||
import { TbDownload, TbEye, TbLink } from "react-icons/tb";
|
||||
import useConfig from "../../hooks/config.hook";
|
||||
import shareService from "../../services/share.service";
|
||||
@@ -18,19 +17,52 @@ import { FileMetaData } from "../../types/File.type";
|
||||
import { Share } from "../../types/share.type";
|
||||
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,
|
||||
setShare,
|
||||
share,
|
||||
isLoading,
|
||||
}: {
|
||||
files?: FileMetaData[];
|
||||
setShare: Dispatch<SetStateAction<Share | undefined>>;
|
||||
share: Share;
|
||||
isLoading: boolean;
|
||||
}) => {
|
||||
const clipboard = useClipboard();
|
||||
const config = useConfig();
|
||||
const modals = useModals();
|
||||
const t = useTranslate();
|
||||
|
||||
const [sort, setSort] = useState<TableSort>({
|
||||
property: undefined,
|
||||
direction: "desc",
|
||||
});
|
||||
|
||||
const sortFiles = () => {
|
||||
if (files && sort.property) {
|
||||
const sortedFiles = files.sort((a: any, b: any) => {
|
||||
if (sort.direction === "asc") {
|
||||
return b[sort.property!].localeCompare(a[sort.property!], undefined, {
|
||||
numeric: true,
|
||||
});
|
||||
} else {
|
||||
return a[sort.property!].localeCompare(b[sort.property!], undefined, {
|
||||
numeric: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
setShare({
|
||||
...share,
|
||||
files: sortedFiles,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const copyFileLink = (file: FileMetaData) => {
|
||||
const link = `${config.get("general.appUrl")}/api/shares/${
|
||||
@@ -39,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} />
|
||||
@@ -52,55 +84,70 @@ const FileList = ({
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(sortFiles, [sort]);
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Size</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{isLoading
|
||||
? skeletonRows
|
||||
: files!.map((file) => (
|
||||
<tr key={file.name}>
|
||||
<td>{file.name}</td>
|
||||
<td>{byteToHumanSizeString(parseInt(file.size))}</td>
|
||||
<td>
|
||||
<Group position="right">
|
||||
{shareService.doesFileSupportPreview(file.name) && (
|
||||
<Box sx={{ display: "block", overflowX: "auto" }}>
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<Group spacing="xs">
|
||||
<FormattedMessage id="share.table.name" />
|
||||
<TableSortIcon sort={sort} setSort={setSort} property="name" />
|
||||
</Group>
|
||||
</th>
|
||||
<th>
|
||||
<Group spacing="xs">
|
||||
<FormattedMessage id="share.table.size" />
|
||||
<TableSortIcon sort={sort} setSort={setSort} property="size" />
|
||||
</Group>
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{isLoading
|
||||
? skeletonRows
|
||||
: files!.map((file) => (
|
||||
<tr key={file.name}>
|
||||
<td>{file.name}</td>
|
||||
<td>{byteToHumanSizeString(parseInt(file.size))}</td>
|
||||
<td>
|
||||
<Group position="right">
|
||||
{shareService.doesFileSupportPreview(file.name) && (
|
||||
<ActionIcon
|
||||
onClick={() =>
|
||||
showFilePreviewModal(share.id, file, modals)
|
||||
}
|
||||
size={25}
|
||||
>
|
||||
<TbEye />
|
||||
</ActionIcon>
|
||||
)}
|
||||
{!share.hasPassword && (
|
||||
<ActionIcon
|
||||
size={25}
|
||||
onClick={() => copyFileLink(file)}
|
||||
>
|
||||
<TbLink />
|
||||
</ActionIcon>
|
||||
)}
|
||||
<ActionIcon
|
||||
component={Link}
|
||||
href={`/share/${share.id}/preview/${
|
||||
file.id
|
||||
}?type=${mime.contentType(file.name)}`}
|
||||
target="_blank"
|
||||
size={25}
|
||||
onClick={async () => {
|
||||
await shareService.downloadFile(share.id, file.id);
|
||||
}}
|
||||
>
|
||||
<TbEye />
|
||||
<TbDownload />
|
||||
</ActionIcon>
|
||||
)}
|
||||
{!share.hasPassword && (
|
||||
<ActionIcon size={25} onClick={() => copyFileLink(file)}>
|
||||
<TbLink />
|
||||
</ActionIcon>
|
||||
)}
|
||||
<ActionIcon
|
||||
size={25}
|
||||
onClick={async () => {
|
||||
await shareService.downloadFile(share.id, file.id);
|
||||
}}
|
||||
>
|
||||
<TbDownload />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</Group>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
159
frontend/src/components/share/FilePreview.tsx
Normal file
159
frontend/src/components/share/FilePreview.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
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<{
|
||||
shareId: string;
|
||||
fileId: string;
|
||||
mimeType: string;
|
||||
setIsNotSupported: Dispatch<SetStateAction<boolean>>;
|
||||
}>({
|
||||
shareId: "",
|
||||
fileId: "",
|
||||
mimeType: "",
|
||||
setIsNotSupported: () => {},
|
||||
});
|
||||
|
||||
const FilePreview = ({
|
||||
shareId,
|
||||
fileId,
|
||||
mimeType,
|
||||
}: {
|
||||
shareId: string;
|
||||
fileId: string;
|
||||
mimeType: string;
|
||||
}) => {
|
||||
const [isNotSupported, setIsNotSupported] = useState(false);
|
||||
if (isNotSupported) return <UnSupportedFile />;
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<FilePreviewContext.Provider
|
||||
value={{ shareId, fileId, mimeType, setIsNotSupported }}
|
||||
>
|
||||
<FileDecider />
|
||||
</FilePreviewContext.Provider>
|
||||
<Button
|
||||
variant="subtle"
|
||||
component={Link}
|
||||
onClick={() => modals.closeAll()}
|
||||
target="_blank"
|
||||
href={`/api/shares/${shareId}/files/${fileId}?download=false`}
|
||||
>
|
||||
View original file
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const FileDecider = () => {
|
||||
const { mimeType, setIsNotSupported } = React.useContext(FilePreviewContext);
|
||||
|
||||
if (mimeType == "application/pdf") {
|
||||
return <PdfPreview />;
|
||||
} else if (mimeType.startsWith("video/")) {
|
||||
return <VideoPreview />;
|
||||
} else if (mimeType.startsWith("image/")) {
|
||||
return <ImagePreview />;
|
||||
} else if (mimeType.startsWith("audio/")) {
|
||||
return <AudioPreview />;
|
||||
} else if (mimeType.startsWith("text/")) {
|
||||
return <TextPreview />;
|
||||
} else {
|
||||
setIsNotSupported(true);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const AudioPreview = () => {
|
||||
const { shareId, fileId, setIsNotSupported } =
|
||||
React.useContext(FilePreviewContext);
|
||||
return (
|
||||
<Center style={{ minHeight: 200 }}>
|
||||
<Stack align="center" spacing={10} style={{ width: "100%" }}>
|
||||
<audio controls style={{ width: "100%" }}>
|
||||
<source
|
||||
src={`/api/shares/${shareId}/files/${fileId}?download=false`}
|
||||
onError={() => setIsNotSupported(true)}
|
||||
/>
|
||||
</audio>
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
|
||||
const VideoPreview = () => {
|
||||
const { shareId, fileId, setIsNotSupported } =
|
||||
React.useContext(FilePreviewContext);
|
||||
return (
|
||||
<video width="100%" controls>
|
||||
<source
|
||||
src={`/api/shares/${shareId}/files/${fileId}?download=false`}
|
||||
onError={() => setIsNotSupported(true)}
|
||||
/>
|
||||
</video>
|
||||
);
|
||||
};
|
||||
|
||||
const ImagePreview = () => {
|
||||
const { shareId, fileId, setIsNotSupported } =
|
||||
React.useContext(FilePreviewContext);
|
||||
return (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={`/api/shares/${shareId}/files/${fileId}?download=false`}
|
||||
alt={`${fileId}_preview`}
|
||||
width="100%"
|
||||
onError={() => setIsNotSupported(true)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const TextPreview = () => {
|
||||
const { shareId, fileId } = React.useContext(FilePreviewContext);
|
||||
const [text, setText] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.get(`/shares/${shareId}/files/${fileId}?download=false`)
|
||||
.then((res) => setText(res.data));
|
||||
}, [shareId, fileId]);
|
||||
|
||||
return (
|
||||
<Center style={{ minHeight: 200 }}>
|
||||
<Stack align="center" spacing={10} style={{ width: "100%" }}>
|
||||
<Text sx={{ whiteSpace: "pre-wrap" }} size="sm">
|
||||
{text}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
|
||||
const PdfPreview = () => {
|
||||
const { shareId, fileId } = React.useContext(FilePreviewContext);
|
||||
if (typeof window !== "undefined") {
|
||||
window.location.href = `/api/shares/${shareId}/files/${fileId}?download=false`;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const UnSupportedFile = () => {
|
||||
return (
|
||||
<Center style={{ minHeight: 200 }}>
|
||||
<Stack align="center" spacing={10}>
|
||||
<Title order={3}>
|
||||
<FormattedMessage id="share.modal.file-preview.error.not-supported.title" />
|
||||
</Title>
|
||||
<Text>
|
||||
<FormattedMessage id="share.modal.file-preview.error.not-supported.description" />
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilePreview;
|
||||
@@ -1,24 +1,21 @@
|
||||
import { ActionIcon, Button, Stack, TextInput, Title } from "@mantine/core";
|
||||
import { useClipboard } from "@mantine/hooks";
|
||||
import { Button, Stack } from "@mantine/core";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||
import { TbCopy } from "react-icons/tb";
|
||||
import toast from "../../../utils/toast.util";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { translateOutsideContext } from "../../../hooks/useTranslate.hook";
|
||||
import CopyTextField from "../../upload/CopyTextField";
|
||||
|
||||
const showCompletedReverseShareModal = (
|
||||
modals: ModalsContextProps,
|
||||
link: string,
|
||||
getReverseShares: () => void
|
||||
) => {
|
||||
const t = translateOutsideContext();
|
||||
return modals.openModal({
|
||||
closeOnClickOutside: false,
|
||||
withCloseButton: false,
|
||||
closeOnEscape: false,
|
||||
title: (
|
||||
<Stack align="stretch" spacing={0}>
|
||||
<Title order={4}>Reverse share link</Title>
|
||||
</Stack>
|
||||
),
|
||||
title: t("account.reverseShares.modal.reverse-share-link"),
|
||||
children: <Body link={link} getReverseShares={getReverseShares} />,
|
||||
});
|
||||
};
|
||||
@@ -30,28 +27,11 @@ const Body = ({
|
||||
link: string;
|
||||
getReverseShares: () => void;
|
||||
}) => {
|
||||
const clipboard = useClipboard({ timeout: 500 });
|
||||
const modals = useModals();
|
||||
|
||||
return (
|
||||
<Stack align="stretch">
|
||||
<TextInput
|
||||
readOnly
|
||||
variant="filled"
|
||||
value={link}
|
||||
rightSection={
|
||||
window.isSecureContext && (
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
clipboard.copy(link);
|
||||
toast.success("Your link was copied to the keyboard.");
|
||||
}}
|
||||
>
|
||||
<TbCopy />
|
||||
</ActionIcon>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<CopyTextField link={link} />
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
@@ -59,7 +39,7 @@ const Body = ({
|
||||
getReverseShares();
|
||||
}}
|
||||
>
|
||||
Done
|
||||
<FormattedMessage id="common.button.done" />
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -8,11 +8,12 @@ import {
|
||||
Stack,
|
||||
Switch,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
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";
|
||||
@@ -25,7 +26,7 @@ const showCreateReverseShareModal = (
|
||||
getReverseShares: () => void
|
||||
) => {
|
||||
return modals.openModal({
|
||||
title: <Title order={4}>Create reverse share</Title>,
|
||||
title: "Create reverse share",
|
||||
children: (
|
||||
<Body
|
||||
showSendEmailNotificationOption={showSendEmailNotificationOption}
|
||||
@@ -43,6 +44,7 @@ const Body = ({
|
||||
showSendEmailNotificationOption: boolean;
|
||||
}) => {
|
||||
const modals = useModals();
|
||||
const t = useTranslate();
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
@@ -80,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>
|
||||
@@ -92,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"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
@@ -126,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)}
|
||||
/>
|
||||
@@ -139,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",
|
||||
})}
|
||||
@@ -156,7 +183,7 @@ const Body = ({
|
||||
)}
|
||||
|
||||
<Button mt="md" type="submit">
|
||||
Create
|
||||
<FormattedMessage id="common.button.create" />
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||
import mime from "mime-types";
|
||||
import { FileMetaData } from "../../../types/File.type";
|
||||
import FilePreview from "../FilePreview";
|
||||
|
||||
const showFilePreviewModal = (
|
||||
shareId: string,
|
||||
file: FileMetaData,
|
||||
modals: ModalsContextProps
|
||||
) => {
|
||||
const mimeType = (mime.contentType(file.name) || "").split(";")[0];
|
||||
return modals.openModal({
|
||||
size: "xl",
|
||||
title: file.name,
|
||||
children: (
|
||||
<FilePreview shareId={shareId} fileId={file.id} mimeType={mimeType} />
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
export default showFilePreviewModal;
|
||||
@@ -1,57 +1,60 @@
|
||||
import { Button, PasswordInput, Stack, Text, Title } from "@mantine/core";
|
||||
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: any
|
||||
submitCallback: (password: string) => Promise<void>
|
||||
) => {
|
||||
const t = translateOutsideContext();
|
||||
return modals.openModal({
|
||||
closeOnClickOutside: false,
|
||||
withCloseButton: false,
|
||||
closeOnEscape: false,
|
||||
title: (
|
||||
<>
|
||||
<Title order={4}>Password required</Title>
|
||||
<Text size="sm">
|
||||
This access this share please enter the password for the share.
|
||||
</Text>
|
||||
</>
|
||||
),
|
||||
title: t("share.modal.password.title"),
|
||||
children: <Body submitCallback={submitCallback} />,
|
||||
});
|
||||
};
|
||||
|
||||
const Body = ({ submitCallback }: { submitCallback: any }) => {
|
||||
const Body = ({
|
||||
submitCallback,
|
||||
}: {
|
||||
submitCallback: (password: string) => Promise<void>;
|
||||
}) => {
|
||||
const [password, setPassword] = useState("");
|
||||
const [passwordWrong, setPasswordWrong] = useState(false);
|
||||
const t = useTranslate();
|
||||
return (
|
||||
<>
|
||||
<Stack align="stretch">
|
||||
<PasswordInput
|
||||
variant="filled"
|
||||
placeholder="Password"
|
||||
error={passwordWrong && "Wrong password"}
|
||||
onFocus={() => setPasswordWrong(false)}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
value={password}
|
||||
/>
|
||||
<Button
|
||||
onClick={() =>
|
||||
submitCallback(password)
|
||||
.then((res: any) => res)
|
||||
.catch((e: any) => {
|
||||
const error = e.response.data.message;
|
||||
if (error == "Wrong password") {
|
||||
setPasswordWrong(true);
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Stack>
|
||||
</>
|
||||
<Stack align="stretch">
|
||||
<Text size="sm">
|
||||
<FormattedMessage id="share.modal.password.description" />
|
||||
</Text>
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
submitCallback(password);
|
||||
}}
|
||||
>
|
||||
<Stack>
|
||||
<PasswordInput
|
||||
variant="filled"
|
||||
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">
|
||||
<FormattedMessage id="common.button.submit" />
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Button, Stack, Text, Title } from "@mantine/core";
|
||||
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,
|
||||
@@ -12,7 +13,7 @@ const showErrorModal = (
|
||||
closeOnClickOutside: false,
|
||||
withCloseButton: false,
|
||||
closeOnEscape: false,
|
||||
title: <Title order={4}>{title}</Title>,
|
||||
title: title,
|
||||
|
||||
children: <Body text={text} />,
|
||||
});
|
||||
@@ -31,7 +32,7 @@ const Body = ({ text }: { text: string }) => {
|
||||
router.back();
|
||||
}}
|
||||
>
|
||||
Go back
|
||||
<FormattedMessage id="common.button.go-back" />
|
||||
</Button>
|
||||
</Stack>
|
||||
</>
|
||||
|
||||
51
frontend/src/components/upload/CopyTextField.tsx
Normal file
51
frontend/src/components/upload/CopyTextField.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { ActionIcon, TextInput } from "@mantine/core";
|
||||
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>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const copyLink = () => {
|
||||
clipboard.copy(props.link);
|
||||
toast.success(t("common.notify.copied"));
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
timerRef.current = setTimeout(() => {
|
||||
setCheckState(false);
|
||||
}, 1500);
|
||||
setCheckState(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
readOnly
|
||||
label={t("common.text.link")}
|
||||
variant="filled"
|
||||
value={props.link}
|
||||
onClick={() => {
|
||||
if (!textClicked) {
|
||||
copyLink();
|
||||
setTextClicked(true);
|
||||
}
|
||||
}}
|
||||
rightSection={
|
||||
window.isSecureContext && (
|
||||
<ActionIcon onClick={copyLink}>
|
||||
{checkState ? <TbCheck /> : <TbCopy />}
|
||||
</ActionIcon>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default CopyTextField;
|
||||
@@ -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>
|
||||
|
||||
@@ -1,63 +1,40 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
Button,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { useClipboard } from "@mantine/hooks";
|
||||
import { Button, Stack, Text } from "@mantine/core";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||
import moment from "moment";
|
||||
import { useRouter } from "next/router";
|
||||
import { TbCopy } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import useTranslate, {
|
||||
translateOutsideContext,
|
||||
} from "../../../hooks/useTranslate.hook";
|
||||
import { Share } from "../../../types/share.type";
|
||||
import toast from "../../../utils/toast.util";
|
||||
import CopyTextField from "../CopyTextField";
|
||||
|
||||
const showCompletedUploadModal = (
|
||||
modals: ModalsContextProps,
|
||||
share: Share,
|
||||
appUrl: string
|
||||
) => {
|
||||
const t = translateOutsideContext();
|
||||
return modals.openModal({
|
||||
closeOnClickOutside: false,
|
||||
withCloseButton: false,
|
||||
closeOnEscape: false,
|
||||
title: (
|
||||
<Stack align="stretch" spacing={0}>
|
||||
<Title order={4}>Share ready</Title>
|
||||
</Stack>
|
||||
),
|
||||
title: t("upload.modal.completed.share-ready"),
|
||||
children: <Body share={share} appUrl={appUrl} />,
|
||||
});
|
||||
};
|
||||
|
||||
const Body = ({ share, appUrl }: { share: Share; appUrl: string }) => {
|
||||
const clipboard = useClipboard({ timeout: 500 });
|
||||
const modals = useModals();
|
||||
const router = useRouter();
|
||||
const t = useTranslate();
|
||||
|
||||
const link = `${appUrl}/s/${share.id}`;
|
||||
|
||||
const link = `${appUrl}/share/${share.id}`;
|
||||
return (
|
||||
<Stack align="stretch">
|
||||
<TextInput
|
||||
readOnly
|
||||
variant="filled"
|
||||
value={link}
|
||||
rightSection={
|
||||
window.isSecureContext && (
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
clipboard.copy(link);
|
||||
toast.success("Your link was copied to the keyboard.");
|
||||
}}
|
||||
>
|
||||
<TbCopy />
|
||||
</ActionIcon>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<CopyTextField link={link} />
|
||||
<Text
|
||||
size="xs"
|
||||
sx={(theme) => ({
|
||||
@@ -66,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
|
||||
@@ -78,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: <Title order={4}>Share</Title>,
|
||||
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,20 +107,19 @@ 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"
|
||||
? t("upload.modal.expires.never")
|
||||
: form.values.expiration_num + form.values.expiration_unit;
|
||||
uploadCallback({
|
||||
id: values.link,
|
||||
@@ -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([]),
|
||||
@@ -22,7 +22,7 @@ export async function middleware(request: NextRequest) {
|
||||
|
||||
// Get config from backend
|
||||
const config = await (
|
||||
await fetch("http://localhost:8080/api/configs")
|
||||
await fetch(`${request.nextUrl.origin}/api/configs`)
|
||||
).json();
|
||||
|
||||
const getConfig = (key: string) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -11,12 +11,14 @@ import axios from "axios";
|
||||
import { getCookie, setCookie } from "cookies-next";
|
||||
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";
|
||||
@@ -24,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]"];
|
||||
|
||||
@@ -32,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);
|
||||
@@ -49,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]);
|
||||
@@ -65,84 +77,99 @@ 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>
|
||||
);
|
||||
}
|
||||
|
||||
// Fetch user and config variables on server side when the first request is made
|
||||
// These will get passed as a page prop to the App component and stored in the contexts
|
||||
App.getInitialProps = async ({ ctx }: { ctx: GetServerSidePropsContext }) => {
|
||||
const { apiURL } = getConfig().serverRuntimeConfig;
|
||||
|
||||
let pageProps: {
|
||||
user?: CurrentUser;
|
||||
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;
|
||||
|
||||
pageProps.user = await axios(`http://localhost:8080/api/users/me`, {
|
||||
pageProps.user = await axios(`${apiURL}/api/users/me`, {
|
||||
headers: { cookie: cookieHeader },
|
||||
})
|
||||
.then((res) => res.data)
|
||||
.catch(() => null);
|
||||
|
||||
pageProps.configVariables = (
|
||||
await axios(`http://localhost:8080/api/configs`)
|
||||
).data;
|
||||
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,18 +14,22 @@ 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 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";
|
||||
import toast from "../../utils/toast.util";
|
||||
import LanguagePicker from "../../components/account/LanguagePicker";
|
||||
|
||||
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">
|
||||
@@ -169,7 +188,7 @@ const Account = () => {
|
||||
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(
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Button,
|
||||
Center,
|
||||
Group,
|
||||
MediaQuery,
|
||||
Space,
|
||||
Stack,
|
||||
Table,
|
||||
@@ -15,11 +16,14 @@ import { useModals } from "@mantine/modals";
|
||||
import moment from "moment";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { TbLink, TbTrash } from "react-icons/tb";
|
||||
import { TbInfoCircle, TbLink, TbTrash } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import Meta from "../../components/Meta";
|
||||
import 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";
|
||||
@@ -28,6 +32,7 @@ const MyShares = () => {
|
||||
const modals = useModals();
|
||||
const clipboard = useClipboard();
|
||||
const config = useConfig();
|
||||
const t = useTranslate();
|
||||
|
||||
const [shares, setShares] = useState<MyShare[]>();
|
||||
|
||||
@@ -39,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>
|
||||
@@ -59,9 +68,21 @@ const MyShares = () => {
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Visitors</th>
|
||||
<th>Expires at</th>
|
||||
<th>
|
||||
<FormattedMessage id="account.shares.table.name" />
|
||||
</th>
|
||||
<MediaQuery smallerThan="md" styles={{ display: "none" }}>
|
||||
<th>
|
||||
<FormattedMessage id="account.shares.table.description" />
|
||||
</th>
|
||||
</MediaQuery>
|
||||
|
||||
<th>
|
||||
<FormattedMessage id="account.shares.table.visitors" />
|
||||
</th>
|
||||
<th>
|
||||
<FormattedMessage id="account.shares.table.expiresAt" />
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -69,6 +90,18 @@ const MyShares = () => {
|
||||
{shares.map((share) => (
|
||||
<tr key={share.id}>
|
||||
<td>{share.id}</td>
|
||||
<MediaQuery smallerThan="sm" styles={{ display: "none" }}>
|
||||
<td
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
maxWidth: "300px",
|
||||
}}
|
||||
>
|
||||
{share.description || ""}
|
||||
</td>
|
||||
</MediaQuery>
|
||||
<td>{share.views}</td>
|
||||
<td>
|
||||
{moment(share.expiration).unix() === 0
|
||||
@@ -77,6 +110,21 @@ const MyShares = () => {
|
||||
</td>
|
||||
<td>
|
||||
<Group position="right">
|
||||
<ActionIcon
|
||||
color="blue"
|
||||
variant="light"
|
||||
size={25}
|
||||
onClick={() => {
|
||||
showShareInformationsModal(
|
||||
modals,
|
||||
share,
|
||||
config.get("general.appUrl"),
|
||||
parseInt(config.get("share.maxSize"))
|
||||
);
|
||||
}}
|
||||
>
|
||||
<TbInfoCircle />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
color="victoria"
|
||||
variant="light"
|
||||
@@ -84,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,
|
||||
@@ -108,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)");
|
||||
@@ -67,7 +70,7 @@ export default function AppShellDemo() {
|
||||
toast.success("Configurations updated successfully");
|
||||
})
|
||||
.catch(toast.axiosError);
|
||||
config.refresh();
|
||||
void config.refresh();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -75,8 +78,12 @@ export default function AppShellDemo() {
|
||||
const index = updatedConfigVariables.findIndex(
|
||||
(item) => item.key === configVariable.key
|
||||
);
|
||||
|
||||
if (index > -1) {
|
||||
updatedConfigVariables[index] = configVariable;
|
||||
updatedConfigVariables[index] = {
|
||||
...updatedConfigVariables[index],
|
||||
...configVariable,
|
||||
};
|
||||
} else {
|
||||
setUpdatedConfigVariables([...updatedConfigVariables, configVariable]);
|
||||
}
|
||||
@@ -90,7 +97,7 @@ export default function AppShellDemo() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title="Configuration" />
|
||||
<Meta title={t("admin.config.title")} />
|
||||
<AppShell
|
||||
styles={{
|
||||
main: {
|
||||
@@ -130,10 +137,27 @@ export default function AppShellDemo() {
|
||||
spacing={0}
|
||||
>
|
||||
<Title order={6}>
|
||||
{configVariableToFriendlyName(configVariable.name)}
|
||||
<FormattedMessage
|
||||
id={`admin.config.${camelToKebab(
|
||||
configVariable.key
|
||||
)}`}
|
||||
/>
|
||||
</Title>
|
||||
<Text color="dimmed" size="sm" mb="xs">
|
||||
{configVariable.description}
|
||||
|
||||
<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>
|
||||
@@ -157,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>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import httpProxyMiddleware from "next-http-proxy-middleware";
|
||||
import getConfig from "next/config";
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
@@ -8,11 +9,13 @@ export const config = {
|
||||
},
|
||||
};
|
||||
|
||||
const { apiURL } = getConfig().serverRuntimeConfig;
|
||||
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => {
|
||||
return httpProxyMiddleware(req, res, {
|
||||
headers: {
|
||||
"X-Forwarded-For": req.socket?.remoteAddress ?? "",
|
||||
},
|
||||
target: "http://localhost:8080",
|
||||
target: apiURL,
|
||||
});
|
||||
};
|
||||
|
||||
14
frontend/src/pages/api/health.tsx
Normal file
14
frontend/src/pages/api/health.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import axios from "axios";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import getConfig from "next/config";
|
||||
|
||||
const { apiURL } = getConfig().serverRuntimeConfig;
|
||||
|
||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const apiStatus = await axios
|
||||
.get(`${apiURL}/api/configs`)
|
||||
.then(() => "OK")
|
||||
.catch(() => "ERROR");
|
||||
|
||||
res.status(apiStatus == "OK" ? 200 : 500).send(apiStatus);
|
||||
};
|
||||
@@ -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"} />
|
||||
</>
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user